//! A 2D axis-aligned [`Rect`]angle, used for UI, viewports, and texture regions.

use glam::Vec2;
use serde::{Deserialize, Serialize};

/// An axis-aligned rectangle defined by its `min` (top-left in a y-down UI
/// space, or bottom-left in y-up) and `max` corners.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Rect {
    /// Minimum corner (smallest x and y).
    pub min: Vec2,
    /// Maximum corner (largest x and y).
    pub max: Vec2,
}

impl Rect {
    /// A zero-area rectangle at the origin.
    pub const ZERO: Self = Self {
        min: Vec2::ZERO,
        max: Vec2::ZERO,
    };

    /// Creates a rectangle from two corners, sorting so `min <= max`.
    #[inline]
    pub fn new(a: Vec2, b: Vec2) -> Self {
        Self {
            min: a.min(b),
            max: a.max(b),
        }
    }

    /// Creates a rectangle from a `min` corner and a size.
    #[inline]
    pub fn from_min_size(min: Vec2, size: Vec2) -> Self {
        Self {
            min,
            max: min + size,
        }
    }

    /// Creates a rectangle from a center point and full size.
    #[inline]
    pub fn from_center_size(center: Vec2, size: Vec2) -> Self {
        let half = size * 0.5;
        Self {
            min: center - half,
            max: center + half,
        }
    }

    /// The width and height as a vector.
    #[inline]
    pub fn size(&self) -> Vec2 {
        (self.max - self.min).max(Vec2::ZERO)
    }

    /// The width (x extent).
    #[inline]
    pub fn width(&self) -> f32 {
        self.size().x
    }

    /// The height (y extent).
    #[inline]
    pub fn height(&self) -> f32 {
        self.size().y
    }

    /// The center point.
    #[inline]
    pub fn center(&self) -> Vec2 {
        (self.min + self.max) * 0.5
    }

    /// The area (`width * height`).
    #[inline]
    pub fn area(&self) -> f32 {
        let s = self.size();
        s.x * s.y
    }

    /// Returns `true` if the rectangle has zero (or inverted) area.
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.min.x >= self.max.x || self.min.y >= self.max.y
    }

    /// Returns `true` if `point` is inside or on the boundary.
    #[inline]
    pub fn contains_point(&self, point: Vec2) -> bool {
        point.cmpge(self.min).all() && point.cmple(self.max).all()
    }

    /// Returns `true` if the two rectangles overlap (touching counts).
    #[inline]
    pub fn intersects(&self, other: &Rect) -> bool {
        self.min.cmple(other.max).all() && self.max.cmpge(other.min).all()
    }

    /// Returns the overlapping region, or [`Rect::ZERO`] if disjoint.
    #[inline]
    pub fn intersection(&self, other: &Rect) -> Rect {
        let min = self.min.max(other.min);
        let max = self.max.min(other.max);
        if min.x > max.x || min.y > max.y {
            Rect::ZERO
        } else {
            Rect { min, max }
        }
    }

    /// Returns the smallest rectangle containing both.
    #[inline]
    pub fn union(&self, other: &Rect) -> Rect {
        Rect {
            min: self.min.min(other.min),
            max: self.max.max(other.max),
        }
    }

    /// Returns the point inside the rectangle closest to `point`.
    #[inline]
    pub fn closest_point(&self, point: Vec2) -> Vec2 {
        point.clamp(self.min, self.max)
    }

    /// Returns a copy expanded outward by `amount` on every side (negative
    /// shrinks).
    #[inline]
    pub fn expanded(&self, amount: f32) -> Rect {
        Rect {
            min: self.min - Vec2::splat(amount),
            max: self.max + Vec2::splat(amount),
        }
    }
}

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

    #[test]
    fn new_sorts_corners() {
        let r = Rect::new(Vec2::new(4.0, 1.0), Vec2::new(0.0, 5.0));
        assert_eq!(r.min, Vec2::new(0.0, 1.0));
        assert_eq!(r.max, Vec2::new(4.0, 5.0));
    }

    #[test]
    fn min_size_and_center_size() {
        let r = Rect::from_min_size(Vec2::new(1.0, 2.0), Vec2::new(4.0, 6.0));
        assert_eq!(r.size(), Vec2::new(4.0, 6.0));
        assert_eq!(r.center(), Vec2::new(3.0, 5.0));

        let c = Rect::from_center_size(Vec2::ZERO, Vec2::new(2.0, 2.0));
        assert_eq!(c.min, Vec2::new(-1.0, -1.0));
        assert_eq!(c.max, Vec2::new(1.0, 1.0));
    }

    #[test]
    fn dimensions_and_area() {
        let r = Rect::from_min_size(Vec2::ZERO, Vec2::new(3.0, 4.0));
        assert_eq!(r.width(), 3.0);
        assert_eq!(r.height(), 4.0);
        assert_eq!(r.area(), 12.0);
    }

    #[test]
    fn empty_detection() {
        assert!(Rect::ZERO.is_empty());
        assert!(Rect::new(Vec2::ZERO, Vec2::new(0.0, 5.0)).is_empty());
        assert!(!Rect::from_min_size(Vec2::ZERO, Vec2::ONE).is_empty());
    }

    #[test]
    fn contains_and_closest() {
        let r = Rect::from_min_size(Vec2::ZERO, Vec2::splat(2.0));
        assert!(r.contains_point(Vec2::ONE));
        assert!(!r.contains_point(Vec2::new(3.0, 1.0)));
        assert_eq!(r.closest_point(Vec2::new(5.0, -1.0)), Vec2::new(2.0, 0.0));
    }

    #[test]
    fn intersection_and_union() {
        let a = Rect::from_min_size(Vec2::ZERO, Vec2::splat(2.0));
        let b = Rect::from_min_size(Vec2::ONE, Vec2::splat(2.0));
        assert!(a.intersects(&b));
        assert_eq!(a.intersection(&b), Rect::new(Vec2::ONE, Vec2::splat(2.0)));
        assert_eq!(a.union(&b), Rect::new(Vec2::ZERO, Vec2::splat(3.0)));

        let c = Rect::from_min_size(Vec2::splat(10.0), Vec2::ONE);
        assert!(!a.intersects(&c));
        assert_eq!(a.intersection(&c), Rect::ZERO);
    }

    #[test]
    fn expanded_grows_and_shrinks() {
        let r = Rect::from_min_size(Vec2::ZERO, Vec2::splat(4.0));
        assert_eq!(
            r.expanded(1.0),
            Rect::new(Vec2::splat(-1.0), Vec2::splat(5.0))
        );
        assert_eq!(r.expanded(-1.0), Rect::new(Vec2::ONE, Vec2::splat(3.0)));
    }
}
