//! A half-line [`Ray`] with an origin and a normalized direction.

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

/// A ray: a point plus a direction, extending to infinity in one direction.
///
/// The direction is normalized on construction so that the parameter `t` in
/// [`Ray::at`] is a true distance.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Ray {
    /// The starting point of the ray.
    pub origin: Vec3,
    /// The (normalized) direction of travel.
    pub direction: Vec3,
}

impl Ray {
    /// Creates a ray, normalizing `direction`.
    ///
    /// If `direction` is zero-length it is left as-is (degenerate ray); callers
    /// that care should validate with [`Ray::is_valid`].
    #[inline]
    pub fn new(origin: Vec3, direction: Vec3) -> Self {
        Self {
            origin,
            direction: direction.normalize_or_zero(),
        }
    }

    /// Creates a ray from an origin toward a target point.
    #[inline]
    pub fn from_to(origin: Vec3, target: Vec3) -> Self {
        Self::new(origin, target - origin)
    }

    /// Returns the point at distance `t` along the ray.
    #[inline]
    pub fn at(&self, t: f32) -> Vec3 {
        self.origin + self.direction * t
    }

    /// Returns `true` if the direction is a valid (non-zero, finite) unit vector.
    #[inline]
    pub fn is_valid(&self) -> bool {
        self.direction.is_finite() && (self.direction.length_squared() - 1.0).abs() <= 1e-4
    }

    /// Returns the point on the ray closest to `point`, clamped to `t >= 0`.
    #[inline]
    pub fn closest_point(&self, point: Vec3) -> Vec3 {
        let t = (point - self.origin).dot(self.direction).max(0.0);
        self.at(t)
    }

    /// Returns the shortest distance from `point` to the ray.
    #[inline]
    pub fn distance_to_point(&self, point: Vec3) -> f32 {
        self.closest_point(point).distance(point)
    }
}

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

    const EPS: f32 = 1e-4;

    #[test]
    fn new_normalizes_direction() {
        let ray = Ray::new(Vec3::ZERO, Vec3::new(0.0, 5.0, 0.0));
        assert!((ray.direction.length() - 1.0).abs() <= EPS);
        assert!(ray.is_valid());
    }

    #[test]
    fn zero_direction_is_invalid() {
        let ray = Ray::new(Vec3::ZERO, Vec3::ZERO);
        assert!(!ray.is_valid());
    }

    #[test]
    fn at_returns_distance_point() {
        let ray = Ray::new(Vec3::new(1.0, 0.0, 0.0), Vec3::X);
        assert!((ray.at(4.0) - Vec3::new(5.0, 0.0, 0.0)).length() <= EPS);
    }

    #[test]
    fn from_to_points_at_target() {
        let ray = Ray::from_to(Vec3::ZERO, Vec3::new(0.0, 0.0, 10.0));
        assert!((ray.direction - Vec3::Z).length() <= EPS);
    }

    #[test]
    fn closest_point_and_distance() {
        let ray = Ray::new(Vec3::ZERO, Vec3::X);
        // Point off to the side.
        let p = Vec3::new(3.0, 4.0, 0.0);
        assert!((ray.closest_point(p) - Vec3::new(3.0, 0.0, 0.0)).length() <= EPS);
        assert!((ray.distance_to_point(p) - 4.0).abs() <= EPS);
    }

    #[test]
    fn closest_point_clamps_behind_origin() {
        let ray = Ray::new(Vec3::ZERO, Vec3::X);
        let p = Vec3::new(-5.0, 2.0, 0.0);
        // Behind the origin → clamps to the origin.
        assert!((ray.closest_point(p) - Vec3::ZERO).length() <= EPS);
    }
}
