///|
/// Geometric sampling helpers for picking, probes, and collision diagnostics.
pub(all) struct GeometryBarycentric {
  u : Double
  v : Double
  w : Double
} derive(Debug, ToJson, Eq)

///|
pub(all) struct GeometryRayHit {
  triangle_index : Int
  distance : Double
  position : Vec3
  normal : Vec3
  barycentric : GeometryBarycentric
  front_facing : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct GeometrySample {
  triangle_index : Int
  position : Vec3
  normal : Vec3
  area : Double
} derive(Debug, ToJson, Eq)

///|
pub(all) struct GeometrySamplingReport {
  triangle_count : Int
  sampled_count : Int
  total_area : Double
  average_area : Double
  degenerate_count : Int
  hits : Array[GeometryRayHit]
} derive(Debug, ToJson, Eq)

///|
pub fn geometry_barycentric(
  u : Double,
  v : Double,
  w : Double,
) -> GeometryBarycentric {
  { u, v, w }
}

///|
pub fn geometry_barycentric_sum(value : GeometryBarycentric) -> Double {
  value.u + value.v + value.w
}

///|
pub fn geometry_barycentric_valid(
  value : GeometryBarycentric,
  tolerance : Double,
) -> Bool {
  value.u >= -tolerance &&
  value.v >= -tolerance &&
  value.w >= -tolerance &&
  Double::abs(geometry_barycentric_sum(value) - 1.0) <= tolerance
}

///|
pub fn geometry_interpolate_vec3(
  a : Vec3,
  b : Vec3,
  c : Vec3,
  barycentric : GeometryBarycentric,
) -> Vec3 {
  {
    x: a.x * barycentric.u + b.x * barycentric.v + c.x * barycentric.w,
    y: a.y * barycentric.u + b.y * barycentric.v + c.y * barycentric.w,
    z: a.z * barycentric.u + b.z * barycentric.v + c.z * barycentric.w,
  }
}

///|
pub fn geometry_triangle_sample(
  triangle_index : Int,
  a : Vec3,
  b : Vec3,
  c : Vec3,
  u : Double,
  v : Double,
) -> GeometrySample {
  let root = if u < 0.0 { 0.0 } else if u > 1.0 { 1.0 } else { u }
  let second_limit = 1.0 - root
  let second = if v < 0.0 {
    0.0
  } else if v > second_limit {
    second_limit
  } else {
    v
  }
  let barycentric = geometry_barycentric(1.0 - root - second, root, second)
  {
    triangle_index,
    position: geometry_interpolate_vec3(a, b, c, barycentric),
    normal: topology_triangle_normal(a, b, c),
    area: topology_triangle_area(a, b, c),
  }
}

///|
pub fn geometry_ray_triangle(
  ray : SpatialRay,
  a : Vec3,
  b : Vec3,
  c : Vec3,
  triangle_index : Int,
  cull_backface : Bool,
) -> GeometryRayHit? {
  let edge_a = topology_subtract(b, a)
  let edge_b = topology_subtract(c, a)
  let cross = topology_cross(ray.direction, edge_b)
  let determinant = topology_dot(edge_a, cross)
  let epsilon = 0.0000001
  if cull_backface && determinant <= epsilon {
    return None
  }
  if !cull_backface && Double::abs(determinant) <= epsilon {
    return None
  }
  let inverse = 1.0 / determinant
  let offset = topology_subtract(ray.origin, a)
  let v = topology_dot(offset, cross) * inverse
  if v < 0.0 || v > 1.0 {
    return None
  }
  let offset_cross = topology_cross(offset, edge_a)
  let w = topology_dot(ray.direction, offset_cross) * inverse
  if w < 0.0 || v + w > 1.0 {
    return None
  }
  let distance = topology_dot(edge_b, offset_cross) * inverse
  if distance < 0.0 {
    return None
  }
  let barycentric = geometry_barycentric(1.0 - v - w, v, w)
  Some({
    triangle_index,
    distance,
    position: spatial_ray_point(ray, distance),
    normal: topology_triangle_normal(a, b, c),
    barycentric,
    front_facing: determinant > 0.0,
  })
}

///|
pub fn geometry_raycast_mesh(
  ray : SpatialRay,
  positions : Array[Vec3],
  triangles : Array[TopologyTriangle],
  cull_backface : Bool,
) -> Array[GeometryRayHit] {
  let hits : Array[GeometryRayHit] = []
  for triangle_index, triangle in triangles {
    match
      (
        positions.get(triangle.a),
        positions.get(triangle.b),
        positions.get(triangle.c),
      ) {
      (Some(a), Some(b), Some(c)) =>
        match
          geometry_ray_triangle(ray, a, b, c, triangle_index, cull_backface) {
          Some(hit) => hits.push(hit)
          None => ()
        }
      _ => ()
    }
  }
  geometry_sort_hits(hits)
  hits
}

///|
fn geometry_sort_hits(hits : Array[GeometryRayHit]) -> Unit {
  let count = hits.length()
  for pass in 0..
          if left.distance > right.distance {
            hits[index] = right
            hits[index + 1] = left
          }
        _ => ()
      }
    }
  }
}

///|
pub fn geometry_total_area(
  positions : Array[Vec3],
  triangles : Array[TopologyTriangle],
) -> Double {
  let mut total = 0.0
  for triangle in triangles {
    match
      (
        positions.get(triangle.a),
        positions.get(triangle.b),
        positions.get(triangle.c),
      ) {
      (Some(a), Some(b), Some(c)) =>
        total = total + topology_triangle_area(a, b, c)
      _ => ()
    }
  }
  total
}

///|
pub fn geometry_degenerate_count(
  positions : Array[Vec3],
  triangles : Array[TopologyTriangle],
  epsilon : Double,
) -> Int {
  let mut count = 0
  for triangle in triangles {
    match
      (
        positions.get(triangle.a),
        positions.get(triangle.b),
        positions.get(triangle.c),
      ) {
      (Some(a), Some(b), Some(c)) =>
        if topology_triangle_area(a, b, c) <= epsilon {
          count = count + 1
        }
      _ => count = count + 1
    }
  }
  count
}

///|
pub fn geometry_sampling_report(
  positions : Array[Vec3],
  triangles : Array[TopologyTriangle],
  ray : SpatialRay?,
) -> GeometrySamplingReport {
  let area = geometry_total_area(positions, triangles)
  let hits = match ray {
    Some(value) => geometry_raycast_mesh(value, positions, triangles, false)
    None => []
  }
  let count = triangles.length()
  {
    triangle_count: count,
    sampled_count: hits.length(),
    total_area: area,
    average_area: if count == 0 {
      0.0
    } else {
      area / Double::from_int(count)
    },
    degenerate_count: geometry_degenerate_count(positions, triangles, 0.0000001),
    hits,
  }
}

///|
pub fn geometry_hit_closest(hits : Array[GeometryRayHit]) -> GeometryRayHit? {
  match hits.get(0) {
    Some(hit) => Some(hit)
    None => None
  }
}

///|
pub fn geometry_hit_uv(
  hit : GeometryRayHit,
  uv_a : Vec3,
  uv_b : Vec3,
  uv_c : Vec3,
) -> Vec3 {
  geometry_interpolate_vec3(uv_a, uv_b, uv_c, hit.barycentric)
}

///|
pub fn geometry_report_lines(report : GeometrySamplingReport) -> Array[String] {
  [
    "triangles=\{report.triangle_count}",
    "sampled=\{report.sampled_count}",
    "total_area=\{report.total_area}",
    "average_area=\{report.average_area}",
    "degenerate=\{report.degenerate_count}",
  ]
}