use serde::Serialize;

use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};

pub const MAX_CONVEX_VERTS: usize = 8;

pub const MAX_PATH_POINTS: usize = 8;

const GEOMETRY_EPSILON: f32 = 1.0e-4;

#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ConvexPoly {
    verts: Vec<LayoutPos>,
}

impl ConvexPoly {
    #[must_use]
    pub fn new(verts: Vec<LayoutPos>) -> Option<Self> {
        if !(3..=MAX_CONVEX_VERTS).contains(&verts.len()) {
            return None;
        }
        let n = verts.len();
        let edges_nondegenerate =
            (0..n).all(|i| edge_length(verts[i], verts[(i + 1) % n]) > GEOMETRY_EPSILON);
        if !edges_nondegenerate {
            return None;
        }
        is_convex(&verts).then_some(Self { verts })
    }

    #[must_use]
    pub fn verts(&self) -> &[LayoutPos] {
        &self.verts
    }

    #[must_use]
    pub fn bounds(&self) -> LayoutRect {
        bounds_of(&self.verts)
    }

    #[must_use]
    pub fn edge_planes(&self) -> Vec<[f32; 3]> {
        let center = centroid(&self.verts);
        let n = self.verts.len();
        (0..n)
            .map(|i| outward_plane(self.verts[i], self.verts[(i + 1) % n], center))
            .collect()
    }
}

#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct PolyPath {
    points: Vec<LayoutPos>,
    closed: bool,
}

impl PolyPath {
    #[must_use]
    pub fn new(points: Vec<LayoutPos>, closed: bool) -> Option<Self> {
        (2..=MAX_PATH_POINTS)
            .contains(&points.len())
            .then_some(Self { points, closed })
    }

    #[must_use]
    pub fn open(points: Vec<LayoutPos>) -> Option<Self> {
        Self::new(points, false)
    }

    #[must_use]
    pub fn closed_loop(points: Vec<LayoutPos>) -> Option<Self> {
        Self::new(points, true)
    }

    #[must_use]
    pub fn points(&self) -> &[LayoutPos] {
        &self.points
    }

    #[must_use]
    pub fn is_closed(&self) -> bool {
        self.closed
    }

    pub fn segments(&self) -> impl Iterator<Item = (LayoutPos, LayoutPos)> + '_ {
        let n = self.points.len();
        let count = if self.closed { n } else { n - 1 };
        (0..count).map(move |i| (self.points[i], self.points[(i + 1) % n]))
    }

    #[must_use]
    pub fn bounds(&self) -> LayoutRect {
        bounds_of(&self.points)
    }
}

fn edge_length(a: LayoutPos, b: LayoutPos) -> f32 {
    (b.x.value() - a.x.value()).hypot(b.y.value() - a.y.value())
}

fn is_convex(verts: &[LayoutPos]) -> bool {
    let n = verts.len();
    let crosses = (0..n).map(|i| turn_cross(verts[i], verts[(i + 1) % n], verts[(i + 2) % n]));
    let signs = crosses.fold((false, false, false), |(pos, neg, any), c| {
        (
            pos || c > GEOMETRY_EPSILON,
            neg || c < -GEOMETRY_EPSILON,
            any || c.abs() > GEOMETRY_EPSILON,
        )
    });
    let (positive, negative, any_nonzero) = signs;
    any_nonzero && !(positive && negative)
}

fn turn_cross(a: LayoutPos, b: LayoutPos, c: LayoutPos) -> f32 {
    let (abx, aby) = (b.x.value() - a.x.value(), b.y.value() - a.y.value());
    let (bcx, bcy) = (c.x.value() - b.x.value(), c.y.value() - b.y.value());
    abx * bcy - aby * bcx
}

fn centroid(verts: &[LayoutPos]) -> (f32, f32) {
    let sum = verts.iter().fold((0.0, 0.0), |(sx, sy), p| {
        (sx + p.x.value(), sy + p.y.value())
    });
    let count = lossless_count(verts.len());
    (sum.0 / count, sum.1 / count)
}

fn outward_plane(a: LayoutPos, b: LayoutPos, center: (f32, f32)) -> [f32; 3] {
    let (dx, dy) = (b.x.value() - a.x.value(), b.y.value() - a.y.value());
    let len = dx.hypot(dy).max(GEOMETRY_EPSILON);
    let (mut nx, mut ny) = (dy / len, -dx / len);
    let d = nx * a.x.value() + ny * a.y.value();
    if nx * center.0 + ny * center.1 - d > 0.0 {
        nx = -nx;
        ny = -ny;
    }
    [nx, ny, nx * a.x.value() + ny * a.y.value()]
}

fn bounds_of(points: &[LayoutPos]) -> LayoutRect {
    let fold_minmax = |pick: fn(LayoutPos) -> f32| {
        points
            .iter()
            .fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), p| {
                (lo.min(pick(*p)), hi.max(pick(*p)))
            })
    };
    let (min_x, max_x) = fold_minmax(|p| p.x.value());
    let (min_y, max_y) = fold_minmax(|p| p.y.value());
    LayoutRect::new(
        LayoutPos::new(LayoutPx::saturating(min_x), LayoutPx::saturating(min_y)),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(max_x - min_x),
            LayoutPx::saturating_nonneg(max_y - min_y),
        ),
    )
}

#[allow(
    clippy::cast_precision_loss,
    reason = "vertex counts stay far below the f32 mantissa limit"
)]
fn lossless_count(len: usize) -> f32 {
    len as f32
}

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

    fn pos(x: f32, y: f32) -> LayoutPos {
        LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y))
    }

    #[test]
    fn a_square_is_convex() {
        assert!(
            ConvexPoly::new(vec![
                pos(0.0, 0.0),
                pos(4.0, 0.0),
                pos(4.0, 4.0),
                pos(0.0, 4.0)
            ])
            .is_some()
        );
    }

    #[test]
    fn a_dart_is_rejected_as_concave() {
        let dart = vec![pos(0.0, 0.0), pos(4.0, 2.0), pos(0.0, 4.0), pos(1.0, 2.0)];
        assert!(ConvexPoly::new(dart).is_none());
    }

    #[test]
    fn fewer_than_three_vertices_is_rejected() {
        assert!(ConvexPoly::new(vec![pos(0.0, 0.0), pos(1.0, 1.0)]).is_none());
    }

    #[test]
    fn more_than_the_cap_is_rejected() {
        let many = (0..=MAX_CONVEX_VERTS)
            .map(|i| pos(lossless_count(i), 0.0))
            .collect();
        assert!(ConvexPoly::new(many).is_none());
    }

    #[test]
    fn collinear_points_are_rejected() {
        assert!(ConvexPoly::new(vec![pos(0.0, 0.0), pos(1.0, 0.0), pos(2.0, 0.0)]).is_none());
    }

    #[test]
    fn coincident_vertices_are_rejected() {
        assert!(ConvexPoly::new(vec![pos(0.0, 0.0), pos(0.0, 0.0), pos(4.0, 4.0)]).is_none());
    }

    #[test]
    fn edge_planes_face_outward_from_every_edge() {
        let Some(poly) = ConvexPoly::new(vec![
            pos(0.0, 0.0),
            pos(4.0, 0.0),
            pos(4.0, 4.0),
            pos(0.0, 4.0),
        ]) else {
            panic!("the unit square is convex");
        };
        let center = (2.0, 2.0);
        poly.edge_planes().into_iter().for_each(|[nx, ny, d]| {
            assert!(
                nx * center.0 + ny * center.1 - d < 0.0,
                "the centroid sits inside every edge half-plane",
            );
            assert!(
                (nx.hypot(ny) - 1.0).abs() < 1.0e-5,
                "edge normals are unit length"
            );
        });
    }

    #[test]
    fn an_open_path_has_one_fewer_segment_than_a_closed_loop() {
        let pts = vec![pos(0.0, 0.0), pos(1.0, 0.0), pos(1.0, 1.0)];
        let Some(open) = PolyPath::open(pts.clone()) else {
            panic!("three points form a path");
        };
        let Some(closed) = PolyPath::closed_loop(pts) else {
            panic!("three points form a loop");
        };
        assert_eq!(open.segments().count(), 2);
        assert_eq!(closed.segments().count(), 3);
    }

    #[test]
    fn a_single_point_is_not_a_path() {
        assert!(PolyPath::new(vec![pos(0.0, 0.0)], false).is_none());
    }

    #[test]
    fn more_points_than_the_cap_is_not_a_path() {
        let many = (0..=MAX_PATH_POINTS)
            .map(|i| pos(lossless_count(i), 0.0))
            .collect();
        assert!(PolyPath::new(many, false).is_none());
    }
}
