///|
/// Accessor and buffer-view accounting. This module validates byte windows
/// before a loader attempts to decode vertex or animation data.

///|
pub(all) struct ComponentInfo {
  name : String
  size : Int
  signed : Bool
  floating : Bool
  normalized_range : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct AccessorLayout {
  accessor_index : Int
  component : ComponentInfo
  component_count : Int
  element_size : Int
  stride : Int
  first_byte : Int
  last_byte_exclusive : Int
  valid : Bool
  problems : Array[String]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct LayoutIssue {
  accessor : Int
  message : String
} derive(Debug, ToJson, Eq)

///|
pub fn component_info(kind : ComponentType) -> ComponentInfo {
  match kind {
    Byte =>
      {
        name: "BYTE",
        size: 1,
        signed: true,
        floating: false,
        normalized_range: true,
      }
    UnsignedByte =>
      {
        name: "UNSIGNED_BYTE",
        size: 1,
        signed: false,
        floating: false,
        normalized_range: true,
      }
    Short =>
      {
        name: "SHORT",
        size: 2,
        signed: true,
        floating: false,
        normalized_range: true,
      }
    UnsignedShort =>
      {
        name: "UNSIGNED_SHORT",
        size: 2,
        signed: false,
        floating: false,
        normalized_range: true,
      }
    UnsignedInt =>
      {
        name: "UNSIGNED_INT",
        size: 4,
        signed: false,
        floating: false,
        normalized_range: false,
      }
    Float =>
      {
        name: "FLOAT",
        size: 4,
        signed: true,
        floating: true,
        normalized_range: false,
      }
  }
}

///|
pub fn accessor_component_count(kind : AccessorType) -> Int {
  match kind {
    Scalar => 1
    Vec2 => 2
    Vec3 => 3
    Vec4 => 4
    Mat2 => 4
    Mat3 => 9
    Mat4 => 16
  }
}

///|
pub fn accessor_alignment(kind : ComponentType) -> Int {
  let size = component_info(kind).size
  if size > 4 {
    4
  } else {
    size
  }
}

///|
pub fn accessor_element_size(accessor : GltfAccessor) -> Int {
  component_info(accessor.component_type).size *
  accessor_component_count(accessor.accessor_type)
}

///|
pub fn accessor_min_value(accessor : GltfAccessor, index : Int) -> Double? {
  accessor.min_values.get(index)
}

///|
pub fn accessor_max_value(accessor : GltfAccessor, index : Int) -> Double? {
  accessor.max_values.get(index)
}

///|
pub fn accessor_is_index_compatible(accessor : GltfAccessor) -> Bool {
  match accessor.accessor_type {
    Scalar =>
      match accessor.component_type {
        UnsignedByte | UnsignedShort | UnsignedInt => true
        _ => false
      }
    _ => false
  }
}

///|
pub fn accessor_layout(doc : GltfDocument, index : Int) -> AccessorLayout {
  if index < 0 || index >= doc.accessors.length() {
    {
      accessor_index: index,
      component: component_info(Float),
      component_count: 0,
      element_size: 0,
      stride: 0,
      first_byte: 0,
      last_byte_exclusive: 0,
      valid: false,
      problems: ["accessor index is outside the document"],
    }
  } else {
    let accessor = doc.accessors[index]
    let component = component_info(accessor.component_type)
    let component_count = accessor_component_count(accessor.accessor_type)
    let element_size = component.size * component_count
    let mut stride = element_size
    let mut first_byte = accessor.byte_offset
    let mut last_byte_exclusive = first_byte
    let problems : Array[String] = []
    match accessor.buffer_view {
      Some(view_index) =>
        if view_index < 0 || view_index >= doc.buffer_views.length() {
          problems.push("bufferView index is outside the document")
        } else {
          let view = doc.buffer_views[view_index]
          match view.byte_stride {
            Some(value) => stride = value
            None => ()
          }
          first_byte = view.byte_offset + accessor.byte_offset
          if stride < element_size {
            problems.push("byteStride is smaller than one accessor element")
          }
          if stride % accessor_alignment(accessor.component_type) != 0 {
            problems.push("byteStride is not aligned to the component size")
          }
          if accessor.count < 0 {
            problems.push("accessor count cannot be negative")
          }
          if accessor.count > 0 {
            last_byte_exclusive = first_byte +
              stride * (accessor.count - 1) +
              element_size
          } else {
            last_byte_exclusive = first_byte
          }
          if accessor.byte_offset < 0 {
            problems.push("accessor byteOffset cannot be negative")
          }
          if accessor.byte_offset +
            (if accessor.count > 0 {
              stride * (accessor.count - 1) + element_size
            } else {
              0
            }) >
            view.byte_length {
            problems.push("accessor data exceeds its bufferView")
          }
          if view.buffer < 0 || view.buffer >= doc.buffers.length() {
            problems.push("bufferView buffer index is outside the document")
          } else if view.byte_offset + view.byte_length >
            doc.buffers[view.buffer].byte_length {
            problems.push("bufferView exceeds its buffer byteLength")
          }
        }
      None =>
        if accessor.count != 0 {
          problems.push(
            "sparse-only accessors are not supported by this data model",
          )
        }
    }
    {
      accessor_index: index,
      component,
      component_count,
      element_size,
      stride,
      first_byte,
      last_byte_exclusive,
      valid: problems.length() == 0,
      problems,
    }
  }
}

///|
pub fn validate_accessor_layouts(doc : GltfDocument) -> Array[LayoutIssue] {
  let issues : Array[LayoutIssue] = []
  for index in 0.. Int {
  view.byte_offset + view.byte_length
}

///|
pub fn buffer_view_is_aligned(view : GltfBufferView) -> Bool {
  view.byte_offset >= 0 && view.byte_length >= 0 && view.byte_offset % 4 == 0
}

///|
pub fn buffer_total_declared_bytes(doc : GltfDocument) -> Int {
  doc.buffers.fold(init=0, (sum, buffer) => sum + buffer.byte_length)
}

///|
pub fn buffer_total_view_bytes(doc : GltfDocument) -> Int {
  doc.buffer_views.fold(init=0, (sum, view) => sum + view.byte_length)
}

///|
pub fn count_interleaved_views(doc : GltfDocument) -> Int {
  doc.buffer_views.fold(init=0, (count, view) => {
    match view.byte_stride {
      Some(_) => count + 1
      None => count
    }
  })
}

///|
pub fn count_sparse_like_accessors(doc : GltfDocument) -> Int {
  doc.accessors.fold(init=0, (count, accessor) => {
    match accessor.buffer_view {
      Some(_) => count
      None => count + 1
    }
  })
}

///|
pub fn primitive_vertex_count(
  doc : GltfDocument,
  primitive : GltfPrimitive,
) -> Int {
  match primitive.attributes.get("POSITION") {
    Some(accessor_index) =>
      match doc.accessors.get(accessor_index) {
        Some(accessor) => accessor.count
        None => 0
      }
    None => 0
  }
}

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

///|
pub fn primitive_is_indexed(primitive : GltfPrimitive) -> Bool {
  primitive.indices is Some(_)
}

///|
pub fn primitive_topology_name(mode : PrimitiveMode) -> String {
  match mode {
    Points => "POINTS"
    Lines => "LINES"
    LineLoop => "LINE_LOOP"
    LineStrip => "LINE_STRIP"
    Triangles => "TRIANGLES"
    TriangleStrip => "TRIANGLE_STRIP"
    TriangleFan => "TRIANGLE_FAN"
  }
}

///|
pub fn primitive_estimated_triangle_count(
  doc : GltfDocument,
  primitive : GltfPrimitive,
) -> Int {
  let count = if primitive_is_indexed(primitive) {
    primitive_index_count(doc, primitive)
  } else {
    primitive_vertex_count(doc, primitive)
  }
  match primitive.mode {
    Triangles => count / 3
    TriangleStrip | TriangleFan => if count >= 3 { count - 2 } else { 0 }
    _ => 0
  }
}

///|
pub fn mesh_estimated_triangle_count(
  doc : GltfDocument,
  mesh : GltfMesh,
) -> Int {
  mesh.primitives.fold(init=0, (sum, primitive) => {
    sum + primitive_estimated_triangle_count(doc, primitive)
  })
}

///|
pub fn document_estimated_triangle_count(doc : GltfDocument) -> Int {
  doc.meshes.fold(init=0, (sum, mesh) => {
    sum + mesh_estimated_triangle_count(doc, mesh)
  })
}

///|
pub fn document_indexed_primitive_ratio(doc : GltfDocument) -> Double {
  let mut total = 0
  let mut indexed = 0
  for mesh in doc.meshes {
    for primitive in mesh.primitives {
      total += 1
      if primitive_is_indexed(primitive) {
        indexed += 1
      }
    }
  }
  if total == 0 {
    0.0
  } else {
    indexed.to_double() / total.to_double()
  }
}