use bone_document::{
    Document, DocumentFolder, Sketch3, Sketch3Edit, Sketch3Entity, Sketch3EntityKind,
    Sketch3Relation, load, save,
};
use bone_kernel::ExtrudeFeature;
use bone_types::{
    Angle, DocumentId, ExtrudeId, Length, Point3, PositiveLength, SketchId, SketchPlaneBasis,
    Tolerance, UnitVec3, degree, millimeter,
};
use proptest::prelude::*;
use slotmap::KeyData;
use tempfile::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 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 extrude_id(idx: u32) -> ExtrudeId {
    ExtrudeId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
}

#[derive(Copy, Clone, Debug)]
enum Step {
    Point(i16, i16),
    Line { ai: u8, bi: u8, cons: bool },
    Circle { ci: u8, r: u16, cons: bool },
    Arc { ci: u8, si: u8, ei: u8 },
    Coincident { pi: u8, xi: u8 },
    Parallel { ai: u8, bi: u8 },
    Perpendicular { ai: u8, bi: u8 },
    Tangent { ai: u8, bi: u8 },
    EqualLines { ai: u8, bi: u8 },
    EqualRounds { ai: u8, bi: u8 },
    Concentric { ai: u8, bi: u8 },
    Midpoint { pi: u8, li: u8 },
    Fix { ei: u8 },
    LinearDim { ai: u8, bi: u8, v: u16 },
    RadiusDim { ti: u8, v: u16 },
    DiameterDim { ti: u8, v: u16 },
    AngularDim { ai: u8, bi: u8, deg: u16 },
}

fn arb_step() -> impl Strategy<Value = Step> {
    prop_oneof![
        (any::<i16>(), any::<i16>()).prop_map(|(x, y)| Step::Point(x, y)),
        (any::<u8>(), any::<u8>(), any::<bool>()).prop_map(|(ai, bi, cons)| Step::Line {
            ai,
            bi,
            cons
        }),
        (any::<u8>(), any::<u16>(), any::<bool>()).prop_map(|(ci, r, cons)| Step::Circle {
            ci,
            r: r % 200 + 1,
            cons
        }),
        (any::<u8>(), any::<u8>(), any::<u8>()).prop_map(|(ci, si, ei)| Step::Arc { ci, si, ei }),
        (any::<u8>(), any::<u8>()).prop_map(|(pi, xi)| Step::Coincident { pi, xi }),
        (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Parallel { ai, bi }),
        (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Perpendicular { ai, bi }),
        (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Tangent { ai, bi }),
        (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::EqualLines { ai, bi }),
        (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::EqualRounds { ai, bi }),
        (any::<u8>(), any::<u8>()).prop_map(|(ai, bi)| Step::Concentric { ai, bi }),
        (any::<u8>(), any::<u8>()).prop_map(|(pi, li)| Step::Midpoint { pi, li }),
        any::<u8>().prop_map(|ei| Step::Fix { ei }),
        (any::<u8>(), any::<u8>(), any::<u16>()).prop_map(|(ai, bi, v)| Step::LinearDim {
            ai,
            bi,
            v
        }),
        (any::<u8>(), any::<u16>()).prop_map(|(ti, v)| Step::RadiusDim { ti, v: v % 200 + 1 }),
        (any::<u8>(), any::<u16>()).prop_map(|(ti, v)| Step::DiameterDim { ti, v: v % 200 + 1 }),
        (any::<u8>(), any::<u8>(), any::<u16>()).prop_map(|(ai, bi, deg)| Step::AngularDim {
            ai,
            bi,
            deg: deg % 359 + 1
        }),
    ]
}

fn entities_of_kind(s: &Sketch3, kind: Sketch3EntityKind) -> Vec<bone_types::SketchEntityId> {
    s.entity_order()
        .iter()
        .copied()
        .filter(|id| s.entities()[*id].kind() == kind)
        .collect()
}

fn pick<T: Copy>(xs: &[T], i: u8) -> Option<T> {
    if xs.is_empty() {
        None
    } else {
        Some(xs[usize::from(i) % xs.len()])
    }
}

fn pick_two_distinct<T: Copy + Eq>(xs: &[T], ai: u8, bi: u8) -> Option<(T, T)> {
    if xs.len() < 2 {
        return None;
    }
    let a = usize::from(ai) % xs.len();
    let offset = usize::from(bi) % (xs.len() - 1) + 1;
    let b = (a + offset) % xs.len();
    Some((xs[a], xs[b]))
}

fn pick_three_distinct<T: Copy + Eq>(xs: &[T], ai: u8, bi: u8, ci: u8) -> Option<(T, T, T)> {
    if xs.len() < 3 {
        return None;
    }
    let a = usize::from(ai) % xs.len();
    let b_off = usize::from(bi) % (xs.len() - 1) + 1;
    let b = (a + b_off) % xs.len();
    let remaining: Vec<_> = (0..xs.len()).filter(|&i| i != a && i != b).collect();
    let c = remaining[usize::from(ci) % remaining.len()];
    Some((xs[a], xs[b], xs[c]))
}

fn rounds(s: &Sketch3) -> Vec<bone_types::SketchEntityId> {
    entities_of_kind(s, Sketch3EntityKind::Arc)
        .into_iter()
        .chain(entities_of_kind(s, Sketch3EntityKind::Circle))
        .collect()
}

fn resolve(s: &Sketch3, step: Step) -> Option<Sketch3Edit> {
    match step {
        Step::Point(..) => Some(resolve_atom(step)),
        Step::Line { .. } | Step::Circle { .. } | Step::Arc { .. } => resolve_entity(s, step),
        Step::Coincident { .. }
        | Step::Parallel { .. }
        | Step::Perpendicular { .. }
        | Step::Tangent { .. }
        | Step::EqualLines { .. }
        | Step::EqualRounds { .. }
        | Step::Concentric { .. }
        | Step::Midpoint { .. }
        | Step::Fix { .. } => resolve_relation(s, step),
        Step::LinearDim { .. }
        | Step::RadiusDim { .. }
        | Step::DiameterDim { .. }
        | Step::AngularDim { .. } => resolve_dimension(s, step),
    }
}

fn resolve_atom(step: Step) -> Sketch3Edit {
    match step {
        Step::Point(x, y) => Sketch3Edit::AddEntity(Sketch3Entity::point(Point3::from_mm(
            f64::from(x) / 100.0,
            f64::from(y) / 100.0,
            0.0,
        ))),
        _ => unreachable!("caller routes only Point here"),
    }
}

fn resolve_entity(s: &Sketch3, step: Step) -> Option<Sketch3Edit> {
    let normal = UnitVec3::z_axis();
    match step {
        Step::Line { ai, bi, cons } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, Sketch3EntityKind::Point), ai, bi)?;
            Some(Sketch3Edit::AddEntity(Sketch3Entity::line(a, b, cons)))
        }
        Step::Circle { ci, r, cons } => {
            let c = pick(&entities_of_kind(s, Sketch3EntityKind::Point), ci)?;
            Some(Sketch3Edit::AddEntity(Sketch3Entity::circle(
                c,
                mm(f64::from(r)),
                normal,
                cons,
            )))
        }
        Step::Arc { ci, si, ei } => {
            let (c, start, end) =
                pick_three_distinct(&entities_of_kind(s, Sketch3EntityKind::Point), ci, si, ei)?;
            Some(Sketch3Edit::AddEntity(Sketch3Entity::arc(
                c, start, end, normal, false,
            )))
        }
        _ => unreachable!("caller routes only Line, Circle, Arc here"),
    }
}

fn resolve_relation(s: &Sketch3, step: Step) -> Option<Sketch3Edit> {
    let rel = match step {
        Step::Coincident { pi, xi } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, Sketch3EntityKind::Point), pi, xi)?;
            Sketch3Relation::Coincident(a, b)
        }
        Step::Parallel { ai, bi } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, Sketch3EntityKind::Line), ai, bi)?;
            Sketch3Relation::Parallel(a, b)
        }
        Step::Perpendicular { ai, bi } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, Sketch3EntityKind::Line), ai, bi)?;
            Sketch3Relation::Perpendicular(a, b)
        }
        Step::Tangent { ai, bi } => {
            let round = rounds(s);
            let lines = entities_of_kind(s, Sketch3EntityKind::Line);
            let a = pick(&round, ai)?;
            let b = if lines.is_empty() {
                let remaining: Vec<_> = round.iter().copied().filter(|id| *id != a).collect();
                pick(&remaining, bi)?
            } else {
                pick(&lines, bi)?
            };
            Sketch3Relation::Tangent(a, b)
        }
        Step::EqualLines { ai, bi } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, Sketch3EntityKind::Line), ai, bi)?;
            Sketch3Relation::Equal(a, b)
        }
        Step::EqualRounds { ai, bi } => {
            let (a, b) = pick_two_distinct(&rounds(s), ai, bi)?;
            Sketch3Relation::Equal(a, b)
        }
        Step::Concentric { ai, bi } => {
            let (a, b) = pick_two_distinct(&rounds(s), ai, bi)?;
            Sketch3Relation::Concentric(a, b)
        }
        Step::Midpoint { pi, li } => {
            let line = pick(&entities_of_kind(s, Sketch3EntityKind::Line), li)?;
            let endpoints = match s.entities().get(line) {
                Some(Sketch3Entity::Line(l)) => [l.a(), l.b()],
                _ => return None,
            };
            let candidates: Vec<_> = entities_of_kind(s, Sketch3EntityKind::Point)
                .into_iter()
                .filter(|id| !endpoints.contains(id))
                .collect();
            let point = pick(&candidates, pi)?;
            Sketch3Relation::Midpoint { point, line }
        }
        Step::Fix { ei } => {
            Sketch3Relation::Fix(pick(&entities_of_kind(s, Sketch3EntityKind::Point), ei)?)
        }
        _ => unreachable!("caller routes only relations here"),
    };
    Some(Sketch3Edit::AddRelation(rel))
}

fn resolve_dimension(s: &Sketch3, step: Step) -> Option<Sketch3Edit> {
    let rel = match step {
        Step::LinearDim { ai, bi, v } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, Sketch3EntityKind::Point), ai, bi)?;
            Sketch3Relation::Distance {
                a,
                b,
                value: mm(f64::from(v)),
                reference: false,
            }
        }
        Step::RadiusDim { ti, v } => Sketch3Relation::Radius {
            target: pick(&rounds(s), ti)?,
            value: mm(f64::from(v)),
            reference: false,
        },
        Step::DiameterDim { ti, v } => Sketch3Relation::Diameter {
            target: pick(&rounds(s), ti)?,
            value: mm(f64::from(v)),
            reference: false,
        },
        Step::AngularDim { ai, bi, deg: d } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, Sketch3EntityKind::Line), ai, bi)?;
            Sketch3Relation::Angle {
                a,
                b,
                value: deg(f64::from(d)),
                reference: false,
            }
        }
        _ => unreachable!("caller routes only dimensions here"),
    };
    Some(Sketch3Edit::AddRelation(rel))
}

fn build(steps: Vec<Step>) -> Sketch3 {
    steps
        .into_iter()
        .fold(Sketch3::on_plane(plane()), |sk, step| {
            match resolve(&sk, step) {
                Some(edit) => sk.apply(edit).0,
                None => sk,
            }
        })
}

proptest! {
    #![proptest_config(ProptestConfig {
        cases: 32,
        .. ProptestConfig::default()
    })]

    #[test]
    fn load_save_roundtrip_preserves_sketch(steps in prop::collection::vec(arb_step(), 0..30)) {
        let sketch = build(steps);
        let dir = tempdir().map_err(|e| TestCaseError::Fail(format!("tempdir: {e}").into()))?;
        let folder = DocumentFolder::new(dir.path().join("fuzz.bone"));
        let mut doc = Document::new(document_id(1), "fuzz".to_owned());
        doc.insert_sketch(sketch_id(1), "S".to_owned(), sketch.clone());
        save(&doc, &folder).map_err(|e| TestCaseError::Fail(format!("save: {e}").into()))?;
        let loaded = load(&folder).map_err(|e| TestCaseError::Fail(format!("load: {e}").into()))?;
        let Some(round) = loaded.sketch3(sketch_id(1)) else {
            return Err(TestCaseError::Fail("sketch missing".into()));
        };
        prop_assert_eq!(round, &sketch);
    }

    #[test]
    fn sketch_plus_extrude_roundtrips_whole_document(
        steps in prop::collection::vec(arb_step(), 0..30),
        depth_mm in 1u16..500,
    ) {
        let sketch = build(steps);
        let dir = tempdir().map_err(|e| TestCaseError::Fail(format!("tempdir: {e}").into()))?;
        let folder = DocumentFolder::new(dir.path().join("ext.bone"));
        let mut doc = Document::new(document_id(1), "ext".to_owned());
        doc.insert_sketch(sketch_id(1), "S".to_owned(), sketch);
        let depth = PositiveLength::new(mm(f64::from(depth_mm)))
            .map_err(|e| TestCaseError::Fail(format!("depth: {e}").into()))?;
        doc.insert_extrude(
            extrude_id(1),
            ExtrudeFeature::blind(sketch_id(1), depth),
        );
        save(&doc, &folder).map_err(|e| TestCaseError::Fail(format!("save: {e}").into()))?;
        let loaded = load(&folder).map_err(|e| TestCaseError::Fail(format!("load: {e}").into()))?;
        prop_assert_eq!(loaded, doc);
    }

    #[test]
    fn double_save_is_bit_identical(steps in prop::collection::vec(arb_step(), 0..30)) {
        let sketch = build(steps);
        let dir = tempdir().map_err(|e| TestCaseError::Fail(format!("tempdir: {e}").into()))?;
        let folder = DocumentFolder::new(dir.path().join("det.bone"));
        let mut doc = Document::new(document_id(1), "det".to_owned());
        doc.insert_sketch(sketch_id(1), "S".to_owned(), sketch);
        save(&doc, &folder).map_err(|e| TestCaseError::Fail(format!("save: {e}").into()))?;
        let first_doc = std::fs::read(folder.document_file())
            .map_err(|e| TestCaseError::Fail(format!("read: {e}").into()))?;
        let first_sketch = std::fs::read(folder.sketch_path(sketch_id(1)))
            .map_err(|e| TestCaseError::Fail(format!("read: {e}").into()))?;
        save(&doc, &folder).map_err(|e| TestCaseError::Fail(format!("save2: {e}").into()))?;
        let second_doc = std::fs::read(folder.document_file())
            .map_err(|e| TestCaseError::Fail(format!("read: {e}").into()))?;
        let second_sketch = std::fs::read(folder.sketch_path(sketch_id(1)))
            .map_err(|e| TestCaseError::Fail(format!("read: {e}").into()))?;
        prop_assert_eq!(first_doc, second_doc);
        prop_assert_eq!(first_sketch, second_sketch);
    }
}
