use bone_document::{
    DimensionKind, Document, DocumentFolder, Sketch, SketchDimension, SketchEdit, SketchEntity,
    SketchEntityKind, SketchParameter, SketchRelation, load, save,
};
use bone_types::{
    Angle, DocumentId, Length, Parameter, Point2, Point3, 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)))
}

#[derive(Copy, Clone, Debug)]
enum Step {
    Point(i16, i16),
    Parameter(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,
    },
    Horizontal {
        li: u8,
    },
    Vertical {
        li: 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,
        driven: bool,
    },
    RadiusDim {
        ti: u8,
        v: u16,
        driven: bool,
    },
    DiameterDim {
        ti: u8,
        v: u16,
        driven: bool,
    },
    AngularDim {
        ai: u8,
        bi: u8,
        deg: u16,
    },
    ToggleConstruction {
        ei: u8,
    },
}

fn arb_step() -> impl Strategy<Value = Step> {
    prop_oneof![
        (any::<i16>(), any::<i16>()).prop_map(|(x, y)| Step::Point(x, y)),
        any::<i16>().prop_map(Step::Parameter),
        (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>().prop_map(|li| Step::Horizontal { li }),
        any::<u8>().prop_map(|li| Step::Vertical { li }),
        (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>(), any::<bool>())
            .prop_map(|(ai, bi, v, driven)| Step::LinearDim { ai, bi, v, driven }),
        (any::<u8>(), any::<u16>(), any::<bool>()).prop_map(|(ti, v, driven)| Step::RadiusDim {
            ti,
            v: v % 200 + 1,
            driven
        }),
        (any::<u8>(), any::<u16>(), any::<bool>()).prop_map(|(ti, v, driven)| Step::DiameterDim {
            ti,
            v: v % 200 + 1,
            driven
        }),
        (any::<u8>(), any::<u8>(), any::<u16>()).prop_map(|(ai, bi, deg)| Step::AngularDim {
            ai,
            bi,
            deg: deg % 359 + 1
        }),
        any::<u8>().prop_map(|ei| Step::ToggleConstruction { ei }),
    ]
}

fn entities_of_kind(s: &Sketch, kind: SketchEntityKind) -> 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 dim_kind(driven: bool) -> DimensionKind {
    if driven {
        DimensionKind::Driven
    } else {
        DimensionKind::Driving
    }
}

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

fn resolve(s: &Sketch, step: Step) -> Option<SketchEdit> {
    match step {
        Step::Point(..) | Step::Parameter(..) => Some(resolve_atom(step)),
        Step::Line { .. } | Step::Circle { .. } | Step::Arc { .. } => resolve_entity(s, step),
        Step::Coincident { .. }
        | Step::Horizontal { .. }
        | Step::Vertical { .. }
        | 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),
        Step::ToggleConstruction { ei } => {
            let id = pick(s.entity_order(), ei)?;
            (!s.entities()[id].is_point()).then_some(SketchEdit::SetConstruction {
                id,
                for_construction: true,
            })
        }
    }
}

fn resolve_atom(step: Step) -> SketchEdit {
    match step {
        Step::Point(x, y) => SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(
            f64::from(x) / 100.0,
            f64::from(y) / 100.0,
        ))),
        Step::Parameter(v) => {
            SketchEdit::AddParameter(SketchParameter::new(Parameter::new(f64::from(v) / 100.0)))
        }
        _ => unreachable!("caller routes only Point and Parameter here"),
    }
}

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

fn resolve_relation(s: &Sketch, step: Step) -> Option<SketchEdit> {
    let rel = match step {
        Step::Coincident { pi, xi } => {
            let p = pick(&entities_of_kind(s, SketchEntityKind::Point), pi)?;
            let others: Vec<_> = s
                .entity_order()
                .iter()
                .copied()
                .filter(|id| *id != p)
                .collect();
            SketchRelation::Coincident(p, pick(&others, xi)?)
        }
        Step::Horizontal { li } => {
            SketchRelation::Horizontal(pick(&entities_of_kind(s, SketchEntityKind::Line), li)?)
        }
        Step::Vertical { li } => {
            SketchRelation::Vertical(pick(&entities_of_kind(s, SketchEntityKind::Line), li)?)
        }
        Step::Parallel { ai, bi } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Line), ai, bi)?;
            SketchRelation::Parallel(a, b)
        }
        Step::Perpendicular { ai, bi } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Line), ai, bi)?;
            SketchRelation::Perpendicular(a, b)
        }
        Step::Tangent { ai, bi } => {
            let round = rounds(s);
            let lines = entities_of_kind(s, SketchEntityKind::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)?
            };
            SketchRelation::Tangent(a, b)
        }
        Step::EqualLines { ai, bi } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Line), ai, bi)?;
            SketchRelation::Equal(a, b)
        }
        Step::EqualRounds { ai, bi } => {
            let (a, b) = pick_two_distinct(&rounds(s), ai, bi)?;
            SketchRelation::Equal(a, b)
        }
        Step::Concentric { ai, bi } => {
            let (a, b) = pick_two_distinct(&rounds(s), ai, bi)?;
            SketchRelation::Concentric(a, b)
        }
        Step::Midpoint { pi, li } => {
            let point = pick(&entities_of_kind(s, SketchEntityKind::Point), pi)?;
            let line = pick(&entities_of_kind(s, SketchEntityKind::Line), li)?;
            SketchRelation::Midpoint { point, line }
        }
        Step::Fix { ei } => SketchRelation::Fix(pick(s.entity_order(), ei)?),
        _ => unreachable!("caller routes only relations here"),
    };
    Some(SketchEdit::AddRelation(rel))
}

fn resolve_dimension(s: &Sketch, step: Step) -> Option<SketchEdit> {
    let dim = match step {
        Step::LinearDim { ai, bi, v, driven } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Point), ai, bi)?;
            SketchDimension::Linear {
                a,
                b,
                value: mm(f64::from(v)),
                kind: dim_kind(driven),
            }
        }
        Step::RadiusDim { ti, v, driven } => SketchDimension::Radius {
            target: pick(&rounds(s), ti)?,
            value: mm(f64::from(v)),
            kind: dim_kind(driven),
        },
        Step::DiameterDim { ti, v, driven } => SketchDimension::Diameter {
            target: pick(&rounds(s), ti)?,
            value: mm(f64::from(v)),
            kind: dim_kind(driven),
        },
        Step::AngularDim { ai, bi, deg: d } => {
            let (a, b) = pick_two_distinct(&entities_of_kind(s, SketchEntityKind::Line), ai, bi)?;
            SketchDimension::Angular {
                a,
                b,
                value: deg(f64::from(d)),
                kind: DimensionKind::Driving,
            }
        }
        _ => unreachable!("caller routes only dimensions here"),
    };
    Some(SketchEdit::AddDimension(dim))
}

fn build(steps: Vec<Step>) -> Sketch {
    steps.into_iter().fold(Sketch::new(plane()), |sk, step| {
        resolve(&sk, step)
            .and_then(|edit| sk.clone().apply(edit).ok().map(|(s, _)| s))
            .unwrap_or(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.sketch(sketch_id(1)) else {
            return Err(TestCaseError::Fail("sketch missing".into()));
        };
        prop_assert_eq!(round, &sketch);
    }

    #[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);
    }
}
