use ray_tracing_core::light::AreaLight;
use ray_tracing_core::prelude::*;
use ray_tracing_core::scene::{Intersection, Scene};
use ray_tracing_material::default::DefaultMaterial;

#[derive(Clone)]
pub struct BasicScene<M> {
    pub(crate) spheres: Vec<(Pos3, Float)>,
    area_light: AreaLight,
    material: M,
}

impl<M> BasicScene<M> {
    pub fn new(material: M) -> Self {
        Self {
            spheres: vec![(Pos3::zero(), 1.0), (Pos3::new(0.0, -4.5, 0.0), 4.0)],
            area_light: AreaLight::new(Color::white()),
            material,
        }
    }
}

impl Default for BasicScene<DefaultMaterial> {
    fn default() -> Self {
        Self::new(DefaultMaterial {})
    }
}

impl<R, M> Scene<R> for BasicScene<M>
where
    R: Rng,
    M: Material<R>,
{
    type Mat<'a>
        = &'a dyn Material<R>
    where
        M: 'a,
        R: 'a;

    type Light<'b>
        = &'b dyn Light<R>
    where
        M: 'b,
        R: 'b;

    fn intersect(
        &self,
        ray: Ray,
        min: Float,
        max: Float,
    ) -> Option<Intersection<R, Self::Mat<'_>, Self::Light<'_>>> {
        let mut intersection: Option<Intersection<R, Self::Mat<'_>, Self::Light<'_>>> = None;

        for &(c, r) in &self.spheres {
            let offset = ray.start() - c;
            let p = Dir3::dot(ray.dir(), offset);
            let delta = p * p - (offset.length_squared() - r * r);

            if delta >= 0.0 {
                let d = -p - Float::sqrt(delta);

                let int = Intersection::new(
                    d,
                    ((ray.start() + d * ray.dir()) - c).normalize(),
                    Some(&self.material as &dyn Material<R>),
                    None,
                    0.0,
                );
                if d >= min && d <= max {
                    if let Some(i) = intersection.as_ref() {
                        if i.t() > d {
                            intersection.replace(int);
                        }
                    } else {
                        intersection = Some(int)
                    }
                }
            }
        }

        Some(intersection.unwrap_or(Intersection::new(
            Float::INFINITY,
            -ray.dir(),
            None,
            Some(&self.area_light),
            0.0,
        )))
    }

    fn sample_light<'b>(
        &self,
        _w_in: Dir3,
        _intersection: &Intersection<R, Self::Mat<'b>, Self::Light<'b>>,
        _rng: &mut R,
    ) -> Option<ray_tracing_core::scene::LightSample<'_, R>>
    where
        Self: 'b,
    {
        None
    }
}
