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

const MIN_SEGMENTS: u32 = 3;

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct TorusSurface {
    plane: Plane3,
    major_radius: Length,
    minor_radius: Length,
    major_start: Angle,
    major_sweep: Angle,
    minor_start: Angle,
    minor_sweep: Angle,
}

impl TorusSurface {
    pub fn new(
        plane: Plane3,
        major_radius: Length,
        minor_radius: Length,
        major: AngularSpan,
        minor: AngularSpan,
        tolerance: Tolerance,
    ) -> Result<Self, KernelError> {
        let major_start = major.start();
        let major_sweep = major.sweep();
        let minor_start = minor.start();
        let minor_sweep = minor.sweep();
        let major = major_radius.get::<millimeter>();
        let minor = minor_radius.get::<millimeter>();
        if !major.is_finite() || !minor.is_finite() {
            return Err(KernelError::DegenerateTorus);
        }
        if major < tolerance.value() || minor < tolerance.value() {
            return Err(KernelError::DegenerateTorus);
        }
        if minor >= major - tolerance.value() {
            return Err(KernelError::DegenerateTorus);
        }
        let major_eps = AngleTolerance::from_arc_length(tolerance, major_radius).radians();
        let minor_eps = AngleTolerance::from_arc_length(tolerance, minor_radius).radians();
        let major_span = major_sweep.get::<radian>().abs();
        let minor_span = minor_sweep.get::<radian>().abs();
        if !major_span.is_finite() || major_span < major_eps || major_span > TAU + major_eps {
            return Err(KernelError::DegenerateTorus);
        }
        if !minor_span.is_finite() || minor_span < minor_eps || minor_span > TAU + minor_eps {
            return Err(KernelError::DegenerateTorus);
        }
        Ok(Self {
            plane,
            major_radius,
            minor_radius,
            major_start,
            major_sweep,
            minor_start,
            minor_sweep,
        })
    }

    #[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 major_radius(self) -> Length {
        self.major_radius
    }

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

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

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

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

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

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

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

    #[must_use]
    fn major_at(self, u: f64) -> f64 {
        self.major_start.get::<radian>() + self.major_sweep.get::<radian>() * u
    }

    #[must_use]
    fn minor_at(self, v: f64) -> f64 {
        self.minor_start.get::<radian>() + self.minor_sweep.get::<radian>() * v
    }

    #[must_use]
    pub(crate) fn orientation(self) -> f64 {
        let product = self.major_sweep.get::<radian>() * self.minor_sweep.get::<radian>();
        if product < 0.0 { -1.0 } else { 1.0 }
    }

    #[must_use]
    fn equatorial(self, theta: f64) -> (f64, f64, f64) {
        let (xx, xy, xz) = self.plane.x_axis().components();
        let (yx, yy, yz) = self.plane.y_axis().components();
        let (ct, st) = (theta.cos(), theta.sin());
        (ct * xx + st * yx, ct * xy + st * yy, ct * xz + st * yz)
    }

    #[must_use]
    fn point_at(self, u: f64, v: f64) -> Point3 {
        let theta = self.major_at(u);
        let psi = self.minor_at(v);
        let (ex, ey, ez) = self.equatorial(theta);
        let (ax, ay, az) = self.plane.normal().components();
        let ring = self.major_radius_mm() + self.minor_radius_mm() * psi.cos();
        let lift = self.minor_radius_mm() * psi.sin();
        let (cx, cy, cz) = self.center().coords_mm();
        Point3::from_mm(
            cx + ring * ex + lift * ax,
            cy + ring * ey + lift * ay,
            cz + ring * ez + lift * az,
        )
    }

    #[must_use]
    fn normal_at(self, u: f64, v: f64) -> UnitVec3 {
        let theta = self.major_at(u);
        let psi = self.minor_at(v);
        let (ex, ey, ez) = self.equatorial(theta);
        let (ax, ay, az) = self.plane.normal().components();
        let (cp, sp) = (psi.cos(), psi.sin());
        let s = self.orientation();
        UnitVec3::new_unchecked(
            s * (cp * ex + sp * ax),
            s * (cp * ey + sp * ay),
            s * (cp * ez + sp * az),
        )
    }
}

impl Surface3 for TorusSurface {
    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 theta = self.major_at(u.value());
        let psi = self.minor_at(v.value());
        let (xx, xy, xz) = self.plane.x_axis().components();
        let (yx, yy, yz) = self.plane.y_axis().components();
        let (ex, ey, ez) = self.equatorial(theta);
        let (ax, ay, az) = self.plane.normal().components();
        let (ct, st) = (theta.cos(), theta.sin());
        let tangent_x = -st * xx + ct * yx;
        let tangent_y = -st * xy + ct * yy;
        let tangent_z = -st * xz + ct * yz;
        let ring = self.major_radius_mm() + self.minor_radius_mm() * psi.cos();
        let u_scale = self.major_sweep.get::<radian>() * ring;
        let du = Vec3::from_mm(
            u_scale * tangent_x,
            u_scale * tangent_y,
            u_scale * tangent_z,
        );
        let v_scale = self.minor_sweep.get::<radian>() * self.minor_radius_mm();
        let (cp, sp) = (psi.cos(), psi.sin());
        let dv = Vec3::from_mm(
            v_scale * (-sp * ex + cp * ax),
            v_scale * (-sp * ey + cp * ay),
            v_scale * (-sp * ez + cp * az),
        );
        (du, dv)
    }

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

    fn bounding_box(&self) -> Aabb3 {
        let (xx, xy, xz) = self.plane.x_axis().components();
        let (yx, yy, yz) = self.plane.y_axis().components();
        let (ax, ay, az) = self.plane.normal().components();
        let reach = self.major_radius_mm() + self.minor_radius_mm();
        let minor = self.minor_radius_mm();
        let half = Vec3::from_mm(
            reach * xx.hypot(yx) + minor * ax.abs(),
            reach * xy.hypot(yy) + minor * ay.abs(),
            reach * xz.hypot(yz) + minor * az.abs(),
        );
        Aabb3::from_corners(self.center() - half, self.center() + half)
    }

    fn contains_point(&self, p: Point3, tolerance: Tolerance) -> bool {
        let (px, py, pn) = circular3::local_coords(self.plane, p);
        let equatorial = (px * px + py * py).sqrt();
        let from_ring = equatorial - self.major_radius_mm();
        let tube = (from_ring * from_ring + pn * pn).sqrt();
        if (tube - self.minor_radius_mm()).abs() > tolerance.value() {
            return false;
        }
        let minor_eps = AngleTolerance::from_arc_length(tolerance, self.minor_radius).radians();
        if !angles::contains(
            pn.atan2(from_ring),
            self.minor_start.get::<radian>(),
            self.minor_sweep.get::<radian>(),
            minor_eps,
        ) {
            return false;
        }
        if equatorial < tolerance.value() {
            return true;
        }
        let major_eps =
            AngleTolerance::from_arc_length(tolerance, Length::new::<millimeter>(equatorial))
                .radians();
        angles::contains(
            py.atan2(px),
            self.major_start.get::<radian>(),
            self.major_sweep.get::<radian>(),
            major_eps,
        )
    }

    fn tessellate(&self, chord: ChordHeightTolerance, angle: AngleTolerance) -> TriMesh {
        let reach = Length::new::<millimeter>(self.major_radius_mm() + self.minor_radius_mm());
        let major = circular3::sample_count(reach, self.major_sweep, chord, MIN_SEGMENTS).max(
            circular3::angular_sample_count(self.major_sweep, angle, MIN_SEGMENTS),
        );
        let minor =
            circular3::sample_count(self.minor_radius, self.minor_sweep, chord, MIN_SEGMENTS).max(
                circular3::angular_sample_count(self.minor_sweep, angle, MIN_SEGMENTS),
            );
        let inv_u = 1.0 / f64::from(major);
        let inv_v = 1.0 / f64::from(minor);
        TriMesh::from_grid(major + 1, minor + 1, |i, j| {
            let u = f64::from(i) * inv_u;
            let v = f64::from(j) * inv_v;
            MeshVertex::new(self.point_at(u, v), self.normal_at(u, v))
        })
    }
}

impl core::fmt::Display for TorusSurface {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "torus_surface{{ c={}, R={} mm, r={} mm, major={}..{} rad, minor={}..{} rad, axis={} }}",
            self.center(),
            self.major_radius_mm(),
            self.minor_radius_mm(),
            self.major_start.get::<radian>(),
            self.major_start.get::<radian>() + self.major_sweep.get::<radian>(),
            self.minor_start.get::<radian>(),
            self.minor_start.get::<radian>() + self.minor_sweep.get::<radian>(),
            self.axis(),
        )
    }
}
