//! A 3D value [`Range3`]: an inclusive `[min, max]` interval per axis.
//!
//! Unlike [`Aabb`](crate::math::Aabb), which models geometry, `Range3` models a
//! *value range* — clamping configuration values, remapping parameters, and
//! describing generation bounds. It provides interpolation and remapping that
//! an AABB intentionally does not.

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

/// An inclusive per-axis range `[min, max]`.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Range3 {
    /// Lower bound on each axis.
    pub min: Vec3,
    /// Upper bound on each axis.
    pub max: Vec3,
}

impl Range3 {
    /// The unit range `[0, 1]` on every axis.
    pub const UNIT: Self = Self {
        min: Vec3::ZERO,
        max: Vec3::ONE,
    };

    /// Creates a range from two bounds, sorting so `min <= max` per axis.
    #[inline]
    pub fn new(a: Vec3, b: Vec3) -> Self {
        Self {
            min: a.min(b),
            max: a.max(b),
        }
    }

    /// Creates a range spanning `[-extent, +extent]` on each axis.
    #[inline]
    pub fn symmetric(extent: Vec3) -> Self {
        Self {
            min: -extent,
            max: extent,
        }
    }

    /// The width of the range on each axis (`max - min`).
    #[inline]
    pub fn span(&self) -> Vec3 {
        self.max - self.min
    }

    /// The midpoint of the range.
    #[inline]
    pub fn center(&self) -> Vec3 {
        (self.min + self.max) * 0.5
    }

    /// Clamps `value` into the range per axis.
    #[inline]
    pub fn clamp(&self, value: Vec3) -> Vec3 {
        value.clamp(self.min, self.max)
    }

    /// Returns `true` if `value` lies within the range (inclusive).
    #[inline]
    pub fn contains(&self, value: Vec3) -> bool {
        value.cmpge(self.min).all() && value.cmple(self.max).all()
    }

    /// Linearly interpolates from `min` to `max` by `t` per axis. `t` is **not**
    /// clamped, so values outside `[0, 1]` extrapolate.
    #[inline]
    pub fn lerp(&self, t: Vec3) -> Vec3 {
        self.min + self.span() * t
    }

    /// The inverse of [`Range3::lerp`]: returns where `value` sits in `[0, 1]`
    /// within the range, per axis. Axes with zero span yield `0.0`.
    #[inline]
    pub fn inverse_lerp(&self, value: Vec3) -> Vec3 {
        let span = self.span();
        let raw = (value - self.min) / span;
        // Guard against division by zero on degenerate axes.
        Vec3::select(span.cmpeq(Vec3::ZERO), Vec3::ZERO, raw)
    }

    /// Remaps `value` from this range into `target`, preserving its relative
    /// position per axis.
    #[inline]
    pub fn remap(&self, value: Vec3, target: &Range3) -> Vec3 {
        target.lerp(self.inverse_lerp(value))
    }
}

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

    const EPS: f32 = 1e-4;

    fn approx(a: Vec3, b: Vec3) -> bool {
        (a - b).length() <= EPS
    }

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

    #[test]
    fn symmetric_and_span_center() {
        let r = Range3::symmetric(Vec3::splat(2.0));
        assert_eq!(r.min, Vec3::splat(-2.0));
        assert_eq!(r.span(), Vec3::splat(4.0));
        assert_eq!(r.center(), Vec3::ZERO);
    }

    #[test]
    fn clamp_and_contains() {
        let r = Range3::new(Vec3::ZERO, Vec3::splat(10.0));
        assert_eq!(
            r.clamp(Vec3::new(-5.0, 5.0, 20.0)),
            Vec3::new(0.0, 5.0, 10.0)
        );
        assert!(r.contains(Vec3::splat(5.0)));
        assert!(!r.contains(Vec3::new(11.0, 5.0, 5.0)));
    }

    #[test]
    fn lerp_and_inverse_round_trip() {
        let r = Range3::new(Vec3::new(2.0, 4.0, 6.0), Vec3::new(4.0, 8.0, 12.0));
        let t = Vec3::new(0.5, 0.25, 0.75);
        let v = r.lerp(t);
        assert!(approx(v, Vec3::new(3.0, 5.0, 10.5)));
        assert!(approx(r.inverse_lerp(v), t));
    }

    #[test]
    fn lerp_extrapolates() {
        let r = Range3::UNIT;
        assert!(approx(r.lerp(Vec3::splat(2.0)), Vec3::splat(2.0)));
        assert!(approx(r.lerp(Vec3::splat(-1.0)), Vec3::splat(-1.0)));
    }

    #[test]
    fn inverse_lerp_degenerate_axis_is_zero() {
        let r = Range3::new(Vec3::new(5.0, 0.0, 0.0), Vec3::new(5.0, 10.0, 10.0));
        // x axis has zero span → 0.0 rather than NaN/inf.
        let result = r.inverse_lerp(Vec3::new(5.0, 5.0, 5.0));
        assert!(result.x.is_finite());
        assert_eq!(result.x, 0.0);
        assert!((result.y - 0.5).abs() <= EPS);
    }

    #[test]
    fn remap_between_ranges() {
        let from = Range3::new(Vec3::ZERO, Vec3::splat(100.0));
        let to = Range3::new(Vec3::ZERO, Vec3::ONE);
        assert!(approx(from.remap(Vec3::splat(50.0), &to), Vec3::splat(0.5)));
    }
}
