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

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

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Arc3 {
    plane: Plane3,
    radius: Length,
    start_angle: Angle,
    sweep_angle: Angle,
}

impl Arc3 {
    pub fn new(
        plane: Plane3,
        radius: Length,
        start_angle: Angle,
        sweep_angle: Angle,
        tolerance: Tolerance,
    ) -> Result<Self, KernelError> {
        if radius.get::<millimeter>() < tolerance.value() {
            return Err(KernelError::DegenerateArc);
        }
        let sweep = sweep_angle.get::<radian>();
        let angle_eps = AngleTolerance::from_arc_length(tolerance, radius).radians();
        if sweep.abs() < angle_eps {
            return Err(KernelError::DegenerateArc);
        }
        if sweep.abs() > TAU + angle_eps {
            return Err(KernelError::DegenerateArc);
        }
        Ok(Self {
            plane,
            radius,
            start_angle,
            sweep_angle,
        })
    }

    #[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 const fn start_angle(self) -> Angle {
        self.start_angle
    }

    #[must_use]
    pub const fn sweep_angle(self) -> Angle {
        self.sweep_angle
    }

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

    #[must_use]
    pub fn start_rad(self) -> f64 {
        self.start_angle.get::<radian>()
    }

    #[must_use]
    pub fn sweep_rad(self) -> f64 {
        self.sweep_angle.get::<radian>()
    }

    #[must_use]
    pub fn as_full_circle(self) -> Circle3 {
        Circle3::from_raw(self.plane, self.radius)
    }

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

    #[must_use]
    pub fn contains_point(self, p: Point3, tolerance: Tolerance) -> bool {
        let (px, py, pn) = circular3::local_coords(self.plane, p);
        let radial = (px * px + py * py).sqrt();
        if (radial - self.radius_mm()).abs() > tolerance.value() || pn.abs() > tolerance.value() {
            return false;
        }
        let angle_eps = AngleTolerance::from_arc_length(tolerance, self.radius).radians();
        angles::contains(py.atan2(px), self.start_rad(), self.sweep_rad(), angle_eps)
    }

    fn angle_at(self, t: Parameter) -> Angle {
        Angle::new::<radian>(self.start_rad() + self.sweep_rad() * t.value())
    }
}

impl Curve3 for Arc3 {
    fn evaluate(&self, t: Parameter) -> Point3 {
        circular3::point_at(self.plane, self.radius, self.angle_at(t))
    }

    fn derivative(&self, t: Parameter) -> Vec3 {
        circular3::tangent_vec(self.plane, self.radius, self.angle_at(t)) * self.sweep_rad()
    }

    fn bounding_box(&self) -> Aabb3 {
        circular3::bounding_box(self.plane, self.radius, self.start_angle, self.sweep_angle)
    }

    fn closest_point(&self, p: Point3, tolerance: Tolerance) -> ClosestPoint3 {
        let (px, py, _) = circular3::local_coords(self.plane, p);
        let radial = (px * px + py * py).sqrt();
        let theta_unclamped = if radial < tolerance.value() {
            self.start_rad()
        } else {
            py.atan2(px)
        };
        let theta = angles::clamp(theta_unclamped, self.start_rad(), self.sweep_rad(), 0.0);
        let sweep = self.sweep_rad();
        let delta = if sweep >= 0.0 {
            (theta - self.start_rad()).rem_euclid(TAU)
        } else {
            (self.start_rad() - theta).rem_euclid(TAU)
        };
        let t = (delta / sweep.abs()).clamp(0.0, 1.0);
        let point = circular3::point_at(self.plane, self.radius, Angle::new::<radian>(theta));
        ClosestPoint3::new(Parameter::new(t), point, (p - point).norm())
    }

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

impl core::fmt::Display for Arc3 {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "arc3{{ c={}, r={} mm, start={} rad, sweep={} rad, normal={} }}",
            self.center(),
            self.radius_mm(),
            self.start_rad(),
            self.sweep_rad(),
            self.normal(),
        )
    }
}
