///|
/// Material system for ray tracing using an enum to dispatch on material type.

pub(all) enum Material {
  Lambertian(Vec3)      // albedo color
  Metal(Vec3, Double)   // albedo, fuzz
  Dielectric(Double)    // index of refraction
  Emissive(Vec3)        // emitted color
  DiffuseLight(Texture) // area light with texture
  Isotropic(Vec3)       // isotropic volume scattering
  Textured(Texture)     // textured dull material
} derive(Debug)

/// Result of a ray scattering off a material.
pub(all) struct ScatterResult {
  attenuation : Vec3
  scattered : Ray
} derive(Debug)

/// Scatter a ray off a material.
pub fn Material::scatter(self : Material, r_in : Ray, rec : HitRecord) -> ScatterResult? {
  match self {
    Lambertian(albedo) => {
      let mut scatter_dir = rec.normal + default_rng().random_unit_vector().0
      if scatter_dir.near_zero() {
        scatter_dir = rec.normal
      }
      let scattered = Ray::new(orig=rec.p, dir=scatter_dir)
      Some({ attenuation: albedo, scattered })
    }
    Metal(albedo, fuzz) => {
      let reflected = reflect(r_in.dir.normalize(), rec.normal)
      let fuzz_offset = default_rng().random_in_unit_sphere().0
      let scattered_dir = reflected + fuzz_offset.mul_scalar(fuzz)
      let scattered = Ray::new(orig=rec.p, dir=scattered_dir)
      if scattered.dir.dot(rec.normal) > 0.0 {
        Some({ attenuation: albedo, scattered })
      } else {
        None
      }
    }
    Dielectric(ir) => {
      let attenuation = { x: 1.0, y: 1.0, z: 1.0 }
      let refraction_ratio = if rec.front_face { 1.0 / ir } else { ir }
      let unit_dir = r_in.dir.normalize()
      let cos_theta = (-unit_dir).dot(rec.normal).min(1.0)
      let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt()
      let cannot_refract = refraction_ratio * sin_theta > 1.0
      let rand_val = default_rng().random_double().0
      let direction = if cannot_refract || reflectance(cos_theta, refraction_ratio) > rand_val {
        reflect(unit_dir, rec.normal)
      } else {
        match refract(unit_dir, rec.normal, refraction_ratio) {
          None => reflect(unit_dir, rec.normal)
          Some(refracted) => refracted
        }
      }
      let scattered = Ray::new(orig=rec.p, dir=direction)
      Some({ attenuation, scattered })
    }
    Emissive(_color) => None
    DiffuseLight(_tex) => None
    Isotropic(albedo) => {
      let scatter_dir = default_rng().random_unit_vector().0
      let scattered = Ray::new(orig=rec.p, dir=scatter_dir)
      Some({ attenuation: albedo, scattered })
    }
    Textured(tex) => {
      let albedo = tex.value(0.0, 0.0, rec.p)
      let mut scatter_dir = rec.normal + default_rng().random_unit_vector().0
      if scatter_dir.near_zero() {
        scatter_dir = rec.normal
      }
      let scattered = Ray::new(orig=rec.p, dir=scatter_dir)
      Some({ attenuation: albedo, scattered })
    }
  }
}

/// Get emitted light from a material.
pub fn Material::emitted(self : Material, u : Double, v : Double, p : Vec3) -> Vec3 {
  match self {
    Emissive(color) => color
    DiffuseLight(tex) => tex.value(u, v, p)
    _ => { x: 0.0, y: 0.0, z: 0.0 }
  }
}

/// Schlick approximation for reflectance.
fn reflectance(cosine : Double, ref_idx : Double) -> Double {
  let r0 = (1.0 - ref_idx) / (1.0 + ref_idx)
  let r02 = r0 * r0
  r02 + (1.0 - r02) * @math.pow(1.0 - cosine, 5.0)
}