//! Curve intersection and proximity detection for paint bucket tool
//!
//! This module provides functions for finding:
//! - Exact intersections between cubic Bezier curves
//! - Self-intersections within a single curve
//! - Closest approach between curves (for gap tolerance)

use vello::kurbo::{CubicBez, ParamCurve, ParamCurveNearest, Point, Shape};

/// Result of a curve intersection
#[derive(Debug, Clone)]
pub struct Intersection {
    /// Parameter t on first curve [0, 1]
    pub t1: f64,
    /// Parameter t on second curve [0, 1] (for curve-curve intersections)
    pub t2: Option<f64>,
    /// Point of intersection
    pub point: Point,
}

/// Result of a close approach between two curves
#[derive(Debug, Clone)]
pub struct CloseApproach {
    /// Parameter on first curve
    pub t1: f64,
    /// Parameter on second curve
    pub t2: f64,
    /// Point on first curve
    pub p1: Point,
    /// Point on second curve
    pub p2: Point,
    /// Distance between the curves
    pub distance: f64,
}

/// Find intersections between two cubic Bezier curves
///
/// Uses recursive subdivision to find intersection points.
/// This is much more robust and faster than sampling.
pub fn find_curve_intersections(curve1: &CubicBez, curve2: &CubicBez) -> Vec<Intersection> {
    let mut intersections = Vec::new();

    // Use subdivision-based intersection detection
    find_intersections_recursive(
        curve1, curve1, 0.0, 1.0,
        curve2, curve2, 0.0, 1.0,
        &mut intersections,
        0, // recursion depth
    );

    // Remove duplicate intersections
    dedup_intersections(&mut intersections, 1.0);

    intersections
}

/// Recursively find intersections using subdivision
///
/// orig_curve1/2 are the original curves (for computing final intersection points)
/// curve1/2 are the current subsegments being tested
/// t1_start/end track the parameter range on the original curve
fn find_intersections_recursive(
    orig_curve1: &CubicBez,
    curve1: &CubicBez,
    t1_start: f64,
    t1_end: f64,
    orig_curve2: &CubicBez,
    curve2: &CubicBez,
    t2_start: f64,
    t2_end: f64,
    intersections: &mut Vec<Intersection>,
    depth: usize,
) {
    // Maximum recursion depth
    const MAX_DEPTH: usize = 20;

    // Minimum parameter range (if smaller, we've found an intersection)
    const MIN_RANGE: f64 = 0.001;

    // Get bounding boxes of current subsegments
    let bbox1 = curve1.bounding_box();
    let bbox2 = curve2.bounding_box();

    // Inflate bounding boxes slightly to account for numerical precision
    let bbox1 = bbox1.inflate(0.1, 0.1);
    let bbox2 = bbox2.inflate(0.1, 0.1);

    // If bounding boxes don't overlap, no intersection
    if !bboxes_overlap(&bbox1, &bbox2) {
        return;
    }

    // If we've recursed deep enough or ranges are small enough, record intersection
    if depth >= MAX_DEPTH ||
       ((t1_end - t1_start) < MIN_RANGE && (t2_end - t2_start) < MIN_RANGE) {
        let t1 = (t1_start + t1_end) / 2.0;
        let t2 = (t2_start + t2_end) / 2.0;

        intersections.push(Intersection {
            t1,
            t2: Some(t2),
            point: orig_curve1.eval(t1),
        });
        return;
    }

    // Subdivide both curves at midpoint (of the current subsegment, which is 0..1)
    let t1_mid = (t1_start + t1_end) / 2.0;
    let t2_mid = (t2_start + t2_end) / 2.0;

    // Create subsegments - these are new curves parameterized 0..1
    let curve1_left = curve1.subsegment(0.0..0.5);
    let curve1_right = curve1.subsegment(0.5..1.0);
    let curve2_left = curve2.subsegment(0.0..0.5);
    let curve2_right = curve2.subsegment(0.5..1.0);

    // Check all four combinations
    find_intersections_recursive(
        orig_curve1, &curve1_left, t1_start, t1_mid,
        orig_curve2, &curve2_left, t2_start, t2_mid,
        intersections, depth + 1
    );

    find_intersections_recursive(
        orig_curve1, &curve1_left, t1_start, t1_mid,
        orig_curve2, &curve2_right, t2_mid, t2_end,
        intersections, depth + 1
    );

    find_intersections_recursive(
        orig_curve1, &curve1_right, t1_mid, t1_end,
        orig_curve2, &curve2_left, t2_start, t2_mid,
        intersections, depth + 1
    );

    find_intersections_recursive(
        orig_curve1, &curve1_right, t1_mid, t1_end,
        orig_curve2, &curve2_right, t2_mid, t2_end,
        intersections, depth + 1
    );
}

/// Check if two bounding boxes overlap
fn bboxes_overlap(bbox1: &vello::kurbo::Rect, bbox2: &vello::kurbo::Rect) -> bool {
    bbox1.x0 <= bbox2.x1 &&
    bbox1.x1 >= bbox2.x0 &&
    bbox1.y0 <= bbox2.y1 &&
    bbox1.y1 >= bbox2.y0
}

/// Find self-intersections within a single cubic Bezier curve
///
/// A curve self-intersects when it crosses itself, forming a loop.
pub fn find_self_intersections(curve: &CubicBez) -> Vec<Intersection> {
    let mut intersections = Vec::new();

    // Sample the curve at regular intervals
    let samples = 50;
    for i in 0..samples {
        let t1 = i as f64 / samples as f64;
        let p1 = curve.eval(t1);

        // Check against all later points
        for j in (i + 5)..samples {  // Skip nearby points to avoid false positives
            let t2 = j as f64 / samples as f64;
            let p2 = curve.eval(t2);
            let dist = (p1 - p2).hypot();

            // If points are very close, we may have a self-intersection
            if dist < 0.5 {
                // Refine to get more accurate parameters
                let (refined_t1, refined_t2) = refine_self_intersection(curve, t1, t2);

                intersections.push(Intersection {
                    t1: refined_t1,
                    t2: Some(refined_t2),
                    point: curve.eval(refined_t1),
                });
            }
        }
    }

    // Remove duplicates
    dedup_intersections(&mut intersections, 0.5);

    intersections
}

/// Find the closest approach between two curves if within tolerance
///
/// Returns Some if the minimum distance between curves is less than tolerance.
pub fn find_closest_approach(
    curve1: &CubicBez,
    curve2: &CubicBez,
    tolerance: f64,
) -> Option<CloseApproach> {
    let mut min_dist = f64::MAX;
    let mut best_t1 = 0.0;
    let mut best_t2 = 0.0;

    // Sample curve1 at regular intervals
    let samples = 50;
    for i in 0..=samples {
        let t1 = i as f64 / samples as f64;
        let p1 = curve1.eval(t1);

        // Find nearest point on curve2
        let nearest = curve2.nearest(p1, 1e-6);
        let dist = (p1 - curve2.eval(nearest.t)).hypot();

        if dist < min_dist {
            min_dist = dist;
            best_t1 = t1;
            best_t2 = nearest.t;
        }
    }

    // If minimum distance is within tolerance, return it
    if min_dist < tolerance {
        Some(CloseApproach {
            t1: best_t1,
            t2: best_t2,
            p1: curve1.eval(best_t1),
            p2: curve2.eval(best_t2),
            distance: min_dist,
        })
    } else {
        None
    }
}

/// Refine self-intersection parameters
fn refine_self_intersection(curve: &CubicBez, mut t1: f64, mut t2: f64) -> (f64, f64) {
    // Refine by moving parameters closer to where curves actually meet
    for _ in 0..5 {
        let p1 = curve.eval(t1);
        let p2 = curve.eval(t2);
        let mid = Point::new((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0);

        // Move both parameters toward the midpoint
        let nearest1 = curve.nearest(mid, 1e-6);
        let nearest2 = curve.nearest(mid, 1e-6);

        // Take whichever is closer to original parameter
        if (nearest1.t - t1).abs() < (nearest2.t - t1).abs() {
            t1 = nearest1.t;
        } else if (nearest2.t - t2).abs() < (nearest1.t - t2).abs() {
            t2 = nearest2.t;
        }
    }

    (t1.clamp(0.0, 1.0), t2.clamp(0.0, 1.0))
}

/// Remove duplicate intersections within a tolerance
fn dedup_intersections(intersections: &mut Vec<Intersection>, tolerance: f64) {
    let mut i = 0;
    while i < intersections.len() {
        let mut j = i + 1;
        while j < intersections.len() {
            let dist = (intersections[i].point - intersections[j].point).hypot();
            if dist < tolerance {
                intersections.remove(j);
            } else {
                j += 1;
            }
        }
        i += 1;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_curve_intersection_simple() {
        // Two curves that cross
        let curve1 = CubicBez::new(
            Point::new(0.0, 0.0),
            Point::new(100.0, 100.0),
            Point::new(100.0, 100.0),
            Point::new(200.0, 200.0),
        );

        let curve2 = CubicBez::new(
            Point::new(200.0, 0.0),
            Point::new(100.0, 100.0),
            Point::new(100.0, 100.0),
            Point::new(0.0, 200.0),
        );

        let intersections = find_curve_intersections(&curve1, &curve2);
        // Should find at least one intersection near the center
        assert!(!intersections.is_empty());
    }

    #[test]
    fn test_self_intersection() {
        // A curve that loops back on itself
        let curve = CubicBez::new(
            Point::new(0.0, 0.0),
            Point::new(100.0, 100.0),
            Point::new(-100.0, 100.0),
            Point::new(0.0, 0.0),
        );

        let intersections = find_self_intersections(&curve);
        // May or may not find intersection depending on curve shape
        // This is mostly testing that the function doesn't crash
        assert!(intersections.len() <= 10);  // Sanity check
    }

    #[test]
    fn test_closest_approach() {
        // Two curves that are close but don't intersect
        let curve1 = CubicBez::new(
            Point::new(0.0, 0.0),
            Point::new(50.0, 0.0),
            Point::new(100.0, 0.0),
            Point::new(150.0, 0.0),
        );

        let curve2 = CubicBez::new(
            Point::new(0.0, 1.5),
            Point::new(50.0, 1.5),
            Point::new(100.0, 1.5),
            Point::new(150.0, 1.5),
        );

        let approach = find_closest_approach(&curve1, &curve2, 2.0);
        assert!(approach.is_some());
        let approach = approach.unwrap();
        assert!(approach.distance < 2.0);
    }
}
