use bone_document::{
    Document, DocumentFolder, DocumentHeader, ExtrudeFile, Sketch3, Sketch3Entity, Sketch3Relation,
    SketchFile, save, to_string,
};
use bone_kernel::ExtrudeFeature;
use bone_types::{
    Angle, DocumentId, Length, 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 add_point(s: &Sketch3, x: f64, y: f64) -> (Sketch3, SketchEntityId) {
    s.with_entity(Sketch3Entity::point(Point3::from_mm(x, y, 0.0)))
}

fn build_full_sketch() -> Sketch3 {
    let n = UnitVec3::z_axis();
    let (s, p0) = add_point(&Sketch3::on_plane(plane()), 0.0, 0.0);
    let (s, p1) = add_point(&s, 10.0, 0.0);
    let (s, p2) = add_point(&s, 10.0, 5.0);
    let (s, p3) = add_point(&s, 0.0, 5.0);
    let (s, major) = add_point(&s, 14.0, 5.0);
    let (s, minor) = add_point(&s, 10.0, 7.0);
    let (s, ctrl) = add_point(&s, 2.0, 8.0);
    let (s, l0) = s.with_entity(Sketch3Entity::line(p0, p1, false));
    let (s, l1) = s.with_entity(Sketch3Entity::line(p1, p2, false));
    let (s, l2) = s.with_entity(Sketch3Entity::line(p2, p3, false));
    let (s, _l3) = s.with_entity(Sketch3Entity::line(p3, p0, true));
    let (s, arc) = s.with_entity(Sketch3Entity::arc(p0, p1, p2, n, false));
    let (s, circle) = s.with_entity(Sketch3Entity::circle(p0, mm(2.5), n, false));
    let (s, _ellipse) = s.with_entity(Sketch3Entity::ellipse(p2, major, minor, n, false));
    let (s, _spline) = s.with_entity(Sketch3Entity::spline(vec![p0, ctrl, p2], true, false));

    let relations = [
        Sketch3Relation::Coincident(p0, p3),
        Sketch3Relation::OnPlane {
            point: p0,
            normal: n,
            offset: mm(0.0),
        },
        Sketch3Relation::AlongAxis {
            line: l0,
            axis: UnitVec3::x_axis(),
        },
        Sketch3Relation::Perpendicular(l0, l1),
        Sketch3Relation::Parallel(l0, l2),
        Sketch3Relation::Tangent(l0, circle),
        Sketch3Relation::Equal(l0, l2),
        Sketch3Relation::Concentric(arc, circle),
        Sketch3Relation::Midpoint {
            point: p2,
            line: l0,
        },
        Sketch3Relation::PointOnLine {
            point: p3,
            line: l1,
        },
        Sketch3Relation::Symmetric {
            a: p1,
            b: p3,
            axis: l0,
        },
        Sketch3Relation::Distance {
            a: p0,
            b: p1,
            value: mm(10.0),
            reference: false,
        },
        Sketch3Relation::Angle {
            a: l0,
            b: l1,
            value: deg(90.0),
            reference: false,
        },
        Sketch3Relation::Radius {
            target: circle,
            value: mm(2.5),
            reference: false,
        },
        Sketch3Relation::Diameter {
            target: circle,
            value: mm(5.0),
            reference: true,
        },
        Sketch3Relation::Fix(p0),
    ];
    relations
        .into_iter()
        .fold(s, |acc, relation| acc.with_relation(relation).0)
}

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::blind(sketch_id(7), depth),
        "Extrude1".to_owned(),
    );
    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());
}
