//! Axis-aligned bounding box ([`Aabb`]).
//!
//! Stored as `min`/`max` corners. An AABB with any `min` component greater than
//! the corresponding `max` is considered *empty* (contains no points), which is
//! the natural identity for union operations.

use crate::math::Ray;
use glam::Vec3;
use serde::{Deserialize, Serialize};

/// An axis-aligned bounding box defined by its minimum and maximum corners.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Aabb {
    /// Minimum corner (smallest x, y, z).
    pub min: Vec3,
    /// Maximum corner (largest x, y, z).
    pub max: Vec3,
}

impl Aabb {
    /// An empty box: `min` is `+inf`, `max` is `-inf`. Unioning any point with
    /// this yields a box tightly bounding that point.
    pub const EMPTY: Self = Self {
        min: Vec3::splat(f32::INFINITY),
        max: Vec3::splat(f32::NEG_INFINITY),
    };

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

    /// Creates an AABB from a center point and half-extents.
    #[inline]
    pub fn from_center_half_extents(center: Vec3, half_extents: Vec3) -> Self {
        Self {
            min: center - half_extents,
            max: center + half_extents,
        }
    }

    /// Builds the tightest AABB containing all `points`. Returns [`Aabb::EMPTY`]
    /// if the iterator is empty.
    pub fn from_points(points: impl IntoIterator<Item = Vec3>) -> Self {
        let mut bb = Self::EMPTY;
        for p in points {
            bb.expand_to_include(p);
        }
        bb
    }

    /// Returns `true` if this box contains no points (any axis inverted).
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.min.x > self.max.x || self.min.y > self.max.y || self.min.z > self.max.z
    }

    /// The center point of the box. Meaningless for an empty box.
    #[inline]
    pub fn center(&self) -> Vec3 {
        (self.min + self.max) * 0.5
    }

    /// The full size (max - min) along each axis.
    #[inline]
    pub fn size(&self) -> Vec3 {
        (self.max - self.min).max(Vec3::ZERO)
    }

    /// Half of [`Aabb::size`].
    #[inline]
    pub fn half_extents(&self) -> Vec3 {
        self.size() * 0.5
    }

    /// The surface area of the box (used by spatial acceleration heuristics).
    #[inline]
    pub fn surface_area(&self) -> f32 {
        let s = self.size();
        2.0 * (s.x * s.y + s.y * s.z + s.z * s.x)
    }

    /// The volume of the box.
    #[inline]
    pub fn volume(&self) -> f32 {
        let s = self.size();
        s.x * s.y * s.z
    }

    /// Grows the box (in place) to include `point`.
    #[inline]
    pub fn expand_to_include(&mut self, point: Vec3) {
        self.min = self.min.min(point);
        self.max = self.max.max(point);
    }

    /// Returns the union of this box and `other` (smallest box containing both).
    #[inline]
    pub fn union(&self, other: &Aabb) -> Aabb {
        Aabb {
            min: self.min.min(other.min),
            max: self.max.max(other.max),
        }
    }

    /// Returns the intersection of two boxes, or [`Aabb::EMPTY`] if disjoint.
    #[inline]
    pub fn intersection(&self, other: &Aabb) -> Aabb {
        let min = self.min.max(other.min);
        let max = self.max.min(other.max);
        if min.x > max.x || min.y > max.y || min.z > max.z {
            Aabb::EMPTY
        } else {
            Aabb { min, max }
        }
    }

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

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

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

    /// The eight corner vertices of the box.
    pub fn corners(&self) -> [Vec3; 8] {
        let (lo, hi) = (self.min, self.max);
        [
            Vec3::new(lo.x, lo.y, lo.z),
            Vec3::new(hi.x, lo.y, lo.z),
            Vec3::new(lo.x, hi.y, lo.z),
            Vec3::new(hi.x, hi.y, lo.z),
            Vec3::new(lo.x, lo.y, hi.z),
            Vec3::new(hi.x, lo.y, hi.z),
            Vec3::new(lo.x, hi.y, hi.z),
            Vec3::new(hi.x, hi.y, hi.z),
        ]
    }

    /// Slab-method ray/box intersection. Returns the entry distance `t` along
    /// the ray if it hits (including when the origin is inside, where `t` is the
    /// clamped near distance), otherwise `None`.
    pub fn ray_intersection(&self, ray: &Ray) -> Option<f32> {
        let inv_dir = Vec3::ONE / ray.direction;
        let t0 = (self.min - ray.origin) * inv_dir;
        let t1 = (self.max - ray.origin) * inv_dir;
        let t_near = t0.min(t1);
        let t_far = t0.max(t1);
        let t_enter = t_near.max_element();
        let t_exit = t_far.min_element();
        if t_enter <= t_exit && t_exit >= 0.0 {
            Some(t_enter.max(0.0))
        } else {
            None
        }
    }
}

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

    #[test]
    fn empty_box_contains_nothing() {
        assert!(Aabb::EMPTY.is_empty());
        assert!(!Aabb::EMPTY.contains_point(Vec3::ZERO));
    }

    #[test]
    fn new_sorts_corners() {
        let bb = Aabb::new(Vec3::new(1.0, 5.0, -2.0), Vec3::new(-1.0, 0.0, 3.0));
        assert_eq!(bb.min, Vec3::new(-1.0, 0.0, -2.0));
        assert_eq!(bb.max, Vec3::new(1.0, 5.0, 3.0));
    }

    #[test]
    fn center_size_extents() {
        let bb = Aabb::from_center_half_extents(Vec3::new(1.0, 2.0, 3.0), Vec3::splat(2.0));
        assert_eq!(bb.center(), Vec3::new(1.0, 2.0, 3.0));
        assert_eq!(bb.size(), Vec3::splat(4.0));
        assert_eq!(bb.half_extents(), Vec3::splat(2.0));
    }

    #[test]
    fn from_points_bounds_all() {
        let bb = Aabb::from_points([
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(2.0, -1.0, 4.0),
            Vec3::new(-3.0, 5.0, 1.0),
        ]);
        assert_eq!(bb.min, Vec3::new(-3.0, -1.0, 0.0));
        assert_eq!(bb.max, Vec3::new(2.0, 5.0, 4.0));
    }

    #[test]
    fn from_points_empty_is_empty() {
        assert!(Aabb::from_points([]).is_empty());
    }

    #[test]
    fn contains_and_closest() {
        let bb = Aabb::new(Vec3::ZERO, Vec3::splat(2.0));
        assert!(bb.contains_point(Vec3::ONE));
        assert!(bb.contains_point(Vec3::ZERO)); // boundary
        assert!(!bb.contains_point(Vec3::new(3.0, 1.0, 1.0)));
        assert_eq!(
            bb.closest_point(Vec3::new(5.0, -1.0, 1.0)),
            Vec3::new(2.0, 0.0, 1.0)
        );
        assert_eq!(bb.closest_point(Vec3::ONE), Vec3::ONE);
    }

    #[test]
    fn union_and_intersection() {
        let a = Aabb::new(Vec3::ZERO, Vec3::splat(2.0));
        let b = Aabb::new(Vec3::ONE, Vec3::splat(3.0));
        assert_eq!(a.union(&b), Aabb::new(Vec3::ZERO, Vec3::splat(3.0)));
        assert_eq!(a.intersection(&b), Aabb::new(Vec3::ONE, Vec3::splat(2.0)));

        let c = Aabb::new(Vec3::splat(5.0), Vec3::splat(6.0));
        assert!(a.intersection(&c).is_empty());
        assert!(!a.intersects(&c));
        assert!(a.intersects(&b));
    }

    #[test]
    fn surface_area_and_volume() {
        let bb = Aabb::new(Vec3::ZERO, Vec3::new(1.0, 2.0, 3.0));
        assert_eq!(bb.volume(), 6.0);
        assert_eq!(bb.surface_area(), 2.0 * (2.0 + 6.0 + 3.0));
    }

    #[test]
    fn corners_count_and_span() {
        let bb = Aabb::new(Vec3::ZERO, Vec3::ONE);
        let corners = bb.corners();
        assert_eq!(corners.len(), 8);
        assert!(corners.contains(&Vec3::ZERO));
        assert!(corners.contains(&Vec3::ONE));
    }

    #[test]
    fn ray_hits_from_outside() {
        let bb = Aabb::new(Vec3::splat(-1.0), Vec3::splat(1.0));
        let ray = Ray::new(Vec3::new(-5.0, 0.0, 0.0), Vec3::X);
        let t = bb.ray_intersection(&ray).expect("should hit");
        assert!((t - 4.0).abs() <= 1e-4);
    }

    #[test]
    fn ray_from_inside_returns_zero() {
        let bb = Aabb::new(Vec3::splat(-1.0), Vec3::splat(1.0));
        let ray = Ray::new(Vec3::ZERO, Vec3::X);
        assert_eq!(bb.ray_intersection(&ray), Some(0.0));
    }

    #[test]
    fn ray_misses() {
        let bb = Aabb::new(Vec3::splat(-1.0), Vec3::splat(1.0));
        let ray = Ray::new(Vec3::new(-5.0, 5.0, 0.0), Vec3::X);
        assert_eq!(bb.ray_intersection(&ray), None);
    }

    #[test]
    fn ray_pointing_away_misses() {
        let bb = Aabb::new(Vec3::splat(-1.0), Vec3::splat(1.0));
        let ray = Ray::new(Vec3::new(-5.0, 0.0, 0.0), Vec3::NEG_X);
        assert_eq!(bb.ray_intersection(&ray), None);
    }
}
