use crate::{
    intersections::{prepare_computations, Intersections, PrecomputedData},
    lights::PointLight,
    materials::{lighting, Material},
    matrices::Matrix4x4,
    rays::Ray,
    shapes::{intersect, Shape},
    tuples::{Color, Tuple},
    Float, BLACK, WHITE,
};

/// World holds all drawable objects and the light(s) that illuminate them.
///
/// # Examples
/// ```
/// use rtchallenge::world::World;
///
/// let w = World::default();
/// assert!(w.objects.is_empty());
/// assert_eq!(w.lights.len(), 0);
/// ```

#[derive(Clone, Debug, Default)]
pub struct World {
    pub lights: Vec<PointLight>,
    pub objects: Vec<Shape>,
}

impl World {
    /// Creates a world suitable for use across multiple tests in from the book.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::world::World;
    ///
    /// let w = World::test_world();
    /// assert_eq!(w.objects.len(), 2);
    /// assert!(!w.lights.is_empty());
    /// ```
    pub fn test_world() -> World {
        let light = PointLight::new(Tuple::point(-10., 10., -10.), WHITE);
        let mut s1 = Shape::sphere();
        s1.set_material(Material {
            color: Color::new(0.8, 1., 0.6),
            diffuse: 0.7,
            specular: 0.2,
            ..Material::default()
        });
        let mut s2 = Shape::sphere();
        s2.set_transform(Matrix4x4::scaling(0.5, 0.5, 0.5));
        World {
            lights: vec![light],
            objects: vec![s1, s2],
        }
    }

    /// Intesects the ray with this world.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{rays::Ray, tuples::Tuple, world::World};
    ///
    /// let w = World::test_world();
    /// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
    /// let xs = w.intersect(&r);
    /// assert_eq!(xs.len(), 4);
    /// assert_eq!(xs[0].t, 4.);
    /// assert_eq!(xs[1].t, 4.5);
    /// assert_eq!(xs[2].t, 5.5);
    /// assert_eq!(xs[3].t, 6.);
    /// ```
    pub fn intersect(&self, r: &Ray) -> Intersections {
        let mut xs: Vec<_> = self
            .objects
            .iter()
            .map(|o| intersect(&o, &r))
            .flatten()
            .collect();
        xs.sort_by(|i1, i2| {
            i1.t.partial_cmp(&i2.t)
                .expect("an intersection has a t value that is NaN")
        });
        Intersections::new(xs)
    }

    /// Compute shaded value for given precomputation.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{
    ///     intersections::{prepare_computations, Intersection},
    ///     lights::PointLight,
    ///     matrices::Matrix4x4,
    ///     rays::Ray,
    ///     shapes::Shape,
    ///     tuples::{Color, Tuple},
    ///     world::World,
    ///     WHITE,
    /// };
    ///
    /// // Shading an intersection.
    /// let w = World::test_world();
    /// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
    /// let s = &w.objects[0];
    /// let i = Intersection::new(4., &s);
    /// let comps = prepare_computations(&i, &r);
    /// let c = w.shade_hit(&comps);
    /// assert_eq!(c, Color::new(0.38066, 0.47583, 0.2855));
    ///
    /// // Shading an intersection from the inside.
    /// let mut w = World::test_world();
    /// w.lights = vec![PointLight::new(Tuple::point(0., 0.25, 0.), WHITE)];
    /// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
    /// let s = &w.objects[1];
    /// let i = Intersection::new(0.5, &s);
    /// let comps = prepare_computations(&i, &r);
    /// let c = w.shade_hit(&comps);
    /// assert_eq!(c, Color::new(0.90498, 0.90498, 0.90498));
    ///
    /// // Shading with an intersection in shadow.
    /// let mut w = World::default();
    /// w.lights = vec![PointLight::new(Tuple::point(0., 0., -10.), WHITE)];
    /// let s1 = Shape::sphere();
    /// let mut s2 = Shape::sphere();
    /// s2.set_transform(Matrix4x4::translation(0., 0., 10.));
    /// w.objects = vec![s1, s2.clone()];
    /// let r = Ray::new(Tuple::point(0., 0., 5.), Tuple::vector(0., 0., 1.));
    /// let i = Intersection::new(4., &s2);
    /// let comps = prepare_computations(&i, &r);
    /// let c = w.shade_hit(&comps);
    /// assert_eq!(c, Color::new(0.1, 0.1, 0.1));
    /// ```
    pub fn shade_hit(&self, comps: &PrecomputedData) -> Color {
        let c = self
            .lights
            .iter()
            .fold(Color::new(0., 0., 0.), |acc, light| {
                let shadowed = self.is_shadowed(comps.over_point, light);
                acc + lighting(
                    &comps.object.material(),
                    light,
                    comps.over_point,
                    comps.eyev,
                    comps.normalv,
                    shadowed,
                )
            });
        c / self.lights.len() as Float
    }
    /// Compute color for given ray fired at the world.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{
    ///     intersections::{prepare_computations, Intersection},
    ///     lights::PointLight,
    ///     rays::Ray,
    ///     shapes::Shape,
    ///     tuples::{Color, Tuple},
    ///     world::World,
    ///     BLACK,
    /// };
    ///
    /// // The color when a ray misses.
    /// let w = World::test_world();
    /// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 1., 0.));
    /// let c = w.color_at(&r);
    /// assert_eq!(c, BLACK);
    ///
    /// // The color when a ray hits.
    /// let w = World::test_world();
    /// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
    /// let c = w.color_at(&r);
    /// assert_eq!(c, Color::new(0.38066, 0.47583, 0.2855));
    ///
    /// // The color with an intersection behind the ray.
    /// let w = {
    ///     let mut w = World::test_world();
    ///     let mut outer = &mut w.objects[0];
    ///     let m = outer.material_mut().ambient = 1.;
    ///     let inner = &mut w.objects[1];
    ///     inner.material_mut().ambient = 1.;
    ///     w
    /// };
    /// let inner = &w.objects[1];
    /// let r = Ray::new(Tuple::point(0., 0., 0.75), Tuple::vector(0., 0., -1.));
    /// let c = w.color_at(&r);
    /// assert_eq!(c, inner.material().color);
    /// ```
    pub fn color_at(&self, r: &Ray) -> Color {
        match self.intersect(r).hit() {
            Some(hit) => {
                let comps = prepare_computations(&hit, r);
                self.shade_hit(&comps)
            }
            None => BLACK,
        }
    }

    /// Determine if point in world is in a shadow.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{
    ///     tuples::{ Tuple},
    ///     shapes::Shape,
    ///     world::World,
    /// };
    ///
    /// let w = World::test_world();
    /// let light = &w.lights[0];
    ///
    /// // There is no shadow when nothing is collinear with point and light.
    /// let p = Tuple::point(0.,10.,0.);
    /// assert_eq!(w.is_shadowed(p,light), false);
    ///
    /// // There shadow when an object is between the point and the light.
    /// let p = Tuple::point(10.,-10.,10.);
    /// assert_eq!(w.is_shadowed(p,light), true);
    ///
    /// // There is no shadow when an object is behind the light.
    /// let p = Tuple::point(-20.,20.,-20.);
    /// assert_eq!(w.is_shadowed(p,light), false);
    ///
    /// // There is no shadow when an object is behind the point.
    /// let p = Tuple::point(-2.,2.,-2.);
    /// assert_eq!(w.is_shadowed(p,light), false);
    pub fn is_shadowed(&self, point: Tuple, light: &PointLight) -> bool {
        let v = light.position - point;
        let distance = v.magnitude();
        let direction = v.normalize();

        let r = Ray::new(point, direction);
        let intersections = self.intersect(&r);
        if let Some(h) = intersections.hit() {
            return h.t < distance;
        }
        false
    }
}
