use bone_kernel::{
    Arc2, BrepSolid, Circle2, Curve2Kind, Curve3, EdgeCurve3, EdgePolyline, ExtrudeDirection,
    ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile, ExtrudeSense, Line2, MergeResult,
    ProfileEdge, ProfileLoop, evaluate_extrude,
};
use bone_types::{
    Angle, ChordHeightTolerance, EdgeRole, FeatureId, Length, Plane3, Point2, PositiveLength,
    SideKind, SketchEntityId, SketchId, Tolerance, UnitVec3, degree, millimeter,
};
use proptest::prelude::*;
use core::f64::consts::{FRAC_PI_2, PI};
use slotmap::{Key, SlotMap};

const TOLERANCE: Tolerance = Tolerance::new(1.0e-9);
const RIGHT_ANGLE_EPS: f64 = 1.0e-6;

fn chord() -> ChordHeightTolerance {
    ChordHeightTolerance::from_mm(0.05)
}

struct Ids {
    features: SlotMap<FeatureId, ()>,
    entities: SlotMap<SketchEntityId, ()>,
}

impl Ids {
    fn new() -> Self {
        Self {
            features: SlotMap::with_key(),
            entities: SlotMap::with_key(),
        }
    }

    fn feature(&mut self) -> FeatureId {
        self.features.insert(())
    }

    fn entity(&mut self) -> SketchEntityId {
        self.entities.insert(())
    }
}

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

fn point(x: f64, y: f64) -> Point2 {
    Point2::from_mm(x, y)
}

fn line(a: Point2, b: Point2) -> Curve2Kind {
    let Ok(segment) = Line2::new(a, b, TOLERANCE) else {
        panic!("line endpoints are distinct");
    };
    Curve2Kind::Line(segment)
}

fn circle(center: Point2, radius: f64) -> Curve2Kind {
    let Ok(disk) = Circle2::new(center, length_mm(radius), TOLERANCE) else {
        panic!("circle radius is positive");
    };
    Curve2Kind::Circle(disk)
}

fn arc(center: Point2, radius: f64, start_deg: f64, sweep_deg: f64) -> Curve2Kind {
    let Ok(segment) = Arc2::new(
        center,
        length_mm(radius),
        Angle::new::<degree>(start_deg),
        Angle::new::<degree>(sweep_deg),
        TOLERANCE,
    ) else {
        panic!("arc sweep is within range");
    };
    Curve2Kind::Arc(segment)
}

fn xy_plane() -> Plane3 {
    let Ok(plane) = Plane3::new(
        bone_types::Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        TOLERANCE,
    ) else {
        panic!("x and y axes are orthonormal");
    };
    plane
}

fn rectangle(ids: &mut Ids, x0: f64, y0: f64, width: f64, height: f64) -> ProfileLoop {
    let corners = [
        point(x0, y0),
        point(x0 + width, y0),
        point(x0 + width, y0 + height),
        point(x0, y0 + height),
    ];
    let edges = (0..4)
        .map(|index| {
            let start = corners[index];
            let end = corners[(index + 1) % 4];
            let corner = ids.entity();
            let segment = ids.entity();
            ProfileEdge::new(line(start, end), segment, corner)
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn circle_loop(ids: &mut Ids, center: Point2, radius: f64) -> ProfileLoop {
    let entity = ids.entity();
    ProfileLoop::Closed {
        curve: circle(center, radius),
        curve_entity: entity,
    }
}

fn triangle(ids: &mut Ids, corners: [Point2; 3]) -> ProfileLoop {
    let edges = (0..3)
        .map(|index| {
            let start = corners[index];
            let end = corners[(index + 1) % 3];
            ProfileEdge::new(line(start, end), ids.entity(), ids.entity())
        })
        .collect();
    ProfileLoop::Open(edges)
}

struct HalfDisk {
    arc_corner: SketchEntityId,
    arc_entity: SketchEntityId,
    line_corner: SketchEntityId,
    line_entity: SketchEntityId,
    radius_mm: f64,
}

impl HalfDisk {
    fn build(ids: &mut Ids, radius_mm: f64) -> (Self, ProfileLoop) {
        let parts = Self {
            arc_corner: ids.entity(),
            arc_entity: ids.entity(),
            line_corner: ids.entity(),
            line_entity: ids.entity(),
            radius_mm,
        };
        let curved = ProfileEdge::new(
            arc(point(0.0, 0.0), radius_mm, 0.0, 180.0),
            parts.arc_entity,
            parts.arc_corner,
        );
        let chord_edge = ProfileEdge::new(
            line(point(-radius_mm, 0.0), point(radius_mm, 0.0)),
            parts.line_entity,
            parts.line_corner,
        );
        (parts, ProfileLoop::Open(vec![curved, chord_edge]))
    }
}

fn blind(depth: f64) -> ExtrudeFeature {
    ExtrudeFeature {
        sketch: SketchId::null(),
        direction: ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        },
        end_condition: ExtrudeEndCondition::Blind {
            depth: positive(depth),
        },
        draft: None,
        thin_wall: None,
        merge_result: MergeResult::Merge,
    }
}

fn positive(depth: f64) -> PositiveLength {
    let Ok(value) = PositiveLength::new(length_mm(depth)) else {
        panic!("{depth} mm is a positive length");
    };
    value
}

fn evaluate(ids: &mut Ids, profile: &ExtrudeProfile, feature: &ExtrudeFeature) -> BrepSolid {
    let extrude = ids.feature();
    let Ok(solid) = evaluate_extrude(extrude, profile, feature) else {
        panic!("the profile and feature describe a buildable extrude");
    };
    solid
}

fn approx(a: f64, b: f64, eps: f64) -> bool {
    (a - b).abs() < eps
}

#[test]
fn unit_cube_emits_twelve_line_edges() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]);
    let solid = evaluate(&mut ids, &profile, &blind(1.0));
    let polylines = solid.edges_for_render(chord());
    assert_eq!(polylines.len(), 12);
    polylines.iter().for_each(|edge| {
        assert!(matches!(edge.curve(), EdgeCurve3::Line(_)), "cube edge is a line");
        assert!(edge.points().len() >= 2);
        let crease = edge.crease();
        assert!(
            approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS),
            "cube edge crease ≈ 90°, got {} rad",
            crease.radians()
        );
    });
}

#[test]
fn cylinder_caps_emit_circle_curves() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)],
    );
    let solid = evaluate(&mut ids, &profile, &blind(10.0));
    let polylines = solid.edges_for_render(chord());
    let kinds: Vec<&EdgeCurve3> = polylines.iter().map(EdgePolyline::curve).collect();
    assert_eq!(
        kinds.len(),
        2,
        "cylinder emits two cap edges, seam side edge is filtered"
    );
    kinds.iter().for_each(|kind| {
        assert!(matches!(kind, EdgeCurve3::Circle(_)), "cap edge is a circle");
    });
    polylines.iter().for_each(|edge| {
        let role = edge.label().role;
        assert!(
            matches!(
                role,
                EdgeRole::StartCapEdge { .. } | EdgeRole::EndCapEdge { .. }
            ),
            "cap edge role"
        );
        let crease = edge.crease();
        assert!(
            approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS),
            "cap crease ≈ 90°"
        );
        assert!(edge.points().len() > 8);
    });
}

#[test]
fn cylinder_seam_side_edge_is_suppressed() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)],
    );
    let solid = evaluate(&mut ids, &profile, &blind(10.0));
    let polylines = solid.edges_for_render(chord());
    let seam_emitted = polylines.iter().any(|edge| {
        matches!(
            edge.label().role,
            EdgeRole::SideEdge {
                side: SideKind::Seam,
                ..
            }
        )
    });
    assert!(!seam_emitted, "seam side edges are filtered from render output");
}

#[test]
fn donut_emits_four_circles() {
    let mut ids = Ids::new();
    let outer = circle_loop(&mut ids, point(0.0, 0.0), 10.0);
    let inner = circle_loop(&mut ids, point(0.0, 0.0), 4.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![outer, inner]);
    let solid = evaluate(&mut ids, &profile, &blind(6.0));
    let polylines = solid.edges_for_render(chord());
    assert_eq!(polylines.len(), 4);
    polylines.iter().for_each(|edge| {
        assert!(matches!(edge.curve(), EdgeCurve3::Circle(_)));
        let crease = edge.crease();
        assert!(approx(crease.radians(), FRAC_PI_2, RIGHT_ANGLE_EPS));
    });
}

#[test]
fn half_disk_cap_emits_line_plus_arc() {
    let mut ids = Ids::new();
    let (parts, half_disk) = HalfDisk::build(&mut ids, 5.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]);
    let solid = evaluate(&mut ids, &profile, &blind(2.0));
    let polylines = solid.edges_for_render(chord());

    let cap_kinds: Vec<&EdgeCurve3> = polylines
        .iter()
        .filter(|edge| {
            matches!(
                edge.label().role,
                EdgeRole::StartCapEdge { from } if from == parts.arc_entity || from == parts.line_entity
            ) || matches!(
                edge.label().role,
                EdgeRole::EndCapEdge { from } if from == parts.arc_entity || from == parts.line_entity
            )
        })
        .map(EdgePolyline::curve)
        .collect();
    let arc_count = cap_kinds
        .iter()
        .filter(|kind| matches!(kind, EdgeCurve3::Arc(_)))
        .count();
    let line_count = cap_kinds
        .iter()
        .filter(|kind| matches!(kind, EdgeCurve3::Line(_)))
        .count();
    assert_eq!(arc_count, 2, "two arc cap edges (start + end)");
    assert_eq!(line_count, 2, "two line cap edges (start + end)");
}

#[test]
fn half_disk_arc_centers_lie_on_cap_planes() {
    let mut ids = Ids::new();
    let depth_mm = 2.0;
    let (parts, half_disk) = HalfDisk::build(&mut ids, 5.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]);
    let solid = evaluate(&mut ids, &profile, &blind(depth_mm));
    let polylines = solid.edges_for_render(chord());

    let cap_arcs: Vec<(f64, &EdgeCurve3)> = polylines
        .iter()
        .filter_map(|edge| match edge.label().role {
            EdgeRole::StartCapEdge { from } if from == parts.arc_entity => {
                Some((0.0, edge.curve()))
            }
            EdgeRole::EndCapEdge { from } if from == parts.arc_entity => {
                Some((depth_mm, edge.curve()))
            }
            _ => None,
        })
        .collect();
    assert_eq!(cap_arcs.len(), 2, "one arc per cap");
    cap_arcs.iter().for_each(|(z_expected, curve)| {
        let EdgeCurve3::Arc(arc3) = curve else {
            panic!("cap arc lifts to Arc3");
        };
        let (cx, cy, cz) = arc3.center().coords_mm();
        assert!(approx(cx, 0.0, 1.0e-9), "arc center x");
        assert!(approx(cy, 0.0, 1.0e-9), "arc center y");
        assert!(approx(cz, *z_expected, 1.0e-9), "arc center z");
        let (nx, ny, nz) = arc3.normal().components();
        assert!(approx(nx, 0.0, 1.0e-9), "arc normal x");
        assert!(approx(ny, 0.0, 1.0e-9), "arc normal y");
        assert!(approx(nz, 1.0, 1.0e-9), "arc normal z");
        assert!(
            approx(arc3.radius().get::<millimeter>(), parts.radius_mm, 1.0e-9),
            "arc radius preserved"
        );
        assert!(approx(arc3.sweep_rad(), PI, 1.0e-9), "arc sweeps π rad");
    });
}

#[test]
fn half_disk_emits_corner_side_pillars() {
    let mut ids = Ids::new();
    let depth_mm = 2.0;
    let (_parts, half_disk) = HalfDisk::build(&mut ids, 5.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]);
    let solid = evaluate(&mut ids, &profile, &blind(depth_mm));
    let polylines = solid.edges_for_render(chord());

    let pillars: Vec<&EdgePolyline> = polylines
        .iter()
        .filter(|edge| {
            matches!(
                edge.label().role,
                EdgeRole::SideEdge {
                    side: SideKind::Corner,
                    ..
                }
            )
        })
        .collect();
    assert_eq!(pillars.len(), 2, "two corner pillars (line/arc junctions)");
    pillars.iter().for_each(|edge| {
        let EdgeCurve3::Line(segment) = edge.curve() else {
            panic!("pillar is a line");
        };
        let (sx, sy, sz) = segment.start().coords_mm();
        let (ex, ey, ez) = segment.end().coords_mm();
        assert!(approx(sx.abs(), 5.0, 1.0e-9), "pillar foot x");
        assert!(approx(sy, 0.0, 1.0e-9), "pillar foot y");
        assert!(approx(ex.abs(), 5.0, 1.0e-9), "pillar head x");
        assert!(approx(ey, 0.0, 1.0e-9), "pillar head y");
        assert!(
            approx((ez - sz).abs(), depth_mm, 1.0e-9),
            "pillar spans depth"
        );
    });
}

#[test]
fn equilateral_prism_side_creases_are_120_degrees() {
    let mut ids = Ids::new();
    let half = 0.5_f64 * 3.0_f64.sqrt();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![triangle(
            &mut ids,
            [
                point(0.0, 0.0),
                point(1.0, 0.0),
                point(0.5, half),
            ],
        )],
    );
    let solid = evaluate(&mut ids, &profile, &blind(1.0));
    let polylines = solid.edges_for_render(chord());

    let side_creases: Vec<f64> = polylines
        .iter()
        .filter(|edge| {
            matches!(
                edge.label().role,
                EdgeRole::SideEdge {
                    side: SideKind::Corner,
                    ..
                }
            )
        })
        .map(|edge| edge.crease().radians())
        .collect();
    assert_eq!(side_creases.len(), 3, "three vertical corner pillars");
    let expected_side = 2.0 * PI / 3.0;
    side_creases.iter().for_each(|value| {
        assert!(
            approx(*value, expected_side, 1.0e-9),
            "side crease ≈ 120°, got {value} rad"
        );
    });

    let cap_creases: Vec<f64> = polylines
        .iter()
        .filter(|edge| {
            matches!(
                edge.label().role,
                EdgeRole::StartCapEdge { .. } | EdgeRole::EndCapEdge { .. }
            )
        })
        .map(|edge| edge.crease().radians())
        .collect();
    assert_eq!(cap_creases.len(), 6, "three start + three end cap edges");
    cap_creases.iter().for_each(|value| {
        assert!(
            approx(*value, FRAC_PI_2, 1.0e-9),
            "cap crease ≈ 90°, got {value} rad"
        );
    });
}

#[test]
fn edges_for_render_is_deterministic() {
    let mut ids = Ids::new();
    let cube = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 1.0, 1.0)]);
    let cube_solid = evaluate(&mut ids, &cube, &blind(1.0));
    let first = cube_solid.edges_for_render(chord());
    let second = cube_solid.edges_for_render(chord());
    assert_eq!(first, second);

    let mut cyl_ids = Ids::new();
    let cyl = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut cyl_ids, point(0.0, 0.0), 5.0)],
    );
    let cyl_solid = evaluate(&mut cyl_ids, &cyl, &blind(10.0));
    let first = cyl_solid.edges_for_render(chord());
    let second = cyl_solid.edges_for_render(chord());
    assert_eq!(first, second);
}

#[test]
fn edges_for_render_is_deterministic_across_evaluations() {
    let mut first_ids = Ids::new();
    let first_profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut first_ids, point(0.0, 0.0), 5.0)],
    );
    let first_extrude = first_ids.feature();
    let Ok(first_solid) = evaluate_extrude(first_extrude, &first_profile, &blind(10.0)) else {
        panic!("first evaluation succeeds");
    };

    let mut second_ids = Ids::new();
    let second_profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut second_ids, point(0.0, 0.0), 5.0)],
    );
    let second_extrude = second_ids.feature();
    let Ok(second_solid) = evaluate_extrude(second_extrude, &second_profile, &blind(10.0)) else {
        panic!("second evaluation succeeds");
    };

    assert_eq!(
        first_solid.edges_for_render(chord()),
        second_solid.edges_for_render(chord()),
        "two evaluations of the same profile + feature produce identical render polylines",
    );
}

#[test]
fn polylines_carry_label_and_crease_for_each_edge() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 2.0, 3.0)]);
    let solid = evaluate(&mut ids, &profile, &blind(4.0));
    let polylines = solid.edges_for_render(chord());
    polylines.iter().for_each(|edge| {
        let id = edge.edge();
        let label = edge.label();
        let crease = edge.crease();
        assert_ne!(id, bone_types::BrepEdgeId::default());
        assert!(label.feature != FeatureId::null());
        assert!(crease.radians().is_finite());
    });
}

#[test]
fn polyline_endpoints_align_with_vertices_order() {
    let mut ids = Ids::new();
    let (_parts, half_disk) = HalfDisk::build(&mut ids, 5.0);
    let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]);
    let solid = evaluate(&mut ids, &profile, &blind(2.0));
    let positions: std::collections::HashMap<bone_types::BrepVertexId, bone_types::Point3> = solid
        .iter_vertices()
        .map(|v| (v.id(), v.position()))
        .collect();
    solid.iter_edges().for_each(|edge| {
        let [start_id, end_id] = edge.vertices();
        if start_id == end_id {
            return;
        }
        let Some(start_pos) = positions.get(&start_id) else {
            return;
        };
        let Some(end_pos) = positions.get(&end_id) else {
            return;
        };
        let curve = edge.curve();
        let curve_start = curve.evaluate(bone_types::Parameter::new(0.0));
        let curve_end = curve.evaluate(bone_types::Parameter::new(1.0));
        let (sx, sy, sz) = start_pos.coords_mm();
        let (cx, cy, cz) = curve_start.coords_mm();
        assert!(
            approx(sx, cx, 1.0e-9) && approx(sy, cy, 1.0e-9) && approx(sz, cz, 1.0e-9),
            "vertices[0] should sit at curve.evaluate(0)"
        );
        let (ex, ey, ez) = end_pos.coords_mm();
        let (kx, ky, kz) = curve_end.coords_mm();
        assert!(
            approx(ex, kx, 1.0e-9) && approx(ey, ky, 1.0e-9) && approx(ez, kz, 1.0e-9),
            "vertices[1] should sit at curve.evaluate(1)"
        );
    });
    let render = solid.edges_for_render(chord());
    let edges_by_id: std::collections::HashMap<_, _> = solid
        .iter_edges()
        .map(|edge| (edge.id(), edge))
        .collect();
    render.iter().for_each(|polyline| {
        let edge = edges_by_id[&polyline.edge()];
        let [start_id, _] = edge.vertices();
        if let Some(start_pos) = positions.get(&start_id) {
            let first = polyline.points()[0];
            let (sx, sy, sz) = start_pos.coords_mm();
            let (fx, fy, fz) = first.coords_mm();
            assert!(
                approx(sx, fx, 1.0e-9) && approx(sy, fy, 1.0e-9) && approx(sz, fz, 1.0e-9),
                "polyline.points()[0] sits at vertices[0]"
            );
        }
    });
}

proptest! {
    #[test]
    fn edges_for_render_is_deterministic_across_rectangles(
        width_mm in 0.5f64..=20.0f64,
        height_mm in 0.5f64..=20.0f64,
        depth_mm in 0.5f64..=20.0f64,
    ) {
        let mut ids = Ids::new();
        let profile = ExtrudeProfile::new(
            xy_plane(),
            vec![rectangle(&mut ids, 0.0, 0.0, width_mm, height_mm)],
        );
        let solid = evaluate(&mut ids, &profile, &blind(depth_mm));
        let first = solid.edges_for_render(chord());
        let second = solid.edges_for_render(chord());
        prop_assert_eq!(first, second);
    }

    #[test]
    fn edges_for_render_is_deterministic_across_cylinders(
        radius_mm in 0.5f64..=20.0f64,
        depth_mm in 0.5f64..=20.0f64,
    ) {
        let mut ids = Ids::new();
        let profile = ExtrudeProfile::new(
            xy_plane(),
            vec![circle_loop(&mut ids, point(0.0, 0.0), radius_mm)],
        );
        let solid = evaluate(&mut ids, &profile, &blind(depth_mm));
        let first = solid.edges_for_render(chord());
        let second = solid.edges_for_render(chord());
        prop_assert_eq!(first, second);
    }

    #[test]
    fn edges_for_render_is_deterministic_across_half_disks(
        radius_mm in 1.0f64..=10.0f64,
        depth_mm in 0.5f64..=10.0f64,
    ) {
        let mut ids = Ids::new();
        let (_parts, half_disk) = HalfDisk::build(&mut ids, radius_mm);
        let profile = ExtrudeProfile::new(xy_plane(), vec![half_disk]);
        let solid = evaluate(&mut ids, &profile, &blind(depth_mm));
        let first = solid.edges_for_render(chord());
        let second = solid.edges_for_render(chord());
        prop_assert_eq!(first, second);
    }
}
