//! Julian date, epoch arithmetic, and Greenwich Mean Sidereal Time.
//!
//! Converts TLE epoch (two-digit year + fractional day) to Julian Date
//! using the Spacetrack Report #3 formula.  GMST is computed via the
//! IAU 1982 formula, sufficient for SGP4 accuracy.

use crate::tle::newtypes::{EpochDay, EpochYear};

// ---------------------------------------------------------------------------
// Julian Date
// ---------------------------------------------------------------------------

/// Absolute time as a Julian Date (days since 4713 BC Jan 1.5).
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct JulianDate(f64);

impl JulianDate {
    #[must_use]
    pub fn new(jd: f64) -> Self {
        Self(jd)
    }

    #[must_use]
    pub fn value(self) -> f64 {
        self.0
    }

    /// Convert a TLE epoch (year + fractional day) to Julian Date.
    ///
    /// Uses the Spacetrack Report #3 formula for Julian Date of January 0.0
    /// of a given year, then adds the fractional day-of-year.
    #[must_use]
    pub fn from_epoch(year: EpochYear, day: EpochDay) -> Self {
        let jd_jan0 = Self::julian_date_of_year(year);
        Self(jd_jan0.0 + day.day())
    }

    /// Julian date of January 0.0 of a given year (Spacetrack Report #3).
    #[must_use]
    fn julian_date_of_year(year: EpochYear) -> Self {
        let y = f64::from(year.value());
        let jd = 367.0 * y
            - f64::floor(7.0 * (y + f64::floor(10.0 / 12.0)) / 4.0)
            + f64::floor(275.0 / 9.0)
            + 1_721_013.5;
        Self(jd)
    }

    /// Difference in minutes between two Julian dates.
    #[must_use]
    pub fn minutes_since(self, earlier: Self) -> f64 {
        (self.0 - earlier.0) * 1440.0
    }
}

// ---------------------------------------------------------------------------
// Greenwich Mean Sidereal Time
// ---------------------------------------------------------------------------

/// GMST angle in radians.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct GmstAngle(f64);

impl GmstAngle {
    #[must_use]
    pub fn new(rad: f64) -> Self {
        Self(rad)
    }

    #[must_use]
    pub fn radians(self) -> f64 {
        self.0
    }

    /// Compute GMST from a Julian Date.
    ///
    /// Uses the IAU 1982 formula (sufficient for SGP4 accuracy).
    #[must_use]
    pub fn from_julian_date(jd: JulianDate) -> Self {
        let t_ut1 = (jd.value() - 2_451_545.0) / 36525.0;
        let gmst_sec = 67310.54841
            + (876_600.0 * 3600.0 + 8_640_184.812_866) * t_ut1
            + 0.093_104 * t_ut1 * t_ut1
            - 6.2e-6 * t_ut1 * t_ut1 * t_ut1;
        // Convert seconds to radians and normalize to [0, 2pi)
        let two_pi = 2.0 * core::f64::consts::PI;
        let gmst_rad = (gmst_sec / 240.0).to_radians();
        let normalized = gmst_rad % two_pi;
        if normalized < 0.0 {
            Self(normalized + two_pi)
        } else {
            Self(normalized)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::Error;

    #[test]
    fn epoch_year_pivot_resolves_correctly() -> Result<(), Error> {
        let y2000 = EpochYear::from_two_digit(0);
        assert_eq!(y2000.value(), 2000);

        let y2024 = EpochYear::from_two_digit(24);
        assert_eq!(y2024.value(), 2024);

        let y1957 = EpochYear::from_two_digit(57);
        assert_eq!(y1957.value(), 1957);

        let y1999 = EpochYear::from_two_digit(99);
        assert_eq!(y1999.value(), 1999);
        Ok(())
    }

    #[test]
    fn julian_date_j2000_epoch() -> Result<(), Error> {
        // J2000.0 = Jan 1.5, 2000 = JD 2451545.0
        let year = EpochYear::from_two_digit(0);
        let day = EpochDay::new(1.5)?;
        let jd = JulianDate::from_epoch(year, day);
        assert!((jd.value() - 2451545.0).abs() < 0.01);
        Ok(())
    }

    #[test]
    fn gmst_is_nonnegative() -> Result<(), Error> {
        let jd = JulianDate::new(2451545.0);
        let gmst = GmstAngle::from_julian_date(jd);
        assert!(gmst.radians() >= 0.0);
        assert!(gmst.radians() < 2.0 * core::f64::consts::PI);
        Ok(())
    }
}
