use bone_document::{
    DimensionKind, DimensionValue, Sketch, SketchDimension, SketchEntity, SketchEntityKind,
};
use bone_types::{Angle, Length, Point2, SketchEntityId};
use bone_ui::strings::StringKey;
use uom::si::angle::radian;
use uom::si::length::millimeter;

use crate::sketch_mode::PendingDimension;
use crate::strings;

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

#[must_use]
pub fn eligibility(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
    let disabled = || Eligibility::Disabled(strings::DIM_HINT_GENERIC);
    match selection {
        [only] => single(sketch, *only).map_or_else(disabled, Eligibility::Eligible),
        [first, second] if first != second => {
            pair(sketch, *first, *second).map_or_else(disabled, Eligibility::Eligible)
        }
        _ => disabled(),
    }
}

#[must_use]
pub fn swap_radius_diameter(dim: SketchDimension) -> Option<SketchDimension> {
    match dim {
        SketchDimension::Radius {
            target,
            value,
            kind,
        } => {
            let diameter = Length::new::<millimeter>(value.get::<millimeter>() * 2.0);
            Some(SketchDimension::Diameter {
                target,
                value: diameter,
                kind,
            })
        }
        SketchDimension::Diameter {
            target,
            value,
            kind,
        } => {
            let radius = Length::new::<millimeter>(value.get::<millimeter>() * 0.5);
            Some(SketchDimension::Radius {
                target,
                value: radius,
                kind,
            })
        }
        SketchDimension::Linear { .. } | SketchDimension::Angular { .. } => None,
    }
}

#[must_use]
pub fn live_anchor(sketch: &Sketch, proto: SketchDimension) -> Option<Point2> {
    match proto {
        SketchDimension::Linear { a, b, .. } => {
            let pa = point_position(sketch, a)?;
            let pb = point_position(sketch, b)?;
            Some(midpoint(pa, pb))
        }
        SketchDimension::Radius { target, .. } => match sketch.entities().get(target)? {
            SketchEntity::Arc(a) => point_position(sketch, a.center()),
            SketchEntity::Circle(c) => point_position(sketch, c.center()),
            _ => None,
        },
        SketchDimension::Diameter { target, .. } => match sketch.entities().get(target)? {
            SketchEntity::Circle(c) => point_position(sketch, c.center()),
            SketchEntity::Arc(a) => point_position(sketch, a.center()),
            _ => None,
        },
        SketchDimension::Angular { a, b, .. } => {
            let (a_start, a_end) = line_endpoints(sketch, a)?;
            let (b_start, b_end) = line_endpoints(sketch, b)?;
            Some(midpoint(midpoint(a_start, a_end), midpoint(b_start, b_end)))
        }
    }
}

#[must_use]
pub const fn placeholder_for(value: DimensionValue) -> StringKey {
    match value {
        DimensionValue::Length(_) => strings::DIM_PLACEHOLDER_LENGTH,
        DimensionValue::Angle(_) => strings::DIM_PLACEHOLDER_ANGLE,
    }
}

fn single(sketch: &Sketch, id: SketchEntityId) -> Option<PendingDimension> {
    let entity = sketch.entities().get(id)?;
    match entity {
        SketchEntity::Point(_) => None,
        SketchEntity::Line(l) => {
            let a = point_position(sketch, l.a())?;
            let b = point_position(sketch, l.b())?;
            let value = Length::new::<millimeter>((b - a).norm_mm());
            Some(PendingDimension {
                proto: SketchDimension::Linear {
                    a: l.a(),
                    b: l.b(),
                    value,
                    kind: DimensionKind::Driving,
                },
                anchor: midpoint(a, b),
            })
        }
        SketchEntity::Arc(a) => {
            let center = point_position(sketch, a.center())?;
            let start = point_position(sketch, a.start())?;
            let value = Length::new::<millimeter>((start - center).norm_mm());
            Some(PendingDimension {
                proto: SketchDimension::Radius {
                    target: id,
                    value,
                    kind: DimensionKind::Driving,
                },
                anchor: center,
            })
        }
        SketchEntity::Circle(c) => {
            let center = point_position(sketch, c.center())?;
            let value = Length::new::<millimeter>(c.radius().get::<millimeter>() * 2.0);
            Some(PendingDimension {
                proto: SketchDimension::Diameter {
                    target: id,
                    value,
                    kind: DimensionKind::Driving,
                },
                anchor: center,
            })
        }
    }
}

fn pair(sketch: &Sketch, a: SketchEntityId, b: SketchEntityId) -> Option<PendingDimension> {
    let ka = sketch.entities().get(a).map(SketchEntity::kind)?;
    let kb = sketch.entities().get(b).map(SketchEntity::kind)?;
    match (ka, kb) {
        (SketchEntityKind::Point, SketchEntityKind::Point) => linear_points(sketch, a, b),
        (SketchEntityKind::Line, SketchEntityKind::Line) => angular_lines(sketch, a, b),
        _ => None,
    }
}

fn linear_points(
    sketch: &Sketch,
    a: SketchEntityId,
    b: SketchEntityId,
) -> Option<PendingDimension> {
    let pa = point_position(sketch, a)?;
    let pb = point_position(sketch, b)?;
    let value = Length::new::<millimeter>((pb - pa).norm_mm());
    Some(PendingDimension {
        proto: SketchDimension::Linear {
            a,
            b,
            value,
            kind: DimensionKind::Driving,
        },
        anchor: midpoint(pa, pb),
    })
}

fn angular_lines(
    sketch: &Sketch,
    a: SketchEntityId,
    b: SketchEntityId,
) -> Option<PendingDimension> {
    let (a_start, a_end) = line_endpoints(sketch, a)?;
    let (b_start, b_end) = line_endpoints(sketch, b)?;
    let probe = SketchDimension::Angular {
        a,
        b,
        value: Angle::new::<radian>(0.0),
        kind: DimensionKind::Driving,
    };
    let DimensionValue::Angle(value) = sketch.measure(probe)? else {
        return None;
    };
    let mid_a = midpoint(a_start, a_end);
    let mid_b = midpoint(b_start, b_end);
    Some(PendingDimension {
        proto: SketchDimension::Angular {
            a,
            b,
            value,
            kind: DimensionKind::Driving,
        },
        anchor: midpoint(mid_a, mid_b),
    })
}

fn line_endpoints(sketch: &Sketch, id: SketchEntityId) -> Option<(Point2, Point2)> {
    let SketchEntity::Line(line) = sketch.entities().get(id)? else {
        return None;
    };
    let a = point_position(sketch, line.a())?;
    let b = point_position(sketch, line.b())?;
    Some((a, b))
}

fn point_position(sketch: &Sketch, id: SketchEntityId) -> Option<Point2> {
    match sketch.entities().get(id)? {
        SketchEntity::Point(p) => Some(p.at()),
        _ => None,
    }
}

fn midpoint(a: Point2, b: Point2) -> Point2 {
    let (ax, ay) = a.coords_mm();
    let (bx, by) = b.coords_mm();
    Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by))
}

#[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::Point2;
    use uom::si::angle::degree;

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

    fn approx_eq(a: f64, b: f64) {
        assert!((a - b).abs() < 1e-9, "{a} !~= {b}");
    }

    #[test]
    fn empty_selection_disabled() {
        assert_eq!(
            eligibility(&fresh(), &[]),
            Eligibility::Disabled(strings::DIM_HINT_GENERIC),
        );
    }

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

    #[test]
    fn single_line_yields_linear_through_endpoints() {
        let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, b) = add_point(s, Point2::from_mm(3.0, 4.0));
        let (s, line) = add_line(s, a, b, false);
        let Eligibility::Eligible(req) = eligibility(&s, &[line]) else {
            panic!("expected eligible");
        };
        match req.proto {
            SketchDimension::Linear {
                a: pa,
                b: pb,
                value,
                kind,
            } => {
                assert_eq!(pa, a);
                assert_eq!(pb, b);
                approx_eq(value.get::<millimeter>(), 5.0);
                assert_eq!(kind, DimensionKind::Driving);
            }
            other => panic!("expected Linear, got {other:?}"),
        }
        let (mx, my) = req.anchor.coords_mm();
        approx_eq(mx, 1.5);
        approx_eq(my, 2.0);
    }

    #[test]
    fn two_points_yield_linear() {
        let (s, a) = add_point(fresh(), Point2::from_mm(-1.0, 0.0));
        let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0));
        let Eligibility::Eligible(req) = eligibility(&s, &[a, b]) else {
            panic!("expected eligible");
        };
        match req.proto {
            SketchDimension::Linear { value, .. } => {
                approx_eq(value.get::<millimeter>(), 2.0);
            }
            other => panic!("expected Linear, got {other:?}"),
        }
    }

    #[test]
    fn arc_default_is_radius() {
        let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, start) = add_point(s, Point2::from_mm(7.0, 0.0));
        let (s, end) = add_point(s, Point2::from_mm(0.0, 7.0));
        let (s, arc) = add_arc(s, c, start, end, false);
        let Eligibility::Eligible(req) = eligibility(&s, &[arc]) else {
            panic!("expected eligible");
        };
        match req.proto {
            SketchDimension::Radius { target, value, .. } => {
                assert_eq!(target, arc);
                approx_eq(value.get::<millimeter>(), 7.0);
            }
            other => panic!("expected Radius, got {other:?}"),
        }
        let (cx, cy) = req.anchor.coords_mm();
        approx_eq(cx, 0.0);
        approx_eq(cy, 0.0);
    }

    #[test]
    fn circle_default_is_diameter() {
        let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, circle) = add_circle(s, c, Length::new::<millimeter>(5.0), false);
        let Eligibility::Eligible(req) = eligibility(&s, &[circle]) else {
            panic!("expected eligible");
        };
        match req.proto {
            SketchDimension::Diameter { target, value, .. } => {
                assert_eq!(target, circle);
                approx_eq(value.get::<millimeter>(), 10.0);
            }
            other => panic!("expected Diameter, got {other:?}"),
        }
    }

    #[test]
    fn two_perpendicular_lines_yield_ninety_degrees() {
        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, p2) = add_point(s, Point2::from_mm(0.0, 0.0));
        let (s, p3) = add_point(s, Point2::from_mm(0.0, 10.0));
        let (s, l1) = add_line(s, p0, p1, false);
        let (s, l2) = add_line(s, p2, p3, false);
        let Eligibility::Eligible(req) = eligibility(&s, &[l1, l2]) else {
            panic!("expected eligible");
        };
        match req.proto {
            SketchDimension::Angular { value, .. } => {
                approx_eq(value.get::<degree>(), 90.0);
            }
            other => panic!("expected Angular, got {other:?}"),
        }
    }

    #[test]
    fn two_parallel_lines_yield_zero_degrees() {
        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, p2) = add_point(s, Point2::from_mm(0.0, 5.0));
        let (s, p3) = add_point(s, Point2::from_mm(10.0, 5.0));
        let (s, l1) = add_line(s, p0, p1, false);
        let (s, l2) = add_line(s, p2, p3, false);
        let Eligibility::Eligible(req) = eligibility(&s, &[l1, l2]) else {
            panic!("expected eligible");
        };
        match req.proto {
            SketchDimension::Angular { value, .. } => {
                approx_eq(value.get::<degree>(), 0.0);
            }
            other => panic!("expected Angular, got {other:?}"),
        }
    }

    #[test]
    fn antiparallel_lines_collapse_to_acute_zero() {
        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, p2) = add_point(s, Point2::from_mm(10.0, 5.0));
        let (s, p3) = add_point(s, Point2::from_mm(0.0, 5.0));
        let (s, l1) = add_line(s, p0, p1, false);
        let (s, l2) = add_line(s, p2, p3, false);
        let Eligibility::Eligible(req) = eligibility(&s, &[l1, l2]) else {
            panic!("expected eligible");
        };
        match req.proto {
            SketchDimension::Angular { value, .. } => {
                approx_eq(value.get::<degree>(), 0.0);
            }
            other => panic!("expected Angular, got {other:?}"),
        }
    }

    #[test]
    fn point_and_line_pair_disabled() {
        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, 5.0));
        assert_eq!(
            eligibility(&s, &[q, line]),
            Eligibility::Disabled(strings::DIM_HINT_GENERIC),
        );
    }

    #[test]
    fn three_entities_disabled() {
        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));
        let (s, c) = add_point(s, Point2::from_mm(0.0, 1.0));
        assert_eq!(
            eligibility(&s, &[a, b, c]),
            Eligibility::Disabled(strings::DIM_HINT_GENERIC),
        );
    }

    #[test]
    fn pair_of_same_id_disabled() {
        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);
        assert_eq!(
            eligibility(&s, &[line, line]),
            Eligibility::Disabled(strings::DIM_HINT_GENERIC),
        );
    }

    #[test]
    fn swap_radius_to_diameter_doubles_value() {
        let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, _start) = add_point(s, Point2::from_mm(3.0, 0.0));
        let (s, _end) = add_point(s, Point2::from_mm(0.0, 3.0));
        let radius = SketchDimension::Radius {
            target: c,
            value: Length::new::<millimeter>(3.0),
            kind: DimensionKind::Driving,
        };
        let Some(diameter) = swap_radius_diameter(radius) else {
            panic!("Radius must swap to Diameter");
        };
        let SketchDimension::Diameter { value, .. } = diameter else {
            panic!("expected Diameter");
        };
        approx_eq(value.get::<millimeter>(), 6.0);
        let _ = s;
    }

    #[test]
    fn swap_diameter_to_radius_halves_value() {
        let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
        let (s, _) = add_point(s, Point2::from_mm(3.0, 0.0));
        let diameter = SketchDimension::Diameter {
            target: c,
            value: Length::new::<millimeter>(8.0),
            kind: DimensionKind::Driving,
        };
        let Some(radius) = swap_radius_diameter(diameter) else {
            panic!("Diameter must swap to Radius");
        };
        let SketchDimension::Radius { value, .. } = radius else {
            panic!("expected Radius");
        };
        approx_eq(value.get::<millimeter>(), 4.0);
        let _ = s;
    }

    #[test]
    fn swap_does_not_affect_linear_or_angular() {
        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 linear = SketchDimension::Linear {
            a: p0,
            b: p1,
            value: Length::new::<millimeter>(1.0),
            kind: DimensionKind::Driving,
        };
        assert!(swap_radius_diameter(linear).is_none());
        let _ = s;
    }
}
