///|
/// Quality gates for automated asset pipelines. The functions are pure and
/// deterministic, making them suitable for pre-commit hooks and CI policies.

///|
pub(all) struct QualityThresholds {
  max_draw_calls : Int
  max_vertices : Int
  max_triangles : Int
  min_indexed_ratio : Double
  max_external_uris : Int
  max_depth : Int
  max_errors : Int
  max_warnings : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct QualityViolation {
  rule : String
  actual : String
  expected : String
  severity : DiagnosticSeverity
} derive(Debug, ToJson, Eq)

///|
pub(all) struct QualityGate {
  passed : Bool
  violations : Array[QualityViolation]
  score : Int
} derive(Debug, ToJson, Eq)

///|
pub fn default_quality_thresholds() -> QualityThresholds {
  {
    max_draw_calls: 2048,
    max_vertices: 5_000_000,
    max_triangles: 10_000_000,
    min_indexed_ratio: 0.8,
    max_external_uris: 512,
    max_depth: 128,
    max_errors: 0,
    max_warnings: 64,
  }
}

///|
pub fn mobile_quality_thresholds() -> QualityThresholds {
  {
    max_draw_calls: 256,
    max_vertices: 1_000_000,
    max_triangles: 2_000_000,
    min_indexed_ratio: 0.95,
    max_external_uris: 64,
    max_depth: 64,
    max_errors: 0,
    max_warnings: 16,
  }
}

///|
fn violation(
  violations : Array[QualityViolation],
  rule : String,
  actual : String,
  expected : String,
  severity : DiagnosticSeverity,
) -> Unit {
  violations.push({ rule, actual, expected, severity })
}

///|
fn score_penalty(severity : DiagnosticSeverity) -> Int {
  match severity {
    Info => 1
    Warning => 5
    Error => 20
  }
}

///|
pub fn quality_gate(
  report : AssetReport,
  thresholds : QualityThresholds,
) -> QualityGate {
  let violations : Array[QualityViolation] = []
  if report.geometry.draw_calls > thresholds.max_draw_calls {
    violation(
      violations,
      "draw_calls",
      "\{report.geometry.draw_calls}",
      "<= \{thresholds.max_draw_calls}",
      Warning,
    )
  }
  if report.geometry.vertices > thresholds.max_vertices {
    violation(
      violations,
      "vertices",
      "\{report.geometry.vertices}",
      "<= \{thresholds.max_vertices}",
      Warning,
    )
  }
  if report.geometry.triangles > thresholds.max_triangles {
    violation(
      violations,
      "triangles",
      "\{report.geometry.triangles}",
      "<= \{thresholds.max_triangles}",
      Warning,
    )
  }
  if report.geometry.indexed_ratio < thresholds.min_indexed_ratio {
    violation(
      violations,
      "indexed_ratio",
      "\{report.geometry.indexed_ratio}",
      ">= \{thresholds.min_indexed_ratio}",
      Warning,
    )
  }
  if report.resources.external_uri_count > thresholds.max_external_uris {
    violation(
      violations,
      "external_uris",
      "\{report.resources.external_uri_count}",
      "<= \{thresholds.max_external_uris}",
      Warning,
    )
  }
  if report.transforms.max_depth > thresholds.max_depth {
    violation(
      violations,
      "scene_depth",
      "\{report.transforms.max_depth}",
      "<= \{thresholds.max_depth}",
      Warning,
    )
  }
  if report.diagnostics.errors > thresholds.max_errors {
    violation(
      violations,
      "diagnostic_errors",
      "\{report.diagnostics.errors}",
      "<= \{thresholds.max_errors}",
      Error,
    )
  }
  if report.diagnostics.warnings > thresholds.max_warnings {
    violation(
      violations,
      "diagnostic_warnings",
      "\{report.diagnostics.warnings}",
      "<= \{thresholds.max_warnings}",
      Warning,
    )
  }
  let mut penalty = 0
  for item in violations {
    penalty += score_penalty(item.severity)
  }
  let score = if penalty >= 100 { 0 } else { 100 - penalty }
  let passed = for_all_quality_violations(violations, item => {
    item.severity is Info
  })
  { passed, violations, score }
}

///|
fn for_all_quality_violations(
  items : Array[QualityViolation],
  predicate : (QualityViolation) -> Bool,
) -> Bool {
  for item in items {
    if !predicate(item) {
      return false
    }
  }
  true
}

///|
pub fn quality_gate_default(report : AssetReport) -> QualityGate {
  quality_gate(report, default_quality_thresholds())
}

///|
pub fn quality_gate_mobile(report : AssetReport) -> QualityGate {
  quality_gate(report, mobile_quality_thresholds())
}

///|
pub fn quality_violation_lines(gate : QualityGate) -> Array[String] {
  gate.violations.map(item => {
    "\{item.rule}: actual=\{item.actual}, expected=\{item.expected}"
  })
}

///|
pub fn material_texture_count(material : GltfMaterial) -> Int {
  let mut count = 0
  match material.pbr_metallic_roughness.base_color_texture {
    Some(_) => count += 1
    None => ()
  }
  match material.pbr_metallic_roughness.metallic_roughness_texture {
    Some(_) => count += 1
    None => ()
  }
  match material.normal_texture {
    Some(_) => count += 1
    None => ()
  }
  match material.occlusion_texture {
    Some(_) => count += 1
    None => ()
  }
  match material.emissive_texture {
    Some(_) => count += 1
    None => ()
  }
  count
}

///|
pub fn document_texture_slot_count(doc : GltfDocument) -> Int {
  doc.materials.fold(init=0, (sum, material) => {
    sum + material_texture_count(material)
  })
}

///|
pub fn document_materials_with_alpha(doc : GltfDocument) -> Int {
  doc.materials.fold(init=0, (sum, material) => {
    match material.alpha_mode {
      Opaque => sum
      Mask | Blend => sum + 1
    }
  })
}

///|
pub fn document_double_sided_materials(doc : GltfDocument) -> Int {
  doc.materials.fold(init=0, (sum, material) => {
    if material.double_sided {
      sum + 1
    } else {
      sum
    }
  })
}

///|
pub fn document_skinned_nodes(doc : GltfDocument) -> Int {
  doc.nodes.fold(init=0, (sum, node) => {
    match node.skin {
      Some(_) => sum + 1
      None => sum
    }
  })
}

///|
pub fn document_animation_channels(doc : GltfDocument) -> Int {
  doc.animations.fold(init=0, (sum, animation) => {
    sum + animation.channels.length()
  })
}

///|
pub fn document_animation_sampler_count(doc : GltfDocument) -> Int {
  doc.animations.fold(init=0, (sum, animation) => {
    sum + animation.samplers.length()
  })
}

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

///|
pub fn document_total_vertex_attributes(doc : GltfDocument) -> Int {
  doc.meshes.fold(init=0, (sum, mesh) => {
    sum +
    mesh.primitives.fold(init=0, (inner, primitive) => {
      inner + primitive.attributes.length()
    })
  })
}

///|
pub fn document_named_node_count(doc : GltfDocument) -> Int {
  doc.nodes.fold(init=0, (sum, node) => {
    match node.name {
      Some(_) => sum + 1
      None => sum
    }
  })
}

///|
pub fn document_named_mesh_count(doc : GltfDocument) -> Int {
  doc.meshes.fold(init=0, (sum, mesh) => {
    match mesh.name {
      Some(_) => sum + 1
      None => sum
    }
  })
}

///|
pub fn document_uri_count(doc : GltfDocument) -> Int {
  let deps = collect_resource_deps(doc)
  deps.external_uris.length() +
  deps.embedded_buffer_count +
  deps.embedded_image_count
}

///|
pub fn material_alpha_cutoff_is_valid(material : GltfMaterial) -> Bool {
  material.alpha_cutoff >= 0.0 && material.alpha_cutoff <= 1.0
}

///|
pub fn material_pbr_is_valid(material : GltfMaterial) -> Bool {
  material.pbr_metallic_roughness.metallic_factor >= 0.0 &&
  material.pbr_metallic_roughness.metallic_factor <= 1.0 &&
  material.pbr_metallic_roughness.roughness_factor >= 0.0 &&
  material.pbr_metallic_roughness.roughness_factor <= 1.0
}

///|
pub fn color_is_valid(color : Color4) -> Bool {
  color.r >= 0.0 &&
  color.r <= 1.0 &&
  color.g >= 0.0 &&
  color.g <= 1.0 &&
  color.b >= 0.0 &&
  color.b <= 1.0 &&
  color.a >= 0.0 &&
  color.a <= 1.0
}

///|
pub fn document_material_parameter_errors(doc : GltfDocument) -> Int {
  doc.materials.fold(init=0, (count, material) => {
    let alpha_error = if material_alpha_cutoff_is_valid(material) {
      0
    } else {
      1
    }
    let pbr_error = if material_pbr_is_valid(material) { 0 } else { 1 }
    let color_error = if color_is_valid(
        material.pbr_metallic_roughness.base_color_factor,
      ) {
      0
    } else {
      1
    }
    count + alpha_error + pbr_error + color_error
  })
}