use bone_types::dimensioned_serde;
use bone_types::{
    Aabb3, Angle, ChordHeightTolerance, Length, Parameter, Plane3, Point3, Tolerance, UnitVec3,
    Vec3,
};
use core::f64::consts::TAU;
use serde::{Deserialize, Serialize};
use uom::si::angle::radian;
use uom::si::length::millimeter;

use crate::KernelError;
use crate::circular3;
use crate::closest::ClosestPoint3;
use crate::curve3::{Curve3, Curve3Kind};

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Circle3 {
    plane: Plane3,
    #[serde(with = "dimensioned_serde::length_si")]
    radius: Length,
}

impl Circle3 {
    pub fn new(plane: Plane3, radius: Length, tolerance: Tolerance) -> Result<Self, KernelError> {
        if radius.get::<millimeter>() < tolerance.value() {
            return Err(KernelError::DegenerateCircle);
        }
        Ok(Self { plane, radius })
    }

    #[must_use]
    pub(crate) const fn from_raw(plane: Plane3, radius: Length) -> Self {
        Self { plane, radius }
    }

    #[must_use]
    pub fn center(self) -> Point3 {
        self.plane.origin()
    }

    #[must_use]
    pub const fn plane(self) -> Plane3 {
        self.plane
    }

    #[must_use]
    pub fn normal(self) -> UnitVec3 {
        self.plane.normal()
    }

    #[must_use]
    pub const fn radius(self) -> Length {
        self.radius
    }

    #[must_use]
    pub fn radius_mm(self) -> f64 {
        self.radius.get::<millimeter>()
    }

    #[must_use]
    pub fn as_kind(self) -> Curve3Kind {
        Curve3Kind::Circle(self)
    }
}

impl Curve3 for Circle3 {
    fn evaluate(&self, t: Parameter) -> Point3 {
        circular3::point_at(
            self.plane,
            self.radius,
            Angle::new::<radian>(TAU * t.value()),
        )
    }

    fn derivative(&self, t: Parameter) -> Vec3 {
        circular3::tangent_vec(
            self.plane,
            self.radius,
            Angle::new::<radian>(TAU * t.value()),
        ) * TAU
    }

    fn bounding_box(&self) -> Aabb3 {
        circular3::bounding_box(
            self.plane,
            self.radius,
            Angle::new::<radian>(0.0),
            Angle::new::<radian>(TAU),
        )
    }

    fn closest_point(&self, p: Point3, tolerance: Tolerance) -> ClosestPoint3 {
        let (px, py, pn) = circular3::local_coords(self.plane, p);
        let radial = (px * px + py * py).sqrt();
        let r = self.radius_mm();
        let theta = if radial < tolerance.value() {
            0.0
        } else {
            py.atan2(px)
        };
        let point = circular3::point_at(self.plane, self.radius, Angle::new::<radian>(theta));
        let t = (theta / TAU).rem_euclid(1.0);
        let distance = Length::new::<millimeter>(((radial - r).powi(2) + pn * pn).sqrt());
        ClosestPoint3::new(Parameter::new(t), point, distance)
    }

    fn tessellate(&self, tolerance: ChordHeightTolerance) -> Vec<Point3> {
        let n = circular3::sample_count(self.radius, Angle::new::<radian>(TAU), tolerance, 3);
        (0..n)
            .map(|i| {
                circular3::point_at(
                    self.plane,
                    self.radius,
                    Angle::new::<radian>(TAU * f64::from(i) / f64::from(n)),
                )
            })
            .collect()
    }
}

impl core::fmt::Display for Circle3 {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "circle3{{ c={}, r={} mm, normal={} }}",
            self.center(),
            self.radius_mm(),
            self.normal(),
        )
    }
}
