use crate::{Aabb, EPSILON, Sphere, Vec2, Vec3, sqrt};

/// A normalized 3D ray.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ray3 {
    /// Ray origin.
    pub origin: Vec3,
    /// Normalized ray direction.
    pub direction: Vec3,
}

impl Ray3 {
    /// Creates a ray and normalizes the direction.
    #[inline]
    pub fn new(origin: Vec3, direction: Vec3) -> Self {
        Self {
            origin,
            direction: direction.normalize(),
        }
    }

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

    /// Intersects this ray with an AABB and returns nearest non-negative `t`.
    pub fn intersect_aabb(self, aabb: Aabb) -> Option<f32> {
        let mut t_min = 0.0_f32;
        let mut t_max = f32::INFINITY;

        for axis in 0..3 {
            let origin = self.origin[axis];
            let direction = self.direction[axis];
            let min = aabb.min[axis];
            let max = aabb.max[axis];

            if direction.abs() <= EPSILON {
                if origin < min || origin > max {
                    return None;
                }
                continue;
            }

            let inv = 1.0 / direction;
            let mut t1 = (min - origin) * inv;
            let mut t2 = (max - origin) * inv;
            if t1 > t2 {
                core::mem::swap(&mut t1, &mut t2);
            }
            t_min = t_min.max(t1);
            t_max = t_max.min(t2);
            if t_min > t_max {
                return None;
            }
        }

        Some(t_min)
    }

    /// Intersects this ray with a sphere and returns nearest non-negative `t`.
    pub fn intersect_sphere(self, center: Vec3, radius: f32) -> Option<f32> {
        self.intersect_bounding_sphere(Sphere::new(center, radius))
    }

    /// Intersects this ray with a sphere and returns nearest non-negative `t`.
    pub fn intersect_bounding_sphere(self, sphere: Sphere) -> Option<f32> {
        let oc = self.origin - sphere.center;
        let a = self.direction.dot(self.direction);
        let b = 2.0 * oc.dot(self.direction);
        let c = oc.dot(oc) - sphere.radius * sphere.radius;
        let discriminant = b * b - 4.0 * a * c;
        if discriminant < 0.0 || a.abs() <= EPSILON {
            return None;
        }
        let root = sqrt(discriminant);
        let t0 = (-b - root) / (2.0 * a);
        let t1 = (-b + root) / (2.0 * a);
        if t0 >= 0.0 {
            Some(t0)
        } else if t1 >= 0.0 {
            Some(t1)
        } else {
            None
        }
    }

    /// Intersects this ray with a triangle using Moller-Trumbore.
    ///
    /// Returns `(t, Vec2::new(u, v))`, where `u` and `v` are barycentric
    /// coordinates and `w = 1 - u - v`.
    pub fn intersect_triangle(self, a: Vec3, b: Vec3, c: Vec3) -> Option<(f32, Vec2)> {
        let edge1 = b - a;
        let edge2 = c - a;
        let pvec = self.direction.cross(edge2);
        let det = edge1.dot(pvec);
        if det.abs() <= EPSILON {
            return None;
        }

        let inv_det = 1.0 / det;
        let tvec = self.origin - a;
        let u = tvec.dot(pvec) * inv_det;
        if !(0.0..=1.0).contains(&u) {
            return None;
        }

        let qvec = tvec.cross(edge1);
        let v = self.direction.dot(qvec) * inv_det;
        if v < 0.0 || u + v > 1.0 {
            return None;
        }

        let t = edge2.dot(qvec) * inv_det;
        if t >= 0.0 {
            Some((t, Vec2::new(u, v)))
        } else {
            None
        }
    }
}

impl Default for Ray3 {
    #[inline]
    fn default() -> Self {
        Self::new(Vec3::ZERO, Vec3::NEG_Z)
    }
}

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

    #[test]
    fn ray_intersects_aabb() {
        let ray = Ray3::new(Vec3::new(0.0, 0.0, 5.0), Vec3::NEG_Z);
        let aabb = Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
        assert_close(ray.intersect_aabb(aabb).unwrap(), 4.0);
    }

    #[test]
    fn ray_intersects_sphere() {
        let ray = Ray3::new(Vec3::new(0.0, 0.0, 5.0), Vec3::NEG_Z);
        assert_close(ray.intersect_sphere(Vec3::ZERO, 1.0).unwrap(), 4.0);
    }

    #[test]
    fn ray_intersects_triangle_with_barycentric_coordinates() {
        let ray = Ray3::new(Vec3::new(0.25, 0.25, 1.0), Vec3::NEG_Z);
        let (t, uv) = ray
            .intersect_triangle(Vec3::ZERO, Vec3::X, Vec3::Y)
            .unwrap();
        assert_close(t, 1.0);
        assert_close(uv.x, 0.25);
        assert_close(uv.y, 0.25);
    }
}
