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

/// Rays have an origin and a direction. This datatype is the 'ray' in 'raytracer'.
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.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{rays::Ray, tuples::Tuple};
    ///
    /// let origin = Tuple::point(1., 2., 3.);
    /// let direction = Tuple::vector(4., 5., 6.);
    /// let r = Ray::new(origin, direction);
    /// assert_eq!(r.origin, origin);
    /// assert_eq!(r.direction, direction);
    /// ```
    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`.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{rays::Ray, tuples::Tuple};
    ///
    /// let r = Ray::new(Tuple::point(2., 3., 4.), Tuple::vector(1., 0., 0.));
    /// assert_eq!(r.position(0.), Tuple::point(2., 3., 4.));
    /// assert_eq!(r.position(1.), Tuple::point(3., 3., 4.));
    /// assert_eq!(r.position(-1.), Tuple::point(1., 3., 4.));
    /// assert_eq!(r.position(2.5), Tuple::point(4.5, 3., 4.));
    /// ```
    pub fn position(&self, t: Float) -> Tuple {
        self.origin + self.direction * t
    }

    /// Apply Matrix4x4 transforms to Ray.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{matrices::Matrix4x4, rays::Ray, tuples::Tuple};
    ///
    /// // Translating a ray
    /// let r = Ray::new(Tuple::point(1., 2., 3.), Tuple::vector(0., 1., 0.));
    /// let m = Matrix4x4::translation(3., 4., 5.);
    /// let r2 = r.transform(m);
    /// assert_eq!(r2.origin, Tuple::point(4., 6., 8.));
    /// assert_eq!(r2.direction, Tuple::vector(0., 1., 0.));
    ///
    /// // Scaling a ray
    /// let r = Ray::new(Tuple::point(1., 2., 3.), Tuple::vector(0., 1., 0.));
    /// let m = Matrix4x4::scaling(2., 3., 4.);
    /// let r2 = r.transform(m);
    /// assert_eq!(r2.origin, Tuple::point(2., 6., 12.));
    /// assert_eq!(r2.direction, Tuple::vector(0., 3., 0.));
    /// ```
    pub fn transform(&self, m: Matrix4x4) -> Ray {
        Ray {
            origin: m * self.origin,
            direction: m * self.direction,
        }
    }
}
