use bone_types::Tolerance;
use uom::si::length::millimeter;

use crate::brep::boolean::as_vec;
use crate::brep::boolean::ssi::{
    AnalyticSurface, InfiniteLine, SurfaceIntersection, intersect_surfaces,
};
use crate::line3::Line3;

use super::{AnalyticPatch, mm};

const SLOPE_EPS: f64 = 1.0e-9;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TrimGap {
    CurvedFace,
}

#[must_use]
pub fn carrier(patch: AnalyticPatch) -> AnalyticSurface {
    match patch {
        AnalyticPatch::Plane(plane) => AnalyticSurface::Plane {
            origin: plane.plane().origin(),
            normal: plane.plane().normal(),
        },
        AnalyticPatch::Cylinder(cylinder) => AnalyticSurface::Cylinder {
            axis_origin: cylinder.center(),
            axis: cylinder.axis(),
            radius: cylinder.radius(),
        },
        AnalyticPatch::Cone(cone) => {
            let slope = cone.half_angle_rad().tan();
            if slope.abs() <= SLOPE_EPS {
                AnalyticSurface::Cylinder {
                    axis_origin: cone.center(),
                    axis: cone.axis(),
                    radius: cone.base_radius(),
                }
            } else {
                let apex = cone.center() + cone.axis().into_vec(mm(-cone.base_radius_mm() / slope));
                AnalyticSurface::Cone {
                    apex,
                    axis: cone.axis(),
                    half_angle: cone.half_angle(),
                }
            }
        }
        AnalyticPatch::Sphere(sphere) => AnalyticSurface::Sphere {
            center: sphere.center(),
            radius: sphere.radius(),
        },
        AnalyticPatch::Torus(torus) => AnalyticSurface::Torus {
            center: torus.center(),
            axis: torus.axis(),
            major_radius: torus.major_radius(),
            minor_radius: torus.minor_radius(),
        },
    }
}

#[must_use]
pub fn face_boundary(
    face: AnalyticPatch,
    neighbor: AnalyticPatch,
    tolerance: Tolerance,
) -> SurfaceIntersection {
    intersect_surfaces(carrier(face), carrier(neighbor), tolerance)
}

pub fn trim_line_to_face(
    line: InfiniteLine,
    face: AnalyticPatch,
    tolerance: Tolerance,
) -> Result<Option<Line3>, TrimGap> {
    let AnalyticPatch::Plane(plane) = face else {
        return Err(TrimGap::CurvedFace);
    };
    let base = plane.plane();
    let to_point = line.point() - base.origin();
    let dir = line.direction();
    let normal = base.normal();
    let off_plane = to_point.dot_mm2(as_vec(normal));
    let dir_normal = dir.dot(normal);
    if off_plane.abs() > tolerance.value() || dir_normal.abs() > SLOPE_EPS {
        return Ok(None);
    }
    let a0 = to_point.dot_mm2(as_vec(base.x_axis()));
    let b0 = to_point.dot_mm2(as_vec(base.y_axis()));
    let du = dir.dot(base.x_axis());
    let dv = dir.dot(base.y_axis());
    let u_extent = plane.u_extent().get::<millimeter>();
    let v_extent = plane.v_extent().get::<millimeter>();
    let Some((lo, hi)) = clip_interval(
        &[(a0, du, 0.0, u_extent), (b0, dv, 0.0, v_extent)],
        tolerance.value(),
    ) else {
        return Ok(None);
    };
    let along = as_vec(dir);
    let start = line.point() + along * lo;
    let end = line.point() + along * hi;
    Ok(Line3::new(start, end, tolerance).ok())
}

fn clip_interval(constraints: &[(f64, f64, f64, f64)], eps: f64) -> Option<(f64, f64)> {
    constraints
        .iter()
        .try_fold(
            (f64::NEG_INFINITY, f64::INFINITY),
            |(lo, hi), &(c0, slope, min, max)| {
                let (low, high) = axis_window(c0, slope, min, max, eps)?;
                let next = (lo.max(low), hi.min(high));
                (next.0 <= next.1 + eps).then_some(next)
            },
        )
        .filter(|(lo, hi)| lo.is_finite() && hi.is_finite() && hi - lo > eps)
}

fn axis_window(c0: f64, slope: f64, min: f64, max: f64, eps: f64) -> Option<(f64, f64)> {
    if slope.abs() <= SLOPE_EPS {
        return (c0 >= min - eps && c0 <= max + eps).then_some((f64::NEG_INFINITY, f64::INFINITY));
    }
    let a = (min - c0) / slope;
    let b = (max - c0) / slope;
    Some((a.min(b), a.max(b)))
}

#[cfg(test)]
mod tests {
    use super::{TrimGap, carrier, face_boundary, trim_line_to_face};
    use crate::angular_span::AngularSpan;
    use crate::brep::boolean::ssi::{AnalyticSurface, IntersectionCurve, SurfaceIntersection};
    use crate::brep::localops::AnalyticPatch;
    use crate::cylinder_surface::CylinderSurface;
    use crate::line3::Line3;
    use crate::plane_surface::PlaneSurface;
    use crate::sphere_surface::SphereSurface;
    use bone_types::{Angle, Length, Plane3, Point3, Tolerance, UnitVec3};
    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 frame(origin: Point3, x: UnitVec3, y: UnitVec3) -> Plane3 {
        let Ok(plane) = Plane3::new(origin, x, y, TOL) else {
            panic!("axes are orthonormal");
        };
        plane
    }

    fn xy(origin: Point3) -> Plane3 {
        frame(origin, UnitVec3::x_axis(), UnitVec3::y_axis())
    }

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

    #[test]
    fn cylinder_carrier_drops_the_bounds() {
        let Ok(cylinder) = CylinderSurface::new(
            xy(Point3::from_mm(1.0, 2.0, 0.0)),
            mm(4.0),
            Angle::new::<radian>(0.0),
            Angle::new::<radian>(TAU),
            mm(10.0),
            TOL,
        ) else {
            panic!("cylinder valid");
        };
        let AnalyticSurface::Cylinder { radius, .. } = carrier(AnalyticPatch::Cylinder(cylinder))
        else {
            panic!("a cylinder carrier is a cylinder");
        };
        assert!((radius.get::<millimeter>() - 4.0).abs() < 1.0e-9);
    }

    #[test]
    fn two_planar_faces_share_a_line() {
        let floor = plane_patch(xy(Point3::from_mm(0.0, 0.0, 0.0)), 8.0, 8.0);
        let wall = plane_patch(
            frame(
                Point3::from_mm(0.0, 3.0, 0.0),
                UnitVec3::x_axis(),
                UnitVec3::z_axis(),
            ),
            8.0,
            8.0,
        );
        let SurfaceIntersection::Curves(curves) = face_boundary(floor, wall, TOL) else {
            panic!("a floor and a wall meet in a line");
        };
        assert_eq!(curves.len(), 1);
        assert!(matches!(curves[0], IntersectionCurve::Line(_)));
    }

    #[test]
    fn line_clips_to_the_planar_face_rectangle() {
        let floor = plane_patch(xy(Point3::from_mm(0.0, 0.0, 0.0)), 6.0, 4.0);
        let wall = plane_patch(
            frame(
                Point3::from_mm(0.0, 2.0, 0.0),
                UnitVec3::x_axis(),
                UnitVec3::z_axis(),
            ),
            6.0,
            6.0,
        );
        let SurfaceIntersection::Curves(curves) = face_boundary(floor, wall, TOL) else {
            panic!("planar faces meet in a line");
        };
        let IntersectionCurve::Line(line) = curves[0] else {
            panic!("expected a line");
        };
        let Ok(Some(segment)) = trim_line_to_face(line, floor, TOL) else {
            panic!("the shared line clips to the floor");
        };
        let length = endpoint_span(&segment);
        assert!(
            (length - 6.0).abs() < 1.0e-6,
            "the segment spans the u extent"
        );
    }

    #[test]
    fn line_clip_against_a_curved_face_is_reported() {
        let floor = plane_patch(xy(Point3::from_mm(0.0, 0.0, 0.0)), 6.0, 4.0);
        let ball = {
            let Ok(sphere) = SphereSurface::new(
                xy(Point3::from_mm(0.0, 0.0, 0.0)),
                mm(5.0),
                AngularSpan::new(Angle::new::<radian>(0.3), Angle::new::<radian>(2.5)),
                AngularSpan::new(Angle::new::<radian>(0.0), Angle::new::<radian>(TAU)),
                TOL,
            ) else {
                panic!("sphere valid");
            };
            AnalyticPatch::Sphere(sphere)
        };
        let SurfaceIntersection::Curves(curves) = face_boundary(floor, ball, TOL) else {
            panic!("a plane through a sphere center meets it in a circle");
        };
        let IntersectionCurve::Circle(_) = curves[0] else {
            panic!("expected a circle");
        };
        let line = crate::brep::boolean::ssi::InfiniteLine::new(
            Point3::from_mm(0.0, 0.0, 0.0),
            UnitVec3::x_axis(),
        );
        assert_eq!(trim_line_to_face(line, ball, TOL), Err(TrimGap::CurvedFace));
    }

    #[test]
    fn line_clear_of_the_face_yields_no_segment() {
        let floor = plane_patch(xy(Point3::from_mm(0.0, 0.0, 0.0)), 6.0, 4.0);
        let line = crate::brep::boolean::ssi::InfiniteLine::new(
            Point3::from_mm(0.0, 20.0, 0.0),
            UnitVec3::x_axis(),
        );
        assert_eq!(trim_line_to_face(line, floor, TOL), Ok(None));
    }

    #[test]
    fn line_parallel_above_the_face_yields_no_segment() {
        let floor = plane_patch(xy(Point3::from_mm(0.0, 0.0, 0.0)), 6.0, 4.0);
        let line = crate::brep::boolean::ssi::InfiniteLine::new(
            Point3::from_mm(0.0, 1.0, 3.0),
            UnitVec3::x_axis(),
        );
        assert_eq!(trim_line_to_face(line, floor, TOL), Ok(None));
    }

    #[test]
    fn line_piercing_the_face_yields_no_segment() {
        let floor = plane_patch(xy(Point3::from_mm(0.0, 0.0, 0.0)), 6.0, 4.0);
        let line = crate::brep::boolean::ssi::InfiniteLine::new(
            Point3::from_mm(3.0, 2.0, -1.0),
            UnitVec3::z_axis(),
        );
        assert_eq!(trim_line_to_face(line, floor, TOL), Ok(None));
    }

    fn endpoint_span(line: &Line3) -> f64 {
        use crate::curve3::Curve3;
        let start = line.evaluate(bone_types::Parameter::new(0.0));
        let end = line.evaluate(bone_types::Parameter::new(1.0));
        (end - start).norm_mm()
    }
}
