use bone_types::{Aabb3, ChordHeightTolerance, Length, Parameter, Point3, Tolerance, Vec3};
use uom::si::length::millimeter;

use crate::KernelError;
use crate::closest::ClosestPoint3;
use crate::curve3::Curve3;

#[derive(Clone, Debug, PartialEq)]
pub struct Polyline3 {
    vertices: Vec<Point3>,
}

impl Polyline3 {
    pub fn new(vertices: Vec<Point3>, tolerance: Tolerance) -> Result<Self, KernelError> {
        if vertices.len() < 2 {
            return Err(KernelError::DegeneratePolyline);
        }
        let has_zero_segment = vertices.windows(2).any(|w| match w {
            [a, b] => (*b - *a).norm_mm() < tolerance.value(),
            _ => false,
        });
        if has_zero_segment {
            return Err(KernelError::DegeneratePolyline);
        }
        Ok(Self { vertices })
    }

    #[must_use]
    pub fn vertices(&self) -> &[Point3] {
        &self.vertices
    }

    #[must_use]
    fn segments(&self) -> u32 {
        u32::try_from(self.vertices.len() - 1).unwrap_or(u32::MAX)
    }
}

impl Curve3 for Polyline3 {
    fn evaluate(&self, t: Parameter) -> Point3 {
        let segments = self.segments();
        let scaled = t.value().clamp(0.0, 1.0) * f64::from(segments);
        self.vertices
            .windows(2)
            .enumerate()
            .find_map(|(index, pair)| {
                let &[from, to] = pair else { return None };
                let lo = f64::from(u32::try_from(index).unwrap_or(u32::MAX));
                let is_last = index + 1 == self.vertices.len() - 1;
                (scaled >= lo && (scaled < lo + 1.0 || is_last))
                    .then(|| from + (to - from) * (scaled - lo))
            })
            .unwrap_or_else(|| unreachable!("Polyline3 holds at least two vertices"))
    }

    fn derivative(&self, t: Parameter) -> Vec3 {
        let segments = self.segments();
        let scaled = t.value().clamp(0.0, 1.0) * f64::from(segments);
        self.vertices
            .windows(2)
            .enumerate()
            .find_map(|(index, pair)| {
                let &[from, to] = pair else { return None };
                let lo = f64::from(u32::try_from(index).unwrap_or(u32::MAX));
                let is_last = index + 1 == self.vertices.len() - 1;
                (scaled >= lo && (scaled < lo + 1.0 || is_last))
                    .then(|| (to - from) * f64::from(segments))
            })
            .unwrap_or_else(|| unreachable!("Polyline3 holds at least two vertices"))
    }

    fn bounding_box(&self) -> Aabb3 {
        Aabb3::from_points(self.vertices.iter().copied())
            .unwrap_or_else(|| unreachable!("Polyline3 holds at least two vertices"))
    }

    fn closest_point(&self, query: Point3, _tolerance: Tolerance) -> ClosestPoint3 {
        let segments = self.segments();
        self.vertices
            .windows(2)
            .enumerate()
            .filter_map(|(index, pair)| {
                let &[from, to] = pair else { return None };
                let edge = to - from;
                let local =
                    ((query - from).dot_mm2(edge) / edge.norm_squared_mm2()).clamp(0.0, 1.0);
                let point = from + edge * local;
                let distance = (query - point).norm_mm();
                let param = (f64::from(u32::try_from(index).unwrap_or(u32::MAX)) + local)
                    / f64::from(segments);
                Some(ClosestPoint3::new(
                    Parameter::new(param),
                    point,
                    Length::new::<millimeter>(distance),
                ))
            })
            .min_by(|left, right| {
                left.distance()
                    .partial_cmp(&right.distance())
                    .unwrap_or(core::cmp::Ordering::Equal)
            })
            .unwrap_or_else(|| unreachable!("Polyline3 holds at least two vertices"))
    }

    fn tessellate(&self, _tolerance: ChordHeightTolerance) -> Vec<Point3> {
        self.vertices.clone()
    }
}

impl core::fmt::Display for Polyline3 {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "polyline3{{ {} verts: ", self.vertices.len())?;
        self.vertices.iter().enumerate().try_for_each(|(i, v)| {
            if i > 0 {
                write!(f, " -> ")?;
            }
            write!(f, "{v}")
        })?;
        write!(f, " }}")
    }
}
