use bone_types::{
    Aabb3, Angle, AngleTolerance, ChordHeightTolerance, Length, Parameter, Plane3, Point3,
    Tolerance, UnitVec3, Vec3,
};
use core::f64::consts::{PI, 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 SphereSurface {
    plane: Plane3,
    radius: Length,
    polar_start: Angle,
    polar_sweep: Angle,
    azimuth_start: Angle,
    azimuth_sweep: Angle,
}

impl SphereSurface {
    pub fn new(
        plane: Plane3,
        radius: Length,
        polar: AngularSpan,
        azimuth: AngularSpan,
        tolerance: Tolerance,
    ) -> Result<Self, KernelError> {
        let polar_start = polar.start();
        let polar_sweep = polar.sweep();
        let azimuth_start = azimuth.start();
        let azimuth_sweep = azimuth.sweep();
        let r = radius.get::<millimeter>();
        let polar = polar_sweep.get::<radian>();
        let azimuth = azimuth_sweep.get::<radian>().abs();
        if !r.is_finite() || !polar.is_finite() || !azimuth.is_finite() {
            return Err(KernelError::DegenerateSphere);
        }
        if r < tolerance.value() {
            return Err(KernelError::DegenerateSphere);
        }
        let angle_eps = AngleTolerance::from_arc_length(tolerance, radius).radians();
        if polar.abs() < angle_eps || polar.abs() > PI + angle_eps {
            return Err(KernelError::DegenerateSphere);
        }
        if azimuth < angle_eps || azimuth > TAU + angle_eps {
            return Err(KernelError::DegenerateSphere);
        }
        let start = polar_start.get::<radian>();
        if !start.is_finite()
            || start < -angle_eps
            || start + polar.max(0.0) > PI + angle_eps
            || start + polar.min(0.0) < -angle_eps
        {
            return Err(KernelError::DegenerateSphere);
        }
        Ok(Self {
            plane,
            radius,
            polar_start,
            polar_sweep,
            azimuth_start,
            azimuth_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 radius(self) -> Length {
        self.radius
    }

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

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

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

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

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

    #[must_use]
    fn polar_at(self, u: f64) -> f64 {
        self.polar_start.get::<radian>() + self.polar_sweep.get::<radian>() * u
    }

    #[must_use]
    fn azimuth_at(self, v: f64) -> f64 {
        self.azimuth_start.get::<radian>() + self.azimuth_sweep.get::<radian>() * v
    }

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

    #[must_use]
    fn radial(self, phi: f64, theta: f64) -> (f64, f64, f64) {
        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 (sp, cp) = (phi.sin(), phi.cos());
        let (st, ct) = (theta.sin(), theta.cos());
        (
            sp * (ct * xx + st * yx) + cp * ax,
            sp * (ct * xy + st * yy) + cp * ay,
            sp * (ct * xz + st * yz) + cp * az,
        )
    }

    #[must_use]
    fn point_at(self, u: f64, v: f64) -> Point3 {
        let (rx, ry, rz) = self.radial(self.polar_at(u), self.azimuth_at(v));
        let r = self.radius_mm();
        let (cx, cy, cz) = self.center().coords_mm();
        Point3::from_mm(cx + r * rx, cy + r * ry, cz + r * rz)
    }

    #[must_use]
    fn normal_at(self, u: f64, v: f64) -> UnitVec3 {
        let (rx, ry, rz) = self.radial(self.polar_at(u), self.azimuth_at(v));
        let s = self.orientation();
        UnitVec3::new_unchecked(s * rx, s * ry, s * rz)
    }
}

impl Surface3 for SphereSurface {
    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 phi = self.polar_at(u.value());
        let theta = self.azimuth_at(v.value());
        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 (sp, cp) = (phi.sin(), phi.cos());
        let (st, ct) = (theta.sin(), theta.cos());
        let r = self.radius_mm();
        let u_scale = r * self.polar_sweep.get::<radian>();
        let v_scale = r * self.azimuth_sweep.get::<radian>() * sp;
        let du = Vec3::from_mm(
            u_scale * (cp * (ct * xx + st * yx) - sp * ax),
            u_scale * (cp * (ct * xy + st * yy) - sp * ay),
            u_scale * (cp * (ct * xz + st * yz) - sp * az),
        );
        let dv = Vec3::from_mm(
            v_scale * (-st * xx + ct * yx),
            v_scale * (-st * xy + ct * yy),
            v_scale * (-st * xz + ct * yz),
        );
        (du, dv)
    }

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

    fn bounding_box(&self) -> Aabb3 {
        let r = self.radius_mm();
        let span = Vec3::from_mm(r, r, r);
        Aabb3::from_corners(self.center() - span, self.center() + span)
    }

    fn contains_point(&self, p: Point3, tolerance: Tolerance) -> bool {
        let (px, py, pn) = circular3::local_coords(self.plane, p);
        let distance = (px * px + py * py + pn * pn).sqrt();
        if (distance - self.radius_mm()).abs() > tolerance.value() {
            return false;
        }
        let equatorial = (px * px + py * py).sqrt();
        let phi = equatorial.atan2(pn);
        let polar_eps = AngleTolerance::from_arc_length(tolerance, self.radius).radians();
        if !angles::contains(
            phi,
            self.polar_start.get::<radian>(),
            self.polar_sweep.get::<radian>(),
            polar_eps,
        ) {
            return false;
        }
        if equatorial < tolerance.value() {
            return true;
        }
        let azimuth_eps =
            AngleTolerance::from_arc_length(tolerance, Length::new::<millimeter>(equatorial))
                .radians();
        angles::contains(
            py.atan2(px),
            self.azimuth_start.get::<radian>(),
            self.azimuth_sweep.get::<radian>(),
            azimuth_eps,
        )
    }

    fn tessellate(&self, chord: ChordHeightTolerance, angle: AngleTolerance) -> TriMesh {
        let meridian =
            circular3::sample_count(self.radius, self.polar_sweep, chord, MIN_SEGMENTS).max(
                circular3::angular_sample_count(self.polar_sweep, angle, MIN_SEGMENTS),
            );
        let around =
            circular3::sample_count(self.radius, self.azimuth_sweep, chord, MIN_SEGMENTS).max(
                circular3::angular_sample_count(self.azimuth_sweep, angle, MIN_SEGMENTS),
            );
        let inv_u = 1.0 / f64::from(meridian);
        let inv_v = 1.0 / f64::from(around);
        TriMesh::from_grid(meridian + 1, around + 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 SphereSurface {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "sphere_surface{{ c={}, r={} mm, polar={}..{} rad, azimuth={}..{} rad, axis={} }}",
            self.center(),
            self.radius_mm(),
            self.polar_start.get::<radian>(),
            self.polar_start.get::<radian>() + self.polar_sweep.get::<radian>(),
            self.azimuth_start.get::<radian>(),
            self.azimuth_start.get::<radian>() + self.azimuth_sweep.get::<radian>(),
            self.axis(),
        )
    }
}
