//! Inward offsetting of polygons (spec §2.5).
//!
//! Used to compute the developable polygon `B'` of a block: `B`
//! shrunk by the road setback distance `d_s`. For convex inputs this
//! is exact. For concave inputs the result is the intersection of the
//! inward half-planes of every edge — a conservative approximation
//! that may shave off concave outer-corner detail but never produces
//! an invalid polygon.

use glam::DVec2;

use super::polygon::Polygon;
use super::EPS_GEOM;

/// Compute the inward offset of `poly` by `distance` while preserving
/// a 1-1 correspondence between input edges and output vertices.
///
/// Vertex `i` of the returned ring is the intersection of the inward
/// offset lines of input edges `i-1` and `i`. This is well-defined
/// for convex inputs and modest offsets; for highly non-convex inputs
/// or large offsets the result may self-intersect, in which case
/// the caller should fall back to [`offset_inward`].
#[must_use]
pub fn correlated_offset_inward(poly: &Polygon, distance: f64) -> Option<Vec<DVec2>> {
    let n = poly.len();
    if n < 3 || distance < 0.0 {
        return None;
    }
    let verts = poly.vertices();
    let mut offset_lines: Vec<(DVec2, DVec2)> = Vec::with_capacity(n);
    for i in 0..n {
        let p = verts[i];
        let q = verts[(i + 1) % n];
        let edge = q - p;
        let len = edge.length();
        if len < EPS_GEOM {
            return None;
        }
        let tangent = edge / len;
        let inward = DVec2::new(-tangent.y, tangent.x);
        let p_off = p + inward * distance;
        let q_off = q + inward * distance;
        offset_lines.push((p_off, q_off));
    }
    let mut new_verts = Vec::with_capacity(n);
    for i in 0..n {
        let prev = offset_lines[(i + n - 1) % n];
        let curr = offset_lines[i];
        let isect = line_line_intersect(prev.0, prev.1, curr.0, curr.1)?;
        new_verts.push(isect);
    }
    Some(new_verts)
}

fn line_line_intersect(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Option<DVec2> {
    let d1 = p2 - p1;
    let d2 = p4 - p3;
    let denom = d1.x * d2.y - d1.y * d2.x;
    if denom.abs() < EPS_GEOM {
        return None;
    }
    let dp = p3 - p1;
    let t = (dp.x * d2.y - dp.y * d2.x) / denom;
    Some(p1 + d1 * t)
}

/// Compute the inward offset of `poly` by `distance`. Returns `None`
/// when the result is empty (block too narrow to offset) or invalid.
///
/// Implementation: clip `poly` once by the inward-shifted half-plane
/// of each edge and validate the result.
#[must_use]
pub fn offset_inward(poly: &Polygon, distance: f64) -> Option<Polygon> {
    if distance.abs() < EPS_GEOM {
        return Some(poly.clone());
    }
    if distance < 0.0 {
        return None;
    }
    let mut working = poly.clone();
    for (a, b) in poly.edges().collect::<Vec<_>>() {
        let edge = b - a;
        let len = edge.length();
        if len < EPS_GEOM {
            continue;
        }
        // Inward normal of a CCW polygon: rotate the tangent +90°
        // (left-perpendicular).
        let tangent = edge / len;
        let inward = DVec2::new(-tangent.y, tangent.x);
        let shifted = a + inward * distance;
        match working.clip_half_plane(shifted, inward) {
            Some(p) => working = p,
            None => return None,
        }
    }
    Some(working)
}

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

    #[test]
    fn rectangle_inward_offset_shrinks() {
        let p = Polygon::new(vec![
            DVec2::new(0.0, 0.0),
            DVec2::new(100.0, 0.0),
            DVec2::new(100.0, 50.0),
            DVec2::new(0.0, 50.0),
        ])
        .unwrap();
        let off = offset_inward(&p, 5.0).unwrap();
        // Result should be the rectangle (5,5)-(95,45), area = 90*40.
        assert!((off.area() - 90.0 * 40.0).abs() < 1e-6);
    }

    #[test]
    fn too_large_offset_returns_none() {
        let p = Polygon::new(vec![
            DVec2::new(0.0, 0.0),
            DVec2::new(10.0, 0.0),
            DVec2::new(10.0, 10.0),
            DVec2::new(0.0, 10.0),
        ])
        .unwrap();
        assert!(offset_inward(&p, 50.0).is_none());
    }

    #[test]
    fn zero_offset_is_identity() {
        let p = Polygon::new(vec![
            DVec2::new(0.0, 0.0),
            DVec2::new(1.0, 0.0),
            DVec2::new(1.0, 1.0),
            DVec2::new(0.0, 1.0),
        ])
        .unwrap();
        let off = offset_inward(&p, 0.0).unwrap();
        assert!((off.area() - p.area()).abs() < 1e-12);
    }
}
