use std::num::{ParseFloatError, ParseIntError};

use chrono::{serde::ts_seconds, DateTime, NaiveDate, NaiveTime, ParseError, Utc};
use serde::Serialize;
use thiserror::Error;
#[cfg(feature = "bindings")]
use ts_rs::TS;

use crate::geo::{Degrees, IntMeters};

#[derive(Serialize, Clone, Copy, Debug)]
#[cfg_attr(feature = "bindings", derive(TS))]
#[cfg_attr(feature = "bindings", ts(export))]
pub struct IgcFix {
    #[serde(with = "ts_seconds")]
    #[cfg_attr(feature = "bindings", ts(as = "i64"))]
    pub time: DateTime<Utc>,
    pub lat: Degrees,
    pub lon: Degrees,
    pub alt: IntMeters,
}

#[derive(Clone, Copy, Debug)]
pub struct BRecord {
    pub time: NaiveTime,
    pub lat: Degrees,
    pub lon: Degrees,
    pub alt: IntMeters,
}

#[derive(Error, Debug)]
pub enum IgcError {
    #[error("Incomplete record")]
    IncompleteRecord,

    #[error("Missing date")]
    MissingDate,

    #[error("Invalid date")]
    InvalidDate(#[from] ParseError),

    #[error("Date overflow")]
    DateOverflow,

    #[error("Invalid value")]
    InvalidValue(#[from] ParseFloatError),

    #[error("Invalid altitude")]
    InvalidAltitude(#[from] ParseIntError),
}

pub fn parse_igc_date(contents: &str) -> Result<NaiveDate, IgcError> {
    for line in contents.lines() {
        if line.starts_with("HFDTE") {
            return parse_date(line);
        }
    }
    Err(IgcError::MissingDate)
}

pub fn parse_b_record(line: &str) -> Result<BRecord, IgcError> {
    let time = line
        .get(1..7)
        .ok_or(IgcError::IncompleteRecord)
        .and_then(parse_time)?;
    let lat = line
        .get(7..15)
        .ok_or(IgcError::IncompleteRecord)
        .and_then(parse_latitude)?;
    let lon = line
        .get(15..24)
        .ok_or(IgcError::IncompleteRecord)
        .and_then(parse_longitude)?;
    let alt = line
        .get(30..35)
        .ok_or(IgcError::IncompleteRecord)
        .and_then(parse_altitude)?;
    Ok(BRecord {
        time,
        lat,
        lon,
        alt,
    })
}

pub fn parse_date(line: &str) -> Result<NaiveDate, IgcError> {
    if line.starts_with("HFDTEDATE:") {
        parse_date_hfdtedate(line)
    } else {
        parse_date_hfdte(line)
    }
}

fn parse_date_hfdte(line: &str) -> Result<NaiveDate, IgcError> {
    match line.get(5..11) {
        Some(digits) => Ok(NaiveDate::parse_from_str(digits, "%d%m%y")?),
        _ => Err(IgcError::IncompleteRecord),
    }
}

fn parse_date_hfdtedate(line: &str) -> Result<NaiveDate, IgcError> {
    match line.get(10..16) {
        Some(digits) => Ok(NaiveDate::parse_from_str(digits, "%d%m%y")?),
        _ => Err(IgcError::IncompleteRecord),
    }
}

fn parse_time(s: &str) -> Result<NaiveTime, IgcError> {
    Ok(NaiveTime::parse_from_str(s, "%H%M%S")?)
}

fn parse_latitude(s: &str) -> Result<Degrees, IgcError> {
    let degree_slice = s.get(0..2);
    let minute_slice = s.get(2..4);
    let remain_slice = s.get(4..7);
    let sign = match s.chars().last() {
        // Southern hemisphere latitude is negative
        Some('S') => -1.0,
        _ => 1.0,
    };
    match (degree_slice, minute_slice, remain_slice) {
        (Some(deg), Some(min), Some(rem)) => {
            Ok(sign * sexagesimal_to_decimal(deg.parse()?, min.parse()?, rem.parse()?))
        }
        _ => Err(IgcError::IncompleteRecord),
    }
}

fn parse_longitude(s: &str) -> Result<Degrees, IgcError> {
    let degree_slice = s.get(0..3);
    let minute_slice = s.get(3..5);
    let remain_slice = s.get(5..8);
    let sign = match s.chars().last() {
        // Western hemisphere longitude is negative
        Some('W') => -1.0,
        _ => 1.0,
    };
    match (degree_slice, minute_slice, remain_slice) {
        (Some(deg), Some(min), Some(rem)) => {
            Ok(sign * sexagesimal_to_decimal(deg.parse()?, min.parse()?, rem.parse()?))
        }
        _ => Err(IgcError::IncompleteRecord),
    }
}

fn sexagesimal_to_decimal(degrees: f64, minutes: f64, thousandths: f64) -> Degrees {
    degrees + (minutes + 1e-3 * thousandths) / 60.0
}

fn parse_altitude(s: &str) -> Result<IntMeters, IgcError> {
    Ok(s.parse()?)
}
