//! # Spherical Position
//!
//! This module defines the core spherical [`Position`] type (center + frame + distance).
//!
//! ## Mathematical Model
//!
//! A spherical position represents a point in space using:
//! - **polar (θ)**: Latitude / declination / altitude — range `[-90°, +90°]`
//! - **azimuth (φ)**: Longitude / right ascension / azimuth — range `[0°, 360°)`
//! - **distance (r)**: Radial distance from the origin
//!
//! # Type Parameters
//! - `C`: The reference center (defines the origin)
//! - `F`: The reference frame (defines axis orientation)
//! - `U`: The length unit for the distance
//!
//! ## Example
//!
//! ```rust
//! use affn::spherical::Position;
//! 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 pos = Position::<WorldOrigin, WorldFrame, Meter>::new_unchecked(
//!     45.0 * DEG,  // polar
//!     30.0 * DEG,  // azimuth
//!     1.0 * M      // distance
//! );
//! ```

use crate::centers;
use crate::frames;
use qtty::angular::Degrees;
use qtty::length::LengthUnit;
use qtty::units::Radian;
use qtty::Quantity;

use std::marker::PhantomData;

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

/// A spherical **position** (center + frame + distance).
///
/// This is the fundamental spherical coordinate type.
/// Spherical directions are represented separately by [`super::direction::Direction<F>`]
/// and intentionally have **no** reference center.
///
/// # Type Parameters
/// - `C`: The reference center (defines the origin)
/// - `F`: The reference frame (defines axis orientation)
/// - `U`: The length unit for the distance
///
/// # Note
///
/// `U` must be a [`LengthUnit`], not just any `Unit`. This ensures that spherical
/// positions always represent physical locations with a meaningful distance.
#[derive(Debug, Clone, Copy)]
pub struct Position<C: centers::ReferenceCenter, F: frames::ReferenceFrame, U: LengthUnit> {
    /// Polar angle (θ) - latitude, declination, or altitude, in degrees.
    pub polar: Degrees,
    /// Azimuthal angle (φ) - longitude, right ascension, or azimuth, in degrees.
    pub azimuth: Degrees,
    /// Radial distance from the origin.
    pub distance: Quantity<U>,

    center_params: C::Params,
    _frame: PhantomData<F>,
}

impl<C, F, U> Position<C, F, U>
where
    C: centers::ReferenceCenter,
    F: frames::ReferenceFrame,
    U: LengthUnit,
{
    /// Constructs a new spherical position with explicit center parameters without canonicalization.
    ///
    /// The caller must ensure angles are within canonical ranges and distance is non-negative.
    /// Prefer [`Position::new_with_params`] when inputs may be out of range.
    pub const fn new_unchecked_with_params(
        center_params: C::Params,
        polar: Degrees,
        azimuth: Degrees,
        distance: Quantity<U>,
    ) -> Self {
        Self {
            polar,
            azimuth,
            distance,
            center_params,
            _frame: PhantomData,
        }
    }

    /// Constructs a new spherical position with explicit center parameters, canonicalizing inputs.
    ///
    /// - `polar` is folded into `[-90°, +90°]`.
    /// - `azimuth` is normalized into `[0°, 360°)`.
    /// - negative `distance` is converted to a positive distance at the
    ///   antipodal direction.
    pub fn new_with_params(
        center_params: C::Params,
        polar: Degrees,
        azimuth: Degrees,
        distance: Quantity<U>,
    ) -> Self {
        let (polar, azimuth) = if distance < Quantity::new(0.0) {
            super::canonicalize_polar_azimuth(-polar, azimuth + Degrees::new(180.0))
        } else {
            super::canonicalize_polar_azimuth(polar, azimuth)
        };

        Self::new_unchecked_with_params(center_params, polar, azimuth, distance.abs())
    }

    /// Returns a reference to the center parameters.
    pub fn center_params(&self) -> &C::Params {
        &self.center_params
    }

    /// Calculates the angular separation between this position 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)
    }

    /// Extracts the corresponding spherical **direction** (frame-only).
    #[must_use]
    pub fn direction(&self) -> super::direction::Direction<F> {
        super::direction::Direction::new_unchecked(self.polar, self.azimuth)
    }

    /// Converts to Cartesian position.
    #[must_use]
    pub fn to_cartesian(&self) -> crate::cartesian::Position<C, F, U>
    where
        F: frames::ReferenceFrame,
    {
        let polar_rad = self.polar.to::<Radian>();
        let azimuth_rad = self.azimuth.to::<Radian>();

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

        crate::cartesian::Position::<C, F, U>::new_with_params(self.center_params.clone(), x, y, z)
    }

    /// Constructs from a Cartesian position.
    #[must_use]
    pub fn from_cartesian(cart: &crate::cartesian::Position<C, F, U>) -> Self
    where
        F: frames::ReferenceFrame,
    {
        let x = cart.x().value();
        let y = cart.y().value();
        let z = cart.z().value();
        let r = cart.distance().value();

        let (polar, azimuth) = if r.abs() < f64::EPSILON {
            (Degrees::new(0.0), Degrees::new(0.0))
        } else {
            super::xyz_to_polar_azimuth(x / r, y / r, z / r)
        };

        Self::new_unchecked_with_params(
            cart.center_params().clone(),
            polar,
            azimuth,
            cart.distance(),
        )
    }
}

impl<C, F, U> Position<C, F, U>
where
    C: centers::ReferenceCenter<Params = ()>,
    F: frames::ReferenceFrame,
    U: LengthUnit,
{
    /// Convenience constructor for centers with `Params = ()` without canonicalization.
    ///
    /// The caller must ensure angles are canonical and distance is non-negative.
    /// Frame-specific `new` constructors generated by `#[derive(ReferenceFrame)]`
    /// canonicalize automatically and are preferred where available.
    pub const fn new_unchecked(polar: Degrees, azimuth: Degrees, distance: Quantity<U>) -> Self {
        Self::new_unchecked_with_params((), polar, azimuth, distance)
    }

    /// The *origin* of this coordinate system (all angles 0, radius 0). AKA Null Vector.
    pub const CENTER: Self = Self::new_unchecked(
        Degrees::new(0.0),
        Degrees::new(0.0),
        Quantity::<U>::new(0.0),
    );
}

impl<C, F, U> Position<C, F, U>
where
    C: centers::ReferenceCenter,
    F: frames::ReferenceFrame,
    U: LengthUnit,
{
    /// Euclidean distance to another position **in the same centre & frame**.
    ///
    /// The result is expressed in the *same unit `U`* as the inputs.
    ///
    /// Only available when the center has no runtime parameters (`C::Params = ()`).
    /// For parameterized centers convert to Cartesian and use
    /// [`Position::try_distance_to`](crate::cartesian::Position::try_distance_to).
    #[must_use]
    pub fn distance_to(&self, other: &Self) -> Quantity<U>
    where
        C: centers::ReferenceCenter<Params = ()>,
        F: frames::ReferenceFrame,
    {
        self.to_cartesian().distance_to(&other.to_cartesian())
    }
}

impl_quantity_fmt_triplet! {
    impl[C, F, U] for Position<C, F, U>
    where {
        C: centers::ReferenceCenter,
        F: frames::ReferenceFrame,
        U: LengthUnit,
    },
    fmt_each: { Quantity<U>, },
    |this, f, FmtOne| {
        let (polar_name, azimuth_name, distance_name) =
            F::spherical_names().unwrap_or(("\u{03b8}", "\u{03c6}", "r"));
        write!(
            f,
            "Center: {}, Frame: {}, {}: ",
            C::center_name(),
            F::frame_name(),
            polar_name
        )?;
        FmtOne::fmt(&this.polar, f)?;
        write!(f, ", {}: ", azimuth_name)?;
        FmtOne::fmt(&this.azimuth, f)?;
        write!(f, ", {}: ", distance_name)?;
        FmtOne::fmt(&this.distance, f)
    }
}

// =============================================================================
// Serde Implementation with Frame-Specific Field Names
// =============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    // Import the derives and traits
    use crate::{DeriveReferenceCenter as ReferenceCenter, DeriveReferenceFrame as ReferenceFrame};
    use qtty::units::Meter;
    use qtty::{DEG, M};
    use std::f64::consts::SQRT_2;

    // 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;

    const EPS: f64 = 1e-6;

    #[test]
    fn test_spherical_coord_creation() {
        // new_unchecked(polar, azimuth, distance)
        let coord = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
            Degrees::new(90.0),
            Degrees::new(45.0),
            1.0 * M,
        );
        assert_eq!(coord.polar.value(), 90.0);
        assert_eq!(coord.azimuth.value(), 45.0);
        assert_eq!(coord.distance.value(), 1.0);
    }

    #[test]
    fn test_spherical_coord_to_string() {
        // new_unchecked(polar, azimuth, distance)
        let coord = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
            Degrees::new(60.0),
            Degrees::new(30.0),
            1000.0 * M,
        );
        let coord_string = coord.to_string();
        assert!(coord_string.contains("θ: 60"));
        assert!(coord_string.contains("φ: 30"));
        assert!(coord_string.contains("r: 1000"));
    }

    #[test]
    fn test_spherical_coord_format_specifiers() {
        let coord = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
            60.0 * DEG,
            30.0 * DEG,
            1000.0 * M,
        );

        let text_prec = format!("{:.2}", coord);
        let expected_r_prec = format!("{:.2}", coord.distance);
        assert!(text_prec.contains(&format!("r: {expected_r_prec}")));

        let text_exp = format!("{:.3e}", coord);
        let expected_theta_exp = format!("{:.3e}", coord.polar);
        assert!(text_exp.contains(&format!("θ: {expected_theta_exp}")));
    }

    #[test]
    fn test_spherical_coord_zero_values() {
        let coord =
            Position::<TestCenter, TestFrame, Meter>::new_unchecked(0.0 * DEG, 0.0 * DEG, 0.0 * M);
        assert_eq!(coord.polar.value(), 0.0);
        assert_eq!(coord.azimuth.value(), 0.0);
        assert_eq!(coord.distance.value(), 0.0);
    }

    #[test]
    fn test_spherical_coord_precision() {
        // new_unchecked(polar, azimuth, distance)
        let coord = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
            45.123456 * DEG,
            90.654321 * DEG,
            1234.56789 * M,
        );
        assert!((coord.polar.value() - 45.123456).abs() < 1e-6);
        assert!((coord.azimuth.value() - 90.654321).abs() < 1e-6);
        assert!((coord.distance - 1234.56789 * M).abs() < 1e-6 * M);
    }

    #[test]
    fn new_with_params_canonicalizes_pole_crossing_without_changing_point() {
        let canonical = Position::<TestCenter, TestFrame, Meter>::new_with_params(
            (),
            100.0 * DEG,
            30.0 * DEG,
            1.0 * M,
        );
        let raw = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
            100.0 * DEG,
            30.0 * DEG,
            1.0 * M,
        );

        assert!((canonical.polar.value() - 80.0).abs() < EPS);
        assert!((canonical.azimuth.value() - 210.0).abs() < EPS);

        let a = canonical.to_cartesian();
        let b = raw.to_cartesian();
        assert!((a.x() - b.x()).abs() < EPS * M);
        assert!((a.y() - b.y()).abs() < EPS * M);
        assert!((a.z() - b.z()).abs() < EPS * M);
    }

    #[test]
    fn new_with_params_maps_negative_distance_to_antipodal_direction() {
        let pos = Position::<TestCenter, TestFrame, Meter>::new_with_params(
            (),
            10.0 * DEG,
            20.0 * DEG,
            -2.0 * M,
        );

        assert!((pos.polar.value() + 10.0).abs() < EPS);
        assert!((pos.azimuth.value() - 200.0).abs() < EPS);
        assert!((pos.distance.value() - 2.0).abs() < EPS);
    }

    #[test]
    fn direction_returns_unit_vector() {
        let pos = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
            10.0 * DEG,
            20.0 * DEG,
            2.5 * M,
        );
        let dir = pos.direction();

        // Direction has implicit radius 1
        // We verify by converting to cartesian and checking the magnitude
        let cart = dir.to_cartesian();
        let magnitude = (cart.x().powi(2) + cart.y().powi(2) + cart.z().powi(2)).sqrt();
        assert!(
            (magnitude - 1.0).abs() < EPS,
            "magnitude should be 1.0, got {}",
            magnitude
        );

        // angular components are preserved
        assert!((dir.polar - 10.0 * DEG).abs() < EPS * DEG);
        assert!((dir.azimuth - 20.0 * DEG).abs() < EPS * DEG);
    }

    #[test]
    fn center_constant_is_origin() {
        let c = Position::<TestCenter, TestFrame, Meter>::CENTER;
        assert_eq!(c.polar.value(), 0.0);
        assert_eq!(c.azimuth.value(), 0.0);
        assert_eq!(c.distance.value(), 0.0);
    }

    #[test]
    fn from_degrees_matches_new_raw() {
        let a = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
            45.0 * DEG,
            30.0 * DEG,
            3.0 * M,
        );
        let b = Position::<TestCenter, TestFrame, Meter>::new_unchecked(
            45.0 * DEG,
            30.0 * DEG,
            3.0 * M,
        );
        assert_eq!(a.polar, b.polar);
        assert_eq!(a.azimuth, b.azimuth);
        assert_eq!(a.distance, b.distance);
    }

    #[test]
    fn distance_identity_zero_and_orthogonal() {
        // identity
        let a =
            Position::<TestCenter, TestFrame, Meter>::new_unchecked(0.0 * DEG, 0.0 * DEG, 1.0 * M);
        let d0 = a.distance_to(&a);
        assert!(d0.abs().value() < EPS);

        // orthogonal points on unit sphere → chord length sqrt(2) * r
        let b =
            Position::<TestCenter, TestFrame, Meter>::new_unchecked(0.0 * DEG, 90.0 * DEG, 1.0 * M);
        let d = a.distance_to(&b);
        assert!((d.value() - SQRT_2).abs() < EPS);
    }

    #[test]
    fn position_with_params_and_center_params() {
        let params = TestParams { id: 9 };
        let pos = Position::<ParamCenter, TestFrame, Meter>::new_unchecked_with_params(
            params.clone(),
            5.0 * DEG,
            10.0 * DEG,
            2.0 * M,
        );
        assert_eq!(pos.center_params(), &params);
        assert!((pos.distance.value() - 2.0).abs() < EPS);
    }

    #[test]
    fn angular_separation_quarter_turn() {
        let a =
            Position::<TestCenter, TestFrame, Meter>::new_unchecked(0.0 * DEG, 0.0 * DEG, 1.0 * M);
        let b =
            Position::<TestCenter, TestFrame, Meter>::new_unchecked(0.0 * DEG, 90.0 * DEG, 1.0 * M);
        let sep = a.angular_separation(b);
        assert!((sep.value() - 90.0).abs() < EPS);
    }

    #[test]
    fn cartesian_roundtrip_and_zero_radius() {
        type CartPos = crate::cartesian::Position<TestCenter, TestFrame, Meter>;

        let cart = CartPos::new(1.0, -1.0, 0.0);
        let sph = Position::from_cartesian(&cart);
        assert!((sph.polar.value()).abs() < EPS);
        assert!((sph.azimuth.value() - 315.0).abs() < EPS);

        let back = sph.to_cartesian();
        assert!((back.x().value() - cart.x().value()).abs() < EPS);
        assert!((back.y().value() - cart.y().value()).abs() < EPS);

        let origin = CartPos::new(0.0, 0.0, 0.0);
        let sph_origin = Position::from_cartesian(&origin);
        assert!((sph_origin.polar.value()).abs() < EPS);
        assert!((sph_origin.azimuth.value()).abs() < EPS);
        assert!(sph_origin.distance.value().abs() < EPS);
    }
}
