use derive_builder::Builder;

use crate::{
    matrices::Matrix4x4,
    shapes::Shape,
    tuples::{Color, Tuple},
    BLACK, WHITE,
};

pub const BLACK_PAT: Pattern = Pattern {
    color: ColorMapper::Constant(BLACK),
    transform: Matrix4x4::identity(),
    inverse_transform: Matrix4x4::identity(),
};
pub const WHITE_PAT: Pattern = Pattern {
    color: ColorMapper::Constant(WHITE),
    transform: Matrix4x4::identity(),
    inverse_transform: Matrix4x4::identity(),
};

#[derive(Debug, PartialEq, Clone)]
pub enum ColorMapper {
    /// TestPattern the color returned is the pattern space point after going through world->object and object->pattern space translation.
    TestPattern,
    /// Solid color, the same sampled every where.
    Constant(Color),
    /// Pattern that alternates between the given patterns along each unit of the X-axis.  The
    /// strip extends infinitely in the positive and negative Y and Z axes.
    Stripe { a: Box<Pattern>, b: Box<Pattern> },
    /// Linear blend between `a` and `b` along the X-axis.
    Gradient { a: Box<Pattern>, b: Box<Pattern> },
    /// Bullseye pattern in the XZ plane.
    Ring { a: Box<Pattern>, b: Box<Pattern> },
    /// Traditional ray tracer tile floor pattern.
    Checkers { a: Box<Pattern>, b: Box<Pattern> },
}

impl From<Color> for ColorMapper {
    fn from(c: Color) -> ColorMapper {
        ColorMapper::Constant(c)
    }
}

impl From<Color> for Box<Pattern> {
    fn from(c: Color) -> Box<Pattern> {
        Box::new(Pattern {
            color: ColorMapper::Constant(c),
            ..Pattern::default()
        })
    }
}

#[derive(Builder, Debug, PartialEq, Clone)]
#[builder(default, pattern = "immutable")]
pub struct Pattern {
    pub color: ColorMapper,
    transform: Matrix4x4,
    #[builder(private, default = "self.default_inverse_transform()?")]
    inverse_transform: Matrix4x4,
}

impl PatternBuilder {
    fn default_inverse_transform(&self) -> Result<Matrix4x4, String> {
        Ok(self.transform.unwrap_or(Matrix4x4::identity()).inverse())
    }
}

impl Default for Pattern {
    fn default() -> Pattern {
        Pattern {
            color: ColorMapper::Constant(WHITE),
            transform: Matrix4x4::identity(),
            inverse_transform: Matrix4x4::identity(),
        }
    }
}

/// Creates a [Pattern] with a color type of [ColorMapper::Constant] from the given [Color]
impl<C> From<C> for Pattern
where
    C: Into<Color>,
{
    fn from(c: C) -> Self {
        Pattern {
            color: ColorMapper::Constant(c.into()),
            ..Pattern::default()
        }
    }
}

/// Builder for creating a material pattern used for testing.  The color returned is the pattern space point
/// after going through world->object and object->pattern space translation.
pub fn test_pattern() -> PatternBuilder {
    PatternBuilder::default().color(ColorMapper::TestPattern)
}

/// Builder for creating a material pattern that alternates between the given colors along each unit of the
/// X-axis.  The strip extends infinitely in the positive and negative Y and Z axes.
pub fn stripe_pattern(a: Pattern, b: Pattern) -> PatternBuilder {
    PatternBuilder::default().color(ColorMapper::Stripe {
        a: a.into(),
        b: b.into(),
    })
}

/// Builder for creating a material pattern that gradually blends between the given colors along
/// the X-axis.
pub fn gradient_pattern(a: Pattern, b: Pattern) -> PatternBuilder {
    PatternBuilder::default().color(ColorMapper::Gradient {
        a: a.into(),
        b: b.into(),
    })
}

/// Builder for creating a material pattern that alternates between the given colors in a ring
/// shape in the XZ plane.
pub fn ring_pattern(a: Pattern, b: Pattern) -> PatternBuilder {
    PatternBuilder::default().color(ColorMapper::Ring {
        a: a.into(),
        b: b.into(),
    })
}

/// Builder for creating a material pattern that alternates between the given colors along the X, Y
/// and Z pattern.  Creates traditional ray tracer tile floor pattern.
pub fn checkers_pattern(a: Pattern, b: Pattern) -> PatternBuilder {
    PatternBuilder::default().color(ColorMapper::Checkers {
        a: a.into(),
        b: b.into(),
    })
}

/// Generic implementation for mapping points to colors according to the given [ColorMapper].
impl Pattern {
    /// Create a pattern used for testing.  The color returned is the pattern space point
    /// after going through world->object and object->pattern space translation.
    pub fn test() -> Pattern {
        Pattern {
            color: ColorMapper::TestPattern,
            ..Pattern::default()
        }
    }

    /// Create a pattern that alternates between the given Patterns along each unit of the
    /// X-axis.  The strip extends infinitely in the positive and negative Y and Z axes.
    pub fn stripe(a: Pattern, b: Pattern) -> Pattern {
        Pattern {
            color: ColorMapper::Stripe {
                a: a.into(),
                b: b.into(),
            },
            ..Pattern::default()
        }
    }

    /// Create a pattern that gradually blends between the given Patterns along the X-axis.
    pub fn gradient(a: Pattern, b: Pattern) -> Pattern {
        Pattern {
            color: ColorMapper::Gradient {
                a: a.into(),
                b: b.into(),
            },
            ..Pattern::default()
        }
    }

    /// Create a pattern that alternates between the given Patterns in a ring in the XZ plane.
    pub fn ring(a: Pattern, b: Pattern) -> Pattern {
        Pattern {
            color: ColorMapper::Ring {
                a: a.into(),
                b: b.into(),
            },
            ..Pattern::default()
        }
    }

    /// Create a pattern that alternates between the given Patterns along the X, Y and Z axis.
    pub fn checkers(a: Pattern, b: Pattern) -> Pattern {
        Pattern {
            color: ColorMapper::Checkers {
                a: a.into(),
                b: b.into(),
            },
            ..Pattern::default()
        }
    }

    /// Sample the color at the given point in untranslated object space.
    pub fn pattern_at(&self, object_point: Tuple) -> Color {
        let point = self.inverse_transform * object_point;
        match &self.color {
            ColorMapper::TestPattern => [point.x, point.y, point.z].into(),
            ColorMapper::Constant(c) => *c,
            ColorMapper::Stripe { a, b } => {
                let x = point.x.floor();
                if x % 2. == 0. {
                    a.pattern_at(point)
                } else {
                    b.pattern_at(point)
                }
            }
            ColorMapper::Gradient { a, b } => {
                let a = a.pattern_at(point);
                let b = b.pattern_at(point);
                let distance = b - a;
                let fraction = point.x - point.x.floor();
                a + distance * fraction
            }
            ColorMapper::Ring { a, b } => {
                let a = a.pattern_at(point);
                let b = b.pattern_at(point);
                let px = point.x;
                let pz = point.z;
                if (px * px + pz * pz).sqrt().floor() % 2. == 0. {
                    a
                } else {
                    b
                }
            }
            ColorMapper::Checkers { a, b } => {
                let a = a.pattern_at(point);
                let b = b.pattern_at(point);
                let d = point.x.floor() + point.y.floor() + point.z.floor();
                if d % 2. == 0. {
                    a
                } else {
                    b
                }
            }
        }
    }
    /// Sample the color at the given world point on the given object.
    /// This function respects the object and the pattern's transform matrix.
    pub fn pattern_at_object(&self, object: &Shape, world_point: Tuple) -> Color {
        let object_point = object.inverse_transform() * world_point;
        self.pattern_at(object_point)
    }
    pub fn transform(&self) -> Matrix4x4 {
        self.transform
    }

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

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

#[cfg(test)]
mod tests {
    use crate::{
        matrices::identity,
        patterns::{ColorMapper, Pattern, BLACK_PAT, WHITE_PAT},
        BLACK, WHITE,
    };

    #[test]
    fn test_create() {
        let pattern = Pattern::test();
        assert_eq!(pattern.transform(), identity());
    }

    #[test]
    fn stripe_create() {
        let pattern = Pattern::stripe(BLACK_PAT, WHITE_PAT);
        assert_eq!(
            pattern.color,
            ColorMapper::Stripe {
                a: BLACK.into(),
                b: WHITE.into(),
            }
        );
    }
    #[test]
    fn gradient_create() {
        println!("SHOE ME SOMETHING");
        println!("* * * 1");
        let pattern = Pattern::gradient(BLACK_PAT, WHITE_PAT);
        println!("* * * 2");
        assert_eq!(
            pattern.color,
            ColorMapper::Gradient {
                a: BLACK.into(),
                b: WHITE.into(),
            }
        );
    }
    #[test]
    fn ring_create() {
        let pattern = Pattern::ring(BLACK_PAT, WHITE_PAT);
        assert_eq!(
            pattern.color,
            ColorMapper::Ring {
                a: BLACK.into(),
                b: WHITE.into()
            }
        );
    }
    #[test]
    fn checkers_create() {
        let pattern = Pattern::checkers(BLACK_PAT, WHITE_PAT);
        assert_eq!(
            pattern.color,
            ColorMapper::Checkers {
                a: BLACK.into(),
                b: WHITE.into()
            }
        );
    }

    mod pattern_at {
        use super::*;
        use crate::tuples::point;

        #[test]
        fn test_returns_coordinates() {
            // A test returns the pattern space coordinates of the point.
            let pattern = Pattern::test();
            let p = point(1., 2., 3.);
            assert_eq!(pattern.pattern_at(p), [p.x, p.y, p.z].into());
        }

        #[test]
        fn stripe_alternates_between_two_colors() {
            // A stripe alternates between two colors.
            let pattern = Pattern::stripe(WHITE_PAT, BLACK_PAT);
            for (p, want) in &[
                // A stripe pattern is constant in y.
                (point(0., 0., 0.), WHITE),
                (point(0., 1., 0.), WHITE),
                (point(0., 2., 0.), WHITE),
                // A stripe pattern is constant in z.
                (point(0., 0., 0.), WHITE),
                (point(0., 0., 1.), WHITE),
                (point(0., 0., 2.), WHITE),
                // A stripe pattern alternates in z.
                (point(0., 0., 0.), WHITE),
                (point(0.9, 0., 0.), WHITE),
                (point(1., 0., 0.), BLACK),
                (point(-0.1, 0., 0.), BLACK),
                (point(-1., 0., 0.), BLACK),
                (point(-1.1, 0., 0.), WHITE),
            ] {
                assert_eq!(pattern.pattern_at(*p), *want, "{:?}", p);
            }
        }

        #[test]
        fn gradient_linearly_interpolates_between_colors() {
            // A gradient linearly interpolates between two colors.
            let pattern = Pattern::gradient(WHITE_PAT, BLACK_PAT);
            assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
            assert_eq!(
                pattern.pattern_at(point(0.25, 0., 0.)),
                [0.75, 0.75, 0.75].into()
            );
            assert_eq!(
                pattern.pattern_at(point(0.5, 0., 0.)),
                [0.5, 0.5, 0.5].into()
            );
            assert_eq!(
                pattern.pattern_at(point(0.75, 0., 0.)),
                [0.25, 0.25, 0.25].into()
            );
        }

        #[test]
        fn ring_extend_in_x_and_z() {
            // A ring should extend both in x and z.
            let pattern = Pattern::ring(WHITE_PAT, BLACK_PAT);
            assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
            assert_eq!(pattern.pattern_at(point(1., 0., 0.)), BLACK);
            assert_eq!(pattern.pattern_at(point(0., 0., 1.)), BLACK);
            // 0.708 is slight more than 2.sqrt()/2.
            assert_eq!(pattern.pattern_at(point(0.708, 0., 0.708)), BLACK);
        }

        #[test]
        fn checkers_repeat_along_x_axis() {
            // Checkers should repeat along X-axis.
            let pattern = Pattern::checkers(WHITE_PAT, BLACK_PAT);
            assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
            assert_eq!(pattern.pattern_at(point(0.99, 0., 0.)), WHITE);
            assert_eq!(pattern.pattern_at(point(1.01, 0., 0.)), BLACK);
        }

        #[test]
        fn checkers_repeat_along_y_axis() {
            // Checkers should repeat along Y-axis.
            let pattern = Pattern::checkers(WHITE_PAT, BLACK_PAT);
            assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
            assert_eq!(pattern.pattern_at(point(0., 0.99, 0.)), WHITE);
            assert_eq!(pattern.pattern_at(point(0., 1.01, 0.)), BLACK);
        }

        #[test]
        fn checkers_repeat_along_z_axis() {
            // Checkers should repeat along Z-axis.
            let pattern = Pattern::checkers(WHITE_PAT, BLACK_PAT);
            assert_eq!(pattern.pattern_at(point(0., 0., 0.)), WHITE);
            assert_eq!(pattern.pattern_at(point(0., 0., 0.99)), WHITE);
            assert_eq!(pattern.pattern_at(point(0., 0., 1.01)), BLACK);
        }
    }

    mod pattern_at_object {
        use std::error::Error;

        use crate::{
            matrices::scaling,
            patterns::{stripe_pattern, BLACK_PAT, WHITE_PAT},
            shapes::{Shape, ShapeBuilder},
            tuples::point,
            WHITE,
        };

        #[test]
        fn stripes_with_an_object_transformation() -> Result<(), Box<dyn Error>> {
            // Stripes with an object transformation.
            let object = ShapeBuilder::sphere()
                .transform(scaling(2., 2., 2.))
                .build()?;
            let pattern = stripe_pattern(WHITE_PAT, BLACK_PAT).build()?;
            let c = pattern.pattern_at_object(&object, point(1.5, 0., 0.));
            assert_eq!(c, WHITE);
            Ok(())
        }

        #[test]
        fn stripes_with_a_pattern_transformation() -> Result<(), Box<dyn Error>> {
            // Stripes with a pattern transformation.
            let object = Shape::sphere();
            let mut pattern = stripe_pattern(WHITE_PAT, BLACK_PAT).build()?;
            pattern.set_transform(scaling(2., 2., 2.));
            let c = pattern.pattern_at_object(&object, point(1.5, 0., 0.));
            assert_eq!(c, WHITE);
            Ok(())
        }
    }
}
