use derive_builder::Builder;

use crate::{
    lights::PointLight,
    patterns::Pattern,
    shapes::Shape,
    tuples::{dot, reflect, Color, Tuple},
    Float, BLACK, WHITE,
};

#[derive(Builder, Debug, PartialEq, Clone)]
#[builder(default)]
pub struct Material {
    #[builder(setter(into))]
    pub color: Pattern,
    pub ambient: Float,
    pub diffuse: Float,
    pub specular: Float,
    pub shininess: Float,
    pub reflective: Float,
    pub transparency: Float,
    pub refractive_index: Float,
}

impl Default for Material {
    /// Creates the default material.
    fn default() -> Material {
        Material {
            color: WHITE.into(),
            ambient: 0.1,
            diffuse: 0.9,
            specular: 0.9,
            shininess: 200.,
            reflective: 0.0,
            transparency: 0.0,
            refractive_index: 1.0,
        }
    }
}

/// Compute lighting contributions using the Phong reflection model.
pub fn lighting(
    material: &Material,
    object: &Shape,
    light: &PointLight,
    point: Tuple,
    eyev: Tuple,
    normalv: Tuple,
    in_shadow: bool,
) -> Color {
    // Combine the surface color with the light's color.
    let color = material.color.pattern_at_object(object, point);
    let effective_color = color * light.intensity;
    // Find the direciton of the light source.
    let lightv = (light.position - point).normalize();
    // Compute the ambient distribution.
    let ambient = effective_color * material.ambient;
    // This is the cosine of the angle between the light vector an the normal
    // vector.  A negative number means the light is on the other side of the
    // surface.
    let light_dot_normal = dot(lightv, normalv);
    let (diffuse, specular) = if light_dot_normal < 0. {
        (BLACK, BLACK)
    } else {
        // Compute the diffuse contribution.
        let diffuse = effective_color * material.diffuse * light_dot_normal;
        // This represents the cosine of the angle between the relfection vector
        // and the eye vector.  A negative number means the light reflects away
        // from the eye.
        let reflectv = reflect(-lightv, normalv);
        let reflect_dot_eye = dot(reflectv, eyev);
        let specular = if reflect_dot_eye <= 0. {
            BLACK
        } else {
            // Compute the specular contribution.
            let factor = reflect_dot_eye.powf(material.shininess);
            light.intensity * material.specular * factor
        };
        (diffuse, specular)
    };
    if in_shadow {
        ambient
    } else {
        ambient + diffuse + specular
    }
}

#[cfg(test)]
mod tests {
    use crate::{materials::Material, WHITE};

    #[test]
    fn default() {
        let m = Material::default();
        assert_eq!(
            m,
            Material {
                color: WHITE.into(),
                ambient: 0.1,
                diffuse: 0.9,
                specular: 0.9,
                shininess: 200.,
                reflective: 0.0,
                transparency: 0.0,
                refractive_index: 1.0,
            }
        );
    }

    mod lighting {
        use crate::{
            lights::PointLight,
            materials::{lighting, Material},
            patterns::{Pattern, BLACK_PAT, WHITE_PAT},
            shapes::Shape,
            tuples::{point, vector, Color},
            Float, BLACK, WHITE,
        };

        #[test]
        fn eye_between_light_and_surface() {
            let in_shadow = false;
            let m = Material::default();
            let position = point(0., 0., 0.);
            let object = Shape::sphere();

            // Lighting with the eye between the light and the surface.
            let eyev = vector(0., 0., -1.);
            let normalv = vector(0., 0., -1.);
            let light = PointLight::new(point(0., 0., -10.), WHITE);
            let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
            assert_eq!(result, Color::new(1.9, 1.9, 1.9));
        }

        #[test]
        fn eye_between_light_and_surface_offset_45() {
            // Lighting with the eye between the light and the surface, eye offset 45°.
            let in_shadow = false;
            let m = Material::default();
            let position = point(0., 0., 0.);
            let object = Shape::sphere();
            let eyev = vector(0., (2. as Float).sqrt() / 2., -(2. as Float).sqrt() / 2.);
            let normalv = vector(0., 0., -1.);
            let light = PointLight::new(point(0., 0., -10.), WHITE);
            let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
            assert_eq!(result, WHITE);
        }
        #[test]
        fn eye_opposite_surface_light_offset_45() {
            // Lighting with the eye opposite surface, light offset 45°.
            let in_shadow = false;
            let m = Material::default();
            let position = point(0., 0., 0.);
            let object = Shape::sphere();
            let eyev = vector(0., 0., -1.);
            let normalv = vector(0., 0., -1.);
            let light = PointLight::new(point(0., 10., -10.), WHITE);
            let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
            assert_eq!(result, Color::new(0.7364, 0.7364, 0.7364));
        }
        #[test]
        fn eye_in_path_of_reflection_vector() {
            // Lighting with the eye in the path of the reflection vector.
            let in_shadow = false;
            let m = Material::default();
            let position = point(0., 0., 0.);
            let object = Shape::sphere();
            let eyev = vector(0., -(2.0 as Float).sqrt() / 2., -(2.0 as Float).sqrt() / 2.);
            let normalv = vector(0., 0., -1.);
            let light = PointLight::new(point(0., 10., -10.), WHITE);
            let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
            assert_eq!(result, Color::new(1.63639, 1.63639, 1.63639));
        }
        #[test]
        fn light_behind_surface() {
            // Lighting with the light behind the surface.
            let in_shadow = false;
            let m = Material::default();
            let position = point(0., 0., 0.);
            let object = Shape::sphere();
            let eyev = vector(0., 0., -1.);
            let normalv = vector(0., 0., -1.);
            let light = PointLight::new(point(0., 0., 10.), WHITE);
            let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
            assert_eq!(result, Color::new(0.1, 0.1, 0.1));
        }
        #[test]
        fn surface_in_shadow() {
            // Lighting with the surface in shadow.
            let m = Material::default();
            let position = point(0., 0., 0.);
            let object = Shape::sphere();
            let in_shadow = true;
            let eyev = vector(0., 0., -1.);
            let normalv = vector(0., 0., -1.);
            let light = PointLight::new(point(0., 0., -10.), WHITE);
            let result = lighting(&m, &object, &light, position, eyev, normalv, in_shadow);
            assert_eq!(result, Color::new(0.1, 0.1, 0.1));
        }
        #[test]
        fn pattern_applied() {
            // Lighting with a pattern applied.
            let object = Shape::sphere();
            let m = Material {
                color: Pattern::stripe(WHITE_PAT, BLACK_PAT),
                ambient: 1.,
                diffuse: 0.,
                specular: 0.,
                ..Material::default()
            };
            let eyev = vector(0., 0., -1.);
            let normalv = vector(0., 0., -1.);
            let light = PointLight::new(point(0., 0., -10.), WHITE);
            let c1 = lighting(
                &m,
                &object,
                &light,
                point(0.9, 0., 0.),
                eyev,
                normalv,
                false,
            );
            let c2 = lighting(
                &m,
                &object,
                &light,
                point(1.1, 0., 0.),
                eyev,
                normalv,
                false,
            );
            assert_eq!(c1, WHITE);
            assert_eq!(c2, BLACK);
        }
    }
}
