use derive_builder::Builder;

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

/// World holds all drawable objects and the light(s) that illuminate them.
#[derive(Builder, Clone, Debug, Default)]
#[builder(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.
    pub fn test_world() -> World {
        let light = PointLight::new(Tuple::point(-10., 10., -10.), WHITE);
        let mut s1 = Shape::sphere();
        s1.material = Material {
            color: [0.8, 1., 0.6].into(),
            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],
        }
    }

    /// Intersects the ray with this world.
    pub fn intersect(&self, r: &Ray) -> Intersections {
        let mut xs: Vec<_> = self.objects.iter().flat_map(|o| intersect(o, r)).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.
    pub fn shade_hit(&self, comps: &PrecomputedData, remaining: usize) -> Color {
        let surface = self
            .lights
            .iter()
            .fold(Color::new(0., 0., 0.), |acc, light| {
                let shadowed = self.is_shadowed(comps.over_point, light);
                let surface = lighting(
                    &comps.object.material,
                    comps.object,
                    light,
                    comps.over_point,
                    comps.eyev,
                    comps.normalv,
                    shadowed,
                );
                acc + surface
            });
        let reflected = self.reflected_color(comps, remaining);
        let refracted = self.refracted_color(comps, remaining);
        let material = &comps.object.material;
        if material.reflective > 0. && material.transparency > 0. {
            let reflectance = schlick(comps);
            surface + reflected * reflectance + refracted * (1. - reflectance)
        } else {
            surface + reflected + refracted
        }
    }
    /// Compute color for given ray fired at the world.
    pub fn color_at(&self, r: &Ray, remaining: usize) -> Color {
        let xs = self.intersect(r);
        match xs.hit() {
            Some(hit) => {
                let comps = prepare_computations(hit, r, &xs);
                self.shade_hit(&comps, remaining)
            }
            None => BLACK,
        }
    }

    /// Determine if point in world is in a shadow.
    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
    }

    /// Compute reflected color for the given precomputation.
    pub fn reflected_color(&self, comps: &PrecomputedData, remaining: usize) -> Color {
        if remaining == 0 {
            return BLACK;
        }
        if comps.object.material.reflective == 0. {
            return BLACK;
        }
        // Fire reflected ray to figure out color.
        let reflect_ray = Ray::new(comps.over_point, comps.reflectv);
        let color = self.color_at(&reflect_ray, remaining - 1);
        color * comps.object.material.reflective
    }

    /// Compute refracted color for the given precomputation.
    pub fn refracted_color(&self, comps: &PrecomputedData, remaining: usize) -> Color {
        if remaining == 0 {
            return BLACK;
        }
        if comps.object.material.transparency == 0. {
            return BLACK;
        }
        // Find the ratio of the first index of refraction to the secon
        // (This is inverted from the definition of Snell's Law.)
        let n_ratio = comps.n1 / comps.n2;

        // cos(theta_i) is the same as the dot product of the two vectors
        // TODO(wathiede): is cosine faster than doc productings?
        let cos_i = dot(comps.eyev, comps.normalv);

        // Find the sin(theta_t)^2 via trigonometric identity.
        let sin2_t = n_ratio * n_ratio * (1. - cos_i * cos_i);

        if sin2_t > 1. {
            // Total internal reflection.
            return BLACK;
        }
        // Find cos(theta_t) via trigonometric identity.
        let cos_t = (1. - sin2_t).sqrt();
        // Compute the direction of the refracted ray.
        let direction = comps.normalv * (n_ratio * cos_i - cos_t) - comps.eyev * n_ratio;
        // Create the refracted ray.
        let refract_ray = Ray::new(comps.under_point, direction);
        // Find he color of the refracted ray, making sure to multiply by the transparency value to
        // account for any opacity.
        self.color_at(&refract_ray, remaining - 1) * comps.object.material.transparency
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        float::consts::SQRT_2,
        intersections::{prepare_computations, Intersection, Intersections},
        lights::PointLight,
        materials::MaterialBuilder,
        matrices::translation,
        patterns::test_pattern,
        rays::Ray,
        shapes::{plane, sphere, Shape, ShapeBuilder},
        tuples::{point, vector},
        world::{World, WorldBuilder},
        Float, BLACK, WHITE,
    };

    mod intersect {
        use super::*;

        #[test]
        fn intersection_with_test_world() {
            let w = World::test_world();
            let r = Ray::new(point(0., 0., -5.), 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.);
        }
    }
    mod world {
        use super::*;

        #[test]
        fn default_world() {
            let w = World::default();
            assert!(w.objects.is_empty());
            assert_eq!(w.lights.len(), 0);
        }
        #[test]
        fn test_world() {
            let w = World::test_world();
            assert_eq!(w.objects.len(), 2);
            assert!(!w.lights.is_empty());
        }
    }
    mod shade_hit {
        use super::*;

        #[test]
        fn shading_an_intersection() {
            // Shading an intersection.
            let w = World::test_world();
            let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
            let s = &w.objects[0];
            let xs = Intersections::from(Intersection::new(4., &s));
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.shade_hit(&comps, 1);
            assert_eq!(c, [0.38066, 0.47583, 0.2855].into());
        }
        #[test]
        fn shading_an_intersection_from_inside() {
            // Shading an intersection from the inside.
            let mut w = World::test_world();
            w.lights = vec![PointLight::new(point(0., 0.25, 0.), WHITE)];
            let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
            let s = &w.objects[1];
            let xs = Intersections::from(Intersection::new(0.5, &s));
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.shade_hit(&comps, 1);
            assert_eq!(c, [0.90498, 0.90498, 0.90498].into());
        }
        #[test]
        fn shading_an_intersection_in_shadow() {
            // Shading with an intersection in shadow.
            let mut w = World::default();
            w.lights = vec![PointLight::new(point(0., 0., -10.), WHITE)];
            let s1 = Shape::sphere();
            let mut s2 = Shape::sphere();
            s2.set_transform(translation(0., 0., 10.));
            w.objects = vec![s1, s2.clone()];
            let r = Ray::new(point(0., 0., 5.), vector(0., 0., 1.));
            let xs = Intersections::from(Intersection::new(4., &s2));
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.shade_hit(&comps, 1);
            assert_eq!(c, [0.1, 0.1, 0.1].into());
        }
        #[test]
        fn shading_with_a_reflective_material() -> Result<(), Box<dyn std::error::Error>> {
            // Shading with a reflective material.
            let mut w = World::test_world();
            let shape = ShapeBuilder::plane()
                .material(MaterialBuilder::default().reflective(0.5).build()?)
                .transform(translation(0., -1., 0.))
                .build()?;
            w.objects.push(shape.clone());
            let r = Ray::new(
                point(0., 0., -3.),
                vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
            );
            let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.shade_hit(&comps, 1);
            assert_eq!(c, [0.87676, 0.92434, 0.82917].into());
            Ok(())
        }
        #[test]
        fn shading_with_a_transparent_material() -> Result<(), Box<dyn std::error::Error>> {
            // shade_hit() with a transparent material.
            let mut w = World::test_world();
            let floor = plane()
                .transform(translation(0., -1., 0.))
                .material(
                    MaterialBuilder::default()
                        .transparency(0.5)
                        .refractive_index(1.5)
                        .build()?,
                )
                .build()?;
            w.objects.push(floor.clone());
            let ball = sphere()
                .material(
                    MaterialBuilder::default()
                        .color([1., 0., 0.])
                        .ambient(0.5)
                        .build()?,
                )
                .transform(translation(0., -3.5, -0.5))
                .build()?;
            w.objects.push(ball);
            let r = Ray::new(
                point(0., 0., -3.),
                vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
            );
            let xs = Intersections::new(vec![Intersection::new((2 as Float).sqrt(), &floor)]);
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.shade_hit(&comps, 5);
            assert_eq!(c, [0.93642, 0.68642, 0.68643].into());
            Ok(())
        }
        #[test]
        fn reflective_transparent_material() -> Result<(), Box<dyn std::error::Error>> {
            let mut w = World::test_world();
            let floor = plane()
                .transform(translation(0., -1., 0.))
                .material(
                    MaterialBuilder::default()
                        .reflective(0.5)
                        .transparency(0.5)
                        .refractive_index(1.5)
                        .build()?,
                )
                .build()?;
            w.objects.push(floor.clone());
            let ball = sphere()
                .transform(translation(0., -3.5, -0.5))
                .material(
                    MaterialBuilder::default()
                        .color([1., 0., 0.])
                        .ambient(0.5)
                        .build()?,
                )
                .build()?;
            w.objects.push(ball);
            let r = Ray::new(point(0., 0., -3.), vector(0., -SQRT_2 / 2., SQRT_2 / 2.));
            let xs = Intersections::new(vec![Intersection::new(SQRT_2, &floor)]);
            let comps = prepare_computations(&xs[0], &r, &xs);
            let color = w.shade_hit(&comps, 5);
            assert_eq!(color, [0.93391, 0.69643, 0.69243].into());
            Ok(())
        }
    }
    mod color_at {
        use super::*;

        #[test]
        fn color_when_ray_misses() {
            // The color when a ray misses.
            let w = World::test_world();
            let r = Ray::new(point(0., 0., -5.), vector(0., 1., 0.));
            let c = w.color_at(&r, 1);
            assert_eq!(c, BLACK);
        }
        #[test]
        fn color_when_ray_hits() {
            // The color when a ray hits.
            let w = World::test_world();
            let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
            let c = w.color_at(&r, 1);
            assert_eq!(c, [0.38066, 0.47583, 0.2855].into());
        }
        #[test]
        fn color_with_an_intersection_behind_ray() {
            // The color with an intersection behind the ray.
            let w = {
                let mut w = World::test_world();
                let mut outer = &mut w.objects[0];
                outer.material.ambient = 1.;
                let inner = &mut w.objects[1];
                inner.material.ambient = 1.;
                w
            };
            let r = Ray::new(point(0., 0., 0.75), vector(0., 0., -1.));
            let c = w.color_at(&r, 1);
            // inner.material.color is WHITE
            assert_eq!(c, WHITE);
        }
        #[test]
        fn ensure_mutually_reflective_surfaces_terminate() -> Result<(), Box<dyn std::error::Error>>
        {
            // Ensure mutually reflective surfaces don't infinitely recurse.
            let lower = ShapeBuilder::plane()
                .material(MaterialBuilder::default().reflective(1.).build()?)
                .transform(translation(0., -1., 0.))
                .build()?;
            let upper = ShapeBuilder::plane()
                .material(MaterialBuilder::default().reflective(1.).build()?)
                .transform(translation(0., 1., 0.))
                .build()?;
            let w = WorldBuilder::default()
                .lights(vec![PointLight::new(point(0., 0., 0.), WHITE)])
                .objects(vec![lower, upper])
                .build()?;
            let r = Ray::new(point(0., 0., 0.), vector(0., 1., 0.));
            // This should complete without stack overflow.
            w.color_at(&r, 1);
            Ok(())
        }
    }
    mod is_shadowed {
        use super::*;

        #[test]
        fn no_shadow_when_nothing_collinear_with_point_and_light() {
            // There is no shadow when nothing is collinear with point and light.
            let w = World::test_world();
            let light = &w.lights[0];

            let p = point(0., 10., 0.);
            assert_eq!(w.is_shadowed(p, light), false);
        }
        #[test]
        fn shadow_when_object_between_point_and_light() {
            // The shadow when an object is between the point and the light.
            let w = World::test_world();
            let light = &w.lights[0];

            let p = point(10., -10., 10.);
            assert_eq!(w.is_shadowed(p, light), true);
        }

        #[test]
        fn no_shadow_when_object_behind_light() {
            // There is no shadow when an object is behind the light.
            let w = World::test_world();
            let light = &w.lights[0];

            let p = point(-20., 20., -20.);
            assert_eq!(w.is_shadowed(p, light), false);
        }
        #[test]
        fn no_shadow_when_object_behind_point() {
            // There is no shadow when an object is behind the point.
            let w = World::test_world();
            let light = &w.lights[0];

            let p = point(-2., 2., -2.);
            assert_eq!(w.is_shadowed(p, light), false);
        }
    }

    mod reflected_color {
        use super::*;

        #[test]
        fn reflected_color_for_nonreflective_material() {
            // The reflected color for a nonreflective material.
            let r = Ray::new(point(0., 0., 0.), vector(0., 0., 1.));
            let mut w = World::test_world();
            w.objects[1].material.ambient = 1.;
            let xs = Intersections::from(Intersection::new(1., &w.objects[1]));
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.reflected_color(&comps, 1);
            assert_eq!(c, BLACK);
        }
        #[test]
        fn reflected_color_for_reflective_material() -> Result<(), Box<dyn std::error::Error>> {
            // The reflected color for a reflective material.
            let mut w = World::test_world();
            let shape = ShapeBuilder::plane()
                .material(MaterialBuilder::default().reflective(0.5).build()?)
                .transform(translation(0., -1., 0.))
                .build()?;
            w.objects.push(shape.clone());
            let r = Ray::new(
                point(0., 0., -3.),
                vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
            );
            let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.reflected_color(&comps, 1);
            assert_eq!(c, [0.19033, 0.23791, 0.14274].into());
            Ok(())
        }
        #[test]
        fn reflected_color_at_maximum_recursion_depth() -> Result<(), Box<dyn std::error::Error>> {
            // The reflected color at the maximum recursion depth.
            let mut w = World::test_world();
            let shape = ShapeBuilder::plane()
                .material(MaterialBuilder::default().reflective(0.5).build()?)
                .transform(translation(0., -1., 0.))
                .build()?;
            w.objects.push(shape.clone());
            let r = Ray::new(
                point(0., 0., -3.),
                vector(0., -(2 as Float).sqrt() / 2., (2 as Float).sqrt() / 2.),
            );
            let xs = Intersections::from(Intersection::new((2 as Float).sqrt(), &shape));
            let comps = prepare_computations(&xs[0], &r, &xs);
            let _ = w.reflected_color(&comps, 0);
            // Just needs to get here without infinite recursion.

            Ok(())
        }
    }
    mod refracted_color {
        use super::*;

        #[test]
        fn refracted_color_with_opaque_surface() {
            // The refracted color with an opaque surface.
            let w = World::test_world();
            let shape = &w.objects[0];
            let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
            let xs = Intersections::new(vec![
                Intersection::new(4., &shape),
                Intersection::new(6., &shape),
            ]);
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.refracted_color(&comps, 5);
            assert_eq!(c, BLACK);
        }
        #[test]
        fn refracted_color_at_maximum_recursion_depth() {
            // The refracted color at the maximum recursive depth.
            let mut w = World::test_world();
            w.objects[0].material.transparency = 1.0;
            w.objects[0].material.refractive_index = 1.5;
            let shape = &w.objects[0];
            let r = Ray::new(point(0., 0., -5.), vector(0., 0., 1.));
            let xs = Intersections::new(vec![
                Intersection::new(4., &shape),
                Intersection::new(6., &shape),
            ]);
            let comps = prepare_computations(&xs[0], &r, &xs);
            let c = w.refracted_color(&comps, 0);
            assert_eq!(c, BLACK);
        }
        #[test]
        fn refracted_color_under_total_internal_reflection() {
            // The refracted color under total internal reflection.
            let mut w = World::test_world();
            w.objects[0].material.transparency = 1.0;
            w.objects[0].material.refractive_index = 1.5;
            let shape = &w.objects[0];
            let r = Ray::new(point(0., 0., (2 as Float).sqrt() / 2.), vector(0., 1., 0.));
            let xs = Intersections::new(vec![
                Intersection::new(-(2 as Float).sqrt() / 2., &shape),
                Intersection::new((2 as Float).sqrt() / 2., &shape),
            ]);
            let comps = prepare_computations(&xs[1], &r, &xs);
            let c = w.refracted_color(&comps, 5);
            assert_eq!(c, BLACK);
        }
        #[test]
        fn refracted_color_with_a_refracted_ray() -> Result<(), Box<dyn std::error::Error>> {
            // The refracted color with a refracted ray.
            let mut w = World::test_world();
            w.objects[0].material.ambient = 1.;
            w.objects[0].material.color = test_pattern().build()?;

            w.objects[1].material.transparency = 1.;
            w.objects[1].material.refractive_index = 1.5;

            let r = Ray::new(point(0., 0., 0.1), vector(0., 1., 0.));
            let a = &w.objects[0];
            let b = &w.objects[1];
            let xs = Intersections::new(vec![
                Intersection::new(-0.9899, &a),
                Intersection::new(-0.4899, &b),
                Intersection::new(0.4899, &b),
                Intersection::new(0.9899, &a),
            ]);
            let comps = prepare_computations(&xs[2], &r, &xs);
            let c = w.refracted_color(&comps, 5);
            assert_eq!(c, [0., 0.99887, 0.04721].into());

            Ok(())
        }
    }
}
