//! # Direction‐type for Spherical Coordinates (unit vectors)
//!
//! A **direction** represents a *unit‐length* pointing vector: spherical
//! coordinates in which the radial component is implicitly fixed to 1.
//!
//! ## Mathematical Foundations
//!
//! Directions are **free vectors**: they live in the vector space, not in affine
//! space. Unlike positions, directions are translation-invariant:
//!
//! - Rotating a direction is valid (frame transformation)
//! - "Translating" a direction to a different origin is **meaningless**
//!
//! Therefore, `Direction<F>` has no center parameter, only a frame `F`.
//!
//! ## Storage
//!
//! A spherical direction stores only the two angles (polar, azimuth). The implicit
//! radius is always 1. This contrasts with [`Position`](super::Position), which
//! stores a radial distance in addition to the angles.
//!
//! ## Angle Convention
//!
//! - **polar (θ)**: Latitude / declination / altitude — range `[-90°, +90°]`
//! - **azimuth (φ)**: Longitude / right ascension / azimuth — normalized to `[0°, 360°)`
//!
//! Constructors canonicalize angles to these ranges.
//!
//! ## Example
//!
//! ```rust
//! use affn::spherical::Direction;
//! use affn::frames::ReferenceFrame;
//! use qtty::units::*; use qtty::{Quantity, M, KM, DEG, RAD, SEC}; use qtty::angular::{Degrees, Radians}; use qtty::length::{Meters, Kilometers};
//!
//! #[derive(Debug, Copy, Clone)]
//! struct WorldFrame;
//! impl ReferenceFrame for WorldFrame {
//!     fn frame_name() -> &'static str { "WorldFrame" }
//! }
//!
//! // Create a spherical direction
//! let dir = Direction::<WorldFrame>::new_unchecked(45.0 * DEG, 30.0 * DEG);
//! ```

use crate::centers::ReferenceCenter;
use crate::frames::ReferenceFrame;
use qtty::angular::Degrees;
use qtty::length::LengthUnit;
use qtty::Quantity;

use std::marker::PhantomData;

// Serde implementations in separate module
#[cfg(feature = "serde")]
#[path = "direction_serde.rs"]
mod direction_serde;

/// A spherical direction (unit vector) in a specific reference frame.
///
/// Directions are frame-dependent but center-independent (free vectors).
/// They cannot undergo center transformations, only frame transformations.
///
/// The direction stores only angles; the implicit radius is always 1.
/// For a spherical coordinate with explicit distance, use [`Position`](super::Position).
///
/// # Type Parameters
/// - `F`: The reference frame (defines axis orientation)
///
/// # Invariants
///
/// - `polar` is in `[-90°, +90°]`
/// - `azimuth` is in `[0°, 360°)`
#[derive(Debug, Clone, Copy)]
pub struct Direction<F: ReferenceFrame> {
    /// Polar angle (θ) - latitude, declination, or altitude, in degrees.
    /// Range: `[-90°, +90°]`
    pub polar: Degrees,
    /// Azimuthal angle (φ) - longitude, right ascension, or azimuth, in degrees.
    /// Range: `[0°, 360°)`
    pub azimuth: Degrees,
    _frame: PhantomData<F>,
}

impl<F: ReferenceFrame> Direction<F> {
    /// Creates a direction from raw angle values without canonicalization.
    ///
    /// The caller must ensure angles are within canonical ranges:
    /// - `polar` in `[-90°, +90°]`
    /// - `azimuth` in `[0°, 360°)`
    ///
    /// For frame-specific constructors that canonicalize automatically, see the
    /// `new` method generated by `#[derive(ReferenceFrame)]` for spherical frames
    /// (e.g., `Direction::<ICRS>::new(ra, dec)`).
    pub const fn new_unchecked(polar: Degrees, azimuth: Degrees) -> Self {
        Self {
            polar,
            azimuth,
            _frame: PhantomData,
        }
    }

    /// Promotes this direction to a full Position with the supplied radial magnitude.
    ///
    /// # Type Parameters
    /// - `C`: The center for the resulting position
    /// - `U`: The length unit for the magnitude
    ///
    /// # Example
    /// ```rust
    /// use affn::spherical::Direction;
    /// use affn::frames::ReferenceFrame;
    /// use affn::centers::ReferenceCenter;
    /// use qtty::units::*; use qtty::{Quantity, M, KM, DEG, RAD, SEC}; use qtty::angular::{Degrees, Radians}; use qtty::length::{Meters, Kilometers};
    ///
    /// #[derive(Debug, Copy, Clone)]
    /// struct WorldFrame;
    /// impl ReferenceFrame for WorldFrame {
    ///     fn frame_name() -> &'static str { "WorldFrame" }
    /// }
    ///
    /// #[derive(Debug, Copy, Clone)]
    /// struct WorldOrigin;
    /// impl ReferenceCenter for WorldOrigin {
    ///     type Params = ();
    ///     fn center_name() -> &'static str { "WorldOrigin" }
    /// }
    ///
    /// let dir = Direction::<WorldFrame>::new_unchecked(0.0*DEG, 0.0*DEG);
    /// let pos = dir.position::<WorldOrigin, Meter>(1.0*M);
    /// assert_eq!(pos.distance.value(), 1.0);
    /// ```
    #[must_use]
    pub fn position<C, U>(&self, magnitude: Quantity<U>) -> super::Position<C, F, U>
    where
        C: ReferenceCenter<Params = ()>,
        U: LengthUnit,
    {
        super::Position::new_unchecked(self.polar, self.azimuth, magnitude)
    }

    /// Promotes this direction to a Position with explicit center parameters.
    #[must_use]
    pub fn position_with_params<C, U>(
        &self,
        center_params: C::Params,
        magnitude: Quantity<U>,
    ) -> super::Position<C, F, U>
    where
        C: ReferenceCenter,
        U: LengthUnit,
    {
        super::Position::new_unchecked_with_params(
            center_params,
            self.polar,
            self.azimuth,
            magnitude,
        )
    }

    /// Calculates the angular separation between this direction and another
    /// using the Vincenty formula (numerically stable).
    pub fn angular_separation(&self, other: &Self) -> Degrees {
        super::angular_separation_impl(self.polar, self.azimuth, other.polar, other.azimuth)
    }

    /// Converts to cartesian Direction.
    pub fn to_cartesian(&self) -> crate::cartesian::Direction<F>
    where
        F: ReferenceFrame,
    {
        use qtty::units::Radian;

        let polar_rad = self.polar.to::<Radian>();
        let azimuth_rad = self.azimuth.to::<Radian>();

        let x = azimuth_rad.cos() * polar_rad.cos();
        let y = azimuth_rad.sin() * polar_rad.cos();
        let z = polar_rad.sin();

        crate::cartesian::Direction::<F>::new(x, y, z)
    }

    /// Constructs a spherical direction from a cartesian Direction.
    ///
    /// The resulting angles are canonicalized:
    /// - `polar` in `[-90°, +90°]`
    /// - `azimuth` in `[0°, 360°)`
    pub fn from_cartesian(cart: &crate::cartesian::Direction<F>) -> Self
    where
        F: ReferenceFrame,
    {
        let (polar, azimuth) = super::xyz_to_polar_azimuth(cart.x(), cart.y(), cart.z());
        Self::new_unchecked(polar, azimuth)
    }
}

impl_quantity_fmt_triplet! {
    impl[F] for Direction<F>
    where { F: ReferenceFrame, },
    fmt_each: {},
    |this, f, FmtOne| {
        let (polar_name, azimuth_name, _) =
            F::spherical_names().unwrap_or(("\u{03b8}", "\u{03c6}", "r"));
        write!(f, "Frame: {}, {}: ", F::frame_name(), polar_name)?;
        FmtOne::fmt(&this.polar, f)?;
        write!(f, ", {}: ", azimuth_name)?;
        FmtOne::fmt(&this.azimuth, f)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    // Import the derives and traits
    use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};
    #[allow(unused_imports)]
    use qtty::angular::{Degrees, Radians};
    #[allow(unused_imports)]
    use qtty::length::{Kilometers, Meters};
    use qtty::units::Meter;
    use qtty::Quantity;
    use qtty::{DEG, M};
    // Define test-specific frame and center
    #[derive(Debug, Copy, Clone, ReferenceFrame)]
    struct TestFrame;
    #[derive(Debug, Copy, Clone, ReferenceCenter)]
    struct TestCenter;

    #[derive(Clone, Debug, Default, PartialEq)]
    struct TestParams {
        id: i32,
    }

    #[derive(Debug, Copy, Clone, ReferenceCenter)]
    #[center(params = TestParams)]
    struct ParamCenter;

    #[test]
    fn creates_valid_spherical_direction() {
        let polar = Degrees::new(45.0);
        let azimuth = Degrees::new(90.0);

        let coord = Direction::<TestFrame>::new_unchecked(polar, azimuth);

        assert_eq!(coord.azimuth.value(), 90.0);
        assert_eq!(coord.polar.value(), 45.0);
    }

    #[test]
    fn displays_coordinate_as_string_correctly() {
        let coord = Direction::<TestFrame>::new_unchecked(Degrees::new(30.0), Degrees::new(60.0));
        let output = coord.to_string();
        assert!(output.contains("θ: 30"), "Missing polar angle");
        assert!(output.contains("φ: 60"), "Missing azimuth");
    }

    #[test]
    fn display_respects_format_specifiers() {
        let coord = Direction::<TestFrame>::new_unchecked(Degrees::new(30.0), Degrees::new(60.0));

        let output_prec = format!("{:.2}", coord);
        let expected_polar_prec = format!("{:.2}", coord.polar);
        assert!(output_prec.contains(&format!("θ: {expected_polar_prec}")));

        let output_exp = format!("{:.3e}", coord);
        let expected_az_exp = format!("{:.3e}", coord.azimuth);
        assert!(output_exp.contains(&format!("φ: {expected_az_exp}")));
    }

    #[test]
    fn maintains_high_precision_on_values() {
        let polar = Degrees::new(45.123_456);
        let azimuth = Degrees::new(90.654_321);

        let coord = Direction::<TestFrame>::new_unchecked(polar, azimuth);

        assert!((coord.polar.value() - 45.123_456).abs() < 1e-6);
        assert!((coord.azimuth.value() - 90.654_321).abs() < 1e-6);
    }

    const EPS: f64 = 1e-6;

    #[test]
    fn position_method_promotes_with_given_radius() {
        let dir = Direction::<TestFrame>::new_unchecked(Degrees::new(-30.0), Degrees::new(120.0));
        let pos = dir.position::<TestCenter, Meter>(Quantity::<Meter>::new(2.0));

        // angles are preserved
        assert!(
            (pos.azimuth.value() - 120.0).abs() < EPS,
            "azimuth mismatch: got {}",
            pos.azimuth.value()
        );
        assert!(
            (pos.polar.value() - (-30.0)).abs() < EPS,
            "polar mismatch: got {}",
            pos.polar.value()
        );

        // distance matches the supplied magnitude
        assert!((pos.distance - 2.0 * M).abs() < EPS * M);
    }

    #[test]
    fn angular_separation_identity() {
        let a = Direction::<TestFrame>::new_unchecked(Degrees::new(45.0), Degrees::new(30.0));
        let sep = a.angular_separation(&a);
        assert!(sep.abs().value() < 1e-10, "expected 0°, got {}", sep);
    }

    // =============================================================================
    // Canonicalization Tests
    // =============================================================================

    #[test]
    fn canonicalizes_azimuth_to_positive_range() {
        use crate::spherical::canonicalize_azimuth;
        assert!((canonicalize_azimuth(Degrees::new(-90.0)).value() - 270.0).abs() < EPS);
        assert!((canonicalize_azimuth(Degrees::new(450.0)).value() - 90.0).abs() < EPS);
        assert!(canonicalize_azimuth(Degrees::new(-720.0)).value().abs() < EPS);
    }

    #[test]
    fn folds_polar_to_valid_range() {
        use crate::spherical::canonicalize_polar;
        assert!((canonicalize_polar(Degrees::new(100.0)).value() - 80.0).abs() < EPS);
        assert!((canonicalize_polar(Degrees::new(-100.0)).value() - (-80.0)).abs() < EPS);
    }

    #[test]
    fn canonicalizes_angle_pair_across_poles() {
        use crate::spherical::canonicalize_polar_azimuth;

        let (polar, azimuth) = canonicalize_polar_azimuth(100.0 * DEG, 30.0 * DEG);
        assert!((polar.value() - 80.0).abs() < EPS);
        assert!((azimuth.value() - 210.0).abs() < EPS);

        let (polar, azimuth) = canonicalize_polar_azimuth(-100.0 * DEG, 30.0 * DEG);
        assert!((polar.value() + 80.0).abs() < EPS);
        assert!((azimuth.value() - 210.0).abs() < EPS);
    }

    // =============================================================================
    // Roundtrip Tests
    // =============================================================================

    #[test]
    fn roundtrip_spherical_cartesian_direction() {
        let original =
            Direction::<TestFrame>::new_unchecked(Degrees::new(45.0), Degrees::new(30.0));
        let cartesian = original.to_cartesian();
        let recovered = Direction::from_cartesian(&cartesian);

        assert!(
            (recovered.polar.value() - original.polar.value()).abs() < EPS,
            "polar mismatch: {} vs {}",
            recovered.polar.value(),
            original.polar.value()
        );
        assert!(
            (recovered.azimuth.value() - original.azimuth.value()).abs() < EPS,
            "azimuth mismatch: {} vs {}",
            recovered.azimuth.value(),
            original.azimuth.value()
        );
    }

    #[test]
    fn roundtrip_at_poles() {
        // North pole
        let north = Direction::<TestFrame>::new_unchecked(Degrees::new(90.0), Degrees::new(0.0));
        let cart_n = north.to_cartesian();
        let recovered_n = Direction::from_cartesian(&cart_n);
        assert!((recovered_n.polar.value() - 90.0).abs() < EPS);

        // South pole
        let south = Direction::<TestFrame>::new_unchecked(Degrees::new(-90.0), Degrees::new(0.0));
        let cart_s = south.to_cartesian();
        let recovered_s = Direction::from_cartesian(&cart_s);
        assert!((recovered_s.polar.value() - (-90.0)).abs() < EPS);
    }

    #[test]
    fn roundtrip_at_azimuth_boundaries() {
        // At azimuth = 0
        let dir0 = Direction::<TestFrame>::new_unchecked(Degrees::new(30.0), Degrees::new(0.0));
        let cart0 = dir0.to_cartesian();
        let rec0 = Direction::from_cartesian(&cart0);
        assert!((rec0.azimuth.value() - 0.0).abs() < EPS);

        // Near azimuth = 360 (should wrap to near 0)
        let dir360 = Direction::<TestFrame>::new_unchecked(Degrees::new(30.0), Degrees::new(359.9));
        let cart360 = dir360.to_cartesian();
        let rec360 = Direction::from_cartesian(&cart360);
        assert!((rec360.azimuth.value() - 359.9).abs() < EPS);
    }

    #[test]
    fn uses_raw_construction() {
        let dir = Direction::<TestFrame>::new_unchecked(Degrees::new(10.0), Degrees::new(20.0));
        assert!((dir.polar.value() - 10.0).abs() < EPS);
        assert!((dir.azimuth.value() - 20.0).abs() < EPS);
    }

    #[test]
    fn position_with_params_preserves_center() {
        let dir = Direction::<TestFrame>::new_unchecked(Degrees::new(15.0), Degrees::new(25.0));
        let params = TestParams { id: 7 };
        let pos = dir.position_with_params::<ParamCenter, Meter>(params.clone(), 2.5 * M);
        assert_eq!(pos.center_params(), &params);
        assert!((pos.distance - 2.5 * M).abs() < EPS * M);
    }
}
