use bone_types::{Length, Parameter, Point2, Tolerance, UnitVec2, Vec2};
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 Line2 {
    start: Point2,
    end: Point2,
}

impl Line2 {
    pub fn new(start: Point2, end: Point2, tolerance: Tolerance) -> Result<Self, KernelError> {
        if (end - start).norm_mm() < tolerance.value() {
            return Err(KernelError::DegenerateLine);
        }
        Ok(Self { start, end })
    }

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

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

    #[must_use]
    pub fn length(self) -> Length {
        (self.end - self.start).norm()
    }

    #[must_use]
    pub fn length_mm(self) -> f64 {
        (self.end - self.start).norm_mm()
    }

    #[must_use]
    pub fn unit_direction(self) -> UnitVec2 {
        let (dx, dy) = (self.end - self.start).coords_mm();
        let len = (dx * dx + dy * dy).sqrt();
        UnitVec2::new_unchecked(dx / len, dy / len)
    }
}

impl Curve2 for Line2 {
    fn evaluate(&self, t: Parameter) -> Point2 {
        let (sx, sy) = self.start.coords_mm();
        let (ex, ey) = self.end.coords_mm();
        let u = t.value();
        Point2::from_mm(sx + u * (ex - sx), sy + u * (ey - sy))
    }

    fn derivative(&self, _t: Parameter) -> Vec2 {
        self.end - self.start
    }

    fn tangent(&self, _t: Parameter) -> UnitVec2 {
        self.unit_direction()
    }

    fn curvature(&self, _t: Parameter) -> Curvature {
        Curvature::ZERO
    }

    fn bounding_box(&self) -> Aabb2 {
        Aabb2::from_corners(self.start, self.end)
    }

    fn closest_point(&self, p: Point2, _tolerance: Tolerance) -> ClosestPoint2 {
        let d = self.end - self.start;
        let v = p - self.start;
        let len_sq = d.norm_squared_mm2();
        let t = (v.dot_mm2(d) / len_sq).clamp(0.0, 1.0);
        let point = self.evaluate(Parameter::new(t));
        let dist_mm = (p - point).norm_mm();
        ClosestPoint2::new(Parameter::new(t), point, Length::new::<millimeter>(dist_mm))
    }

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

impl core::fmt::Display for Line2 {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "line2{{ {} -> {} }}", self.start, self.end)
    }
}
