use bone_document::{
    DimensionKind, DimensionValue, Document, EditOutcome, EvaluatedSketch, FeatureNode, Sketch,
    SketchDimension, SketchEdit, SketchEntity, SketchRelation, evaluate_extrude, evaluate_sketch,
};
use bone_kernel::{
    BrepEdge, BrepFace, BrepSolid, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature,
    ExtrudeSense, MergeResult,
};
use bone_types::{
    DocumentId, EdgeLabel, FaceLabel, FeatureId, Length, Point2, Point3, PositiveLength,
    SketchDimensionId, SketchId, SketchPlaneBasis, Tolerance, UnitVec3, millimeter,
};
use slotmap::KeyData;

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

fn plane() -> SketchPlaneBasis {
    let Ok(basis) = SketchPlaneBasis::new(
        Point3::origin(),
        UnitVec3::x_axis(),
        UnitVec3::y_axis(),
        Tolerance::new(1e-9),
    ) else {
        panic!("xy plane");
    };
    basis
}

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

fn sketch_id(idx: u32) -> SketchId {
    SketchId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

fn document_id(idx: u32) -> DocumentId {
    DocumentId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

fn feature_id(idx: u32) -> FeatureId {
    FeatureId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

fn horizontal_line_sketch(dim_mm: f64) -> Sketch {
    let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
        SketchEntity::point(Point2::from_mm(0.0, 0.0)),
    )) else {
        panic!("a");
    };
    let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
        Point2::from_mm(1.0, 0.0),
    ))) else {
        panic!("b");
    };
    let Ok((s, EditOutcome::Entity(line))) =
        s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
    else {
        panic!("line");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Fix(a))) else {
        panic!("fix");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Horizontal(line))) else {
        panic!("horizontal");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
        a,
        b,
        value: mm(dim_mm),
        kind: DimensionKind::Driving,
    })) else {
        panic!("dim");
    };
    s
}

fn conflicting_sketch() -> Sketch {
    let Ok((s, EditOutcome::Entity(a))) = Sketch::new(plane()).apply(SketchEdit::AddEntity(
        SketchEntity::point(Point2::from_mm(0.0, 0.0)),
    )) else {
        panic!("a");
    };
    let Ok((s, EditOutcome::Entity(b))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
        Point2::from_mm(1.0, 0.0),
    ))) else {
        panic!("b");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddRelation(SketchRelation::Fix(a))) else {
        panic!("fix");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
        a,
        b,
        value: mm(5.0),
        kind: DimensionKind::Driving,
    })) else {
        panic!("dim1");
    };
    let Ok((s, _)) = s.apply(SketchEdit::AddDimension(SketchDimension::Linear {
        a,
        b,
        value: mm(10.0),
        kind: DimensionKind::Driving,
    })) else {
        panic!("dim2");
    };
    s
}

#[test]
fn evaluate_sketch_returns_solved_variant_on_success() {
    let sketch = horizontal_line_sketch(10.0);
    let output = evaluate_sketch(&sketch);
    let EvaluatedSketch::Solved(solved) = output else {
        panic!("expected Solved, got {output:?}");
    };
    let order = solved.entity_order();
    let b = order[1];
    let SketchEntity::Point(p) = solved.entities()[b] else {
        panic!("b is point");
    };
    let (x, y) = p.at().coords_mm();
    assert!((x - 10.0).abs() < 1e-9, "x = {x}");
    assert!(y.abs() < 1e-9, "y = {y}");
}

#[test]
fn evaluate_sketch_returns_failed_variant_on_conflict() {
    let sketch = conflicting_sketch();
    let output = evaluate_sketch(&sketch);
    assert!(
        matches!(output, EvaluatedSketch::Failed(_)),
        "expected Failed, got {output:?}"
    );
}

#[test]
fn document_resolves_feature_id_back_to_sketch() {
    let mut doc = Document::new(document_id(1), "d".to_owned());
    let sid = sketch_id(7);
    doc.insert_sketch(sid, "S".to_owned(), horizontal_line_sketch(10.0));
    let feature = doc.feature_tree().iter().find_map(|(fid, node)| {
        matches!(node, FeatureNode::Sketch(id) if id == sid).then_some(fid)
    });
    let Some(fid) = feature else {
        panic!("sketch feature present");
    };
    let Some(sketch) = doc.sketch_of_feature(fid) else {
        panic!("sketch addressable through feature id");
    };
    let Some(direct) = doc.sketch(sid) else {
        panic!("sketch in map");
    };
    assert_eq!(sketch, direct);
}

#[test]
fn removed_feature_id_is_never_reused() {
    let mut doc = Document::new(document_id(1), "d".to_owned());
    let sid_a = sketch_id(10);
    let sid_b = sketch_id(11);
    doc.insert_sketch(sid_a, "A".to_owned(), horizontal_line_sketch(10.0));
    let Some(fid_a) = doc.feature_tree().iter().find_map(|(fid, node)| {
        matches!(node, FeatureNode::Sketch(id) if id == sid_a).then_some(fid)
    }) else {
        panic!("A feature present");
    };

    let Some(sketch_a) = doc.sketch(sid_a) else {
        panic!("A in map");
    };
    let EvaluatedSketch::Solved(solved_a) = evaluate_sketch(sketch_a) else {
        panic!("A solves");
    };

    doc.remove_sketch(sid_a);
    doc.insert_sketch(sid_b, "B".to_owned(), horizontal_line_sketch(20.0));
    let Some(fid_b) = doc.feature_tree().iter().find_map(|(fid, node)| {
        matches!(node, FeatureNode::Sketch(id) if id == sid_b).then_some(fid)
    }) else {
        panic!("B feature present");
    };
    assert_ne!(
        fid_a, fid_b,
        "a removed feature id is never handed to a later feature"
    );

    let Some(sketch_b) = doc.sketch(sid_b) else {
        panic!("B in map");
    };
    let EvaluatedSketch::Solved(solved_b) = evaluate_sketch(sketch_b) else {
        panic!("B solves");
    };
    assert_ne!(
        solved_a, solved_b,
        "distinct feature ids cache distinct geometry"
    );
}

fn dimensioned_rectangle(width_mm: f64, height_mm: f64) -> (Sketch, SketchDimensionId) {
    let Ok((with_points, points)) = Sketch::new(plane()).apply_all(vec![
        SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))),
        SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(7.0, 0.5))),
        SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(7.5, 3.2))),
        SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.3, 2.9))),
    ]) else {
        panic!("corners");
    };
    let [c0, c1, c2, c3] = [0, 1, 2, 3].map(|i| entity_of(points[i]));
    let Ok((with_edges, edges)) = with_points.apply_all(vec![
        SketchEdit::AddEntity(SketchEntity::line(c0, c1, false)),
        SketchEdit::AddEntity(SketchEntity::line(c1, c2, false)),
        SketchEdit::AddEntity(SketchEntity::line(c2, c3, false)),
        SketchEdit::AddEntity(SketchEntity::line(c3, c0, false)),
    ]) else {
        panic!("edges");
    };
    let [bottom, right, top, left] = [0, 1, 2, 3].map(|i| entity_of(edges[i]));
    let Ok((constrained, outcomes)) = with_edges.apply_all(vec![
        SketchEdit::AddRelation(SketchRelation::Horizontal(bottom)),
        SketchEdit::AddRelation(SketchRelation::Horizontal(top)),
        SketchEdit::AddRelation(SketchRelation::Vertical(right)),
        SketchEdit::AddRelation(SketchRelation::Vertical(left)),
        SketchEdit::AddRelation(SketchRelation::Fix(c0)),
        SketchEdit::AddDimension(SketchDimension::Linear {
            a: c0,
            b: c1,
            value: mm(width_mm),
            kind: DimensionKind::Driving,
        }),
        SketchEdit::AddDimension(SketchDimension::Linear {
            a: c1,
            b: c2,
            value: mm(height_mm),
            kind: DimensionKind::Driving,
        }),
    ]) else {
        panic!("constraints");
    };
    let Some(width_dim) = outcomes.iter().find_map(|outcome| match outcome {
        EditOutcome::Dimension(id) => Some(*id),
        _ => None,
    }) else {
        panic!("width dimension allocated");
    };
    (constrained, width_dim)
}

fn entity_of(outcome: EditOutcome) -> bone_types::SketchEntityId {
    let EditOutcome::Entity(id) = outcome else {
        panic!("expected an entity outcome");
    };
    id
}

fn blind(depth_mm: f64) -> ExtrudeFeature {
    let Ok(depth) = PositiveLength::new(mm(depth_mm)) else {
        panic!("{depth_mm} mm is positive");
    };
    ExtrudeFeature {
        sketch: SketchId::default(),
        direction: ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        },
        end_condition: ExtrudeEndCondition::Blind { depth },
        draft: None,
        thin_wall: None,
        merge_result: MergeResult::Merge,
    }
}

fn face_labels(solid: &BrepSolid) -> Vec<FaceLabel> {
    solid.iter_faces().map(BrepFace::label).collect()
}

fn edge_labels(solid: &BrepSolid) -> Vec<EdgeLabel> {
    solid.iter_edges().map(BrepEdge::label).collect()
}

#[test]
fn evaluate_extrude_reports_failed_sketch() {
    let extrude = evaluate_extrude(
        feature_id(9),
        &evaluate_sketch(&conflicting_sketch()),
        &blind(5.0),
    );
    assert!(extrude.result().is_err());
    assert!(extrude.solid().is_none());
    assert!(extrude.generation().is_none());
}

proptest::proptest! {
    #[test]
    fn extrude_re_evaluates_under_edited_width(width_mm in 2.0f64..40.0) {
        let extrude_feature = feature_id(42);
        let feature = blind(10.0);
        let (base, width_dim) = dimensioned_rectangle(10.0, 5.0);
        let baseline = evaluate_extrude(extrude_feature, &evaluate_sketch(&base), &feature);

        let Ok((edited, _)) = base.apply(SketchEdit::UpdateDimensionValue {
            id: width_dim,
            value: DimensionValue::Length(mm(width_mm)),
        }) else {
            panic!("width edit applies");
        };
        let widened = evaluate_extrude(extrude_feature, &evaluate_sketch(&edited), &feature);

        let (Some(before), Some(after)) = (baseline.solid(), widened.solid()) else {
            panic!("both widths extrude");
        };
        proptest::prop_assert_eq!(face_labels(before), face_labels(after));
        proptest::prop_assert_eq!(edge_labels(before), edge_labels(after));
        proptest::prop_assert!(after.validate(TOL).is_ok());
        let Some(bbox) = after.bounding_box() else {
            panic!("solid has a bounding box");
        };
        let (dx, _, _) = bbox.extent().coords_mm();
        proptest::prop_assert!((dx - width_mm).abs() < 1e-6, "x-extent {} tracks width {}", dx, width_mm);
    }
}
