//! Newtypes for TLE orbital elements.
//!
//! Every TLE field is represented as a newtype with private internals
//! and a validated constructor.  Angular quantities are stored in radians
//! internally but constructed from degrees where that is the TLE convention.

use crate::error::{Error, TleParseDetail};

// ---------------------------------------------------------------------------
// Macro to reduce boilerplate for f64 newtypes with range validation
// ---------------------------------------------------------------------------

macro_rules! newtype_f64 {
    (
        $(#[$meta:meta])*
        $name:ident, $field:ident
    ) => {
        $(#[$meta])*
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        pub struct $name(f64);

        impl $name {
            #[must_use]
            pub fn $field(self) -> f64 {
                self.0
            }
        }
    };
}

// ---------------------------------------------------------------------------
// TLE line number (1 or 2)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineNumber(u8);

impl LineNumber {
    /// Create a new `LineNumber`.
    ///
    /// # Errors
    ///
    /// Returns an error if `n` is not 1 or 2.
    pub fn new(n: u8) -> Result<Self, Error> {
        match n {
            1 | 2 => Ok(Self(n)),
            _ => Err(Error::InvalidLineFormat {
                line_number: Self(n),
                reason: "line number must be 1 or 2",
            }),
        }
    }

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

// ---------------------------------------------------------------------------
// Classification (U = unclassified, C = classified, S = secret)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Classification {
    Unclassified,
    Classified,
    Secret,
}

impl Classification {
    /// Parse a classification character.
    ///
    /// # Errors
    ///
    /// Returns an error if `c` is not 'U', 'C', or 'S'.
    pub fn from_char(c: char) -> Result<Self, Error> {
        match c {
            'U' => Ok(Self::Unclassified),
            'C' => Ok(Self::Classified),
            'S' => Ok(Self::Secret),
            _ => Err(TleParseDetail::MalformedInt {
                field: "classification",
                raw: c.to_string(),
            }
            .into()),
        }
    }

    #[must_use]
    pub fn as_char(self) -> char {
        match self {
            Self::Unclassified => 'U',
            Self::Classified => 'C',
            Self::Secret => 'S',
        }
    }
}

// ---------------------------------------------------------------------------
// Satellite number (NORAD catalog number, 1..=99999)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SatelliteNumber(u32);

impl SatelliteNumber {
    /// Create a new `SatelliteNumber`.
    ///
    /// # Errors
    ///
    /// Returns an error if `n` exceeds 99999.
    pub fn new(n: u32) -> Result<Self, Error> {
        if n <= 99999 {
            Ok(Self(n))
        } else {
            Err(TleParseDetail::MalformedInt {
                field: "satellite_number",
                raw: n.to_string(),
            }
            .into())
        }
    }

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

// ---------------------------------------------------------------------------
// Epoch year (two-digit, resolved to four-digit via 57-pivot)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EpochYear(u16);

impl EpochYear {
    /// Create from a two-digit year.  Years >= 57 map to 1900s; < 57 to 2000s.
    #[must_use]
    pub fn from_two_digit(y: u8) -> Self {
        if y >= 57 {
            Self(u16::from(y) + 1900)
        } else {
            Self(u16::from(y) + 2000)
        }
    }

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

// ---------------------------------------------------------------------------
// Epoch day (fractional day of year, 1.0..367.0)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// Fractional day of the year (1-based).
    EpochDay, day
}

impl EpochDay {
    /// Create a new `EpochDay`.
    ///
    /// # Errors
    ///
    /// Returns an error if `d` is outside [1.0, 367.0).
    pub fn new(d: f64) -> Result<Self, Error> {
        if (1.0..367.0).contains(&d) {
            Ok(Self(d))
        } else {
            Err(TleParseDetail::MalformedFloat {
                field: "epoch_day",
                raw: d.to_string(),
            }
            .into())
        }
    }
}

// ---------------------------------------------------------------------------
// Inclination (radians, stored internally; constructed from degrees)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// Orbital inclination in radians.
    Inclination, radians
}

impl Inclination {
    /// Create an `Inclination` from degrees.
    ///
    /// # Errors
    ///
    /// Returns an error if the resulting radians are outside [0, pi].
    pub fn from_degrees(deg: f64) -> Result<Self, Error> {
        let rad = deg.to_radians();
        if (0.0..=core::f64::consts::PI).contains(&rad) {
            Ok(Self(rad))
        } else {
            Err(TleParseDetail::MalformedFloat {
                field: "inclination",
                raw: deg.to_string(),
            }
            .into())
        }
    }

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

// ---------------------------------------------------------------------------
// Right Ascension of Ascending Node (radians)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// RAAN in radians.
    Raan, radians
}

impl Raan {
    /// Create a `Raan` from degrees.
    ///
    /// # Errors
    ///
    /// This conversion is infallible but returns `Result` for API consistency.
    pub fn from_degrees(deg: f64) -> Result<Self, Error> {
        Ok(Self(deg.to_radians()))
    }

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

// ---------------------------------------------------------------------------
// Eccentricity (dimensionless, [0, 1) for closed orbits)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// Orbital eccentricity (dimensionless).
    Eccentricity, value
}

impl Eccentricity {
    /// Create a new `Eccentricity`.
    ///
    /// # Errors
    ///
    /// Returns an error if `e` is outside [0.0, 1.0).
    pub fn new(e: f64) -> Result<Self, Error> {
        if (0.0..1.0).contains(&e) {
            Ok(Self(e))
        } else {
            Err(TleParseDetail::MalformedFloat {
                field: "eccentricity",
                raw: e.to_string(),
            }
            .into())
        }
    }
}

// ---------------------------------------------------------------------------
// Argument of Perigee (radians)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// Argument of perigee in radians.
    ArgPerigee, radians
}

impl ArgPerigee {
    /// Create an `ArgPerigee` from degrees.
    ///
    /// # Errors
    ///
    /// This conversion is infallible but returns `Result` for API consistency.
    pub fn from_degrees(deg: f64) -> Result<Self, Error> {
        Ok(Self(deg.to_radians()))
    }

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

// ---------------------------------------------------------------------------
// Mean Anomaly (radians)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// Mean anomaly in radians.
    MeanAnomaly, radians
}

impl MeanAnomaly {
    /// Create a `MeanAnomaly` from degrees.
    ///
    /// # Errors
    ///
    /// This conversion is infallible but returns `Result` for API consistency.
    pub fn from_degrees(deg: f64) -> Result<Self, Error> {
        Ok(Self(deg.to_radians()))
    }

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

// ---------------------------------------------------------------------------
// Mean Motion (revolutions per day)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// Mean motion in revolutions per day.
    MeanMotion, revs_per_day
}

impl MeanMotion {
    /// Create a new `MeanMotion`.
    ///
    /// # Errors
    ///
    /// Returns an error if `n` is not positive.
    pub fn new(n: f64) -> Result<Self, Error> {
        if n > 0.0 {
            Ok(Self(n))
        } else {
            Err(TleParseDetail::MalformedFloat {
                field: "mean_motion",
                raw: n.to_string(),
            }
            .into())
        }
    }

    /// Orbital period in minutes.
    #[must_use]
    pub fn period_minutes(self) -> f64 {
        1440.0 / self.0
    }
}

// ---------------------------------------------------------------------------
// BSTAR drag coefficient (1/earth-radii)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// BSTAR drag coefficient in 1/earth-radii.
    Bstar, value
}

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

// ---------------------------------------------------------------------------
// First derivative of mean motion (rev/day^2, divided by 2)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// First time derivative of mean motion, divided by 2 (rev/day^2).
    MeanMotionDot, value
}

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

// ---------------------------------------------------------------------------
// Second derivative of mean motion (rev/day^3, divided by 6)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// Second time derivative of mean motion, divided by 6 (rev/day^3).
    MeanMotionDdot, value
}

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

// ---------------------------------------------------------------------------
// Element set number
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ElementSetNumber(u16);

impl ElementSetNumber {
    #[must_use]
    pub fn new(n: u16) -> Self {
        Self(n)
    }

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

// ---------------------------------------------------------------------------
// Revolution number at epoch
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RevolutionNumber(u32);

impl RevolutionNumber {
    #[must_use]
    pub fn new(n: u32) -> Self {
        Self(n)
    }

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

// ---------------------------------------------------------------------------
// Satellite name (line 0, optional)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SatelliteName(String);

impl SatelliteName {
    #[must_use]
    pub fn new(s: &str) -> Self {
        Self(s.trim().to_string())
    }

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

// ---------------------------------------------------------------------------
// Minutes since epoch (propagation time offset)
// ---------------------------------------------------------------------------

newtype_f64! {
    /// Minutes since TLE epoch (the propagation time parameter).
    MinutesSinceEpoch, value
}

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