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::circular3;
use crate::mesh::{MeshVertex, TriMesh};
use crate::surface3::Surface3;

const MIN_SEGMENTS: u32 = 3;

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

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

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

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

    #[must_use]
    pub fn axis(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 const fn height(self) -> Length {
        self.height
    }

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

    #[must_use]
    pub fn height_mm(self) -> f64 {
        self.height.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]
    fn angle_at(self, u: f64) -> Angle {
        Angle::new::<radian>(self.start_rad() + self.sweep_rad() * u)
    }

    #[must_use]
    fn point_at(self, u: f64, v: f64) -> Point3 {
        let base = circular3::point_at(self.plane, self.radius, self.angle_at(u));
        base + self.plane.normal().into_vec(self.height * v)
    }

    #[must_use]
    pub(crate) fn orientation(self) -> f64 {
        if self.sweep_rad() < 0.0 { -1.0 } else { 1.0 }
    }

    #[must_use]
    fn normal_at(self, u: f64) -> UnitVec3 {
        let theta = self.start_rad() + self.sweep_rad() * u;
        let (xx, xy, xz) = self.plane.x_axis().components();
        let (yx, yy, yz) = self.plane.y_axis().components();
        let (c, s) = (theta.cos(), theta.sin());
        let radial = UnitVec3::new_unchecked(c * xx + s * yx, c * xy + s * yy, c * xz + s * yz);
        if self.orientation() < 0.0 {
            radial.reversed()
        } else {
            radial
        }
    }
}

impl Surface3 for CylinderSurface {
    fn evaluate(&self, u: Parameter, v: Parameter) -> Point3 {
        self.point_at(u.value(), v.value())
    }

    fn partials(&self, u: Parameter, _v: Parameter) -> (Vec3, Vec3) {
        let tangent = circular3::tangent_vec(self.plane, self.radius, self.angle_at(u.value()))
            * self.sweep_rad();
        let axial = self.plane.normal().into_vec(self.height);
        (tangent, axial)
    }

    fn normal(&self, u: Parameter, _v: Parameter) -> UnitVec3 {
        self.normal_at(u.value())
    }

    fn bounding_box(&self) -> Aabb3 {
        let base =
            circular3::bounding_box(self.plane, self.radius, self.start_angle, self.sweep_angle);
        let top_origin = self.plane.origin() + self.plane.normal().into_vec(self.height);
        let top_plane = Plane3::new_unchecked(top_origin, self.plane.x_axis(), self.plane.y_axis());
        let top =
            circular3::bounding_box(top_plane, self.radius, self.start_angle, self.sweep_angle);
        base.union(top)
    }

    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() {
            return false;
        }
        if pn < -tolerance.value() || pn > self.height_mm() + 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 tessellate(&self, chord: ChordHeightTolerance, angle: AngleTolerance) -> TriMesh {
        let n = circular3::sample_count(self.radius, self.sweep_angle, chord, MIN_SEGMENTS).max(
            circular3::angular_sample_count(self.sweep_angle, angle, MIN_SEGMENTS),
        );
        let inv_n = 1.0 / f64::from(n);
        TriMesh::from_grid(n + 1, 2, |i, j| {
            let u = f64::from(i) * inv_n;
            MeshVertex::new(self.point_at(u, f64::from(j)), self.normal_at(u))
        })
    }
}

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