use bone_types::{Angle, Length, Parameter, Point2, Tolerance, Vec2};
use core::f64::consts::TAU;
use uom::si::angle::radian;
use uom::si::length::millimeter;

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

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum OffsetError {
    Collapsed,
    SharpArcCorner,
    Degenerate,
}

const WELD_TOLERANCE_MM: f64 = 1.0e-6;
const ARC_AREA_SAMPLES: u32 = 24;
const PARALLEL_SIN_EPS: f64 = 1.0e-9;

#[derive(Copy, Clone)]
enum Off {
    Line {
        start: Point2,
        end: Point2,
    },
    Arc {
        center: Point2,
        radius_mm: f64,
        start_rad: f64,
        sweep_rad: f64,
    },
}

impl Off {
    fn start_point(self) -> Point2 {
        match self {
            Self::Line { start, .. } => start,
            Self::Arc {
                center,
                radius_mm,
                start_rad,
                ..
            } => polar(center, radius_mm, start_rad),
        }
    }

    fn end_point(self) -> Point2 {
        match self {
            Self::Line { end, .. } => end,
            Self::Arc {
                center,
                radius_mm,
                start_rad,
                sweep_rad,
            } => polar(center, radius_mm, start_rad + sweep_rad),
        }
    }

    fn set_start(&mut self, point: Point2) {
        match self {
            Self::Line { start, .. } => *start = point,
            Self::Arc {
                center,
                start_rad,
                sweep_rad,
                ..
            } => {
                let end = *start_rad + *sweep_rad;
                let new_start = angle_of(*center, point);
                *sweep_rad = directed_span(new_start, end, *sweep_rad);
                *start_rad = new_start;
            }
        }
    }

    fn set_end(&mut self, point: Point2) {
        match self {
            Self::Line { end, .. } => *end = point,
            Self::Arc {
                center,
                start_rad,
                sweep_rad,
                ..
            } => {
                let new_end = angle_of(*center, point);
                *sweep_rad = directed_span(*start_rad, new_end, *sweep_rad);
            }
        }
    }

    fn into_curve(self, tolerance: Tolerance) -> Result<Curve2Kind, OffsetError> {
        match self {
            Self::Line { start, end } => Line2::new(start, end, tolerance)
                .map(Curve2Kind::Line)
                .map_err(|_| OffsetError::Collapsed),
            Self::Arc {
                center,
                radius_mm,
                start_rad,
                sweep_rad,
            } => Arc2::new(
                center,
                Length::new::<millimeter>(radius_mm),
                Angle::new::<radian>(start_rad),
                Angle::new::<radian>(sweep_rad),
                tolerance,
            )
            .map(Curve2Kind::Arc)
            .map_err(|_| OffsetError::Collapsed),
        }
    }
}

pub fn offset_loop(
    curves: &[Curve2Kind],
    distance_mm: f64,
    tolerance: Tolerance,
) -> Result<Vec<Curve2Kind>, OffsetError> {
    if curves.is_empty() {
        return Err(OffsetError::Degenerate);
    }
    if let [Curve2Kind::Circle(circle)] = curves {
        let new_radius = circle.radius_mm() + distance_mm;
        if new_radius < tolerance.value() {
            return Err(OffsetError::Collapsed);
        }
        let offset = Circle2::new(
            circle.center(),
            Length::new::<millimeter>(new_radius),
            tolerance,
        )
        .map_err(|_| OffsetError::Collapsed)?;
        return Ok(vec![Curve2Kind::Circle(offset)]);
    }

    let area_sign = if signed_area(curves) >= 0.0 {
        1.0
    } else {
        -1.0
    };
    let mut offsets: Vec<Off> = curves
        .iter()
        .map(|curve| naive_offset(*curve, distance_mm, area_sign, tolerance))
        .collect::<Result<_, _>>()?;

    let count = offsets.len();
    let joints: Vec<Point2> = (0..count)
        .map(|i| {
            let prev = offsets[i];
            let next = offsets[(i + 1) % count];
            resolve_joint(prev, next)
        })
        .collect::<Result<_, _>>()?;

    (0..count).for_each(|i| {
        offsets[i].set_end(joints[i]);
        offsets[(i + 1) % count].set_start(joints[i]);
    });

    let result: Vec<Curve2Kind> = offsets
        .into_iter()
        .map(|off| off.into_curve(tolerance))
        .collect::<Result<_, _>>()?;

    let result_area = signed_area(&result);
    if result_area.abs() < tolerance.value() || (result_area >= 0.0) != (area_sign >= 0.0) {
        return Err(OffsetError::Collapsed);
    }
    let folded = curves
        .iter()
        .zip(&result)
        .any(|(source, offset)| direction_reversed(*source, *offset));
    if folded {
        return Err(OffsetError::Collapsed);
    }
    Ok(result)
}

pub fn offset_open_chain(
    curves: &[Curve2Kind],
    signed_distance_mm: f64,
    tolerance: Tolerance,
) -> Result<Vec<Curve2Kind>, OffsetError> {
    if curves.is_empty() {
        return Err(OffsetError::Degenerate);
    }
    if curves
        .iter()
        .any(|curve| matches!(curve, Curve2Kind::Circle(_)))
    {
        return Err(OffsetError::Degenerate);
    }
    let mut offsets: Vec<Off> = curves
        .iter()
        .map(|curve| naive_offset(*curve, signed_distance_mm, 1.0, tolerance))
        .collect::<Result<_, _>>()?;
    let joints: Vec<Point2> = (0..offsets.len().saturating_sub(1))
        .map(|i| resolve_joint(offsets[i], offsets[i + 1]))
        .collect::<Result<_, _>>()?;
    joints.iter().enumerate().for_each(|(i, &joint)| {
        offsets[i].set_end(joint);
        offsets[i + 1].set_start(joint);
    });
    offsets
        .into_iter()
        .map(|off| off.into_curve(tolerance))
        .collect()
}

pub fn reverse_curve(curve: Curve2Kind, tolerance: Tolerance) -> Result<Curve2Kind, OffsetError> {
    match curve {
        Curve2Kind::Line(line) => Line2::new(line.end(), line.start(), tolerance)
            .map(Curve2Kind::Line)
            .map_err(|_| OffsetError::Collapsed),
        Curve2Kind::Arc(arc) => Arc2::new(
            arc.center(),
            arc.radius(),
            Angle::new::<radian>(arc.start_rad() + arc.sweep_rad()),
            Angle::new::<radian>(-arc.sweep_rad()),
            tolerance,
        )
        .map(Curve2Kind::Arc)
        .map_err(|_| OffsetError::Collapsed),
        Curve2Kind::Circle(_) | Curve2Kind::Ellipse(_) | Curve2Kind::Bezier(_) => {
            Err(OffsetError::Degenerate)
        }
    }
}

fn direction_reversed(source: Curve2Kind, offset: Curve2Kind) -> bool {
    match (source, offset) {
        (Curve2Kind::Line(a), Curve2Kind::Line(b)) => {
            let (ax, ay) = (a.end() - a.start()).coords_mm();
            let (bx, by) = (b.end() - b.start()).coords_mm();
            ax * bx + ay * by <= 0.0
        }
        (Curve2Kind::Arc(a), Curve2Kind::Arc(b)) => {
            a.sweep_rad().signum() != b.sweep_rad().signum()
        }
        _ => false,
    }
}

fn naive_offset(
    curve: Curve2Kind,
    distance_mm: f64,
    area_sign: f64,
    tolerance: Tolerance,
) -> Result<Off, OffsetError> {
    match curve {
        Curve2Kind::Line(line) => {
            let (dx, dy) = line.unit_direction().components();
            let shift = Vec2::from_mm(dy * distance_mm * area_sign, -dx * distance_mm * area_sign);
            Ok(Off::Line {
                start: line.start() + shift,
                end: line.end() + shift,
            })
        }
        Curve2Kind::Arc(arc) => {
            let new_radius = arc.radius_mm() + distance_mm * arc.sweep_rad().signum() * area_sign;
            if new_radius < tolerance.value() {
                return Err(OffsetError::Collapsed);
            }
            Ok(Off::Arc {
                center: arc.center(),
                radius_mm: new_radius,
                start_rad: arc.start_rad(),
                sweep_rad: arc.sweep_rad(),
            })
        }
        Curve2Kind::Circle(_) | Curve2Kind::Ellipse(_) | Curve2Kind::Bezier(_) => {
            Err(OffsetError::Degenerate)
        }
    }
}

fn resolve_joint(prev: Off, next: Off) -> Result<Point2, OffsetError> {
    let pa = prev.end_point();
    let pb = next.start_point();
    if (pa - pb).norm_mm() < WELD_TOLERANCE_MM {
        return Ok(midpoint(pa, pb));
    }
    match (prev, next) {
        (Off::Line { start: a0, end: a1 }, Off::Line { start: b0, end: b1 }) => {
            infinite_line_intersection(a0, a1, b0, b1).ok_or(OffsetError::Collapsed)
        }
        _ => Err(OffsetError::SharpArcCorner),
    }
}

fn infinite_line_intersection(a0: Point2, a1: Point2, b0: Point2, b1: Point2) -> Option<Point2> {
    let (ax, ay) = a0.coords_mm();
    let (ux, uy) = (a1 - a0).coords_mm();
    let (bx, by) = b0.coords_mm();
    let (vx, vy) = (b1 - b0).coords_mm();
    let denom = ux * vy - uy * vx;
    let scale = ux.hypot(uy) * vx.hypot(vy);
    if denom.abs() <= scale * PARALLEL_SIN_EPS {
        return None;
    }
    let t = ((bx - ax) * vy - (by - ay) * vx) / denom;
    Some(Point2::from_mm(ax + t * ux, ay + t * uy))
}

fn polar(center: Point2, radius_mm: f64, angle_rad: f64) -> Point2 {
    let (cx, cy) = center.coords_mm();
    Point2::from_mm(
        cx + radius_mm * angle_rad.cos(),
        cy + radius_mm * angle_rad.sin(),
    )
}

fn angle_of(center: Point2, point: Point2) -> f64 {
    let (cx, cy) = center.coords_mm();
    let (px, py) = point.coords_mm();
    (py - cy).atan2(px - cx)
}

fn directed_span(start_rad: f64, end_rad: f64, original_sweep: f64) -> f64 {
    let raw = end_rad - start_rad;
    let turns = ((original_sweep - raw) / TAU).round();
    raw + turns * TAU
}

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

fn signed_area(curves: &[Curve2Kind]) -> f64 {
    let points: Vec<(f64, f64)> = curves
        .iter()
        .flat_map(|curve| {
            let samples = match curve {
                Curve2Kind::Line(_) => 1,
                Curve2Kind::Arc(_)
                | Curve2Kind::Circle(_)
                | Curve2Kind::Ellipse(_)
                | Curve2Kind::Bezier(_) => ARC_AREA_SAMPLES,
            };
            (0..samples).map(move |i| {
                let t = f64::from(i) / f64::from(samples);
                curve.evaluate(Parameter::new(t)).coords_mm()
            })
        })
        .collect();
    let n = points.len();
    points
        .iter()
        .enumerate()
        .map(|(i, &(x0, y0))| {
            let (x1, y1) = points[(i + 1) % n];
            x0 * y1 - x1 * y0
        })
        .sum::<f64>()
        / 2.0
}

#[cfg(test)]
mod tests {
    use super::{OffsetError, offset_loop};
    use crate::arc2::Arc2;
    use crate::circle2::Circle2;
    use crate::curve2::{Curve2, Curve2Kind};
    use crate::line2::Line2;
    use bone_types::{Angle, Length, Parameter, Point2, Tolerance};
    use core::f64::consts::{FRAC_PI_2, TAU};
    use uom::si::angle::radian;
    use uom::si::length::millimeter;

    const TOL: Tolerance = Tolerance::new(1.0e-6);

    fn ok<T, E: core::fmt::Debug>(result: Result<T, E>) -> T {
        match result {
            Ok(value) => value,
            Err(error) => panic!("{error:?}"),
        }
    }

    fn line(x0: f64, y0: f64, x1: f64, y1: f64) -> Curve2Kind {
        Curve2Kind::Line(ok(Line2::new(
            Point2::from_mm(x0, y0),
            Point2::from_mm(x1, y1),
            TOL,
        )))
    }

    fn square(side: f64) -> Vec<Curve2Kind> {
        vec![
            line(0.0, 0.0, side, 0.0),
            line(side, 0.0, side, side),
            line(side, side, 0.0, side),
            line(0.0, side, 0.0, 0.0),
        ]
    }

    fn loop_area(curves: &[Curve2Kind]) -> f64 {
        let points: Vec<(f64, f64)> = curves
            .iter()
            .flat_map(|curve| {
                (0..16).map(move |i| {
                    curve
                        .evaluate(Parameter::new(f64::from(i) / 16.0))
                        .coords_mm()
                })
            })
            .collect();
        let n = points.len();
        points
            .iter()
            .enumerate()
            .map(|(i, &(x0, y0))| {
                let (x1, y1) = points[(i + 1) % n];
                x0 * y1 - x1 * y0
            })
            .sum::<f64>()
            / 2.0
    }

    #[test]
    fn outward_offset_of_a_square_grows_each_side() {
        let offset = ok(offset_loop(&square(10.0), 2.0, TOL));
        assert_eq!(offset.len(), 4);
        assert!((loop_area(&offset).abs() - 14.0 * 14.0).abs() < 1.0e-6);
    }

    #[test]
    fn inward_offset_of_a_square_shrinks_each_side() {
        let offset = ok(offset_loop(&square(10.0), -3.0, TOL));
        assert!((loop_area(&offset).abs() - 4.0 * 4.0).abs() < 1.0e-6);
    }

    #[test]
    fn over_shrinking_a_square_collapses() {
        assert_eq!(
            offset_loop(&square(10.0), -6.0, TOL),
            Err(OffsetError::Collapsed)
        );
    }

    #[test]
    fn circle_offsets_to_a_concentric_circle() {
        let circle = ok(Circle2::new(
            Point2::from_mm(1.0, 2.0),
            Length::new::<millimeter>(5.0),
            TOL,
        ));
        let offset = ok(offset_loop(&[Curve2Kind::Circle(circle)], 1.5, TOL));
        let [Curve2Kind::Circle(grown)] = offset.as_slice() else {
            panic!("circle stays a circle");
        };
        assert!((grown.radius_mm() - 6.5).abs() < 1.0e-9);
        assert_eq!(grown.center(), Point2::from_mm(1.0, 2.0));
    }

    #[test]
    fn rounded_slot_tangent_corners_weld_without_error() {
        let offset = ok(offset_loop(&rounded_slot(5.0, 20.0), 2.0, TOL));
        assert_eq!(offset.len(), 4);
    }

    fn rounded_slot(r: f64, len: f64) -> Vec<Curve2Kind> {
        let arc_right = ok(Arc2::new(
            Point2::from_mm(len, 0.0),
            Length::new::<millimeter>(r),
            Angle::new::<radian>(-FRAC_PI_2),
            Angle::new::<radian>(core::f64::consts::PI),
            TOL,
        ));
        let arc_left = ok(Arc2::new(
            Point2::from_mm(0.0, 0.0),
            Length::new::<millimeter>(r),
            Angle::new::<radian>(FRAC_PI_2),
            Angle::new::<radian>(core::f64::consts::PI),
            TOL,
        ));
        vec![
            line(0.0, -r, len, -r),
            Curve2Kind::Arc(arc_right),
            line(len, r, 0.0, r),
            Curve2Kind::Arc(arc_left),
        ]
    }

    #[test]
    fn inward_offset_of_a_rounded_slot_trims_arcs_without_flipping() {
        let offset = ok(offset_loop(&rounded_slot(5.0, 20.0), -2.0, TOL));
        assert_eq!(offset.len(), 4);
        let area = loop_area(&offset).abs();
        let expected = core::f64::consts::PI * 3.0 * 3.0 + 2.0 * 3.0 * 20.0;
        assert!(
            (area - expected).abs() < 1.0,
            "the inward slot keeps its trimmed radius-three arcs, got area {area}",
        );
    }

    #[test]
    fn sharp_arc_corner_is_reported() {
        let arc = ok(Arc2::new(
            Point2::from_mm(0.0, 0.0),
            Length::new::<millimeter>(5.0),
            Angle::new::<radian>(0.0),
            Angle::new::<radian>(FRAC_PI_2),
            TOL,
        ));
        let loop_curves = vec![
            Curve2Kind::Arc(arc),
            line(0.0, 5.0, 0.0, 0.0),
            line(0.0, 0.0, 5.0, 0.0),
        ];
        assert_eq!(
            offset_loop(&loop_curves, 1.0, TOL),
            Err(OffsetError::SharpArcCorner)
        );
    }

    #[test]
    fn offset_is_deterministic() {
        let a = ok(offset_loop(&square(8.0), 1.25, TOL));
        let b = ok(offset_loop(&square(8.0), 1.25, TOL));
        assert_eq!(a, b);
        let _ = TAU;
    }
}
