use bone_types::{Length, Plane3, Tolerance};
use uom::si::length::millimeter;

use crate::angular_span::AngularSpan;
use crate::cone_surface::ConeSurface;
use crate::cylinder_surface::CylinderSurface;
use crate::plane_surface::PlaneSurface;
use crate::sphere_surface::SphereSurface;
use crate::torus_surface::TorusSurface;

use super::{AnalyticPatch, mm};

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct OffsetDistance(Length);

impl OffsetDistance {
    #[must_use]
    pub const fn new(distance: Length) -> Self {
        Self(distance)
    }

    #[must_use]
    pub fn from_mm(value: f64) -> Self {
        Self(Length::new::<millimeter>(value))
    }

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

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

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum OffsetDegeneracy {
    MinRadiusExceeded { min_radius: Length },
    SelfOverlap,
    ApexSingularity,
    NonFinite,
}

pub type OffsetResult = Result<AnalyticPatch, OffsetDegeneracy>;

pub fn offset_surface(
    patch: AnalyticPatch,
    distance: OffsetDistance,
    tolerance: Tolerance,
) -> OffsetResult {
    if !distance.mm().is_finite() {
        return Err(OffsetDegeneracy::NonFinite);
    }
    let signed = distance.mm() * patch.outward_sign();
    match patch {
        AnalyticPatch::Plane(plane) => Ok(offset_plane(plane, distance)),
        AnalyticPatch::Cylinder(cylinder) => offset_cylinder(cylinder, signed, tolerance),
        AnalyticPatch::Cone(cone) => offset_cone(cone, signed, tolerance),
        AnalyticPatch::Sphere(sphere) => offset_sphere(sphere, signed, tolerance),
        AnalyticPatch::Torus(torus) => offset_torus(torus, signed, tolerance),
    }
}

fn collapsed(min_radius: f64) -> OffsetDegeneracy {
    OffsetDegeneracy::MinRadiusExceeded {
        min_radius: mm(min_radius),
    }
}

fn offset_plane(plane: PlaneSurface, distance: OffsetDistance) -> AnalyticPatch {
    let base = plane.plane();
    let origin = base.origin() + base.normal().into_vec(distance.length());
    let shifted = Plane3::new_unchecked(origin, base.x_axis(), base.y_axis());
    AnalyticPatch::Plane(PlaneSurface::new_unchecked(
        shifted,
        plane.u_extent(),
        plane.v_extent(),
    ))
}

fn offset_cylinder(cylinder: CylinderSurface, signed: f64, tolerance: Tolerance) -> OffsetResult {
    let radius = cylinder.radius_mm() + signed;
    if radius <= tolerance.value() {
        return Err(collapsed(cylinder.radius_mm()));
    }
    CylinderSurface::new(
        cylinder.plane(),
        mm(radius),
        cylinder.start_angle(),
        cylinder.sweep_angle(),
        cylinder.height(),
        tolerance,
    )
    .map(AnalyticPatch::Cylinder)
    .map_err(|_| collapsed(cylinder.radius_mm()))
}

fn offset_cone(cone: ConeSurface, signed: f64, tolerance: Tolerance) -> OffsetResult {
    if cone.base_radius_mm().min(cone.top_radius_mm()) <= tolerance.value() {
        return Err(OffsetDegeneracy::ApexSingularity);
    }
    let across = cone.half_angle_rad().cos();
    let along = cone.half_angle_rad().sin();
    let radial = signed * across;
    let base = cone.base_radius_mm() + radial;
    let top = cone.top_radius_mm() + radial;
    let min_radius = cone.min_radius_of_curvature().get::<millimeter>();
    if base.min(top) <= tolerance.value() {
        return Err(collapsed(min_radius));
    }
    let apex = cone.plane();
    let origin = apex.origin() + apex.normal().into_vec(mm(-signed * along));
    let shifted = Plane3::new_unchecked(origin, apex.x_axis(), apex.y_axis());
    ConeSurface::new(
        shifted,
        mm(base),
        cone.half_angle(),
        AngularSpan::new(cone.start_angle(), cone.sweep_angle()),
        cone.height(),
        tolerance,
    )
    .map(AnalyticPatch::Cone)
    .map_err(|_| collapsed(min_radius))
}

fn offset_sphere(sphere: SphereSurface, signed: f64, tolerance: Tolerance) -> OffsetResult {
    let radius = sphere.radius_mm() + signed;
    if radius <= tolerance.value() {
        return Err(collapsed(sphere.radius_mm()));
    }
    SphereSurface::new(
        sphere.plane(),
        mm(radius),
        AngularSpan::new(sphere.polar_start(), sphere.polar_sweep()),
        AngularSpan::new(sphere.azimuth_start(), sphere.azimuth_sweep()),
        tolerance,
    )
    .map(AnalyticPatch::Sphere)
    .map_err(|_| collapsed(sphere.radius_mm()))
}

fn offset_torus(torus: TorusSurface, signed: f64, tolerance: Tolerance) -> OffsetResult {
    let minor = torus.minor_radius_mm() + signed;
    if minor <= tolerance.value() {
        return Err(collapsed(torus.minor_radius_mm()));
    }
    if minor >= torus.major_radius_mm() - tolerance.value() {
        return Err(OffsetDegeneracy::SelfOverlap);
    }
    TorusSurface::new(
        torus.plane(),
        torus.major_radius(),
        mm(minor),
        AngularSpan::new(torus.major_start(), torus.major_sweep()),
        AngularSpan::new(torus.minor_start(), torus.minor_sweep()),
        tolerance,
    )
    .map(AnalyticPatch::Torus)
    .map_err(|_| collapsed(torus.minor_radius_mm()))
}

#[cfg(test)]
mod tests {
    use super::{AnalyticPatch, OffsetDegeneracy, OffsetDistance, offset_surface};
    use crate::angular_span::AngularSpan;
    use crate::cone_surface::ConeSurface;
    use crate::cylinder_surface::CylinderSurface;
    use crate::plane_surface::PlaneSurface;
    use crate::sphere_surface::SphereSurface;
    use crate::surface3::Surface3;
    use crate::torus_surface::TorusSurface;
    use bone_types::{Angle, Length, Parameter, Plane3, Point3, Tolerance, UnitVec3, Vec3};
    use core::f64::consts::TAU;
    use uom::si::angle::radian;
    use uom::si::length::millimeter;

    const TOL: Tolerance = Tolerance::new(1.0e-7);

    fn mm(value: f64) -> Length {
        Length::new::<millimeter>(value)
    }

    fn rad(value: f64) -> Angle {
        Angle::new::<radian>(value)
    }

    fn xy_plane() -> Plane3 {
        Plane3::new_unchecked(
            Point3::from_mm(0.0, 0.0, 0.0),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
        )
    }

    fn plane_patch() -> AnalyticPatch {
        let Ok(plane) = PlaneSurface::new(xy_plane(), mm(8.0), mm(6.0), TOL) else {
            panic!("plane extents are valid");
        };
        AnalyticPatch::Plane(plane)
    }

    fn cylinder_patch(sweep: f64) -> AnalyticPatch {
        let Ok(cylinder) =
            CylinderSurface::new(xy_plane(), mm(5.0), rad(0.0), rad(sweep), mm(10.0), TOL)
        else {
            panic!("cylinder dimensions are valid");
        };
        AnalyticPatch::Cylinder(cylinder)
    }

    fn cone_patch() -> AnalyticPatch {
        let Ok(cone) = ConeSurface::new(
            xy_plane(),
            mm(4.0),
            rad(0.4),
            AngularSpan::new(rad(0.0), rad(TAU)),
            mm(6.0),
            TOL,
        ) else {
            panic!("cone dimensions are valid");
        };
        AnalyticPatch::Cone(cone)
    }

    fn sphere_patch() -> AnalyticPatch {
        let Ok(sphere) = SphereSurface::new(
            xy_plane(),
            mm(5.0),
            AngularSpan::new(rad(0.3), rad(2.5)),
            AngularSpan::new(rad(0.0), rad(TAU)),
            TOL,
        ) else {
            panic!("sphere spans are valid");
        };
        AnalyticPatch::Sphere(sphere)
    }

    fn torus_patch() -> AnalyticPatch {
        let Ok(torus) = TorusSurface::new(
            xy_plane(),
            mm(10.0),
            mm(3.0),
            AngularSpan::new(rad(0.0), rad(TAU)),
            AngularSpan::new(rad(0.0), rad(TAU)),
            TOL,
        ) else {
            panic!("torus radii are valid");
        };
        AnalyticPatch::Torus(torus)
    }

    fn point_at(patch: AnalyticPatch, u: f64, v: f64) -> Point3 {
        let (u, v) = (Parameter::new(u), Parameter::new(v));
        match patch {
            AnalyticPatch::Plane(s) => s.evaluate(u, v),
            AnalyticPatch::Cylinder(s) => s.evaluate(u, v),
            AnalyticPatch::Cone(s) => s.evaluate(u, v),
            AnalyticPatch::Sphere(s) => s.evaluate(u, v),
            AnalyticPatch::Torus(s) => s.evaluate(u, v),
        }
    }

    fn normal_at(patch: AnalyticPatch, u: f64, v: f64) -> UnitVec3 {
        let (u, v) = (Parameter::new(u), Parameter::new(v));
        match patch {
            AnalyticPatch::Plane(s) => s.normal(u, v),
            AnalyticPatch::Cylinder(s) => s.normal(u, v),
            AnalyticPatch::Cone(s) => s.normal(u, v),
            AnalyticPatch::Sphere(s) => s.normal(u, v),
            AnalyticPatch::Torus(s) => s.normal(u, v),
        }
    }

    fn radius_mm(patch: AnalyticPatch) -> f64 {
        match patch {
            AnalyticPatch::Cylinder(s) => s.radius_mm(),
            AnalyticPatch::Sphere(s) => s.radius_mm(),
            AnalyticPatch::Cone(s) => s.base_radius_mm(),
            AnalyticPatch::Torus(s) => s.minor_radius_mm(),
            AnalyticPatch::Plane(_) => panic!("a plane carries no radius"),
        }
    }

    fn drift(a: Point3, b: Point3) -> f64 {
        (a - b).norm_mm()
    }

    fn offset_point(patch: AnalyticPatch, u: f64, v: f64, distance: f64) -> Point3 {
        let normal = normal_at(patch, u, v);
        let scaled: Vec3 = normal.into_vec(mm(distance));
        point_at(patch, u, v) + scaled
    }

    fn assert_offset_identity(patch: AnalyticPatch, distance: f64) {
        let Ok(offset) = offset_surface(patch, OffsetDistance::from_mm(distance), TOL) else {
            panic!("offset of {distance} mm is non-degenerate");
        };
        [0.0, 0.2, 0.5, 0.9, 1.0].iter().for_each(|&u| {
            [0.0, 0.37, 1.0].iter().for_each(|&v| {
                let expected = offset_point(patch, u, v, distance);
                let actual = point_at(offset, u, v);
                assert!(
                    drift(expected, actual) < 1.0e-9,
                    "offset point at ({u},{v}) drifts {} mm",
                    drift(expected, actual)
                );
                let (ex, ey, ez) = normal_at(patch, u, v).components();
                let (ax, ay, az) = normal_at(offset, u, v).components();
                assert!(
                    (ex - ax).abs() < 1.0e-9
                        && (ey - ay).abs() < 1.0e-9
                        && (ez - az).abs() < 1.0e-9,
                    "offset normal at ({u},{v}) rotated"
                );
            });
        });
    }

    #[test]
    fn offset_lands_on_normal_ray_every_surface() {
        [
            plane_patch(),
            cylinder_patch(TAU),
            cone_patch(),
            sphere_patch(),
            torus_patch(),
        ]
        .into_iter()
        .for_each(|patch| {
            assert_offset_identity(patch, 0.75);
            assert_offset_identity(patch, -0.6);
        });
    }

    #[test]
    fn plane_offset_never_collapses() {
        let plane = plane_patch();
        assert!(offset_surface(plane, OffsetDistance::from_mm(1.0e6), TOL).is_ok());
        assert!(offset_surface(plane, OffsetDistance::from_mm(-1.0e6), TOL).is_ok());
        assert_eq!(plane.min_radius_of_curvature(), None);
    }

    #[test]
    fn cylinder_inward_past_radius_exceeds_min_radius() {
        let cylinder = cylinder_patch(TAU);
        assert_eq!(cylinder.min_radius_of_curvature(), Some(mm(5.0)));
        let Ok(grown) = offset_surface(cylinder, OffsetDistance::from_mm(2.0), TOL) else {
            panic!("growing a cylinder is valid");
        };
        assert!((radius_mm(grown) - 7.0).abs() < 1.0e-9);
        let Ok(shrunk) = offset_surface(cylinder, OffsetDistance::from_mm(-2.0), TOL) else {
            panic!("shrinking within the radius is valid");
        };
        assert!((radius_mm(shrunk) - 3.0).abs() < 1.0e-9);
        assert_eq!(
            offset_surface(cylinder, OffsetDistance::from_mm(-5.0), TOL),
            Err(OffsetDegeneracy::MinRadiusExceeded {
                min_radius: mm(5.0)
            })
        );
    }

    #[test]
    fn negative_sweep_cylinder_offsets_inward_for_positive_distance() {
        let inner_wall = cylinder_patch(-TAU);
        let Ok(shrunk) = offset_surface(inner_wall, OffsetDistance::from_mm(2.0), TOL) else {
            panic!("a hole wall shrinks under a positive offset along its inward normal");
        };
        assert!((radius_mm(shrunk) - 3.0).abs() < 1.0e-9);
    }

    #[test]
    fn cone_offset_preserves_half_angle_and_shifts_apex() {
        let cone = cone_patch();
        let across = 0.4_f64.cos();
        let along = 0.4_f64.sin();
        let Ok(grown) = offset_surface(cone, OffsetDistance::from_mm(1.0), TOL) else {
            panic!("growing a cone is valid");
        };
        let AnalyticPatch::Cone(out) = grown else {
            panic!("offset of a cone is a cone");
        };
        assert!((out.half_angle_rad() - 0.4).abs() < 1.0e-12);
        assert!((out.base_radius_mm() - (4.0 + across)).abs() < 1.0e-9);
        let (_, _, oz) = out.center().coords_mm();
        assert!((oz - (-along)).abs() < 1.0e-9, "apex shifts along the axis");
    }

    #[test]
    fn cone_inward_collapses_at_the_small_end() {
        let cone = cone_patch();
        let across = 0.4_f64.cos();
        let expected = mm(4.0 / across);
        assert_eq!(cone.min_radius_of_curvature(), Some(expected));
        assert_eq!(
            offset_surface(cone, OffsetDistance::from_mm(-4.0 / across), TOL),
            Err(OffsetDegeneracy::MinRadiusExceeded {
                min_radius: expected
            })
        );
    }

    #[test]
    fn sphere_offset_changes_radius_and_collapses() {
        let sphere = sphere_patch();
        assert_eq!(sphere.min_radius_of_curvature(), Some(mm(5.0)));
        let Ok(grown) = offset_surface(sphere, OffsetDistance::from_mm(1.5), TOL) else {
            panic!("growing a sphere is valid");
        };
        assert!((radius_mm(grown) - 6.5).abs() < 1.0e-9);
        assert_eq!(
            offset_surface(sphere, OffsetDistance::from_mm(-5.0), TOL),
            Err(OffsetDegeneracy::MinRadiusExceeded {
                min_radius: mm(5.0)
            })
        );
    }

    #[test]
    fn torus_inward_collapses_and_outward_self_overlaps() {
        let torus = torus_patch();
        assert_eq!(torus.min_radius_of_curvature(), Some(mm(3.0)));
        let Ok(shrunk) = offset_surface(torus, OffsetDistance::from_mm(-1.0), TOL) else {
            panic!("shrinking the tube within its radius is valid");
        };
        assert!((radius_mm(shrunk) - 2.0).abs() < 1.0e-9);
        assert_eq!(
            offset_surface(torus, OffsetDistance::from_mm(-3.0), TOL),
            Err(OffsetDegeneracy::MinRadiusExceeded {
                min_radius: mm(3.0)
            })
        );
        assert_eq!(
            offset_surface(torus, OffsetDistance::from_mm(8.0), TOL),
            Err(OffsetDegeneracy::SelfOverlap)
        );
    }

    #[test]
    fn fat_torus_min_radius_uses_the_inner_equator() {
        let Ok(torus) = TorusSurface::new(
            xy_plane(),
            mm(4.0),
            mm(3.0),
            AngularSpan::new(rad(0.0), rad(TAU)),
            AngularSpan::new(rad(0.0), rad(TAU)),
            TOL,
        ) else {
            panic!("a torus with major just over minor is valid");
        };
        let patch = AnalyticPatch::Torus(torus);
        assert_eq!(patch.min_radius_of_curvature(), Some(mm(1.0)));
    }

    #[test]
    fn negative_sweep_cone_offsets_along_the_reversed_normal() {
        let Ok(cone) = ConeSurface::new(
            xy_plane(),
            mm(4.0),
            rad(0.4),
            AngularSpan::new(rad(0.0), rad(-TAU)),
            mm(6.0),
            TOL,
        ) else {
            panic!("a negative-sweep cone is valid");
        };
        let patch = AnalyticPatch::Cone(cone);
        assert_offset_identity(patch, 0.75);
        assert_offset_identity(patch, -0.6);
    }

    #[test]
    fn apex_touching_cone_cannot_offset_either_way() {
        let Ok(cone) = ConeSurface::new(
            xy_plane(),
            mm(0.0),
            rad(0.4),
            AngularSpan::new(rad(0.0), rad(TAU)),
            mm(6.0),
            TOL,
        ) else {
            panic!("a cone closed to its apex is a valid surface");
        };
        let patch = AnalyticPatch::Cone(cone);
        assert_eq!(
            offset_surface(patch, OffsetDistance::from_mm(1.0), TOL),
            Err(OffsetDegeneracy::ApexSingularity)
        );
        assert_eq!(
            offset_surface(patch, OffsetDistance::from_mm(-1.0), TOL),
            Err(OffsetDegeneracy::ApexSingularity)
        );
    }

    #[test]
    fn non_finite_distance_is_reported_for_every_surface() {
        [
            plane_patch(),
            cylinder_patch(TAU),
            cone_patch(),
            sphere_patch(),
            torus_patch(),
        ]
        .into_iter()
        .for_each(|patch| {
            assert_eq!(
                offset_surface(patch, OffsetDistance::from_mm(f64::NAN), TOL),
                Err(OffsetDegeneracy::NonFinite)
            );
            assert_eq!(
                offset_surface(patch, OffsetDistance::from_mm(f64::INFINITY), TOL),
                Err(OffsetDegeneracy::NonFinite)
            );
        });
    }

    #[test]
    fn offset_is_deterministic() {
        let torus = torus_patch();
        let distance = OffsetDistance::from_mm(0.6);
        assert_eq!(
            offset_surface(torus, distance, TOL),
            offset_surface(torus, distance, TOL)
        );
    }

    proptest::proptest! {
        #[test]
        fn offset_then_reverse_recovers_radius(d in 0.1f64..=1.5f64) {
            [cylinder_patch(TAU), sphere_patch(), torus_patch()]
                .into_iter()
                .for_each(|patch| {
                    let start = radius_mm(patch);
                    let Ok(out) = offset_surface(patch, OffsetDistance::from_mm(d), TOL) else {
                        panic!("a small outward offset is valid");
                    };
                    let Ok(back) = offset_surface(out, OffsetDistance::from_mm(-d), TOL) else {
                        panic!("reversing the offset is valid");
                    };
                    assert!((radius_mm(back) - start).abs() < 1.0e-9);
                });
        }

        #[test]
        fn offset_point_stays_on_the_normal_ray(
            d in -1.5f64..=1.5f64,
            u in 0.0f64..=1.0f64,
            v in 0.0f64..=1.0f64,
        ) {
            let patch = cone_patch();
            let Ok(out) = offset_surface(patch, OffsetDistance::from_mm(d), TOL) else {
                panic!("a small cone offset is valid");
            };
            let expected = offset_point(patch, u, v, d);
            proptest::prop_assert!(drift(expected, point_at(out, u, v)) < 1.0e-9);
        }
    }
}
