//! Distances, containment and measurement.

use crate::{Point, Rect};

/// Distance from `p` to the segment `a`–`b`.
pub fn dist_to_segment(p: Point, a: Point, b: Point) -> f64 {
    let ab = b - a;
    let l2 = ab.dot(ab);
    if l2 <= 1e-18 {
        return p.dist(a);
    }
    let t = ((p - a).dot(ab) / l2).clamp(0.0, 1.0);
    p.dist(a + ab * t)
}

/// Distance from `p` to a polyline (closing it back to the start if `closed`).
pub fn dist_to_polyline(p: Point, pts: &[Point], closed: bool) -> f64 {
    let mut best = match pts.first() {
        Some(f) => p.dist(*f),
        None => return f64::INFINITY,
    };
    for w in pts.windows(2) {
        if let [a, b] = w {
            best = best.min(dist_to_segment(p, *a, *b));
        }
    }
    if closed {
        if let (Some(f), Some(l)) = (pts.first(), pts.last()) {
            best = best.min(dist_to_segment(p, *l, *f));
        }
    }
    best
}

/// Even-odd point-in-polygon test.
pub fn point_in_polygon(p: Point, poly: &[Point]) -> bool {
    let n = poly.len();
    if n < 3 {
        return false;
    }
    let mut inside = false;
    let mut j = n - 1;
    for i in 0..n {
        let (Some(a), Some(b)) = (poly.get(i), poly.get(j)) else {
            break;
        };
        if (a.y > p.y) != (b.y > p.y) {
            let x = (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x;
            if p.x < x {
                inside = !inside;
            }
        }
        j = i;
    }
    inside
}

/// Unsigned area of a simple polygon (shoelace formula).
pub fn polygon_area(poly: &[Point]) -> f64 {
    let n = poly.len();
    if n < 3 {
        return 0.0;
    }
    let mut s = 0.0;
    for i in 0..n {
        if let (Some(a), Some(b)) = (poly.get(i), poly.get((i + 1) % n)) {
            s += a.cross(*b);
        }
    }
    (s / 2.0).abs()
}

/// Total length of a polyline.
pub fn polyline_length(pts: &[Point], closed: bool) -> f64 {
    let mut l: f64 = pts
        .windows(2)
        .map(|w| match w {
            [a, b] => a.dist(*b),
            _ => 0.0,
        })
        .sum();
    if closed {
        if let (Some(f), Some(e)) = (pts.first(), pts.last()) {
            l += f.dist(*e);
        }
    }
    l
}

/// Whether segments `a1`–`a2` and `b1`–`b2` intersect.
pub fn segments_intersect(a1: Point, a2: Point, b1: Point, b2: Point) -> bool {
    let d1 = (a2 - a1).cross(b1 - a1);
    let d2 = (a2 - a1).cross(b2 - a1);
    let d3 = (b2 - b1).cross(a1 - b1);
    let d4 = (b2 - b1).cross(a2 - b1);
    ((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0))
}

/// Whether the rectangle `r` lies completely inside the polygon `poly`
/// (even-odd): all four corners are inside and no polygon edge crosses a
/// side of the rectangle.
pub fn polygon_contains_rect(poly: &[Point], r: &Rect) -> bool {
    let n = poly.len();
    if n < 3 || r.is_empty() {
        return false;
    }
    let c = r.corners();
    if !c.iter().all(|q| point_in_polygon(*q, poly)) {
        return false;
    }
    for i in 0..n {
        let (Some(a), Some(b)) = (poly.get(i), poly.get((i + 1) % n)) else {
            return false;
        };
        for k in 0..4 {
            let (Some(p), Some(q)) = (c.get(k), c.get((k + 1) % 4)) else {
                return false;
            };
            if segments_intersect(*a, *b, *p, *q) {
                return false;
            }
        }
    }
    true
}

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

    #[test]
    fn segment_distance() {
        let d = dist_to_segment(Point::new(5.0, 5.0), Point::ZERO, Point::new(10.0, 0.0));
        assert!((d - 5.0).abs() < 1e-12);
        let d = dist_to_segment(Point::new(-3.0, 4.0), Point::ZERO, Point::new(10.0, 0.0));
        assert!((d - 5.0).abs() < 1e-12);
    }

    #[test]
    fn polygon_contains_rect_cases() {
        let sq = [
            Point::ZERO,
            Point::new(10.0, 0.0),
            Point::new(10.0, 10.0),
            Point::new(0.0, 10.0),
        ];
        let r = |a: f64, b: f64, c: f64, d: f64| Rect::new(Point::new(a, b), Point::new(c, d));
        assert!(polygon_contains_rect(&sq, &r(2.0, 2.0, 8.0, 8.0)));
        assert!(!polygon_contains_rect(&sq, &r(5.0, 5.0, 15.0, 8.0)));
        // A notch cuts through the rectangle although all corners are inside.
        let u = [
            Point::ZERO,
            Point::new(10.0, 0.0),
            Point::new(10.0, 10.0),
            Point::new(6.0, 10.0),
            Point::new(6.0, 4.0),
            Point::new(4.0, 4.0),
            Point::new(4.0, 10.0),
            Point::new(0.0, 10.0),
        ];
        assert!(!polygon_contains_rect(&u, &r(1.0, 3.0, 9.0, 8.0)));
        assert!(polygon_contains_rect(&u, &r(1.0, 1.0, 9.0, 3.0)));
    }

    #[test]
    fn polygon_tests() {
        let sq = [
            Point::ZERO,
            Point::new(10.0, 0.0),
            Point::new(10.0, 10.0),
            Point::new(0.0, 10.0),
        ];
        assert!(point_in_polygon(Point::new(5.0, 5.0), &sq));
        assert!(!point_in_polygon(Point::new(15.0, 5.0), &sq));
        assert!((polygon_area(&sq) - 100.0).abs() < 1e-12);
        assert!((polyline_length(&sq, true) - 40.0).abs() < 1e-12);
        assert!(segments_intersect(
            Point::ZERO,
            Point::new(10.0, 10.0),
            Point::new(0.0, 10.0),
            Point::new(10.0, 0.0)
        ));
    }
}
