///|
/// Hitable geometry with BVH-accelerated hit testing.

pub(all) enum Hitable {
  Sphere(Sphere)
  Plane(Plane)
  Triangle(Triangle)
  BoxShape(BoxShape)
  Cylinder(Cylinder)
  Disk(Disk)
  Cone(Cone)
  Torus(Torus)
} derive(Debug)

pub(all) struct HitRecord {
  p : Vec3
  normal : Vec3
  t : Double
  front_face : Bool
  material : Material
} derive(Debug)

pub fn HitRecord::set_face_normal(self : HitRecord, r : Ray, outward_normal : Vec3) -> HitRecord {
  let front_face = r.dir.dot(outward_normal) < 0.0
  let normal = if front_face { outward_normal } else { -outward_normal }
  { ..self, front_face, normal }
}

/// Per-object hit test.
pub fn Hitable::hit(self : Hitable, r : Ray, t_min~ : Double, t_max~ : Double) -> HitRecord? {
  match self {
    Sphere(s) => {
      let oc = r.orig - s.center
      let a = r.dir.length_squared()
      let half_b = oc.dot(r.dir)
      let c = oc.length_squared() - s.radius * s.radius
      let discriminant = half_b * half_b - a * c

      if discriminant < 0.0 { return None }

      let sqrtd = discriminant.sqrt()
      let mut root = (-half_b - sqrtd) / a
      if root < t_min || root > t_max {
        root = (-half_b + sqrtd) / a
        if root < t_min || root > t_max { return None }
      }

      let p = r.at(root)
      let outward_normal = (p - s.center).div_scalar(s.radius)
      let mut rec = { p, normal: outward_normal, t: root, front_face: false, material: s.material }
      rec = rec.set_face_normal(r, outward_normal)
      Some(rec)
    }
    Plane(pl) => pl.hit_plane(r, t_min=t_min, t_max=t_max)
    Triangle(tri) => tri.hit_triangle(r, t_min=t_min, t_max=t_max)
    BoxShape(bx) => bx.hit_box(r, t_min=t_min, t_max=t_max)
    Cylinder(cyl) => cyl.hit_cylinder(r, t_min=t_min, t_max=t_max)
    Disk(dk) => dk.hit_disk(r, t_min=t_min, t_max=t_max)
    Cone(cn) => cn.hit_cone(r, t_min=t_min, t_max=t_max)
    Torus(tor) => tor.hit_torus(r, t_min=t_min, t_max=t_max)
  }
}

/// World with optional BVH acceleration.
pub(all) struct HitableList {
  objects : Array[Hitable]
  bvh_ready : Bool
} derive(Debug)

pub fn HitableList::new() -> HitableList {
  { objects: Array::new(capacity=16), bvh_ready: false }
}

pub fn HitableList::add(self : HitableList, object : Hitable) -> HitableList {
  let objs = self.objects
  objs.push(object)
  { objects: objs, bvh_ready: false }
}

/// Build BVH acceleration structure. Must be called before rendering
/// for scenes with more than a handful of objects.
pub fn HitableList::build_bvh(self : HitableList) -> HitableList {
  if self.objects.length() <= 1 || self.bvh_ready {
    return self
  }
  let _root = bvh_build_from(self.objects)
  { ..self, bvh_ready: true }
}

/// BVH-accelerated hit. When BVH is built, uses hierarchical traversal.
/// Otherwise falls back to linear scan.
pub fn HitableList::hit(self : HitableList, r : Ray, t_min~ : Double, t_max~ : Double) -> HitRecord? {
  if !(self.bvh_ready) || self.objects.length() < 1 {
    return self.hit_linear(r, t_min=t_min, t_max=t_max)
  }
  let root = bvh_build_from(self.objects)
  match root {
    None => self.hit_linear(r, t_min=t_min, t_max=t_max)
    Some(node) => bvh_hit(node, self.objects, r, t_min=t_min, t_max=t_max)
  }
}

/// Linear scan hit (fallback).
fn HitableList::hit_linear(self : HitableList, r : Ray, t_min~ : Double, t_max~ : Double) -> HitRecord? {
  let mut closest = t_max
  let mut result : HitRecord? = None
  for i in 0.. ()
      Some(rec) => { closest = rec.t; result = Some(rec) }
    }
  }
  result
}