use bone_kernel::{
    BrepError, BrepFace, BrepSolid, Circle2, Curve2Kind, DraftAngle, DraftDirection,
    DraftMagnitude, ExtrudeEndCondition, ExtrudeFeature, ExtrudeProfile, Line2, ProfileDefect,
    ProfileEdge, ProfileLoop, ThinWall, ThinWallDirection, evaluate_extrude,
};
use bone_types::{
    Angle, AngleTolerance, ChordHeightTolerance, FeatureId, Length, Plane3, Point2, PositiveLength,
    SketchEntityId, SketchId, Tolerance, UnitVec3, degree, millimeter,
};
use core::f64::consts::PI;
use slotmap::{Key, SlotMap};

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

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 mm(value: f64) -> Length {
    Length::new::<millimeter>(value)
}

fn ok<T, E: core::fmt::Debug>(result: Result<T, E>) -> T {
    match result {
        Ok(value) => value,
        Err(error) => panic!("{error:?}"),
    }
}

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

fn line(a: Point2, b: Point2) -> Curve2Kind {
    Curve2Kind::Line(ok(Line2::new(a, b, TOLERANCE)))
}

fn xy_plane() -> Plane3 {
    ok(Plane3::new(
        bone_types::Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        TOLERANCE,
    ))
}

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| {
            ProfileEdge::new(
                line(corners[index], corners[(index + 1) % 4]),
                ids.entity(),
                ids.entity(),
            )
        })
        .collect();
    ProfileLoop::Open(edges)
}

fn circle_loop(ids: &mut Ids, center: Point2, radius: f64) -> ProfileLoop {
    ProfileLoop::Closed {
        curve: Curve2Kind::Circle(ok(Circle2::new(center, mm(radius), TOLERANCE))),
        curve_entity: ids.entity(),
    }
}

fn positive(value: f64) -> PositiveLength {
    ok(PositiveLength::new(mm(value)))
}

fn blind(depth: f64) -> ExtrudeFeature {
    ExtrudeFeature::blind(SketchId::null(), positive(depth))
}

fn draft_angle(degrees: f64, direction: DraftDirection) -> DraftAngle {
    DraftAngle::new(
        ok(DraftMagnitude::new(Angle::new::<degree>(degrees))),
        direction,
    )
}

fn thin(direction: ThinWallDirection, thickness: f64) -> ThinWall {
    ThinWall {
        thickness: positive(thickness),
        direction,
        thickness2: None,
        cap_ends: false,
        auto_fillet: None,
    }
}

fn build(ids: &mut Ids, profile: &ExtrudeProfile, feature: &ExtrudeFeature) -> BrepSolid {
    let extrude = ids.feature();
    let solid = ok(evaluate_extrude(extrude, profile, feature));
    assert!(
        solid.validate(TOLERANCE).is_ok(),
        "result is a closed solid"
    );
    solid
}

fn mesh_volume(solid: &BrepSolid) -> f64 {
    let mesh = ok(solid.tessellate(
        ChordHeightTolerance::from_mm(0.001),
        AngleTolerance::from_radians(0.02),
    ));
    mesh.faces()
        .iter()
        .flat_map(|slab| {
            let positions = slab.positions().to_vec();
            slab.triangles().iter().map(move |triangle| {
                let p = |index: u32| positions[index as usize].coords_mm();
                let (ax, ay, az) = p(triangle[0]);
                let (bx, by, bz) = p(triangle[1]);
                let (cx, cy, cz) = p(triangle[2]);
                let cross = (by * cz - bz * cy, bz * cx - bx * cz, bx * cy - by * cx);
                (ax * cross.0 + ay * cross.1 + az * cross.2) / 6.0
            })
        })
        .sum()
}

fn face_count(solid: &BrepSolid) -> usize {
    solid.iter_faces().count()
}

fn dump(solid: &BrepSolid) -> String {
    let section = |entries: Vec<String>| entries.join("\n");
    let faces = section(
        solid
            .iter_faces()
            .map(|face| format!("  {}", face.label()))
            .collect(),
    );
    let edges = section(
        solid
            .iter_edges()
            .map(|edge| format!("  {}", edge.label()))
            .collect(),
    );
    let vertices = section(
        solid
            .iter_vertices()
            .map(|vertex| format!("  {}", vertex.label()))
            .collect(),
    );
    format!(
        "faces:\n{faces}\nedges:\n{edges}\nvertices:\n{vertices}\ncounts: faces={} edges={} vertices={}",
        solid.iter_faces().count(),
        solid.iter_edges().count(),
        solid.iter_vertices().count(),
    )
}

#[test]
fn thin_outward_rectangle_is_a_square_tube() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        thin_wall: Some(thin(ThinWallDirection::Outward, 2.0)),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    assert!(
        (mesh_volume(&solid) - 480.0).abs() < 1.0e-4,
        "wall area (14^2 - 10^2) times depth 5",
    );
}

#[test]
fn thin_inward_rectangle_carves_the_interior() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        thin_wall: Some(thin(ThinWallDirection::Inward, 2.0)),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    assert!(
        (mesh_volume(&solid) - 320.0).abs() < 1.0e-4,
        "wall area (10^2 - 6^2) times depth 5",
    );
}

#[test]
fn thin_midplane_rectangle_splits_thickness_either_side() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        thin_wall: Some(thin(ThinWallDirection::MidPlane, 2.0)),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    assert!(
        (mesh_volume(&solid) - 400.0).abs() < 1.0e-4,
        "wall area (11^2 - 9^2) times depth 5",
    );
}

#[test]
fn thin_circle_is_a_round_tube() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)],
    );
    let feature = ExtrudeFeature {
        thin_wall: Some(thin(ThinWallDirection::Inward, 1.0)),
        ..blind(4.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let expected = PI * (25.0 - 16.0) * 4.0;
    assert!(
        (mesh_volume(&solid) - expected).abs() < 0.5,
        "annulus pi*(5^2-4^2) times depth 4, got {}",
        mesh_volume(&solid),
    );
}

#[test]
fn draft_outward_square_is_a_truncated_pyramid() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        draft: Some(draft_angle(45.0, DraftDirection::Outward)),
        ..blind(6.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let volume = mesh_volume(&solid);
    assert!(
        volume > 0.0,
        "outward normals give positive volume, got {volume}"
    );
    assert_eq!(face_count(&solid), 6, "four drafted walls plus two caps");
    assert!(
        (volume - 1608.0).abs() < 1.0e-3,
        "frustum h/3*(a^2+ab+b^2) with a=10,b=22,h=6, got {volume}",
    );
}

#[test]
fn draft_inward_square_narrows_toward_the_top() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 20.0, 20.0)]);
    let feature = ExtrudeFeature {
        draft: Some(draft_angle(45.0, DraftDirection::Inward)),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let volume = mesh_volume(&solid);
    assert!(
        (volume - 1166.66667).abs() < 1.0e-3,
        "frustum with a=20,b=10,h=5, got {volume}",
    );
}

#[test]
fn draft_outward_circle_is_a_cone_frustum() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut ids, point(0.0, 0.0), 10.0)],
    );
    let feature = ExtrudeFeature {
        draft: Some(draft_angle(45.0, DraftDirection::Outward)),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let volume = mesh_volume(&solid);
    let expected = PI * 5.0 / 3.0 * (100.0 + 150.0 + 225.0);
    assert!(
        (volume - expected).abs() < 1.0,
        "cone frustum pi*h/3*(R^2+Rr+r^2), got {volume} expected {expected}",
    );
}

#[test]
fn draft_is_deterministic() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        draft: Some(draft_angle(10.0, DraftDirection::Outward)),
        ..blind(4.0)
    };
    let extrude = ids.feature();
    let first = ok(evaluate_extrude(extrude, &profile, &feature));
    let second = ok(evaluate_extrude(extrude, &profile, &feature));
    assert_eq!(
        first.iter_faces().map(BrepFace::label).collect::<Vec<_>>(),
        second.iter_faces().map(BrepFace::label).collect::<Vec<_>>(),
    );
    assert!((mesh_volume(&first) - mesh_volume(&second)).abs() < 1.0e-9);
}

#[test]
fn draft_walls_carry_distinct_side_labels() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        draft: Some(draft_angle(20.0, DraftDirection::Outward)),
        ..blind(4.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let labels: Vec<_> = solid.iter_faces().map(BrepFace::label).collect();
    let unique: std::collections::HashSet<_> = labels.iter().collect();
    assert_eq!(
        unique.len(),
        labels.len(),
        "every drafted face is uniquely named"
    );
}

#[test]
fn midplane_draft_is_a_double_frustum() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        end_condition: ExtrudeEndCondition::MidPlane {
            depth: positive(6.0),
        },
        draft: Some(draft_angle(45.0, DraftDirection::Outward)),
        ..blind(6.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let volume = mesh_volume(&solid);
    assert_eq!(
        face_count(&solid),
        10,
        "four lower walls, four upper walls, two caps",
    );
    assert!(
        (volume - 1032.0).abs() < 1.0e-3,
        "two frustums of 3/3*(10^2+10*16+16^2) about a side-10 waist, got {volume}",
    );
    let extrude = ids.feature();
    let first = ok(evaluate_extrude(extrude, &profile, &feature));
    let second = ok(evaluate_extrude(extrude, &profile, &feature));
    assert_eq!(
        dump(&first),
        dump(&second),
        "double frustum is deterministic"
    );
}

#[test]
fn direction2_independent_drafts_build_a_dual_taper() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        direction2: Some(bone_kernel::Direction2 {
            end_condition: ExtrudeEndCondition::Blind {
                depth: positive(2.0),
            },
            draft: Some(draft_angle(45.0, DraftDirection::Outward)),
        }),
        draft: Some(draft_angle(45.0, DraftDirection::Outward)),
        ..blind(4.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let volume = mesh_volume(&solid);
    let forward = 4.0 / 3.0 * (100.0 + 10.0 * 18.0 + 18.0 * 18.0);
    let backward = 2.0 / 3.0 * (100.0 + 10.0 * 14.0 + 14.0 * 14.0);
    let expected = forward + backward;
    assert!(
        (volume - expected).abs() < 1.0e-3,
        "forward frustum (b=18) plus backward frustum (b=14) about a side-10 waist, got {volume} expected {expected}",
    );
}

#[test]
fn thin_with_a_hole_walls_both_boundaries() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![
            rectangle(&mut ids, 0.0, 0.0, 20.0, 20.0),
            rectangle(&mut ids, 8.0, 8.0, 4.0, 4.0),
        ],
    );
    let feature = ExtrudeFeature {
        thin_wall: Some(thin(ThinWallDirection::Inward, 1.0)),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let volume = mesh_volume(&solid);
    let expected = ((400.0 - 324.0) + (36.0 - 16.0)) * 5.0;
    assert!(
        (volume - expected).abs() < 1.0e-3,
        "outer band (20^2-18^2) plus hole band (6^2-4^2) times depth 5, got {volume}",
    );
    let extrude = ids.feature();
    let first = ok(evaluate_extrude(extrude, &profile, &feature));
    let second = ok(evaluate_extrude(extrude, &profile, &feature));
    assert_eq!(
        dump(&first),
        dump(&second),
        "thin with holes is deterministic"
    );
}

#[test]
fn thin_cap_ends_seals_a_hollow_box() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let mut wall = thin(ThinWallDirection::Outward, 2.0);
    wall.cap_ends = true;
    let feature = ExtrudeFeature {
        thin_wall: Some(wall),
        ..blind(10.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let volume = mesh_volume(&solid);
    let expected = 14.0 * 14.0 * 10.0 - 10.0 * 10.0 * 6.0;
    assert!(
        (volume - expected).abs() < 1.0e-3,
        "14x14x10 outer box less a 10x10x6 internal cavity, got {volume}",
    );
    let extrude = ids.feature();
    let first = ok(evaluate_extrude(extrude, &profile, &feature));
    let second = ok(evaluate_extrude(extrude, &profile, &feature));
    assert_eq!(
        dump(&first),
        dump(&second),
        "capped thin box is deterministic"
    );
}

#[test]
fn thin_auto_fillet_rounds_the_wall_corners() {
    use core::f64::consts::PI;
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let mut wall = thin(ThinWallDirection::Outward, 2.0);
    wall.auto_fillet = Some(positive(2.0));
    let feature = ExtrudeFeature {
        thin_wall: Some(wall),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    let volume = mesh_volume(&solid);
    let expected = (48.0 + 12.0 * PI) * 5.0;
    assert!(
        (volume - expected).abs() < 0.5,
        "rounded wall area (48 + 12*pi) times depth 5, got {volume} expected {expected}",
    );
    assert!(
        face_count(&solid) > 10,
        "rounded corners add cylindrical wall faces, got {}",
        face_count(&solid),
    );
}

#[test]
fn thin_auto_fillet_on_a_curved_wall_is_refused() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(
        xy_plane(),
        vec![circle_loop(&mut ids, point(0.0, 0.0), 5.0)],
    );
    let mut wall = thin(ThinWallDirection::Outward, 2.0);
    wall.auto_fillet = Some(positive(2.0));
    let feature = ExtrudeFeature {
        thin_wall: Some(wall),
        ..blind(5.0)
    };
    let extrude = ids.feature();
    assert!(
        evaluate_extrude(extrude, &profile, &feature).is_err(),
        "a circular wall has no straight corners to fillet, so auto-fillet is refused",
    );
}

fn dumbbell(ids: &mut Ids) -> ProfileLoop {
    let corners = [
        point(0.0, 0.0),
        point(4.0, 0.0),
        point(4.0, 3.0),
        point(6.0, 3.0),
        point(6.0, 0.0),
        point(10.0, 0.0),
        point(10.0, 8.0),
        point(6.0, 8.0),
        point(6.0, 5.0),
        point(4.0, 5.0),
        point(4.0, 8.0),
        point(0.0, 8.0),
    ];
    let edges = (0..corners.len())
        .map(|index| {
            ProfileEdge::new(
                line(corners[index], corners[(index + 1) % corners.len()]),
                ids.entity(),
                ids.entity(),
            )
        })
        .collect();
    ProfileLoop::Open(edges)
}

#[test]
fn a_draft_that_folds_a_thin_neck_is_rejected() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![dumbbell(&mut ids)]);
    let feature = ExtrudeFeature {
        draft: Some(draft_angle(20.0, DraftDirection::Inward)),
        ..blind(5.0)
    };
    let extrude = ids.feature();
    assert!(
        matches!(
            evaluate_extrude(extrude, &profile, &feature),
            Err(BrepError::InvalidProfile {
                reason: ProfileDefect::SelfIntersectingLoop
            })
        ),
        "a drafted top whose thin neck folds past itself must be refused",
    );
}

#[test]
fn drafted_square_label_snapshot() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        draft: Some(draft_angle(15.0, DraftDirection::Outward)),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    insta::assert_snapshot!(dump(&solid));
}

#[test]
fn thin_square_label_snapshot() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        thin_wall: Some(thin(ThinWallDirection::Inward, 2.0)),
        ..blind(5.0)
    };
    let solid = build(&mut ids, &profile, &feature);
    insta::assert_snapshot!(dump(&solid));
}

proptest::proptest! {
    #[test]
    fn drafted_boxes_are_deterministic(
        side in 4.0f64..40.0,
        depth in 1.0f64..20.0,
        draft_deg in 1.0f64..30.0,
    ) {
        let mut ids = Ids::new();
        let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, side, side)]);
        let feature = ExtrudeFeature {
            draft: Some(draft_angle(draft_deg, DraftDirection::Outward)),
            ..blind(depth)
        };
        let extrude = ids.feature();
        let first = ok(evaluate_extrude(extrude, &profile, &feature));
        let second = ok(evaluate_extrude(extrude, &profile, &feature));
        proptest::prop_assert_eq!(dump(&first), dump(&second));
    }
}

proptest::proptest! {
    #[test]
    fn thin_boxes_are_deterministic(
        side in 8.0f64..40.0,
        depth in 1.0f64..20.0,
        thickness in 0.5f64..3.0,
    ) {
        let mut ids = Ids::new();
        let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, side, side)]);
        let feature = ExtrudeFeature {
            thin_wall: Some(thin(ThinWallDirection::Inward, thickness)),
            ..blind(depth)
        };
        let extrude = ids.feature();
        let first = ok(evaluate_extrude(extrude, &profile, &feature));
        let second = ok(evaluate_extrude(extrude, &profile, &feature));
        proptest::prop_assert_eq!(dump(&first), dump(&second));
    }
}

#[test]
fn over_thick_inward_thin_collapses() {
    let mut ids = Ids::new();
    let profile = ExtrudeProfile::new(xy_plane(), vec![rectangle(&mut ids, 0.0, 0.0, 10.0, 10.0)]);
    let feature = ExtrudeFeature {
        thin_wall: Some(thin(ThinWallDirection::Inward, 6.0)),
        ..blind(5.0)
    };
    let extrude = ids.feature();
    assert!(
        evaluate_extrude(extrude, &profile, &feature).is_err(),
        "a 6 mm inward wall on a 10 mm square self-overlaps",
    );
}
