///|
/// Production geometry inspection built on top of the checked binary reader.
///
/// These routines are deliberately renderer-independent. Importers and asset
/// build jobs can use the same triangle accounting, bounds, channel coverage,
/// and cache estimates before a mesh reaches a runtime-specific upload path.

///|
pub(all) struct TriangleIndex {
  a : Int
  b : Int
  c : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) enum PrimitiveWinding {
  Unknown
  Clockwise
  CounterClockwise
  Degenerate
} derive(Debug, ToJson, Eq)

///|
pub(all) struct GeometryChannelSummary {
  position : Bool
  normal : Bool
  tangent : Bool
  texcoord_0 : Bool
  texcoord_1 : Bool
  color_0 : Bool
  joints_0 : Bool
  weights_0 : Bool
  custom_count : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct TriangleMetrics {
  triangle_count : Int
  degenerate_count : Int
  area : Double
  min_area : Double
  max_area : Double
  winding : PrimitiveWinding
  valid_index_count : Int
  invalid_index_count : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct PrimitiveGeometryReport {
  mesh_index : Int
  primitive_index : Int
  topology : String
  vertex_count : Int
  index_count : Int
  triangle_count : Int
  bounds : Bounds3
  channels : GeometryChannelSummary
  triangles : TriangleMetrics
  attribute_bytes : Int
  indexed : Bool
  cache_hit_ratio : Double
  overdraw_proxy : Double
} derive(Debug, ToJson, Eq)

///|
pub(all) struct MeshGeometryReport {
  mesh_index : Int
  primitive_count : Int
  vertex_count : Int
  index_count : Int
  triangle_count : Int
  degenerate_triangle_count : Int
  bounds : Bounds3
  attribute_bytes : Int
  cache_hit_ratio : Double
  primitives : Array[PrimitiveGeometryReport]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct DocumentGeometryReport {
  mesh_count : Int
  primitive_count : Int
  vertex_count : Int
  index_count : Int
  triangle_count : Int
  degenerate_triangle_count : Int
  bounds : Bounds3
  attribute_bytes : Int
  indexed_ratio : Double
  cache_hit_ratio : Double
  meshes : Array[MeshGeometryReport]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct GeometryBudget {
  max_vertices : Int
  max_indices : Int
  max_triangles : Int
  max_draw_calls : Int
  max_attribute_bytes : Int
  min_cache_hit_ratio : Double
  reject_degenerate_triangles : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct GeometryBudgetResult {
  passed : Bool
  violations : Array[String]
  vertex_overage : Int
  index_overage : Int
  triangle_overage : Int
  draw_call_overage : Int
  attribute_byte_overage : Int
  cache_ratio_shortfall : Double
} derive(Debug, ToJson, Eq)

///|
pub fn default_geometry_budget() -> GeometryBudget {
  {
    max_vertices: 10_000_000,
    max_indices: 30_000_000,
    max_triangles: 10_000_000,
    max_draw_calls: 20_000,
    max_attribute_bytes: 512 * 1024 * 1024,
    min_cache_hit_ratio: 0.35,
    reject_degenerate_triangles: false,
  }
}

///|
pub fn mobile_geometry_budget() -> GeometryBudget {
  {
    max_vertices: 250_000,
    max_indices: 750_000,
    max_triangles: 250_000,
    max_draw_calls: 1_000,
    max_attribute_bytes: 64 * 1024 * 1024,
    min_cache_hit_ratio: 0.55,
    reject_degenerate_triangles: true,
  }
}

///|
pub fn primitive_attribute_accessor(
  primitive : GltfPrimitive,
  name : String,
) -> Int? {
  primitive.attributes.get(name)
}

///|
pub fn primitive_has_attribute(
  primitive : GltfPrimitive,
  name : String,
) -> Bool {
  primitive.attributes.get(name) is Some(_)
}

///|
pub fn primitive_channel_summary(
  primitive : GltfPrimitive,
) -> GeometryChannelSummary {
  let known : Map[String, Bool] = Map([])
  let position = primitive_has_attribute(primitive, "POSITION")
  let normal = primitive_has_attribute(primitive, "NORMAL")
  let tangent = primitive_has_attribute(primitive, "TANGENT")
  let texcoord_0 = primitive_has_attribute(primitive, "TEXCOORD_0")
  let texcoord_1 = primitive_has_attribute(primitive, "TEXCOORD_1")
  let color_0 = primitive_has_attribute(primitive, "COLOR_0")
  let joints_0 = primitive_has_attribute(primitive, "JOINTS_0")
  let weights_0 = primitive_has_attribute(primitive, "WEIGHTS_0")
  known["POSITION"] = true
  known["NORMAL"] = true
  known["TANGENT"] = true
  known["TEXCOORD_0"] = true
  known["TEXCOORD_1"] = true
  known["COLOR_0"] = true
  known["JOINTS_0"] = true
  known["WEIGHTS_0"] = true
  let mut custom_count = 0
  for name, _ in primitive.attributes {
    if !known.contains(name) {
      custom_count += 1
    }
  }
  {
    position,
    normal,
    tangent,
    texcoord_0,
    texcoord_1,
    color_0,
    joints_0,
    weights_0,
    custom_count,
  }
}

///|
pub fn primitive_positions(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
) -> Array[Vec3] {
  match primitive_attribute_accessor(primitive, "POSITION") {
    Some(accessor) => decode_position_values(doc, asset, accessor)
    None => []
  }
}

///|
pub fn primitive_normals(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
) -> Array[Vec3] {
  match primitive_attribute_accessor(primitive, "NORMAL") {
    Some(accessor) => decode_normal_values(doc, asset, accessor)
    None => []
  }
}

///|
pub fn primitive_tangents(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
) -> Array[Vec4] {
  match primitive_attribute_accessor(primitive, "TANGENT") {
    Some(accessor) => decode_accessor_vectors(doc, asset, accessor).values
    None => []
  }
}

///|
pub fn primitive_texcoords(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
  set : Int,
) -> Array[Vec2] {
  let name = "TEXCOORD_\{set}"
  match primitive_attribute_accessor(primitive, name) {
    Some(accessor) => decode_texcoord_values(doc, asset, accessor)
    None => []
  }
}

///|
pub fn primitive_colors(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
) -> Array[Vec4] {
  match primitive_attribute_accessor(primitive, "COLOR_0") {
    Some(accessor) => decode_accessor_vectors(doc, asset, accessor).values
    None => []
  }
}

///|
pub fn primitive_indices(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
) -> Array[Int] {
  match primitive.indices {
    Some(accessor) => decode_index_values(doc, asset, accessor)
    None => {
      let count = primitive_vertex_count(doc, primitive)
      let values : Array[Int] = []
      for index in 0.. Int {
  primitive_indices(doc, asset, primitive).length()
}

///|
pub fn triangle_index_is_valid(
  triangle : TriangleIndex,
  vertex_count : Int,
) -> Bool {
  triangle.a >= 0 &&
  triangle.b >= 0 &&
  triangle.c >= 0 &&
  triangle.a < vertex_count &&
  triangle.b < vertex_count &&
  triangle.c < vertex_count
}

///|
pub fn triangle_index_has_duplicate_vertex(triangle : TriangleIndex) -> Bool {
  triangle.a == triangle.b ||
  triangle.b == triangle.c ||
  triangle.a == triangle.c
}

///|
fn triangle_array_from_indices(
  indices : Array[Int],
  mode : PrimitiveMode,
) -> Array[TriangleIndex] {
  let triangles : Array[TriangleIndex] = []
  match mode {
    Triangles => {
      let limit = indices.length() - indices.length() % 3
      for index in 0..<((if limit < 0 { 0 } else { limit }) / 3) {
        triangles.push({
          a: indices[index * 3],
          b: indices[index * 3 + 1],
          c: indices[index * 3 + 2],
        })
      }
    }
    TriangleStrip =>
      if indices.length() >= 3 {
        for index in 0..<(indices.length() - 2) {
          let a = indices[index]
          let b = indices[index + 1]
          let c = indices[index + 2]
          if index % 2 == 0 {
            triangles.push({ a, b, c })
          } else {
            triangles.push({ a: b, b: a, c })
          }
        }
      }
    TriangleFan =>
      if indices.length() >= 3 {
        let root = indices[0]
        for index in 1..<(indices.length() - 1) {
          triangles.push({ a: root, b: indices[index], c: indices[index + 1] })
        }
      }
    _ => ()
  }
  triangles
}

///|
pub fn primitive_triangles(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
) -> Array[TriangleIndex] {
  triangle_array_from_indices(
    primitive_indices(doc, asset, primitive),
    primitive.mode,
  )
}

///|
pub fn triangle_position(
  positions : Array[Vec3],
  triangle : TriangleIndex,
  vertex : Int,
) -> Vec3? {
  let index = match vertex {
    0 => triangle.a
    1 => triangle.b
    _ => triangle.c
  }
  positions.get(index)
}

///|
pub fn triangle_edge_ab(
  positions : Array[Vec3],
  triangle : TriangleIndex,
) -> Vec3? {
  match (positions.get(triangle.a), positions.get(triangle.b)) {
    (Some(a), Some(b)) => Some(vec3_sub(b, a))
    _ => None
  }
}

///|
pub fn triangle_edge_ac(
  positions : Array[Vec3],
  triangle : TriangleIndex,
) -> Vec3? {
  match (positions.get(triangle.a), positions.get(triangle.c)) {
    (Some(a), Some(c)) => Some(vec3_sub(c, a))
    _ => None
  }
}

///|
pub fn triangle_cross(
  positions : Array[Vec3],
  triangle : TriangleIndex,
) -> Vec3? {
  match
    (
      triangle_edge_ab(positions, triangle),
      triangle_edge_ac(positions, triangle),
    ) {
    (Some(ab), Some(ac)) => Some(vec3_cross(ab, ac))
    _ => None
  }
}

///|
pub fn triangle_area(
  positions : Array[Vec3],
  triangle : TriangleIndex,
) -> Double {
  match triangle_cross(positions, triangle) {
    Some(cross) => Double::sqrt(vec3_length_squared(cross)) * 0.5
    None => 0.0
  }
}

///|
pub fn triangle_normal(
  positions : Array[Vec3],
  triangle : TriangleIndex,
) -> Vec3 {
  match triangle_cross(positions, triangle) {
    None => { x: 0.0, y: 0.0, z: 0.0 }
    Some(cross) => {
      let length = Double::sqrt(vec3_length_squared(cross))
      if length <= 0.0 {
        { x: 0.0, y: 0.0, z: 0.0 }
      } else {
        vec3_scale(cross, 1.0 / length)
      }
    }
  }
}

///|
pub fn triangle_winding(
  positions : Array[Vec3],
  triangle : TriangleIndex,
) -> PrimitiveWinding {
  match triangle_cross(positions, triangle) {
    None => Degenerate
    Some(cross) => {
      let magnitude = vec3_length_squared(cross)
      if magnitude <= 0.0000000001 {
        Degenerate
      } else if cross.z < 0.0 {
        Clockwise
      } else {
        CounterClockwise
      }
    }
  }
}

///|
pub fn positions_bounds(positions : Array[Vec3]) -> Bounds3 {
  let mut bounds = bounds_empty()
  for position in positions {
    bounds = bounds_include(bounds, position)
  }
  bounds
}

///|
pub fn bounds_contains_point(bounds : Bounds3, point : Vec3) -> Bool {
  bounds.valid &&
  point.x >= bounds.min.x &&
  point.x <= bounds.max.x &&
  point.y >= bounds.min.y &&
  point.y <= bounds.max.y &&
  point.z >= bounds.min.z &&
  point.z <= bounds.max.z
}

///|
pub fn bounds_contains_bounds(outer : Bounds3, inner : Bounds3) -> Bool {
  if !inner.valid {
    true
  } else {
    bounds_contains_point(outer, inner.min) &&
    bounds_contains_point(outer, inner.max)
  }
}

///|
pub fn bounds_volume(bounds : Bounds3) -> Double {
  if !bounds.valid {
    0.0
  } else {
    let extent = bounds_extent(bounds)
    extent.x * extent.y * extent.z
  }
}

///|
pub fn bounds_diagonal_length(bounds : Bounds3) -> Double {
  if !bounds.valid {
    0.0
  } else {
    Double::sqrt(vec3_length_squared(bounds_extent(bounds)))
  }
}

///|
pub fn bounds_distance_to_point(bounds : Bounds3, point : Vec3) -> Double {
  if !bounds.valid {
    0.0
  } else {
    let dx = if point.x < bounds.min.x {
      bounds.min.x - point.x
    } else if point.x > bounds.max.x {
      point.x - bounds.max.x
    } else {
      0.0
    }
    let dy = if point.y < bounds.min.y {
      bounds.min.y - point.y
    } else if point.y > bounds.max.y {
      point.y - bounds.max.y
    } else {
      0.0
    }
    let dz = if point.z < bounds.min.z {
      bounds.min.z - point.z
    } else if point.z > bounds.max.z {
      point.z - bounds.max.z
    } else {
      0.0
    }
    Double::sqrt(dx * dx + dy * dy + dz * dz)
  }
}

///|
fn metrics_for_triangles(
  positions : Array[Vec3],
  triangles : Array[TriangleIndex],
) -> TriangleMetrics {
  let mut area = 0.0
  let mut min_area = 0.0
  let mut max_area = 0.0
  let mut degenerate = 0
  let mut valid_indices = 0
  let mut invalid_indices = 0
  let mut first_winding : PrimitiveWinding = Unknown
  for triangle in triangles {
    if triangle_index_is_valid(triangle, positions.length()) {
      valid_indices += 3
      let value = triangle_area(positions, triangle)
      area += value
      if value <= 0.0000000001 {
        degenerate += 1
      }
      if min_area == 0.0 || value < min_area {
        min_area = value
      }
      if value > max_area {
        max_area = value
      }
      let winding = triangle_winding(positions, triangle)
      if first_winding == Unknown {
        first_winding = winding
      } else if first_winding != winding && winding != Degenerate {
        first_winding = Unknown
      }
    } else {
      invalid_indices += 3
    }
  }
  {
    triangle_count: triangles.length(),
    degenerate_count: degenerate,
    area,
    min_area,
    max_area,
    winding: if triangles.length() == 0 {
      Unknown
    } else {
      first_winding
    },
    valid_index_count: valid_indices,
    invalid_index_count: invalid_indices,
  }
}

///|
pub fn primitive_triangle_metrics(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
) -> TriangleMetrics {
  let positions = primitive_positions(doc, asset, primitive)
  let triangles = primitive_triangles(doc, asset, primitive)
  metrics_for_triangles(positions, triangles)
}

///|
pub fn triangle_count_for_mode(
  mode : PrimitiveMode,
  element_count : Int,
) -> Int {
  if element_count < 0 {
    0
  } else {
    match mode {
      Triangles => element_count / 3
      TriangleStrip | TriangleFan =>
        if element_count >= 3 {
          element_count - 2
        } else {
          0
        }
      _ => 0
    }
  }
}

///|
pub fn line_count_for_mode(mode : PrimitiveMode, element_count : Int) -> Int {
  if element_count < 0 {
    0
  } else {
    match mode {
      Lines => element_count / 2
      LineLoop => element_count
      LineStrip => if element_count >= 2 { element_count - 1 } else { 0 }
      _ => 0
    }
  }
}

///|
pub fn point_count_for_mode(mode : PrimitiveMode, element_count : Int) -> Int {
  match mode {
    Points => if element_count < 0 { 0 } else { element_count }
    _ => 0
  }
}

///|
pub fn primitive_attribute_bytes(
  doc : GltfDocument,
  primitive : GltfPrimitive,
) -> Int {
  let mut total = 0
  for _, accessor_index in primitive.attributes {
    match doc.accessors.get(accessor_index) {
      Some(accessor) =>
        total += accessor_element_size(accessor) * accessor.count
      None => ()
    }
  }
  total
}

///|
pub fn primitive_index_bytes(
  doc : GltfDocument,
  primitive : GltfPrimitive,
) -> Int {
  match primitive.indices {
    Some(accessor_index) =>
      match doc.accessors.get(accessor_index) {
        Some(accessor) => accessor_element_size(accessor) * accessor.count
        None => 0
      }
    None => 0
  }
}

///|
pub fn primitive_total_bytes(
  doc : GltfDocument,
  primitive : GltfPrimitive,
) -> Int {
  primitive_attribute_bytes(doc, primitive) +
  primitive_index_bytes(doc, primitive)
}

///|
pub fn primitive_vertex_reuse_ratio(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
) -> Double {
  let indices = primitive_indices(doc, asset, primitive)
  if indices.length() == 0 {
    0.0
  } else {
    let unique : Map[Int, Bool] = Map([])
    for index in indices {
      unique[index] = true
    }
    1.0 - unique.length().to_double() / indices.length().to_double()
  }
}

///|
pub fn simulate_vertex_cache(indices : Array[Int], cache_size : Int) -> Double {
  if indices.length() == 0 || cache_size <= 0 {
    0.0
  } else {
    let cache : Array[Int] = []
    let mut hits = 0
    for index in indices {
      let mut found = false
      for old in cache {
        if old == index {
          found = true
        }
      }
      if found {
        hits += 1
      } else {
        cache.push(index)
        if cache.length() > cache_size {
          ignore(cache.remove(0))
        }
      }
    }
    hits.to_double() / indices.length().to_double()
  }
}

///|
pub fn primitive_cache_hit_ratio(
  doc : GltfDocument,
  asset : BinaryAsset,
  primitive : GltfPrimitive,
  cache_size? : Int = 16,
) -> Double {
  simulate_vertex_cache(primitive_indices(doc, asset, primitive), cache_size)
}

///|
pub fn primitive_overdraw_proxy(
  positions : Array[Vec3],
  triangles : Array[TriangleIndex],
) -> Double {
  if triangles.length() == 0 || positions.length() == 0 {
    0.0
  } else {
    let bounds = positions_bounds(positions)
    let surface = bounds_surface_area(bounds)
    let average_area = triangles.fold(init=0.0, (sum, triangle) => {
        sum + triangle_area(positions, triangle)
      }) /
      triangles.length().to_double()
    if surface <= 0.0 {
      0.0
    } else {
      average_area / surface
    }
  }
}

///|
pub fn primitive_geometry_report(
  doc : GltfDocument,
  asset : BinaryAsset,
  mesh_index : Int,
  primitive_index : Int,
  primitive : GltfPrimitive,
) -> PrimitiveGeometryReport {
  let positions = primitive_positions(doc, asset, primitive)
  let indices = primitive_indices(doc, asset, primitive)
  let triangles = triangle_array_from_indices(indices, primitive.mode)
  let triangle_metrics = metrics_for_triangles(positions, triangles)
  {
    mesh_index,
    primitive_index,
    topology: primitive_topology_name(primitive.mode),
    vertex_count: positions.length(),
    index_count: indices.length(),
    triangle_count: triangles.length(),
    bounds: positions_bounds(positions),
    channels: primitive_channel_summary(primitive),
    triangles: triangle_metrics,
    attribute_bytes: primitive_total_bytes(doc, primitive),
    indexed: primitive.indices is Some(_),
    cache_hit_ratio: simulate_vertex_cache(indices, 16),
    overdraw_proxy: primitive_overdraw_proxy(positions, triangles),
  }
}

///|
pub fn mesh_geometry_report(
  doc : GltfDocument,
  asset : BinaryAsset,
  mesh_index : Int,
) -> MeshGeometryReport {
  match doc.meshes.get(mesh_index) {
    None =>
      {
        mesh_index,
        primitive_count: 0,
        vertex_count: 0,
        index_count: 0,
        triangle_count: 0,
        degenerate_triangle_count: 0,
        bounds: bounds_empty(),
        attribute_bytes: 0,
        cache_hit_ratio: 0.0,
        primitives: [],
      }
    Some(mesh) => {
      let reports : Array[PrimitiveGeometryReport] = []
      let mut vertices = 0
      let mut indices = 0
      let mut triangles = 0
      let mut degenerates = 0
      let mut bytes = 0
      let mut cache_weight = 0.0
      let mut bounds = bounds_empty()
      for index, primitive in mesh.primitives {
        let report = primitive_geometry_report(
          doc, asset, mesh_index, index, primitive,
        )
        reports.push(report)
        vertices += report.vertex_count
        indices += report.index_count
        triangles += report.triangle_count
        degenerates += report.triangles.degenerate_count
        bytes += report.attribute_bytes
        cache_weight += report.cache_hit_ratio * report.index_count.to_double()
        bounds = bounds_union(bounds, report.bounds)
      }
      let cache_ratio = if indices == 0 {
        0.0
      } else {
        cache_weight / indices.to_double()
      }
      {
        mesh_index,
        primitive_count: mesh.primitives.length(),
        vertex_count: vertices,
        index_count: indices,
        triangle_count: triangles,
        degenerate_triangle_count: degenerates,
        bounds,
        attribute_bytes: bytes,
        cache_hit_ratio: cache_ratio,
        primitives: reports,
      }
    }
  }
}

///|
pub fn all_mesh_geometry_reports(
  doc : GltfDocument,
  asset : BinaryAsset,
) -> Array[MeshGeometryReport] {
  let reports : Array[MeshGeometryReport] = []
  for index in 0.. DocumentGeometryReport {
  let meshes = all_mesh_geometry_reports(doc, asset)
  let mut vertices = 0
  let mut indices = 0
  let mut triangles = 0
  let mut degenerates = 0
  let mut bytes = 0
  let mut cache_weight = 0.0
  let mut primitive_count = 0
  let mut indexed = 0
  let mut bounds = bounds_empty()
  for mesh in meshes {
    vertices += mesh.vertex_count
    indices += mesh.index_count
    triangles += mesh.triangle_count
    degenerates += mesh.degenerate_triangle_count
    bytes += mesh.attribute_bytes
    primitive_count += mesh.primitive_count
    cache_weight += mesh.cache_hit_ratio * mesh.index_count.to_double()
    bounds = bounds_union(bounds, mesh.bounds)
  }
  for mesh in doc.meshes {
    for primitive in mesh.primitives {
      if primitive.indices is Some(_) {
        indexed += 1
      }
    }
  }
  {
    mesh_count: doc.meshes.length(),
    primitive_count,
    vertex_count: vertices,
    index_count: indices,
    triangle_count: triangles,
    degenerate_triangle_count: degenerates,
    bounds,
    attribute_bytes: bytes,
    indexed_ratio: if primitive_count == 0 {
      0.0
    } else {
      indexed.to_double() / primitive_count.to_double()
    },
    cache_hit_ratio: if indices == 0 {
      0.0
    } else {
      cache_weight / indices.to_double()
    },
    meshes,
  }
}

///|
pub fn geometry_budget_result(
  report : DocumentGeometryReport,
  budget : GeometryBudget,
) -> GeometryBudgetResult {
  let vertex_overage = if report.vertex_count > budget.max_vertices {
    report.vertex_count - budget.max_vertices
  } else {
    0
  }
  let index_overage = if report.index_count > budget.max_indices {
    report.index_count - budget.max_indices
  } else {
    0
  }
  let triangle_overage = if report.triangle_count > budget.max_triangles {
    report.triangle_count - budget.max_triangles
  } else {
    0
  }
  let draw_calls = report.primitive_count
  let draw_call_overage = if draw_calls > budget.max_draw_calls {
    draw_calls - budget.max_draw_calls
  } else {
    0
  }
  let attribute_byte_overage = if report.attribute_bytes >
    budget.max_attribute_bytes {
    report.attribute_bytes - budget.max_attribute_bytes
  } else {
    0
  }
  let cache_ratio_shortfall = if report.cache_hit_ratio <
    budget.min_cache_hit_ratio {
    budget.min_cache_hit_ratio - report.cache_hit_ratio
  } else {
    0.0
  }
  let violations : Array[String] = []
  if vertex_overage > 0 {
    violations.push("vertex budget exceeded by \{vertex_overage}")
  }
  if index_overage > 0 {
    violations.push("index budget exceeded by \{index_overage}")
  }
  if triangle_overage > 0 {
    violations.push("triangle budget exceeded by \{triangle_overage}")
  }
  if draw_call_overage > 0 {
    violations.push("draw-call budget exceeded by \{draw_call_overage}")
  }
  if attribute_byte_overage > 0 {
    violations.push(
      "attribute memory budget exceeded by \{attribute_byte_overage} bytes",
    )
  }
  if cache_ratio_shortfall > 0.0 {
    violations.push("vertex cache ratio is below the configured minimum")
  }
  if budget.reject_degenerate_triangles && report.degenerate_triangle_count > 0 {
    violations.push(
      "geometry contains \{report.degenerate_triangle_count} degenerate triangles",
    )
  }
  {
    passed: violations.length() == 0,
    violations,
    vertex_overage,
    index_overage,
    triangle_overage,
    draw_call_overage,
    attribute_byte_overage,
    cache_ratio_shortfall,
  }
}

///|
pub fn primitive_missing_channel_count(summary : GeometryChannelSummary) -> Int {
  let mut count = 0
  if !summary.position {
    count += 1
  }
  if !summary.normal {
    count += 1
  }
  if !summary.texcoord_0 {
    count += 1
  }
  if !summary.weights_0 && summary.joints_0 {
    count += 1
  }
  if summary.weights_0 && !summary.joints_0 {
    count += 1
  }
  count
}

///|
pub fn primitive_channel_is_complete(
  summary : GeometryChannelSummary,
  skinned : Bool,
) -> Bool {
  summary.position && (!skinned || (summary.joints_0 && summary.weights_0))
}

///|
pub fn primitive_has_custom_channels(summary : GeometryChannelSummary) -> Bool {
  summary.custom_count > 0
}

///|
pub fn mesh_bounds_with_transforms(
  doc : GltfDocument,
  asset : BinaryAsset,
  mesh_index : Int,
) -> Bounds3 {
  let report = mesh_geometry_report(doc, asset, mesh_index)
  report.bounds
}

///|
pub fn geometry_report_summary_lines(
  report : DocumentGeometryReport,
) -> Array[String] {
  [
    "meshes=\{report.mesh_count}, primitives=\{report.primitive_count}",
    "vertices=\{report.vertex_count}, indices=\{report.index_count}",
    "triangles=\{report.triangle_count}, degenerates=\{report.degenerate_triangle_count}",
    "attribute_bytes=\{report.attribute_bytes}",
    "indexed_ratio=\{report.indexed_ratio}, cache_hit_ratio=\{report.cache_hit_ratio}",
  ]
}

///|
pub fn geometry_report_is_empty(report : DocumentGeometryReport) -> Bool {
  report.mesh_count == 0 || report.primitive_count == 0
}

///|
pub fn geometry_report_has_invalid_indices(
  report : DocumentGeometryReport,
) -> Bool {
  for mesh in report.meshes {
    for primitive in mesh.primitives {
      if primitive.triangles.invalid_index_count > 0 {
        return true
      }
    }
  }
  false
}

///|
pub fn geometry_report_has_degenerate_triangles(
  report : DocumentGeometryReport,
) -> Bool {
  report.degenerate_triangle_count > 0
}

///|
pub fn geometry_report_average_triangles_per_mesh(
  report : DocumentGeometryReport,
) -> Double {
  if report.mesh_count == 0 {
    0.0
  } else {
    report.triangle_count.to_double() / report.mesh_count.to_double()
  }
}

///|
pub fn geometry_report_average_vertices_per_triangle(
  report : DocumentGeometryReport,
) -> Double {
  if report.triangle_count == 0 {
    0.0
  } else {
    report.vertex_count.to_double() / report.triangle_count.to_double()
  }
}

///|
pub fn geometry_report_largest_mesh(report : DocumentGeometryReport) -> Int? {
  let mut best : Int? = None
  let mut best_triangles = -1
  for mesh in report.meshes {
    if mesh.triangle_count > best_triangles {
      best_triangles = mesh.triangle_count
      best = Some(mesh.mesh_index)
    }
  }
  best
}

///|
pub fn geometry_report_largest_bounds(
  report : DocumentGeometryReport,
) -> Bounds3 {
  match geometry_report_largest_mesh(report) {
    None => bounds_empty()
    Some(index) =>
      match report.meshes.get(index) {
        Some(mesh) => mesh.bounds
        None => bounds_empty()
      }
  }
}