use std::ops::Index;

use crate::{
    rays::Ray,
    shapes::Shape,
    tuples::{dot, Tuple},
    Float, EPSILON,
};

#[derive(Debug, Clone, PartialEq)]
pub struct Intersection<'i> {
    pub t: Float,
    pub object: &'i Shape,
}

impl<'i> Intersection<'i> {
    /// Create new `Intersection` at the given `t` that hits the given `object`.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{intersections::Intersection, shapes::Shape};
    ///
    /// // An intersection ecapsulates t and object.
    /// let s = Shape::sphere();
    /// let i = Intersection::new(3.5, &s);
    /// assert_eq!(i.t, 3.5);
    /// assert_eq!(i.object, &s);
    /// ```
    pub fn new(t: Float, object: &Shape) -> Intersection {
        Intersection { t, object }
    }
}

/// Aggregates `Intersection`s.
///
/// # Examples
/// ```
/// use rtchallenge::{
///     intersections::{Intersection, Intersections},
///     rays::Ray,
///     shapes::{intersect, Shape},
///     tuples::Tuple,
/// };
///
/// let s = Shape::sphere();
/// let i1 = Intersection::new(1., &s);
/// let i2 = Intersection::new(2., &s);
/// let xs = Intersections::new(vec![i1, i2]);
/// assert_eq!(xs.len(), 2);
/// assert_eq!(xs[0].t, 1.);
/// assert_eq!(xs[1].t, 2.);
///
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let xs = intersect(&s, &r);
/// assert_eq!(xs.len(), 2);
/// assert_eq!(xs[0].object, &s);
/// assert_eq!(xs[1].object, &s);
/// ```
#[derive(Debug, Default, PartialEq)]
pub struct Intersections<'i>(Vec<Intersection<'i>>);

impl<'i> Intersections<'i> {
    pub fn new(xs: Vec<Intersection<'i>>) -> Intersections {
        Intersections(xs)
    }
    pub fn len(&self) -> usize {
        self.0.len()
    }
    /// Finds nearest hit for this collection of intersections.
    ///
    /// # Examples
    /// ```
    /// use rtchallenge::{
    ///     intersections::{Intersection, Intersections},
    ///     rays::Ray,
    ///     shapes::{intersect, Shape},
    ///     tuples::Tuple,
    /// };
    ///
    /// // The hit, when all intersections have positive t.
    /// let s = Shape::sphere();
    /// let i1 = Intersection::new(1., &s);
    /// let i2 = Intersection::new(2., &s);
    /// let xs = Intersections::new(vec![i2, i1.clone()]);
    /// let i = xs.hit();
    /// assert_eq!(i, Some(&i1));
    ///
    /// // The hit, when some intersections have negative t.
    /// let s = Shape::sphere();
    /// let i1 = Intersection::new(-1., &s);
    /// let i2 = Intersection::new(1., &s);
    /// let xs = Intersections::new(vec![i2.clone(), i1]);
    /// let i = xs.hit();
    /// assert_eq!(i, Some(&i2));
    ///
    /// // The hit, when all intersections have negative t.
    /// let s = Shape::sphere();
    /// let i1 = Intersection::new(-2., &s);
    /// let i2 = Intersection::new(-1., &s);
    /// let xs = Intersections::new(vec![i2, i1]);
    /// let i = xs.hit();
    /// assert_eq!(i, None);
    ///
    /// // The hit is always the lowest nonnegative intersection.
    /// let s = Shape::sphere();
    /// let i1 = Intersection::new(5., &s);
    /// let i2 = Intersection::new(7., &s);
    /// let i3 = Intersection::new(-3., &s);
    /// let i4 = Intersection::new(2., &s);
    /// let xs = Intersections::new(vec![i1, i2, i3, i4.clone()]);
    /// let i = xs.hit();
    /// assert_eq!(i, Some(&i4));
    /// ```
    pub fn hit(&self) -> Option<&Intersection> {
        self.0.iter().filter(|i| i.t > 0.).min_by(|i1, i2| {
            i1.t.partial_cmp(&i2.t)
                .expect("an intersection has a t value that is NaN")
        })
    }
}

impl<'i> IntoIterator for Intersections<'i> {
    type Item = Intersection<'i>;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl<'i> Index<usize> for Intersections<'i> {
    type Output = Intersection<'i>;
    fn index(&self, idx: usize) -> &Self::Output {
        &self.0[idx]
    }
}

#[derive(Debug)]
pub struct PrecomputedData<'i> {
    pub t: Float,
    pub object: &'i Shape,
    pub point: Tuple,
    pub over_point: Tuple,
    pub eyev: Tuple,
    pub normalv: Tuple,
    pub inside: bool,
}

/// Precomputes data common to all intersections.
///
/// # Examples
/// ```
/// use rtchallenge::{
///     intersections::{prepare_computations, Intersection, Intersections},
///     rays::Ray,
///     matrices::Matrix4x4,
///     shapes::{intersect, Shape},
///     tuples::Tuple,
///     EPSILON
/// };
///
/// // Precomputing the state of an intersection.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let shape = Shape::sphere();
/// let i = Intersection::new(4., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert_eq!(comps.t, i.t);
/// assert_eq!(comps.object, i.object);
/// assert_eq!(comps.point, Tuple::point(0., 0., -1.));
/// assert_eq!(comps.eyev, Tuple::vector(0., 0., -1.));
/// assert_eq!(comps.normalv, Tuple::vector(0., 0., -1.));
///
/// // The hit, when an intersection occurs on the outside.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let shape = Shape::sphere();
/// let i = Intersection::new(4., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert_eq!(comps.inside, false);
///
/// // The hit, when an intersection occurs on the inside.
/// let r = Ray::new(Tuple::point(0., 0., 0.), Tuple::vector(0., 0., 1.));
/// let shape = Shape::sphere();
/// let i = Intersection::new(1., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert_eq!(comps.point, Tuple::point(0., 0., 1.));
/// assert_eq!(comps.eyev, Tuple::vector(0., 0., -1.));
/// assert_eq!(comps.inside, true);
//// // Normal would have been (0, 0, 1), but is inverted when inside.
/// assert_eq!(comps.normalv, Tuple::vector(0., 0., -1.));
///
/// // The hit should offset the point.
/// let r = Ray::new(Tuple::point(0., 0., -5.), Tuple::vector(0., 0., 1.));
/// let mut shape = Shape::sphere();
/// shape .set_transform(Matrix4x4::translation(0.,0.,1.));
/// let i = Intersection::new(5., &shape);
/// let comps = prepare_computations(&i, &r);
/// assert!(comps.over_point.z< -EPSILON/2.);
/// assert!(comps.point.z>comps.over_point.z);
/// ```
pub fn prepare_computations<'i>(i: &'i Intersection, r: &Ray) -> PrecomputedData<'i> {
    let point = r.position(i.t);
    let normalv = i.object.normal_at(point);
    let eyev = -r.direction;
    let (inside, normalv) = if dot(normalv, eyev) < 0. {
        (true, -normalv)
    } else {
        (false, normalv)
    };

    let over_point = point + normalv * EPSILON;
    PrecomputedData {
        t: i.t,
        object: i.object,
        point,
        over_point,
        normalv,
        inside,
        eyev,
    }
}
