pub use uom::si::angle::{degree, radian};
pub use uom::si::f64::{Angle, Length};
pub use uom::si::length::millimeter;

pub mod dimensioned_serde;
pub mod schema;
pub mod solver;
pub mod space;

pub use schema::{SchemaHeader, SchemaVersion};
pub use solver::{
    BudgetCeiling, DegreesOfFreedom, NewtonDamping, NewtonStepTolerance, Parameter, ParameterIndex,
    ParentIndex, ResidualIndex, SketchItemId, SketchStatus, SolverResidual, SolverSeed,
};
pub use space::{Point2, Point3, SketchPlaneBasis, UnitVec2, UnitVec3, Vec2};

#[cfg(feature = "testing")]
pub mod testing;

#[derive(Debug, thiserror::Error)]
pub enum TypesError {
    #[error("sketch plane axes not orthogonal: |x·y|={0}")]
    NonOrthogonalPlaneAxes(f64),
    #[error("axis vector is zero-length")]
    ZeroLengthAxis,
}

pub type Result<T, E = TypesError> = core::result::Result<T, E>;

slotmap::new_key_type! {
    pub struct FaceId;
    pub struct EdgeId;
    pub struct VertexId;
    pub struct ShellId;
    pub struct SolidId;
    pub struct LoopId;
    pub struct WireId;
    pub struct FeatureId;
    pub struct NodeId;
    pub struct DocumentId;
    pub struct SketchId;
    pub struct SketchEntityId;
    pub struct SketchRelationId;
    pub struct SketchDimensionId;
    pub struct SketchParameterId;
}

impl SketchId {
    /// Encodes this id as an opaque `u64`, stable for the same slotmap slot+version
    /// within a single process. Use only for deterministic widget-key derivation; the
    /// representation is not portable across builds or persisted artifacts.
    #[must_use]
    pub fn as_u64(self) -> u64 {
        use slotmap::Key;
        self.data().as_ffi()
    }
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Tolerance(f64);

impl Tolerance {
    #[must_use]
    pub const fn new(value: f64) -> Self {
        Self(value)
    }

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

impl core::fmt::Display for Tolerance {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "tol={}", self.0)
    }
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct AngleTolerance(f64);

impl AngleTolerance {
    pub const ZERO: Self = Self(0.0);

    #[must_use]
    pub const fn from_radians(value: f64) -> Self {
        Self(value)
    }

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

    #[must_use]
    pub fn from_arc_length(linear: Tolerance, radius: Length) -> Self {
        let r = radius.get::<millimeter>().abs();
        if r > linear.value() {
            Self(linear.value() / r)
        } else {
            Self(linear.value())
        }
    }
}

impl core::fmt::Display for AngleTolerance {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "tol_angle={} rad", self.0)
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Angle, AngleTolerance, DegreesOfFreedom, DocumentId, EdgeId, FaceId, FeatureId, Length,
        LoopId, NodeId, Parameter, Point2, Point3, ShellId, SketchDimensionId, SketchEntityId,
        SketchId, SketchParameterId, SketchPlaneBasis, SketchRelationId, SolidId, SolverResidual,
        Tolerance, UnitVec2, UnitVec3, Vec2, VertexId, WireId, degree, millimeter, radian,
    };
    use slotmap::Key;

    #[test]
    fn id_null_is_null() {
        assert!(FaceId::null().is_null());
        assert!(EdgeId::null().is_null());
        assert!(VertexId::null().is_null());
        assert!(ShellId::null().is_null());
        assert!(SolidId::null().is_null());
        assert!(LoopId::null().is_null());
        assert!(WireId::null().is_null());
        assert!(FeatureId::null().is_null());
        assert!(NodeId::null().is_null());
        assert!(DocumentId::null().is_null());
        assert!(SketchId::null().is_null());
        assert!(SketchEntityId::null().is_null());
        assert!(SketchRelationId::null().is_null());
        assert!(SketchDimensionId::null().is_null());
        assert!(SketchParameterId::null().is_null());
    }

    #[test]
    fn ids_are_distinct_types() {
        let _f: FaceId = FaceId::null();
        let _e: EdgeId = EdgeId::null();
        let _s: SketchEntityId = SketchEntityId::null();
        let _r: SketchRelationId = SketchRelationId::null();
    }

    #[test]
    fn length_mm_roundtrip() {
        let l = Length::new::<millimeter>(12.5);
        assert!((l.get::<millimeter>() - 12.5).abs() < f64::EPSILON);
    }

    #[test]
    fn angle_deg_converts_to_rad() {
        let a = Angle::new::<degree>(180.0);
        assert!((a.get::<radian>() - core::f64::consts::PI).abs() < 1e-12);
    }

    #[test]
    fn tolerance_display() {
        let t = Tolerance::new(1e-6);
        assert_eq!(format!("{t}"), "tol=0.000001");
        assert!((t.value() - 1e-6).abs() < f64::EPSILON);
    }

    #[test]
    fn angle_tolerance_from_arc_length_scales_by_radius() {
        let linear = Tolerance::new(1e-3);
        let r = Length::new::<millimeter>(10.0);
        let a = AngleTolerance::from_arc_length(linear, r);
        assert!((a.radians() - 1e-4).abs() < 1e-15);
    }

    #[test]
    fn angle_tolerance_degenerate_radius_falls_back_to_linear() {
        let linear = Tolerance::new(1e-3);
        let r = Length::new::<millimeter>(0.0);
        let a = AngleTolerance::from_arc_length(linear, r);
        assert!((a.radians() - 1e-3).abs() < 1e-15);
    }

    #[test]
    fn angle_tolerance_zero_constant() {
        assert!(AngleTolerance::ZERO.radians().abs() < f64::EPSILON);
    }

    #[test]
    fn point2_minus_point2_is_vec2() {
        let a = Point2::from_mm(10.0, 5.0);
        let b = Point2::from_mm(4.0, 2.0);
        let d: Vec2 = a - b;
        assert!((d.x().get::<millimeter>() - 6.0).abs() < 1e-12);
        assert!((d.y().get::<millimeter>() - 3.0).abs() < 1e-12);
    }

    #[test]
    fn point2_plus_vec2_is_point2() {
        let p = Point2::from_mm(1.0, 2.0);
        let v = Vec2::from_mm(10.0, 20.0);
        let q: Point2 = p + v;
        assert!((q.x().get::<millimeter>() - 11.0).abs() < 1e-12);
        assert!((q.y().get::<millimeter>() - 22.0).abs() < 1e-12);
    }

    #[test]
    fn vec2_perp_and_dot() {
        let v = Vec2::from_mm(3.0, 4.0);
        assert!((v.norm_mm() - 5.0).abs() < 1e-12);
        let p = v.perp_ccw();
        assert!((p.x().get::<millimeter>() + 4.0).abs() < 1e-12);
        assert!((p.y().get::<millimeter>() - 3.0).abs() < 1e-12);
        assert!(v.dot_mm2(p).abs() < 1e-12);
        assert!((v.cross_z_mm2(Vec2::from_mm(1.0, 0.0)) + 4.0).abs() < 1e-12);
    }

    #[test]
    fn vec2_try_normalize_rejects_zero() {
        let v = Vec2::zero();
        let r = v.try_normalize(Tolerance::new(1e-9));
        assert!(r.is_err());
    }

    #[test]
    fn vec2_try_normalize_unit() {
        let v = Vec2::from_mm(0.0, 7.0);
        let Ok(u) = v.try_normalize(Tolerance::new(1e-9)) else {
            panic!("nonzero");
        };
        let (ux, uy) = u.components();
        assert!(ux.abs() < 1e-12);
        assert!((uy - 1.0).abs() < 1e-12);
    }

    #[test]
    fn unit_vec2_axes_are_orthogonal() {
        let x = UnitVec2::x_axis();
        let y = UnitVec2::y_axis();
        assert!(x.dot(y).abs() < 1e-15);
        assert_eq!(x.perp_ccw().components(), y.components());
    }

    #[test]
    fn unit_vec2_rejects_zero_axis() {
        let r = UnitVec2::try_from_components(0.0, 0.0, Tolerance::new(1e-9));
        assert!(r.is_err());
    }

    #[test]
    fn unit_vec2_into_vec_scales_by_length() {
        let u = UnitVec2::x_axis();
        let v = u.into_vec(Length::new::<millimeter>(3.0));
        assert!((v.x().get::<millimeter>() - 3.0).abs() < 1e-12);
        assert!(v.y().get::<millimeter>().abs() < 1e-12);
    }

    #[test]
    fn vec2_scalar_and_negation() {
        let v = Vec2::from_mm(3.0, -4.0);
        let w = 2.0 * (-v);
        assert!((w.x().get::<millimeter>() + 6.0).abs() < 1e-12);
        assert!((w.y().get::<millimeter>() - 8.0).abs() < 1e-12);
    }

    #[test]
    fn point3_roundtrip() {
        let p = Point3::from_mm(1.5, 2.5, 3.5);
        assert!((p.x().get::<millimeter>() - 1.5).abs() < 1e-12);
        assert!((p.y().get::<millimeter>() - 2.5).abs() < 1e-12);
        assert!((p.z().get::<millimeter>() - 3.5).abs() < 1e-12);
    }

    #[test]
    fn sketch_plane_basis_accepts_orthogonal_axes() {
        let Ok(basis) = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
            Tolerance::new(1e-9),
        ) else {
            panic!("x and y unit axes are orthogonal");
        };
        let (nx, ny, nz) = basis.normal().components();
        assert!((nx).abs() < 1e-12);
        assert!((ny).abs() < 1e-12);
        assert!((nz - 1.0).abs() < 1e-12);
    }

    #[test]
    fn sketch_plane_basis_reorthonormalizes_within_tolerance() {
        let Ok(nearly) = UnitVec3::try_from_components(1e-10, 1.0, 0.0) else {
            panic!("input vector is nonzero");
        };
        let Ok(basis) = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            nearly,
            Tolerance::new(1e-9),
        ) else {
            panic!("within tolerance");
        };
        let dot_after = basis.x_axis().dot(basis.y_axis()).abs();
        assert!(
            dot_after < 1e-15,
            "re-orthonormalized y should be orthogonal, got {dot_after}"
        );
    }

    #[test]
    fn sketch_plane_basis_rejects_non_orthogonal_axes() {
        let Ok(skew) = UnitVec3::try_from_components(1.0, 1.0, 0.0) else {
            panic!("input vector is nonzero");
        };
        let result = SketchPlaneBasis::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            skew,
            Tolerance::new(1e-9),
        );
        assert!(result.is_err());
    }

    #[test]
    fn unit_vec3_rejects_zero_axis() {
        let r = UnitVec3::try_from_components(0.0, 0.0, 0.0);
        assert!(r.is_err());
    }

    #[test]
    fn parameter_display_and_value() {
        let p = Parameter::new(0.25);
        assert_eq!(format!("{p}"), "param=0.25");
        assert!((p.value() - 0.25).abs() < f64::EPSILON);
    }

    #[test]
    fn degrees_of_freedom_display_and_value() {
        let d = DegreesOfFreedom::new(7);
        assert_eq!(format!("{d}"), "dof=7");
        assert_eq!(d.value(), 7);
    }

    #[test]
    fn solver_residual_display_and_value() {
        let r = SolverResidual::new(1.5e-6);
        assert_eq!(format!("{r}"), "res=0.0000015");
        assert!((r.value() - 1.5e-6).abs() < f64::EPSILON);
    }
}
