use bone_kernel::{Arc3, Circle3, Curve3Kind, IntersectionSet3, Line3, intersect_curves_3};
use bone_types::{Angle, Length, Plane3, Point3, Tolerance, UnitVec3};
use core::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
use uom::si::angle::radian;
use uom::si::length::millimeter;

const TOL: Tolerance = Tolerance::new(1e-9);

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

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

fn xz_plane(origin: Point3) -> Plane3 {
    Plane3::new_unchecked(origin, UnitVec3::x_axis(), UnitVec3::z_axis())
}

fn line(start: Point3, end: Point3) -> Curve3Kind {
    let Ok(seg) = Line3::new(start, end, TOL) else {
        panic!("line endpoints distinct");
    };
    seg.as_kind()
}

fn circle(plane: Plane3, radius: f64) -> Curve3Kind {
    let Ok(disc) = Circle3::new(plane, mm(radius), TOL) else {
        panic!("radius positive");
    };
    disc.as_kind()
}

fn arc(plane: Plane3, radius: f64, start: f64, sweep: f64) -> Curve3Kind {
    let Ok(span) = Arc3::new(
        plane,
        mm(radius),
        Angle::new::<radian>(start),
        Angle::new::<radian>(sweep),
        TOL,
    ) else {
        panic!("sweep nonzero");
    };
    span.as_kind()
}

fn row(label: &str, first: &Curve3Kind, second: &Curve3Kind) -> String {
    format!("{label} = {}", intersect_curves_3(first, second, TOL))
}

fn approx(point: Point3, x: f64, y: f64, z: f64) -> bool {
    let (px, py, pz) = point.coords_mm();
    (px - x).abs() < 1e-9 && (py - y).abs() < 1e-9 && (pz - z).abs() < 1e-9
}

#[test]
fn intersection_matrix_surface() {
    let diag_up = line(
        Point3::from_mm(-5.0, -5.0, 0.0),
        Point3::from_mm(5.0, 5.0, 0.0),
    );
    let diag_down = line(
        Point3::from_mm(-5.0, 5.0, 0.0),
        Point3::from_mm(5.0, -5.0, 0.0),
    );
    let skew = line(
        Point3::from_mm(-5.0, 0.0, 3.0),
        Point3::from_mm(5.0, 0.0, 3.0),
    );
    let collinear_overlap = line(
        Point3::from_mm(-2.0, -2.0, 0.0),
        Point3::from_mm(2.0, 2.0, 0.0),
    );
    let collinear_touch = line(
        Point3::from_mm(5.0, 5.0, 0.0),
        Point3::from_mm(9.0, 9.0, 0.0),
    );
    let parallel = line(
        Point3::from_mm(-5.0, -4.0, 0.0),
        Point3::from_mm(5.0, 6.0, 0.0),
    );
    let upright_segment = line(
        Point3::from_mm(3.0, 0.0, -5.0),
        Point3::from_mm(3.0, 0.0, 5.0),
    );
    let in_plane = line(
        Point3::from_mm(-6.0, 0.0, 0.0),
        Point3::from_mm(6.0, 0.0, 0.0),
    );

    let flat_circle = circle(xy_plane(Point3::origin()), 3.0);
    let shifted_circle = circle(xy_plane(Point3::from_mm(4.0, 0.0, 0.0)), 3.0);
    let twin_circle = circle(xy_plane(Point3::origin()), 3.0);
    let upright_circle = circle(xz_plane(Point3::origin()), 3.0);

    let flat_arc = arc(xy_plane(Point3::origin()), 3.0, -FRAC_PI_2, PI);
    let distant_arc = arc(
        xy_plane(Point3::origin()),
        3.0,
        FRAC_PI_2 + FRAC_PI_4,
        FRAC_PI_4,
    );
    let upright_arc = arc(xz_plane(Point3::origin()), 3.0, -FRAC_PI_2, PI);

    let rows = [
        row("line_cross_line", &diag_up, &diag_down),
        row("line_skew_line", &diag_up, &skew),
        row("line_collinear_overlap", &diag_up, &collinear_overlap),
        row("line_collinear_touch", &diag_up, &collinear_touch),
        row("line_parallel", &diag_up, &parallel),
        row("line_pierces_circle", &upright_segment, &flat_circle),
        row("line_in_plane_circle", &in_plane, &flat_circle),
        row("line_in_plane_arc", &in_plane, &flat_arc),
        row("circle_circle_coplanar", &flat_circle, &shifted_circle),
        row("circle_circle_same", &flat_circle, &twin_circle),
        row("circle_circle_perpendicular", &flat_circle, &upright_circle),
        row("circle_arc_coplanar", &shifted_circle, &flat_arc),
        row("arc_arc_hit", &flat_arc, &upright_arc),
        row("arc_arc_miss", &flat_arc, &distant_arc),
    ]
    .join("\n");
    insta::assert_snapshot!(rows);
}

#[test]
fn skew_lines_do_not_meet() {
    let horizontal = line(
        Point3::from_mm(-5.0, 0.0, 0.0),
        Point3::from_mm(5.0, 0.0, 0.0),
    );
    let lifted = line(
        Point3::from_mm(0.0, -5.0, 2.0),
        Point3::from_mm(0.0, 5.0, 2.0),
    );
    assert_eq!(
        intersect_curves_3(&horizontal, &lifted, TOL),
        IntersectionSet3::Empty
    );
}

#[test]
fn crossing_lines_meet_at_origin() {
    let diag_up = line(
        Point3::from_mm(-5.0, -5.0, 1.0),
        Point3::from_mm(5.0, 5.0, 1.0),
    );
    let diag_down = line(
        Point3::from_mm(-5.0, 5.0, 1.0),
        Point3::from_mm(5.0, -5.0, 1.0),
    );
    let IntersectionSet3::One(meet) = intersect_curves_3(&diag_up, &diag_down, TOL) else {
        panic!("crossing lines meet once");
    };
    assert!(approx(meet, 0.0, 0.0, 1.0));
}

#[test]
fn line_pierces_circle_plane_on_curve() {
    let upright_segment = line(
        Point3::from_mm(3.0, 0.0, -5.0),
        Point3::from_mm(3.0, 0.0, 5.0),
    );
    let disc = circle(xy_plane(Point3::origin()), 3.0);
    let IntersectionSet3::One(hit) = intersect_curves_3(&upright_segment, &disc, TOL) else {
        panic!("line through (3,0,0) hits the circle once");
    };
    assert!(approx(hit, 3.0, 0.0, 0.0));
}

#[test]
fn perpendicular_circles_meet_at_two_points() {
    let flat_disc = circle(xy_plane(Point3::origin()), 5.0);
    let upright_disc = circle(xz_plane(Point3::origin()), 5.0);
    let IntersectionSet3::Two(first, second) = intersect_curves_3(&flat_disc, &upright_disc, TOL)
    else {
        panic!("circles in perpendicular planes share two points");
    };
    let hit_plus = approx(first, 5.0, 0.0, 0.0) || approx(second, 5.0, 0.0, 0.0);
    let hit_minus = approx(first, -5.0, 0.0, 0.0) || approx(second, -5.0, 0.0, 0.0);
    assert!(
        hit_plus && hit_minus,
        "expected (±5, 0, 0), got {first} and {second}"
    );
}

#[test]
fn near_parallel_circles_keep_their_two_hits() {
    let center = Point3::from_mm(10.0, 20.0, 30.0);
    let flat = circle(xy_plane(center), 5.0);
    let theta: f64 = 1e-8;
    let tilted = circle(
        Plane3::new_unchecked(
            center,
            UnitVec3::x_axis(),
            UnitVec3::new_unchecked(0.0, theta.cos(), theta.sin()),
        ),
        5.0,
    );
    let IntersectionSet3::Two(p, q) = intersect_curves_3(&flat, &tilted, TOL) else {
        panic!("circles tilted by 1e-8 rad still cross twice on the shared x-axis");
    };
    let near = |pt: Point3, x: f64| {
        let (px, py, pz) = pt.coords_mm();
        (px - x).abs() < 1e-6 && (py - 20.0).abs() < 1e-6 && (pz - 30.0).abs() < 1e-6
    };
    assert!(
        (near(p, 15.0) && near(q, 5.0)) || (near(p, 5.0) && near(q, 15.0)),
        "expected (15,20,30) and (5,20,30), got {p} and {q}"
    );
}

#[test]
fn coplanar_same_circle_is_coincident() {
    let original = circle(xy_plane(Point3::origin()), 3.0);
    let duplicate = circle(xy_plane(Point3::origin()), 3.0);
    assert_eq!(
        intersect_curves_3(&original, &duplicate, TOL),
        IntersectionSet3::Coincident
    );
}

#[test]
fn coincident_arcs_with_opposite_normals_share_both_endpoints() {
    let upper = arc(xy_plane(Point3::origin()), 3.0, 0.0, PI);
    let lower = arc(
        Plane3::new_unchecked(
            Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::new_unchecked(0.0, -1.0, 0.0),
        ),
        3.0,
        0.0,
        PI,
    );
    let IntersectionSet3::Two(p, q) = intersect_curves_3(&upper, &lower, TOL) else {
        panic!("two half-circles on the same circle meet at both shared endpoints");
    };
    let hit_plus = approx(p, 3.0, 0.0, 0.0) || approx(q, 3.0, 0.0, 0.0);
    let hit_minus = approx(p, -3.0, 0.0, 0.0) || approx(q, -3.0, 0.0, 0.0);
    assert!(
        hit_plus && hit_minus,
        "expected (±3, 0, 0), got {p} and {q}"
    );
}

#[test]
fn coincident_arcs_with_one_dimensional_overlap_are_coincident() {
    let lead = arc(xy_plane(Point3::origin()), 3.0, 0.0, PI);
    let trail = arc(xy_plane(Point3::origin()), 3.0, FRAC_PI_2, PI);
    assert_eq!(
        intersect_curves_3(&lead, &trail, TOL),
        IntersectionSet3::Coincident
    );
}

#[test]
fn coincident_arcs_abutting_at_one_endpoint_meet_once() {
    let lead = arc(xy_plane(Point3::origin()), 3.0, 0.0, PI);
    let trail = arc(xy_plane(Point3::origin()), 3.0, PI, FRAC_PI_2);
    let IntersectionSet3::One(touch) = intersect_curves_3(&lead, &trail, TOL) else {
        panic!("arcs abutting only at angle pi meet exactly once");
    };
    assert!(
        approx(touch, -3.0, 0.0, 0.0),
        "expected (-3, 0, 0), got {touch}"
    );
}
