use crate::{
    intersections::{Intersection, Intersections},
    materials::Material,
    matrices::Matrix4x4,
    rays::Ray,
    tuples::{dot, Tuple},
};

#[derive(Debug, PartialEq, Clone)]
enum Geometry {
    /// Sphere represents the unit-sphere (radius of unit 1.) at the origin 0., 0., 0.
    Sphere,
}

#[derive(Debug, PartialEq, Clone)]
pub struct Shape {
    transform: Matrix4x4,
    inverse_transform: Matrix4x4,
    material: Material,
    geometry: Geometry,
}

impl Shape {
    /// # Examples
    /// ```
    /// use rtchallenge::{materials::Material, matrices::Matrix4x4, shapes::Shape};
    ///
    /// // A sphere's default transform is the identity matrix.
    /// let s = Shape::sphere();
    /// assert_eq!(s.transform(), Matrix4x4::identity());
    ///
    /// // It can be changed by directly setting the transform member.
    /// let mut s = Shape::sphere();
    /// let t = Matrix4x4::translation(2., 3., 4.);
    /// s.set_transform(t.clone());
    /// assert_eq!(s.transform(), t);
    ///
    /// // Default Sphere has the default material.
    /// assert_eq!(s.material(), &Material::default());
    /// // It can be overridden.
    /// let mut s = Shape::sphere();
    /// let mut m = Material::default();
    /// m.ambient = 1.;
    /// *s.material_mut() = m.clone();
    /// assert_eq!(s.material(), &m);
    /// ```
    pub fn sphere() -> Shape {
        Shape {
            transform: Matrix4x4::identity(),
            inverse_transform: Matrix4x4::identity(),
            material: Material::default(),
            geometry: Geometry::Sphere,
        }
    }
    /// Find the normal at the point on the sphere.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{
    ///     float::consts::PI, materials::Material, matrices::Matrix4x4, shapes::Shape, tuples::Tuple,
    ///     Float,
    /// };
    ///
    /// // Normal on X-axis
    /// let s = Shape::sphere();
    /// let n = s.normal_at(Tuple::point(1., 0., 0.));
    /// assert_eq!(n, Tuple::vector(1., 0., 0.));
    ///
    /// // Normal on Y-axis
    /// let s = Shape::sphere();
    /// let n = s.normal_at(Tuple::point(0., 1., 0.));
    /// assert_eq!(n, Tuple::vector(0., 1., 0.));
    ///
    /// // Normal on Z-axis
    /// let s = Shape::sphere();
    /// let n = s.normal_at(Tuple::point(0., 0., 1.));
    /// assert_eq!(n, Tuple::vector(0., 0., 1.));
    ///
    /// // Normal on a sphere at a nonaxial point.
    /// let s = Shape::sphere();
    /// let n = s.normal_at(Tuple::point(
    ///     (3. as Float).sqrt() / 3.,
    ///     (3. as Float).sqrt() / 3.,
    ///     (3. as Float).sqrt() / 3.,
    /// ));
    /// assert_eq!(
    ///     n,
    ///     Tuple::vector(
    ///         (3. as Float).sqrt() / 3.,
    ///         (3. as Float).sqrt() / 3.,
    ///         (3. as Float).sqrt() / 3.,
    ///     )
    /// );
    /// // Normals returned are normalized.
    /// let s = Shape::sphere();
    /// let n = s.normal_at(Tuple::point(
    ///     (3. as Float).sqrt() / 3.,
    ///     (3. as Float).sqrt() / 3.,
    ///     (3. as Float).sqrt() / 3.,
    /// ));
    /// assert_eq!(n, n.normalize());
    ///
    /// // Compute the normal on a translated sphere.
    /// let mut s = Shape::sphere();
    /// s.set_transform(Matrix4x4::translation(0., 1., 0.));
    /// let n = s.normal_at(Tuple::point(0., 1.70711, -0.70711));
    /// assert_eq!(n, Tuple::vector(0., 0.70711, -0.70711));
    ///
    /// // Compute the normal on a transformed sphere.
    /// let mut s = Shape::sphere();
    /// s.set_transform(Matrix4x4::scaling(1., 0.5, 1.) * Matrix4x4::rotation_z(PI / 5.));
    /// let n = s.normal_at(Tuple::point(
    ///     0.,
    ///     (2. as Float).sqrt() / 2.,
    ///     -(2. as Float).sqrt() / 2.,
    /// ));
    /// assert_eq!(n, Tuple::vector(0., 0.97014, -0.24254));
    /// ```
    #[cfg(not(feature = "disable-inverse-cache"))]
    pub fn normal_at(&self, world_point: Tuple) -> Tuple {
        let object_point = self.inverse_transform * world_point;
        let object_normal = match self.geometry {
            Geometry::Sphere => object_point - Tuple::point(0., 0., 0.),
        };
        let mut world_normal = self.inverse_transform.transpose() * object_normal;
        world_normal.w = 0.;
        world_normal.normalize()
    }
    #[cfg(feature = "disable-inverse-cache")]
    pub fn normal_at(&self, world_point: Tuple) -> Tuple {
        let object_point = self.transform.inverse() * world_point;
        let object_normal = match self.geometry {
            Geometry::Sphere => object_point - Tuple::point(0., 0., 0.),
        };
        let mut world_normal = self.transform.inverse().transpose() * object_normal;
        world_normal.w = 0.;
        world_normal.normalize()
    }

    pub fn transform(&self) -> Matrix4x4 {
        self.transform
    }

    pub fn set_transform(&mut self, t: Matrix4x4) {
        self.transform = t;
        self.inverse_transform = t.inverse();
    }

    pub fn material(&self) -> &Material {
        &self.material
    }

    pub fn material_mut(&mut self) -> &mut Material {
        &mut self.material
    }

    pub fn set_material(&mut self, m: Material) {
        self.material = m;
    }
}

/// Intersect a ray with a sphere.
///
/// # Examples
/// ```
/// use rtchallenge::{
///     intersections::{Intersection, Intersections},
///     matrices::Matrix4x4,
///     rays::Ray,
///     shapes::{intersect, Shape},
///     tuples::Tuple,
/// };
///
/// // A ray intersects a sphere in two points.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let s = Shape::sphere();
/// let xs = intersect(&s, &r);
/// assert_eq!(
///     xs,
///     Intersections::new(vec![Intersection::new(4., &s), Intersection::new(6., &s)])
/// );
///
/// // A ray intersects a sphere at a tangent.
/// let r = Ray::new(Tuple::point(0., 2., -5.), Tuple::vector(0., 0., 1.));
/// let s = Shape::sphere();
/// let xs = intersect(&s, &r);
/// assert_eq!(xs, Intersections::default());
///
/// // A ray originates inside a sphere.
/// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
/// let s = Shape::sphere();
/// let xs = intersect(&s, &r);
/// assert_eq!(
///     xs,
///     Intersections::new(vec![Intersection::new(-1., &s), Intersection::new(1., &s)])
/// );
///
/// // A sphere is behind a ray.
/// let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.));
/// let s = Shape::sphere();
/// let xs = intersect(&s, &r);
/// assert_eq!(
///     xs,
///     Intersections::new(vec![Intersection::new(-6., &s), Intersection::new(-4., &s)])
/// );
///
/// // Intersect a scaled sphere with a ray.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut s = Shape::sphere();
/// s.set_transform(Matrix4x4::scaling(2., 2., 2.));
/// let xs = intersect(&s, &r);
/// assert_eq!(xs.len(), 2, "xs {:?}", xs);
/// assert_eq!(xs[0].t, 3., "xs {:?}", xs);
/// assert_eq!(xs[1].t, 7., "xs {:?}", xs);
///
/// // Intersect a translated sphere with a ray.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut s = Shape::sphere();
/// s.set_transform(Matrix4x4::translation(5., 0., 0.));
/// let xs = intersect(&s, &r);
/// assert_eq!(xs.len(), 0);
/// ```
pub fn intersect<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
    match shape.geometry {
        Geometry::Sphere => intersect_rtc(shape, ray),
    }
}

fn intersect_rtc<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
    let ray = ray.transform(shape.inverse_transform);
    let sphere_to_ray = ray.origin - Tuple::point(0., 0., 0.);
    let a = dot(ray.direction, ray.direction);
    let b = 2. * dot(ray.direction, sphere_to_ray);
    let c = dot(sphere_to_ray, sphere_to_ray) - 1.;
    let discriminant = b * b - 4. * a * c;
    if discriminant < 0. {
        return Intersections::default();
    }
    Intersections::new(vec![
        Intersection::new((-b - discriminant.sqrt()) / (2. * a), &shape),
        Intersection::new((-b + discriminant.sqrt()) / (2. * a), &shape),
    ])
}
fn intersect_rtiow<'s>(shape: &'s Shape, ray: &Ray) -> Intersections<'s> {
    let ray = ray.transform(shape.inverse_transform);
    let oc = ray.origin - Tuple::point(0., 0., 0.);
    let a = dot(ray.direction, ray.direction);
    let b = dot(oc, ray.direction);
    let c = dot(oc, oc) - 1.;
    let discriminant = b * b - a * c;
    if discriminant < 0. {
        return Intersections::default();
    }
    Intersections::new(vec![
        Intersection::new((-b - discriminant.sqrt()) / a, &shape),
        Intersection::new((-b + discriminant.sqrt()) / a, &shape),
    ])
}
