use bone_types::{Length, Parameter, Point2, Tolerance, UnitVec2, Vec2};
use core::f64::consts::TAU;
use uom::si::length::millimeter;

use crate::KernelError;
use crate::aabb::Aabb2;
use crate::closest::ClosestPoint2;
use crate::curvature::Curvature;
use crate::curve2::{Curve2, Curve2Kind};

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Circle2 {
    center: Point2,
    radius: Length,
}

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

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

    #[must_use]
    pub const fn center(self) -> Point2 {
        self.center
    }

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

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

impl Curve2 for Circle2 {
    fn evaluate(&self, t: Parameter) -> Point2 {
        let (cx, cy) = self.center.coords_mm();
        let r = self.radius_mm();
        let theta = TAU * t.value();
        Point2::from_mm(cx + r * theta.cos(), cy + r * theta.sin())
    }

    fn derivative(&self, t: Parameter) -> Vec2 {
        let r = self.radius_mm();
        let theta = TAU * t.value();
        Vec2::from_mm(-r * TAU * theta.sin(), r * TAU * theta.cos())
    }

    fn tangent(&self, t: Parameter) -> UnitVec2 {
        let theta = TAU * t.value();
        UnitVec2::new_unchecked(-theta.sin(), theta.cos())
    }

    fn curvature(&self, _t: Parameter) -> Curvature {
        Curvature::from_radius(self.radius)
    }

    fn bounding_box(&self) -> Aabb2 {
        let (cx, cy) = self.center.coords_mm();
        let r = self.radius_mm();
        Aabb2::from_corners(
            Point2::from_mm(cx - r, cy - r),
            Point2::from_mm(cx + r, cy + r),
        )
    }

    fn closest_point(&self, p: Point2, tolerance: Tolerance) -> ClosestPoint2 {
        let (cx, cy) = self.center.coords_mm();
        let (px, py) = p.coords_mm();
        let dx = px - cx;
        let dy = py - cy;
        let dist = (dx * dx + dy * dy).sqrt();
        let r = self.radius_mm();
        if dist < tolerance.value() {
            let projected = Point2::from_mm(cx + r, cy);
            let distance = Length::new::<millimeter>(r);
            return ClosestPoint2::new(Parameter::new(0.0), projected, distance);
        }
        let theta = dy.atan2(dx);
        let t_raw = theta / TAU;
        let t = t_raw.rem_euclid(1.0);
        let projected = Point2::from_mm(cx + r * theta.cos(), cy + r * theta.sin());
        let distance = Length::new::<millimeter>((dist - r).abs());
        ClosestPoint2::new(Parameter::new(t), projected, distance)
    }

    fn as_kind(&self) -> Curve2Kind {
        Curve2Kind::Circle(*self)
    }
}

impl core::fmt::Display for Circle2 {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "circle2{{ c={}, r={} mm }}",
            self.center,
            self.radius.get::<millimeter>(),
        )
    }
}
