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

use crate::brep::boolean::ssi::{InfiniteLine, IntersectionCurve, SsiGap, SurfaceIntersection};
use crate::circle3::Circle3;

use super::AnalyticPatch;
use super::offset::{OffsetDegeneracy, OffsetDistance, offset_surface};
use super::trim::face_boundary;

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

impl BlendRadius {
    #[must_use]
    pub fn new(radius: Length) -> Option<Self> {
        (radius.get::<millimeter>() > 0.0).then_some(Self(radius))
    }

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

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NormalSide {
    Along,
    Against,
}

impl NormalSide {
    fn offset(self, radius: BlendRadius) -> OffsetDistance {
        let signed = match self {
            Self::Along => radius.length(),
            Self::Against => -radius.length(),
        };
        OffsetDistance::new(signed)
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum BlendSpine {
    Cylindrical { axis: InfiniteLine, radius: Length },
    Toroidal { spine: Circle3, radius: Length },
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum BlendGap {
    OffsetDegenerate(OffsetDegeneracy),
    NoSpine,
    MultipleSpines,
    Unsupported(SsiGap),
}

pub fn rolling_ball_blend(
    face_a: AnalyticPatch,
    face_b: AnalyticPatch,
    radius: BlendRadius,
    side_a: NormalSide,
    side_b: NormalSide,
    tolerance: Tolerance,
) -> Result<BlendSpine, BlendGap> {
    let spine_a = offset_surface(face_a, side_a.offset(radius), tolerance)
        .map_err(BlendGap::OffsetDegenerate)?;
    let spine_b = offset_surface(face_b, side_b.offset(radius), tolerance)
        .map_err(BlendGap::OffsetDegenerate)?;
    match face_boundary(spine_a, spine_b, tolerance) {
        SurfaceIntersection::Curves(curves) => spine_from(&curves, radius),
        SurfaceIntersection::Empty | SurfaceIntersection::Coincident => Err(BlendGap::NoSpine),
        SurfaceIntersection::Unsupported(gap) => Err(BlendGap::Unsupported(gap)),
    }
}

fn spine_from(curves: &[IntersectionCurve], radius: BlendRadius) -> Result<BlendSpine, BlendGap> {
    match curves {
        [IntersectionCurve::Line(axis)] => Ok(BlendSpine::Cylindrical {
            axis: *axis,
            radius: radius.length(),
        }),
        [IntersectionCurve::Circle(spine)] => Ok(BlendSpine::Toroidal {
            spine: *spine,
            radius: radius.length(),
        }),
        _ => Err(BlendGap::MultipleSpines),
    }
}

#[cfg(test)]
mod tests {
    use super::{BlendGap, BlendRadius, BlendSpine, NormalSide, rolling_ball_blend};
    use crate::angular_span::AngularSpan;
    use crate::brep::localops::{AnalyticPatch, OffsetDegeneracy};
    use crate::cylinder_surface::CylinderSurface;
    use crate::plane_surface::PlaneSurface;
    use crate::sphere_surface::SphereSurface;
    use bone_types::{Angle, Length, Plane3, Point3, Tolerance, UnitVec3};
    use core::f64::consts::{PI, 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 frame(origin: Point3, x: UnitVec3, y: UnitVec3) -> Plane3 {
        let Ok(plane) = Plane3::new(origin, x, y, TOL) else {
            panic!("axes are orthonormal");
        };
        plane
    }

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

    #[test]
    fn right_dihedral_blends_to_a_cylinder_axis() {
        let floor = plane_patch(frame(
            Point3::from_mm(0.0, 0.0, 0.0),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
        ));
        let wall = plane_patch(frame(
            Point3::from_mm(0.0, 0.0, 0.0),
            UnitVec3::z_axis(),
            UnitVec3::x_axis(),
        ));
        let Some(radius) = BlendRadius::new(mm(2.0)) else {
            panic!("a positive blend radius is valid");
        };
        let Ok(BlendSpine::Cylindrical { axis, radius: out }) = rolling_ball_blend(
            floor,
            wall,
            radius,
            NormalSide::Against,
            NormalSide::Against,
            TOL,
        ) else {
            panic!("two planes at a right angle blend to a fillet cylinder");
        };
        assert!((out.get::<millimeter>() - 2.0).abs() < 1.0e-9);
        let (dx, dy, dz) = axis.direction().components();
        assert!(dx.abs() > 0.99 && dy.abs() < 1.0e-6 && dz.abs() < 1.0e-6);
        let (_, py, pz) = axis.point().coords_mm();
        assert!(
            (py.abs() - 2.0).abs() < 1.0e-6 && (pz.abs() - 2.0).abs() < 1.0e-6,
            "the axis sits one radius off each face"
        );
    }

    #[test]
    fn sphere_and_plane_blend_to_a_toroidal_spine() {
        let center = frame(
            Point3::from_mm(0.0, 0.0, 0.0),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
        );
        let Ok(sphere) = SphereSurface::new(
            center,
            mm(5.0),
            AngularSpan::new(Angle::new::<radian>(0.0), Angle::new::<radian>(PI)),
            AngularSpan::new(Angle::new::<radian>(0.0), Angle::new::<radian>(TAU)),
            TOL,
        ) else {
            panic!("a full sphere is valid");
        };
        let floor = plane_patch(center);
        let Some(blend_radius) = BlendRadius::new(mm(2.0)) else {
            panic!("a positive blend radius is valid");
        };
        let Ok(BlendSpine::Toroidal { radius, .. }) = rolling_ball_blend(
            AnalyticPatch::Sphere(sphere),
            floor,
            blend_radius,
            NormalSide::Along,
            NormalSide::Along,
            TOL,
        ) else {
            panic!("a sphere meeting a plane rolls a torus fillet");
        };
        assert!((radius.get::<millimeter>() - 2.0).abs() < 1.0e-9);
    }

    #[test]
    fn over_thick_blend_on_a_thin_cylinder_is_degenerate() {
        let Ok(bore) = CylinderSurface::new(
            frame(
                Point3::from_mm(0.0, 0.0, 0.0),
                UnitVec3::x_axis(),
                UnitVec3::y_axis(),
            ),
            mm(1.0),
            Angle::new::<radian>(0.0),
            Angle::new::<radian>(TAU),
            mm(10.0),
            TOL,
        ) else {
            panic!("cylinder valid");
        };
        let floor = plane_patch(frame(
            Point3::from_mm(0.0, 0.0, 0.0),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
        ));
        let Some(radius) = BlendRadius::new(mm(5.0)) else {
            panic!("a positive blend radius is valid");
        };
        assert_eq!(
            rolling_ball_blend(
                AnalyticPatch::Cylinder(bore),
                floor,
                radius,
                NormalSide::Against,
                NormalSide::Against,
                TOL,
            ),
            Err(BlendGap::OffsetDegenerate(
                OffsetDegeneracy::MinRadiusExceeded {
                    min_radius: mm(1.0)
                }
            ))
        );
    }

    #[test]
    fn blend_is_deterministic() {
        let floor = plane_patch(frame(
            Point3::from_mm(0.0, 0.0, 0.0),
            UnitVec3::x_axis(),
            UnitVec3::y_axis(),
        ));
        let wall = plane_patch(frame(
            Point3::from_mm(0.0, 0.0, 0.0),
            UnitVec3::z_axis(),
            UnitVec3::x_axis(),
        ));
        let Some(radius) = BlendRadius::new(mm(1.5)) else {
            panic!("a positive blend radius is valid");
        };
        let first = rolling_ball_blend(
            floor,
            wall,
            radius,
            NormalSide::Against,
            NormalSide::Against,
            TOL,
        );
        let second = rolling_ball_blend(
            floor,
            wall,
            radius,
            NormalSide::Against,
            NormalSide::Against,
            TOL,
        );
        assert_eq!(first, second);
    }
}
