use bone_document::{Sketch, SketchEntity, SketchEntityKind, SketchRelation};
use bone_types::SketchEntityId;
use bone_ui::strings::StringKey;

use crate::strings;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RelationKind {
    Coincident,
    Horizontal,
    Vertical,
    Parallel,
    Perpendicular,
    Tangent,
    Equal,
    Concentric,
    Midpoint,
    Fix,
}

#[derive(Clone, Debug, PartialEq)]
pub enum Eligibility {
    Eligible(SketchRelation),
    Disabled(StringKey),
}

#[derive(Copy, Clone)]
struct RelationDescriptor {
    key: &'static str,
    label: StringKey,
    check: fn(&Sketch, &[SketchEntityId]) -> Eligibility,
}

impl RelationKind {
    pub const ALL: &'static [Self] = &[
        Self::Coincident,
        Self::Horizontal,
        Self::Vertical,
        Self::Parallel,
        Self::Perpendicular,
        Self::Tangent,
        Self::Equal,
        Self::Concentric,
        Self::Midpoint,
        Self::Fix,
    ];

    const fn descriptor(self) -> RelationDescriptor {
        match self {
            Self::Coincident => RelationDescriptor {
                key: "rel.coincident",
                label: strings::TOOL_COINCIDENT,
                check: coincident,
            },
            Self::Horizontal => RelationDescriptor {
                key: "rel.horizontal",
                label: strings::TOOL_HORIZONTAL,
                check: horizontal,
            },
            Self::Vertical => RelationDescriptor {
                key: "rel.vertical",
                label: strings::TOOL_VERTICAL,
                check: vertical,
            },
            Self::Parallel => RelationDescriptor {
                key: "rel.parallel",
                label: strings::TOOL_PARALLEL,
                check: parallel,
            },
            Self::Perpendicular => RelationDescriptor {
                key: "rel.perpendicular",
                label: strings::TOOL_PERPENDICULAR,
                check: perpendicular,
            },
            Self::Tangent => RelationDescriptor {
                key: "rel.tangent",
                label: strings::TOOL_TANGENT,
                check: tangent,
            },
            Self::Equal => RelationDescriptor {
                key: "rel.equal",
                label: strings::TOOL_EQUAL,
                check: equal,
            },
            Self::Concentric => RelationDescriptor {
                key: "rel.concentric",
                label: strings::TOOL_CONCENTRIC,
                check: concentric,
            },
            Self::Midpoint => RelationDescriptor {
                key: "rel.midpoint",
                label: strings::TOOL_MIDPOINT,
                check: midpoint,
            },
            Self::Fix => RelationDescriptor {
                key: "rel.fix",
                label: strings::TOOL_FIX,
                check: fix,
            },
        }
    }

    #[must_use]
    pub const fn key(self) -> &'static str {
        self.descriptor().key
    }

    #[must_use]
    pub const fn label(self) -> StringKey {
        self.descriptor().label
    }
}

#[must_use]
pub fn eligibility(
    kind: RelationKind,
    sketch: &Sketch,
    selection: &[SketchEntityId],
) -> Eligibility {
    (kind.descriptor().check)(sketch, selection)
}

fn coincident(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    pair_eligibility(
        sketch,
        selection,
        strings::REL_HINT_COINCIDENT,
        |a, b| a == SketchEntityKind::Point || b == SketchEntityKind::Point,
        SketchRelation::Coincident,
    )
}

fn horizontal(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    single_eligibility(
        sketch,
        selection,
        strings::REL_HINT_ONE_LINE,
        |k| k == SketchEntityKind::Line,
        SketchRelation::Horizontal,
    )
}

fn vertical(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    single_eligibility(
        sketch,
        selection,
        strings::REL_HINT_ONE_LINE,
        |k| k == SketchEntityKind::Line,
        SketchRelation::Vertical,
    )
}

fn parallel(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    pair_eligibility(
        sketch,
        selection,
        strings::REL_HINT_TWO_LINES,
        both_lines,
        SketchRelation::Parallel,
    )
}

fn perpendicular(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    pair_eligibility(
        sketch,
        selection,
        strings::REL_HINT_TWO_LINES,
        both_lines,
        SketchRelation::Perpendicular,
    )
}

fn tangent(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    pair_eligibility(
        sketch,
        selection,
        strings::REL_HINT_TANGENT,
        |a, b| !is_point(a) && !is_point(b) && (is_curved(a) || is_curved(b)),
        SketchRelation::Tangent,
    )
}

fn equal(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    pair_eligibility(
        sketch,
        selection,
        strings::REL_HINT_EQUAL,
        |a, b| both_lines(a, b) || (is_curved(a) && is_curved(b)),
        SketchRelation::Equal,
    )
}

fn concentric(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    pair_eligibility(
        sketch,
        selection,
        strings::REL_HINT_TWO_CIRCULAR,
        |a, b| is_curved(a) && is_curved(b),
        SketchRelation::Concentric,
    )
}

fn midpoint(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    let disabled = || Eligibility::Disabled(strings::REL_HINT_MIDPOINT);
    let Some((a, ka, b, kb)) = pair_kinds(sketch, selection) else {
        return disabled();
    };
    let (point, line) = match (ka, kb) {
        (SketchEntityKind::Point, SketchEntityKind::Line) => (a, b),
        (SketchEntityKind::Line, SketchEntityKind::Point) => (b, a),
        _ => return disabled(),
    };
    let Some(SketchEntity::Line(host)) = sketch.entities().get(line) else {
        return disabled();
    };
    if host.a() == point || host.b() == point {
        return disabled();
    }
    Eligibility::Eligible(SketchRelation::Midpoint { point, line })
}

fn fix(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    single_eligibility(
        sketch,
        selection,
        strings::REL_HINT_ENTITY,
        |_| true,
        SketchRelation::Fix,
    )
}

fn pair_eligibility(
    sketch: &Sketch,
    selection: &[SketchEntityId],
    hint: StringKey,
    predicate: impl Fn(SketchEntityKind, SketchEntityKind) -> bool,
    ctor: impl Fn(SketchEntityId, SketchEntityId) -> SketchRelation,
) -> Eligibility {
    match pair_kinds(sketch, selection) {
        Some((a, ka, b, kb)) if predicate(ka, kb) => Eligibility::Eligible(ctor(a, b)),
        _ => Eligibility::Disabled(hint),
    }
}

fn single_eligibility(
    sketch: &Sketch,
    selection: &[SketchEntityId],
    hint: StringKey,
    predicate: impl Fn(SketchEntityKind) -> bool,
    ctor: impl Fn(SketchEntityId) -> SketchRelation,
) -> Eligibility {
    match selection {
        [id] => entity_kind(sketch, *id)
            .filter(|k| predicate(*k))
            .map_or(Eligibility::Disabled(hint), |_| {
                Eligibility::Eligible(ctor(*id))
            }),
        _ => Eligibility::Disabled(hint),
    }
}

fn pair_kinds(
    sketch: &Sketch,
    selection: &[SketchEntityId],
) -> Option<(
    SketchEntityId,
    SketchEntityKind,
    SketchEntityId,
    SketchEntityKind,
)> {
    let [a, b] = selection else { return None };
    if a == b {
        return None;
    }
    let ka = entity_kind(sketch, *a)?;
    let kb = entity_kind(sketch, *b)?;
    Some((*a, ka, *b, kb))
}

fn entity_kind(sketch: &Sketch, id: SketchEntityId) -> Option<SketchEntityKind> {
    sketch.entities().get(id).map(SketchEntity::kind)
}

const fn is_curved(kind: SketchEntityKind) -> bool {
    matches!(kind, SketchEntityKind::Arc | SketchEntityKind::Circle)
}

const fn is_point(kind: SketchEntityKind) -> bool {
    matches!(kind, SketchEntityKind::Point)
}

const fn both_lines(a: SketchEntityKind, b: SketchEntityKind) -> bool {
    matches!((a, b), (SketchEntityKind::Line, SketchEntityKind::Line))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sketch_mode::Plane;
    use crate::tools::{add_arc, add_circle, add_line, add_point};
    use bone_types::{Length, Point2};
    use uom::si::length::millimeter;

    fn fresh() -> Sketch {
        Sketch::new(Plane::Xy.basis())
    }

    #[test]
    fn relation_kind_all_matches_descriptor_arms() {
        assert_eq!(RelationKind::ALL.len(), 10);
        let keys: std::collections::BTreeSet<_> =
            RelationKind::ALL.iter().map(|k| k.key()).collect();
        assert_eq!(keys.len(), 10, "every kind has a unique widget key");
    }

    #[test]
    fn horizontal_eligible_on_single_line() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(5.0, 0.0));
        let (s, line) = add_line(s, p0, p1, false);
        assert_eq!(
            eligibility(RelationKind::Horizontal, &s, &[line]),
            Eligibility::Eligible(SketchRelation::Horizontal(line)),
        );
    }

    #[test]
    fn horizontal_disabled_on_point_selection() {
        let (s, p) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        assert_eq!(
            eligibility(RelationKind::Horizontal, &s, &[p]),
            Eligibility::Disabled(strings::REL_HINT_ONE_LINE),
        );
    }

    #[test]
    fn horizontal_disabled_on_empty_selection() {
        assert_eq!(
            eligibility(RelationKind::Horizontal, &fresh(), &[]),
            Eligibility::Disabled(strings::REL_HINT_ONE_LINE),
        );
    }

    #[test]
    fn vertical_eligible_on_single_line() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(0.0, 5.0));
        let (s, line) = add_line(s, p0, p1, false);
        assert_eq!(
            eligibility(RelationKind::Vertical, &s, &[line]),
            Eligibility::Eligible(SketchRelation::Vertical(line)),
        );
    }

    #[test]
    fn fix_eligible_on_any_single_entity() {
        let (s, p) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        assert_eq!(
            eligibility(RelationKind::Fix, &s, &[p]),
            Eligibility::Eligible(SketchRelation::Fix(p)),
        );
    }

    #[test]
    fn fix_eligible_on_line_arc_circle() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(5.0, 0.0));
        let (s, p2) = add_point(s, Point2::from_mm(0.0, 5.0));
        let (s, line) = add_line(s, p0, p1, false);
        let (s, circle) = add_circle(s, p0, Length::new::<millimeter>(2.0), false);
        let (s, arc) = add_arc(s, p0, p1, p2, false);
        [line, circle, arc].into_iter().for_each(|id| {
            assert_eq!(
                eligibility(RelationKind::Fix, &s, &[id]),
                Eligibility::Eligible(SketchRelation::Fix(id)),
            );
        });
    }

    #[test]
    fn fix_disabled_on_empty_or_pair() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
        assert_eq!(
            eligibility(RelationKind::Fix, &s, &[]),
            Eligibility::Disabled(strings::REL_HINT_ENTITY),
        );
        assert_eq!(
            eligibility(RelationKind::Fix, &s, &[p0, p1]),
            Eligibility::Disabled(strings::REL_HINT_ENTITY),
        );
    }

    #[test]
    fn coincident_eligible_on_two_points() {
        let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0));
        assert_eq!(
            eligibility(RelationKind::Coincident, &s, &[a, b]),
            Eligibility::Eligible(SketchRelation::Coincident(a, b)),
        );
    }

    #[test]
    fn coincident_eligible_on_point_and_line() {
        let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, b) = add_point(s, Point2::from_mm(5.0, 0.0));
        let (s, line) = add_line(s, a, b, false);
        let (s, q) = add_point(s, Point2::from_mm(2.5, 0.0));
        assert_eq!(
            eligibility(RelationKind::Coincident, &s, &[q, line]),
            Eligibility::Eligible(SketchRelation::Coincident(q, line)),
        );
    }

    #[test]
    fn coincident_eligible_on_point_and_circle() {
        let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, circle) = add_circle(s, c, Length::new::<millimeter>(2.0), false);
        let (s, q) = add_point(s, Point2::from_mm(1.5, 1.5));
        assert_eq!(
            eligibility(RelationKind::Coincident, &s, &[q, circle]),
            Eligibility::Eligible(SketchRelation::Coincident(q, circle)),
        );
    }

    #[test]
    fn coincident_disabled_on_two_lines() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
        let (s, p2) = add_point(s, Point2::from_mm(0.0, 1.0));
        let (s, p3) = add_point(s, Point2::from_mm(1.0, 1.0));
        let (s, l1) = add_line(s, p0, p1, false);
        let (s, l2) = add_line(s, p2, p3, false);
        assert_eq!(
            eligibility(RelationKind::Coincident, &s, &[l1, l2]),
            Eligibility::Disabled(strings::REL_HINT_COINCIDENT),
        );
    }

    #[test]
    fn parallel_eligible_on_two_lines() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
        let (s, p2) = add_point(s, Point2::from_mm(0.0, 1.0));
        let (s, p3) = add_point(s, Point2::from_mm(1.0, 1.0));
        let (s, l1) = add_line(s, p0, p1, false);
        let (s, l2) = add_line(s, p2, p3, false);
        assert_eq!(
            eligibility(RelationKind::Parallel, &s, &[l1, l2]),
            Eligibility::Eligible(SketchRelation::Parallel(l1, l2)),
        );
    }

    #[test]
    fn perpendicular_disabled_on_line_and_circle() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
        let (s, line) = add_line(s, p0, p1, false);
        let (s, circle) = add_circle(s, p0, Length::new::<millimeter>(2.0), false);
        assert_eq!(
            eligibility(RelationKind::Perpendicular, &s, &[line, circle]),
            Eligibility::Disabled(strings::REL_HINT_TWO_LINES),
        );
    }

    #[test]
    fn tangent_eligible_on_line_and_circle() {
        let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(5.0, 0.0));
        let (s, p2) = add_point(s, Point2::from_mm(5.0, 3.0));
        let (s, line) = add_line(s, p1, p2, false);
        let (s, circle) = add_circle(s, c, Length::new::<millimeter>(2.0), false);
        assert_eq!(
            eligibility(RelationKind::Tangent, &s, &[line, circle]),
            Eligibility::Eligible(SketchRelation::Tangent(line, circle)),
        );
        assert_eq!(
            eligibility(RelationKind::Tangent, &s, &[circle, line]),
            Eligibility::Eligible(SketchRelation::Tangent(circle, line)),
        );
    }

    #[test]
    fn tangent_eligible_on_two_circles() {
        let (s, c1) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, c2) = add_point(s, Point2::from_mm(10.0, 0.0));
        let (s, k1) = add_circle(s, c1, Length::new::<millimeter>(3.0), false);
        let (s, k2) = add_circle(s, c2, Length::new::<millimeter>(3.0), false);
        assert_eq!(
            eligibility(RelationKind::Tangent, &s, &[k1, k2]),
            Eligibility::Eligible(SketchRelation::Tangent(k1, k2)),
        );
    }

    #[test]
    fn tangent_disabled_on_two_points() {
        let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0));
        assert_eq!(
            eligibility(RelationKind::Tangent, &s, &[a, b]),
            Eligibility::Disabled(strings::REL_HINT_TANGENT),
        );
    }

    #[test]
    fn equal_eligible_on_two_lines() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
        let (s, p2) = add_point(s, Point2::from_mm(0.0, 1.0));
        let (s, p3) = add_point(s, Point2::from_mm(1.0, 1.0));
        let (s, l1) = add_line(s, p0, p1, false);
        let (s, l2) = add_line(s, p2, p3, false);
        assert_eq!(
            eligibility(RelationKind::Equal, &s, &[l1, l2]),
            Eligibility::Eligible(SketchRelation::Equal(l1, l2)),
        );
    }

    #[test]
    fn equal_eligible_on_two_circles() {
        let (s, c1) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, c2) = add_point(s, Point2::from_mm(10.0, 0.0));
        let (s, k1) = add_circle(s, c1, Length::new::<millimeter>(3.0), false);
        let (s, k2) = add_circle(s, c2, Length::new::<millimeter>(2.0), false);
        assert_eq!(
            eligibility(RelationKind::Equal, &s, &[k1, k2]),
            Eligibility::Eligible(SketchRelation::Equal(k1, k2)),
        );
    }

    #[test]
    fn equal_disabled_on_line_and_circle() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
        let (s, line) = add_line(s, p0, p1, false);
        let (s, circle) = add_circle(s, p0, Length::new::<millimeter>(2.0), false);
        assert_eq!(
            eligibility(RelationKind::Equal, &s, &[line, circle]),
            Eligibility::Disabled(strings::REL_HINT_EQUAL),
        );
    }

    #[test]
    fn concentric_eligible_on_circle_and_arc() {
        let (s, c1) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, ks) = add_point(s, Point2::from_mm(3.0, 0.0));
        let (s, ke) = add_point(s, Point2::from_mm(0.0, 3.0));
        let (s, arc) = add_arc(s, c1, ks, ke, false);
        let (s, circle) = add_circle(s, c1, Length::new::<millimeter>(2.0), false);
        assert_eq!(
            eligibility(RelationKind::Concentric, &s, &[arc, circle]),
            Eligibility::Eligible(SketchRelation::Concentric(arc, circle)),
        );
    }

    #[test]
    fn concentric_eligible_on_two_circles() {
        let (s, c1) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, c2) = add_point(s, Point2::from_mm(10.0, 0.0));
        let (s, k1) = add_circle(s, c1, Length::new::<millimeter>(3.0), false);
        let (s, k2) = add_circle(s, c2, Length::new::<millimeter>(2.0), false);
        assert_eq!(
            eligibility(RelationKind::Concentric, &s, &[k1, k2]),
            Eligibility::Eligible(SketchRelation::Concentric(k1, k2)),
        );
    }

    #[test]
    fn midpoint_eligible_in_either_order() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(10.0, 0.0));
        let (s, line) = add_line(s, p0, p1, false);
        let (s, q) = add_point(s, Point2::from_mm(5.0, 0.0));
        assert_eq!(
            eligibility(RelationKind::Midpoint, &s, &[q, line]),
            Eligibility::Eligible(SketchRelation::Midpoint { point: q, line }),
        );
        assert_eq!(
            eligibility(RelationKind::Midpoint, &s, &[line, q]),
            Eligibility::Eligible(SketchRelation::Midpoint { point: q, line }),
        );
    }

    #[test]
    fn midpoint_disabled_on_two_points() {
        let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0));
        assert_eq!(
            eligibility(RelationKind::Midpoint, &s, &[a, b]),
            Eligibility::Disabled(strings::REL_HINT_MIDPOINT),
        );
    }

    #[test]
    fn midpoint_disabled_when_point_is_line_endpoint() {
        let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, b) = add_point(s, Point2::from_mm(10.0, 0.0));
        let (s, line) = add_line(s, a, b, false);
        assert_eq!(
            eligibility(RelationKind::Midpoint, &s, &[a, line]),
            Eligibility::Disabled(strings::REL_HINT_MIDPOINT),
        );
        assert_eq!(
            eligibility(RelationKind::Midpoint, &s, &[line, b]),
            Eligibility::Disabled(strings::REL_HINT_MIDPOINT),
        );
    }

    #[test]
    fn tangent_disabled_on_two_lines() {
        let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
        let (s, p2) = add_point(s, Point2::from_mm(0.0, 1.0));
        let (s, p3) = add_point(s, Point2::from_mm(1.0, 1.0));
        let (s, l1) = add_line(s, p0, p1, false);
        let (s, l2) = add_line(s, p2, p3, false);
        assert_eq!(
            eligibility(RelationKind::Tangent, &s, &[l1, l2]),
            Eligibility::Disabled(strings::REL_HINT_TANGENT),
        );
    }

    #[test]
    fn pair_relations_reject_same_id_twice() {
        let (s, p) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let cases = [
            (RelationKind::Coincident, strings::REL_HINT_COINCIDENT),
            (RelationKind::Parallel, strings::REL_HINT_TWO_LINES),
            (RelationKind::Perpendicular, strings::REL_HINT_TWO_LINES),
            (RelationKind::Tangent, strings::REL_HINT_TANGENT),
            (RelationKind::Equal, strings::REL_HINT_EQUAL),
            (RelationKind::Concentric, strings::REL_HINT_TWO_CIRCULAR),
            (RelationKind::Midpoint, strings::REL_HINT_MIDPOINT),
        ];
        cases.into_iter().for_each(|(kind, hint)| {
            assert_eq!(
                eligibility(kind, &s, &[p, p]),
                Eligibility::Disabled(hint),
                "{kind:?} must reject [a, a]",
            );
        });
    }
}
