///|
/// Mesh-level diagnostics used by importers to estimate GPU and CPU costs.

///|
pub(all) struct PrimitiveReport {
  mesh : Int
  primitive : Int
  mode : String
  vertex_count : Int
  index_count : Int
  triangle_count : Int
  attribute_count : Int
  attribute_names : Array[String]
  indexed : Bool
  material : Int?
  has_position : Bool
  has_normal : Bool
  has_tangent : Bool
  has_texcoord0 : Bool
  has_color0 : Bool
  has_joints0 : Bool
  has_weights0 : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct MeshCostReport {
  mesh : Int
  name : String?
  primitives : Array[PrimitiveReport]
  vertex_count : Int
  triangle_count : Int
  draw_calls : Int
  attribute_slots : Int
  estimated_vertex_bytes : Int
} derive(Debug, ToJson, Eq)

///|
fn has_attribute(primitive : GltfPrimitive, name : String) -> Bool {
  primitive.attributes.contains(name)
}

///|
pub fn primitive_report(
  doc : GltfDocument,
  mesh : Int,
  primitive_index : Int,
) -> PrimitiveReport {
  let primitive = doc.meshes[mesh].primitives[primitive_index]
  {
    mesh,
    primitive: primitive_index,
    mode: primitive_topology_name(primitive.mode),
    vertex_count: primitive_vertex_count(doc, primitive),
    index_count: primitive_index_count(doc, primitive),
    triangle_count: primitive_estimated_triangle_count(doc, primitive),
    attribute_count: primitive.attributes.length(),
    attribute_names: primitive.attributes.keys().collect(),
    indexed: primitive_is_indexed(primitive),
    material: primitive.material,
    has_position: has_attribute(primitive, "POSITION"),
    has_normal: has_attribute(primitive, "NORMAL"),
    has_tangent: has_attribute(primitive, "TANGENT"),
    has_texcoord0: has_attribute(primitive, "TEXCOORD_0"),
    has_color0: has_attribute(primitive, "COLOR_0"),
    has_joints0: has_attribute(primitive, "JOINTS_0"),
    has_weights0: has_attribute(primitive, "WEIGHTS_0"),
  }
}

///|
fn estimate_attribute_bytes(name : String) -> Int {
  match name {
    "POSITION" => 12
    "NORMAL" => 12
    "TANGENT" => 16
    "TEXCOORD_0" | "TEXCOORD_1" => 8
    "COLOR_0" => 16
    "JOINTS_0" => 8
    "WEIGHTS_0" => 16
    _ => 16
  }
}

///|
fn estimate_vertex_bytes(primitive : PrimitiveReport) -> Int {
  primitive.attribute_names.fold(init=0, (sum, name) => {
    sum + estimate_attribute_bytes(name)
  }) *
  primitive.vertex_count
}

///|
pub fn mesh_cost_report(doc : GltfDocument, mesh_index : Int) -> MeshCostReport {
  let mesh = doc.meshes[mesh_index]
  let primitives = []
  for primitive_index in 0.. {
    sum + item.vertex_count
  })
  let triangle_count = primitives.fold(init=0, (sum, item) => {
    sum + item.triangle_count
  })
  let attribute_slots = primitives.fold(init=0, (sum, item) => {
    sum + item.attribute_count
  })
  let estimated_vertex_bytes = primitives.fold(init=0, (sum, item) => {
    sum + estimate_vertex_bytes(item)
  })
  {
    mesh: mesh_index,
    name: mesh.name,
    primitives,
    vertex_count,
    triangle_count,
    draw_calls: primitives.length(),
    attribute_slots,
    estimated_vertex_bytes,
  }
}

///|
pub fn all_mesh_cost_reports(doc : GltfDocument) -> Array[MeshCostReport] {
  let result = []
  for index in 0.. Int {
  all_mesh_cost_reports(doc).fold(init=0, (sum, report) => {
    sum + report.estimated_vertex_bytes
  })
}

///|
pub fn total_attribute_slots(doc : GltfDocument) -> Int {
  all_mesh_cost_reports(doc).fold(init=0, (sum, report) => {
    sum + report.attribute_slots
  })
}

///|
pub fn mesh_with_most_vertices(doc : GltfDocument) -> Int? {
  if doc.meshes.length() == 0 {
    return None
  }
  let mut selected = 0
  let mut largest = 0
  for index in 0.. largest {
      largest = count
      selected = index
    }
  }
  Some(selected)
}

///|
pub fn mesh_with_most_triangles(doc : GltfDocument) -> Int? {
  if doc.meshes.length() == 0 {
    return None
  }
  let mut selected = 0
  let mut largest = 0
  for index in 0.. largest {
      largest = count
      selected = index
    }
  }
  Some(selected)
}

///|
pub fn unindexed_mesh_count(doc : GltfDocument) -> Int {
  all_mesh_cost_reports(doc).fold(init=0, (count, report) => {
    if report.primitives.any(item => !item.indexed) {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn missing_normal_primitive_count(doc : GltfDocument) -> Int {
  all_mesh_cost_reports(doc).fold(init=0, (count, report) => {
    count +
    report.primitives.fold(init=0, (inner, item) => {
      if item.has_normal {
        inner
      } else {
        inner + 1
      }
    })
  })
}

///|
pub fn skinned_primitive_count(doc : GltfDocument) -> Int {
  all_mesh_cost_reports(doc).fold(init=0, (count, report) => {
    count +
    report.primitives.fold(init=0, (inner, item) => {
      if item.has_joints0 && item.has_weights0 {
        inner + 1
      } else {
        inner
      }
    })
  })
}

///|
pub fn primitive_uses_material(
  doc : GltfDocument,
  mesh : Int,
  primitive : Int,
  material : Int,
) -> Bool {
  match doc.meshes.get(mesh) {
    Some(value) =>
      match value.primitives.get(primitive) {
        Some(item) => item.material == Some(material)
        None => false
      }
    None => false
  }
}

///|
pub fn material_usage_counts(doc : GltfDocument) -> Array[Int] {
  let counts = Array::make(doc.materials.length(), 0)
  for mesh in doc.meshes {
    for primitive in mesh.primitives {
      match primitive.material {
        Some(index) =>
          if index >= 0 && index < counts.length() {
            counts[index] += 1
          }
        None => ()
      }
    }
  }
  counts
}

///|
pub fn mesh_uses_skin(doc : GltfDocument, mesh : Int) -> Bool {
  for node in doc.nodes {
    if node.mesh == Some(mesh) && node.skin is Some(_) {
      return true
    }
  }
  false
}

///|
pub fn mesh_instance_count(doc : GltfDocument, mesh : Int) -> Int {
  doc.nodes.fold(init=0, (count, node) => {
    if node.mesh == Some(mesh) {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn instanced_mesh_indices(doc : GltfDocument) -> Array[Int] {
  let result = []
  for index in 0.. 1 {
      result.push(index)
    }
  }
  result
}

///|
pub fn mesh_draw_call_budget(doc : GltfDocument, budget : Int) -> Array[Int] {
  let result = []
  for report in all_mesh_cost_reports(doc) {
    if report.draw_calls > budget {
      result.push(report.mesh)
    }
  }
  result
}

///|
pub fn primitive_attribute_signature(primitive : GltfPrimitive) -> String {
  let names : Array[String] = primitive.attributes.keys().collect()
  names.sort()
  names.fold(init="", (result, name) => {
    if result == "" {
      name
    } else {
      result + "," + name
    }
  })
}

///|
pub fn mesh_attribute_signatures(
  doc : GltfDocument,
  mesh : Int,
) -> Array[String] {
  doc.meshes[mesh].primitives.map(primitive_attribute_signature)
}