use crate::{matrices::Matrix4x4, tuples::Tuple, Float};

/// Rays have an origin and a direction. This datatype is the 'ray' in 'raytracer'.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Ray {
    pub origin: Tuple,
    pub direction: Tuple,
}

impl Ray {
    /// Create a ray with the given origin point and direction vector.
    /// Will panic if origin not a point or direction not a vector.
    pub fn new(origin: Tuple, direction: Tuple) -> Ray {
        assert!(origin.is_point(), "Ray origin must be a point");
        assert!(direction.is_vector(), "Ray direction must be a vector");
        Ray { origin, direction }
    }

    /// Compute a point from the given distance along the `Ray`.
    pub fn position(&self, t: Float) -> Tuple {
        self.origin + self.direction * t
    }

    /// Apply Matrix4x4 transforms to Ray.
    pub fn transform(&self, m: Matrix4x4) -> Ray {
        Ray {
            origin: m * self.origin,
            direction: m * self.direction,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        matrices::Matrix4x4,
        rays::Ray,
        tuples::{point, vector},
    };
    #[test]
    fn create() {
        let origin = point(1., 2., 3.);
        let direction = vector(4., 5., 6.);
        let r = Ray::new(origin, direction);
        assert_eq!(r.origin, origin);
        assert_eq!(r.direction, direction);
    }
    #[test]
    fn position() {
        let r = Ray::new(point(2., 3., 4.), vector(1., 0., 0.));
        assert_eq!(r.position(0.), point(2., 3., 4.));
        assert_eq!(r.position(1.), point(3., 3., 4.));
        assert_eq!(r.position(-1.), point(1., 3., 4.));
        assert_eq!(r.position(2.5), point(4.5, 3., 4.));
    }
    #[test]
    fn transform_translating_ray() {
        // Translating a ray
        let r = Ray::new(point(1., 2., 3.), vector(0., 1., 0.));
        let m = Matrix4x4::translation(3., 4., 5.);
        let r2 = r.transform(m);
        assert_eq!(r2.origin, point(4., 6., 8.));
        assert_eq!(r2.direction, vector(0., 1., 0.));
    }
    #[test]
    fn transform_scaling_ray() {
        // Scaling a ray
        let r = Ray::new(point(1., 2., 3.), vector(0., 1., 0.));
        let m = Matrix4x4::scaling(2., 3., 4.);
        let r2 = r.transform(m);
        assert_eq!(r2.origin, point(2., 6., 12.));
        assert_eq!(r2.direction, vector(0., 3., 0.));
    }
}
