use bone_document::{
    DimensionKind, Document, DocumentFolder, DocumentHeader, ExtrudeFile, Sketch, SketchDimension,
    SketchEdit, SketchEntity, SketchFile, SketchRelation, save, to_string,
};
use bone_kernel::{
    ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult,
};
use bone_types::{
    Angle, DocumentId, Length, Point2, Point3, PositiveLength, SketchEntityId, SketchId,
    SketchPlaneBasis, Tolerance, UnitVec3, degree, millimeter,
};
use slotmap::KeyData;
use tempfile::{TempDir, tempdir};

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 deg(v: f64) -> Angle {
    Angle::new::<degree>(v)
}

fn ok_dir() -> TempDir {
    let Ok(dir) = tempdir() else {
        panic!("tempdir");
    };
    dir
}

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 point_of(s: &Sketch, i: usize) -> SketchEntityId {
    s.entity_order()[i]
}

fn add_entity(s: Sketch, entity: SketchEntity) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddEntity(entity)) else {
        panic!("add entity");
    };
    next
}

fn add_relation(s: Sketch, relation: SketchRelation) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddRelation(relation)) else {
        panic!("add relation");
    };
    next
}

fn add_dimension(s: Sketch, dim: SketchDimension) -> Sketch {
    let Ok((next, _)) = s.apply(SketchEdit::AddDimension(dim)) else {
        panic!("add dim");
    };
    next
}

fn first_of_kind(s: &Sketch, kind: bone_document::SketchEntityKind) -> SketchEntityId {
    let Some(&id) = s.entity_order().iter().find(|id| {
        matches!(
            s.entities().get(**id).map(bone_document::SketchEntity::kind),
            Some(k) if k == kind
        )
    }) else {
        panic!("no entity of kind {kind:?}");
    };
    id
}

fn build_full_sketch() -> Sketch {
    let coords = [
        Point2::from_mm(0.0, 0.0),
        Point2::from_mm(10.0, 0.0),
        Point2::from_mm(10.0, 5.0),
        Point2::from_mm(0.0, 5.0),
    ];
    let with_points = coords.into_iter().fold(Sketch::new(plane()), |s, p| {
        add_entity(s, SketchEntity::point(p))
    });
    let p0 = point_of(&with_points, 0);
    let p1 = point_of(&with_points, 1);
    let p2 = point_of(&with_points, 2);
    let p3 = point_of(&with_points, 3);

    let with_lines = [
        (p0, p1, false),
        (p1, p2, false),
        (p2, p3, false),
        (p3, p0, true),
    ]
    .into_iter()
    .fold(with_points, |s, (a, b, cons)| {
        add_entity(s, SketchEntity::line(a, b, cons))
    });
    let with_arc = add_entity(with_lines, SketchEntity::arc(p0, p1, p2, false));
    let with_circle = add_entity(with_arc, SketchEntity::circle(p0, mm(2.5), false));

    let line_ids: Vec<_> = with_circle
        .entity_order()
        .iter()
        .copied()
        .filter(|id| {
            matches!(
                with_circle
                    .entities()
                    .get(*id)
                    .map(bone_document::SketchEntity::kind),
                Some(bone_document::SketchEntityKind::Line)
            )
        })
        .collect();
    let arc_id = first_of_kind(&with_circle, bone_document::SketchEntityKind::Arc);
    let circle_id = first_of_kind(&with_circle, bone_document::SketchEntityKind::Circle);

    let relations = [
        SketchRelation::Coincident(p0, line_ids[0]),
        SketchRelation::Horizontal(line_ids[0]),
        SketchRelation::Vertical(line_ids[1]),
        SketchRelation::Parallel(line_ids[0], line_ids[2]),
        SketchRelation::Perpendicular(line_ids[0], line_ids[1]),
        SketchRelation::Tangent(line_ids[0], circle_id),
        SketchRelation::Equal(line_ids[0], line_ids[2]),
        SketchRelation::Concentric(arc_id, circle_id),
        SketchRelation::Midpoint {
            point: p2,
            line: line_ids[0],
        },
        SketchRelation::Fix(p0),
    ];
    let with_relations = relations.into_iter().fold(with_circle, add_relation);

    let dimensions = [
        SketchDimension::Linear {
            a: p0,
            b: p1,
            value: mm(10.0),
            kind: DimensionKind::Driving,
        },
        SketchDimension::Radius {
            target: circle_id,
            value: mm(2.5),
            kind: DimensionKind::Driven,
        },
        SketchDimension::Diameter {
            target: circle_id,
            value: mm(5.0),
            kind: DimensionKind::Driving,
        },
        SketchDimension::Angular {
            a: line_ids[0],
            b: line_ids[1],
            value: deg(90.0),
            kind: DimensionKind::Driving,
        },
    ];
    dimensions.into_iter().fold(with_relations, add_dimension)
}

fn assert_ron<T: serde::Serialize>(value: &T) -> String {
    let Ok(ron) = to_string(value) else {
        panic!("ron");
    };
    ron
}

#[test]
fn document_header_ron_surface() {
    let mut doc = Document::new(document_id(1), "demo".to_owned());
    let sid = sketch_id(7);
    doc.insert_sketch(sid, "Sketch1".to_owned(), build_full_sketch());
    doc.set_parameter("width".to_owned(), 10.0);

    let ron = assert_ron(doc.header());
    insta::assert_snapshot!("document_header", ron);
}

#[test]
fn sketch_file_ron_surface() {
    let file = SketchFile::new(build_full_sketch());
    let ron = assert_ron(&file);
    insta::assert_snapshot!("sketch_file", ron);
}

#[test]
fn extrude_file_ron_surface() {
    let Ok(depth) = PositiveLength::new(mm(10.0)) else {
        panic!("positive depth");
    };
    let file = ExtrudeFile::new(ExtrudeFeature {
        sketch: sketch_id(7),
        direction: ExtrudeDirection::Normal {
            sense: ExtrudeSense::Forward,
        },
        end_condition: ExtrudeEndCondition::Blind { depth },
        draft: None,
        thin_wall: None,
        merge_result: MergeResult::Merge,
    });
    let ron = assert_ron(&file);
    insta::assert_snapshot!("extrude_file", ron);
}

#[test]
fn saved_folder_layout_is_stable() {
    let dir = ok_dir();
    let folder = DocumentFolder::new(dir.path().join("pinned.bone"));
    let mut doc = Document::new(document_id(1), "pinned".to_owned());
    doc.insert_sketch(sketch_id(1), "Rect".to_owned(), build_full_sketch());
    let Ok(()) = save(&doc, &folder) else {
        panic!("save");
    };

    let mut entries: Vec<String> = walk_relative(folder.path()).collect();
    entries.sort();
    let listing = entries.join("\n");
    insta::assert_snapshot!("folder_listing", listing);
}

fn walk_relative(root: &std::path::Path) -> impl Iterator<Item = String> + '_ {
    fn walk(root: &std::path::Path, path: &std::path::Path, out: &mut Vec<String>) {
        let Ok(iter) = std::fs::read_dir(path) else {
            return;
        };
        iter.flatten().for_each(|entry| {
            let p = entry.path();
            let rel = p
                .strip_prefix(root)
                .unwrap_or(&p)
                .to_string_lossy()
                .into_owned();
            if p.is_dir() {
                out.push(format!("{rel}/"));
                walk(root, &p, out);
            } else {
                out.push(rel);
            }
        });
    }
    let mut out = Vec::new();
    walk(root, root, &mut out);
    out.into_iter()
}

#[test]
fn document_header_roundtrips_through_ron() {
    let mut doc = Document::new(document_id(1), "rt".to_owned());
    doc.insert_sketch(sketch_id(1), "S".to_owned(), build_full_sketch());
    let ron = assert_ron(doc.header());
    let Ok(parsed) = bone_document::from_str::<DocumentHeader>(&ron) else {
        panic!("parse");
    };
    assert_eq!(&parsed, doc.header());
}
