use super::{utility, Color, HitRecord, Ray, Vec3};

pub struct Lambertian {
    albedo: Color,
}

pub struct Metal {
    albedo: Color,
    fuzz: f64,
}

pub struct Mirror {
    albedo: Color,
}

pub struct Dielectric {
    ir: f64,
}

pub trait Material: Sync + Send {
    fn scatter(
        &self,
        r_in: &Ray,
        rec: &HitRecord,
        attenuation: &mut Color,
        scattered: &mut Ray,
    ) -> bool;
}

impl Lambertian {
    pub fn new(a: &Color) -> Self {
        Lambertian { albedo: a.clone() }
    }
}

impl Material for Lambertian {
    fn scatter(
        &self,
        _r_in: &Ray,
        rec: &HitRecord,
        attenuation: &mut Color,
        scattered: &mut Ray,
    ) -> bool {
        let mut scatter_direction = rec.normal + Vec3::random_unit_vector();

        if scatter_direction.near_zero() {
            scatter_direction = rec.normal;
        }

        *scattered = Ray::new(rec.p, scatter_direction);
        *attenuation = self.albedo.clone();
        return true;
    }
}

impl Metal {
    pub fn new(a: &Color, f: f64) -> Self {
        Metal {
            albedo: *a,
            fuzz: f,
        }
    }
}

impl Material for Metal {
    fn scatter(
        &self,
        r_in: &Ray,
        rec: &HitRecord,
        attenuation: &mut Color,
        scattered: &mut Ray,
    ) -> bool {
        let reflected = Vec3::reflect(&Vec3::unit_vector(r_in.direction()), &rec.normal);

        *scattered = Ray::new(rec.p, reflected + self.fuzz * Vec3::random_in_unit_sphere());
        *attenuation = self.albedo.clone();

        return Vec3::dot(scattered.direction(), rec.normal) > 0.0;
    }
}

impl Mirror {
    pub fn new(a: &Color) -> Self {
        Mirror { albedo: *a }
    }
}

impl Material for Mirror {
    fn scatter(
        &self,
        r_in: &Ray,
        rec: &HitRecord,
        attenuation: &mut Color,
        scattered: &mut Ray,
    ) -> bool {
        if utility::random_f64() > 0.8 {
            // Reflektiert
            let reflected = Vec3::reflect(&Vec3::unit_vector(r_in.direction()), &rec.normal);
            *scattered = Ray::new(rec.p, reflected);
            *attenuation = self.albedo.clone();

            return Vec3::dot(scattered.direction(), rec.normal) > 0.0;
        } else {
            // Geht geradeaus durch
            let reflected = r_in.direction().clone();
            *scattered = Ray::new(rec.p, reflected);

            *attenuation = self.albedo.clone();

            return true;
        };
    }
}

impl Dielectric {
    pub fn new(index_of_refraction: f64) -> Self {
        Dielectric {
            ir: index_of_refraction,
        }
    }

    fn reflectance(cosine: f64, ref_idx: f64) -> f64 {
        let r0 = ((1.0 - ref_idx) / (1.0 + ref_idx)).powi(2);
        return r0 + (1.0 - r0) * (1.0 - cosine).powi(5);
    }
}

impl Material for Dielectric {
    fn scatter(
        &self,
        r_in: &Ray,
        rec: &HitRecord,
        attenuation: &mut Color,
        scattered: &mut Ray,
    ) -> bool {
        *attenuation = Color::new(1.0, 1.0, 1.0);
        let refraction_ratio = if rec.front_face {
            1.0 / self.ir
        } else {
            self.ir
        };

        let unit_direction = Vec3::unit_vector(r_in.direction());
        let cos_theta = Vec3::dot(-1.0 * unit_direction, rec.normal).min(1.0);
        let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();

        let mut direction = Vec3::refract(&unit_direction, &rec.normal, refraction_ratio); // can reflect

        if refraction_ratio * sin_theta > 1.0
            || Dielectric::reflectance(cos_theta, refraction_ratio) > utility::random_f64()
        {
            // must reflect
            direction = Vec3::reflect(&unit_direction, &rec.normal);
        }

        *scattered = Ray::new(rec.p, direction);
        return true;
    }
}
