use bone_types::{AngleTolerance, Point2, Tolerance};
use core::f64::consts::TAU;

use crate::arc2::{Arc2, wrap_delta};
use crate::circle2::Circle2;
use crate::curve2::Curve2Kind;
use crate::line2::Line2;

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum IntersectionSet {
    Empty,
    One(Point2),
    Two(Point2, Point2),
    Coincident,
}

impl core::fmt::Display for IntersectionSet {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::Empty => write!(f, "empty"),
            Self::One(p) => write!(f, "one{{ {p} }}"),
            Self::Two(a, b) => write!(f, "two{{ {a}, {b} }}"),
            Self::Coincident => write!(f, "coincident"),
        }
    }
}

#[must_use]
pub fn intersect_curves(a: &Curve2Kind, b: &Curve2Kind, tolerance: Tolerance) -> IntersectionSet {
    match (a, b) {
        (Curve2Kind::Line(l), Curve2Kind::Line(m)) => line_line(l, m, tolerance),
        (Curve2Kind::Line(l), Curve2Kind::Circle(c))
        | (Curve2Kind::Circle(c), Curve2Kind::Line(l)) => line_circle(l, c, tolerance),
        (Curve2Kind::Line(l), Curve2Kind::Arc(a)) | (Curve2Kind::Arc(a), Curve2Kind::Line(l)) => {
            line_arc(l, a, tolerance)
        }
        (Curve2Kind::Circle(c), Curve2Kind::Circle(d)) => circle_circle(c, d, tolerance),
        (Curve2Kind::Circle(c), Curve2Kind::Arc(a))
        | (Curve2Kind::Arc(a), Curve2Kind::Circle(c)) => circle_arc(c, a, tolerance),
        (Curve2Kind::Arc(a), Curve2Kind::Arc(b)) => arc_arc(a, b, tolerance),
    }
}

fn line_line(a: &Line2, b: &Line2, tolerance: Tolerance) -> IntersectionSet {
    let (ax, ay) = a.start().coords_mm();
    let (ux, uy) = a.unit_direction().components();
    let la = a.length_mm();
    let (cx, cy) = b.start().coords_mm();
    let (vx, vy) = b.unit_direction().components();
    let lb = b.length_mm();
    let eps = tolerance.value();

    let det = ux * vy - uy * vx;
    let angle_eps = eps / la.max(lb);

    if det.abs() < angle_eps {
        let dx = cx - ax;
        let dy = cy - ay;
        let perp = dx * uy - dy * ux;
        if perp.abs() > eps {
            return IntersectionSet::Empty;
        }
        let (ex, ey) = b.end().coords_mm();
        let t_c = dx * ux + dy * uy;
        let t_d = (ex - ax) * ux + (ey - ay) * uy;
        let (t_lo, t_hi) = if t_c <= t_d { (t_c, t_d) } else { (t_d, t_c) };
        let overlap_lo = t_lo.max(0.0);
        let overlap_hi = t_hi.min(la);
        let width = overlap_hi - overlap_lo;
        if width < -eps {
            return IntersectionSet::Empty;
        }
        if width <= eps {
            let t = f64::midpoint(overlap_lo, overlap_hi);
            return IntersectionSet::One(Point2::from_mm(ax + t * ux, ay + t * uy));
        }
        return IntersectionSet::Coincident;
    }

    let t = ((cx - ax) * vy - (cy - ay) * vx) / det;
    let s = ((cx - ax) * uy - (cy - ay) * ux) / det;
    if !in_segment(t, la, eps) || !in_segment(s, lb, eps) {
        return IntersectionSet::Empty;
    }
    IntersectionSet::One(Point2::from_mm(ax + t * ux, ay + t * uy))
}

fn line_circle(line: &Line2, circle: &Circle2, tolerance: Tolerance) -> IntersectionSet {
    let (ax, ay) = line.start().coords_mm();
    let (ux, uy) = line.unit_direction().components();
    let la = line.length_mm();
    let (cx, cy) = circle.center().coords_mm();
    let r = circle.radius_mm();
    let eps = tolerance.value();

    let fx = ax - cx;
    let fy = ay - cy;
    let b_coef = 2.0 * (fx * ux + fy * uy);
    let c_coef = fx * fx + fy * fy - r * r;
    let disc = b_coef * b_coef - 4.0 * c_coef;
    if disc < -eps * eps {
        return IntersectionSet::Empty;
    }
    let sqrt_disc = disc.max(0.0).sqrt();
    let t1 = f64::midpoint(-b_coef, -sqrt_disc);
    let t2 = f64::midpoint(-b_coef, sqrt_disc);

    let p = |t: f64| Point2::from_mm(ax + t * ux, ay + t * uy);
    let in1 = in_segment(t1, la, eps);
    let in2 = in_segment(t2, la, eps);
    if sqrt_disc < eps {
        if in1 {
            IntersectionSet::One(p(t1))
        } else {
            IntersectionSet::Empty
        }
    } else {
        match (in1, in2) {
            (true, true) => IntersectionSet::Two(p(t1), p(t2)),
            (true, false) => IntersectionSet::One(p(t1)),
            (false, true) => IntersectionSet::One(p(t2)),
            (false, false) => IntersectionSet::Empty,
        }
    }
}

fn line_arc(line: &Line2, arc: &Arc2, tolerance: Tolerance) -> IntersectionSet {
    let circle = arc.as_full_circle();
    match line_circle(line, &circle, tolerance) {
        IntersectionSet::Empty => IntersectionSet::Empty,
        IntersectionSet::One(p) => {
            if arc.contains_point(p, tolerance) {
                IntersectionSet::One(p)
            } else {
                IntersectionSet::Empty
            }
        }
        IntersectionSet::Two(p, q) => filter_two_by_arc(p, q, arc, tolerance),
        IntersectionSet::Coincident => unreachable!("line_circle never returns Coincident"),
    }
}

fn circle_circle(a: &Circle2, b: &Circle2, tolerance: Tolerance) -> IntersectionSet {
    let (ax, ay) = a.center().coords_mm();
    let (bx, by) = b.center().coords_mm();
    let ra = a.radius_mm();
    let rb = b.radius_mm();
    let eps = tolerance.value();

    let dx = bx - ax;
    let dy = by - ay;
    let d_sq = dx * dx + dy * dy;
    let d = d_sq.sqrt();

    if d < eps && (ra - rb).abs() < eps {
        return IntersectionSet::Coincident;
    }
    if d > ra + rb + eps {
        return IntersectionSet::Empty;
    }
    if d + ra.min(rb) < ra.max(rb) - eps {
        return IntersectionSet::Empty;
    }
    if d < eps {
        return IntersectionSet::Empty;
    }

    let a_proj = (d_sq + ra * ra - rb * rb) / (2.0 * d);
    let h_sq = ra * ra - a_proj * a_proj;
    let mx = ax + a_proj * dx / d;
    let my = ay + a_proj * dy / d;

    if h_sq.abs() < eps * eps {
        return IntersectionSet::One(Point2::from_mm(mx, my));
    }
    if h_sq < 0.0 {
        return IntersectionSet::Empty;
    }
    let h = h_sq.sqrt();
    let px = -dy / d * h;
    let py = dx / d * h;
    IntersectionSet::Two(
        Point2::from_mm(mx + px, my + py),
        Point2::from_mm(mx - px, my - py),
    )
}

fn circle_arc(circle: &Circle2, arc: &Arc2, tolerance: Tolerance) -> IntersectionSet {
    let arc_full = arc.as_full_circle();
    match circle_circle(circle, &arc_full, tolerance) {
        IntersectionSet::Empty => IntersectionSet::Empty,
        IntersectionSet::Coincident => IntersectionSet::Coincident,
        IntersectionSet::One(p) => {
            if arc.contains_point(p, tolerance) {
                IntersectionSet::One(p)
            } else {
                IntersectionSet::Empty
            }
        }
        IntersectionSet::Two(p, q) => filter_two_by_arc(p, q, arc, tolerance),
    }
}

fn arc_arc(a: &Arc2, b: &Arc2, tolerance: Tolerance) -> IntersectionSet {
    let a_full = a.as_full_circle();
    let b_full = b.as_full_circle();
    match circle_circle(&a_full, &b_full, tolerance) {
        IntersectionSet::Empty => IntersectionSet::Empty,
        IntersectionSet::Coincident => same_circle_arc_overlap(a, b, tolerance),
        IntersectionSet::One(p) => {
            if a.contains_point(p, tolerance) && b.contains_point(p, tolerance) {
                IntersectionSet::One(p)
            } else {
                IntersectionSet::Empty
            }
        }
        IntersectionSet::Two(p, q) => {
            let p_ok = a.contains_point(p, tolerance) && b.contains_point(p, tolerance);
            let q_ok = a.contains_point(q, tolerance) && b.contains_point(q, tolerance);
            match (p_ok, q_ok) {
                (true, true) => IntersectionSet::Two(p, q),
                (true, false) => IntersectionSet::One(p),
                (false, true) => IntersectionSet::One(q),
                (false, false) => IntersectionSet::Empty,
            }
        }
    }
}

fn same_circle_arc_overlap(a: &Arc2, b: &Arc2, tolerance: Tolerance) -> IntersectionSet {
    let eps_angle = AngleTolerance::from_arc_length(tolerance, a.radius()).radians();
    let (lo_a, mag_a) = canonical_range(a);
    let (lo_b, mag_b) = canonical_range(b);

    if mag_a >= TAU - eps_angle || mag_b >= TAU - eps_angle {
        return IntersectionSet::Coincident;
    }

    let hi_a = lo_a + mag_a;

    let overlaps: [Option<(f64, f64)>; 3] = [-TAU, 0.0, TAU].map(|shift| {
        let lo_b_s = lo_b + shift;
        let hi_b_s = lo_b_s + mag_b;
        let o_lo = lo_a.max(lo_b_s);
        let o_hi = hi_a.min(hi_b_s);
        (o_hi + eps_angle >= o_lo).then_some((o_lo, o_hi))
    });

    let has_1d = overlaps
        .iter()
        .any(|o| matches!(o, Some((s, e)) if e - s > eps_angle));
    if has_1d {
        return IntersectionSet::Coincident;
    }

    let points: Vec<f64> = overlaps
        .iter()
        .filter_map(|o| o.map(|(s, e)| (0.5 * (s + e)).rem_euclid(TAU)))
        .fold(Vec::new(), |mut acc, t| {
            if !acc.iter().any(|&p: &f64| angle_close(p, t, eps_angle)) {
                acc.push(t);
            }
            acc
        });

    match points.as_slice() {
        [] => IntersectionSet::Empty,
        [t] => IntersectionSet::One(arc_point_at_angle(a, *t)),
        [t1, t2, ..] => {
            IntersectionSet::Two(arc_point_at_angle(a, *t1), arc_point_at_angle(a, *t2))
        }
    }
}

fn canonical_range(arc: &Arc2) -> (f64, f64) {
    let start = arc.start_rad();
    let sweep = arc.sweep_rad();
    let lo = if sweep >= 0.0 { start } else { start + sweep };
    (lo.rem_euclid(TAU), sweep.abs())
}

fn arc_point_at_angle(arc: &Arc2, theta: f64) -> Point2 {
    let (cx, cy) = arc.center().coords_mm();
    let r = arc.radius_mm();
    Point2::from_mm(cx + r * theta.cos(), cy + r * theta.sin())
}

fn angle_close(a: f64, b: f64, eps: f64) -> bool {
    wrap_delta(a - b).abs() < eps
}

fn filter_two_by_arc(p: Point2, q: Point2, arc: &Arc2, tolerance: Tolerance) -> IntersectionSet {
    let p_in = arc.contains_point(p, tolerance);
    let q_in = arc.contains_point(q, tolerance);
    match (p_in, q_in) {
        (true, true) => IntersectionSet::Two(p, q),
        (true, false) => IntersectionSet::One(p),
        (false, true) => IntersectionSet::One(q),
        (false, false) => IntersectionSet::Empty,
    }
}

fn in_segment(t: f64, length: f64, eps: f64) -> bool {
    t >= -eps && t <= length + eps
}
