///|
/// Production-oriented diagnostics. Validation is intentionally represented as
/// data so CI, an editor integration, and the CLI can apply different policies.

///|
pub(all) enum DiagnosticSeverity {
  Info
  Warning
  Error
} derive(Debug, ToJson, Eq)

///|
pub(all) enum DiagnosticCode {
  EmptyDocument
  InvalidVersion
  MissingDefaultScene
  InvalidReference
  InvalidAccessorLayout
  UnindexedGeometry
  MissingPosition
  MissingMaterial
  UnusedResource
  DeepScene
  LargeBuffer
  ExternalDependency
  AnimationReference
  SkinReference
} derive(Debug, ToJson, Eq)

///|
pub(all) struct Diagnostic {
  severity : DiagnosticSeverity
  code : DiagnosticCode
  path : String
  message : String
  hint : String?
} derive(Debug, ToJson, Eq)

///|
pub(all) struct DiagnosticSummary {
  infos : Int
  warnings : Int
  errors : Int
  diagnostics : Array[Diagnostic]
  passed : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct DiagnosticPolicy {
  fail_on_warning : Bool
  max_scene_depth : Int
  max_buffer_bytes : Int
  require_default_scene : Bool
  require_position_attribute : Bool
} derive(Debug, ToJson, Eq)

///|
pub fn default_diagnostic_policy() -> DiagnosticPolicy {
  {
    fail_on_warning: false,
    max_scene_depth: 128,
    max_buffer_bytes: 256 * 1024 * 1024,
    require_default_scene: false,
    require_position_attribute: true,
  }
}

///|
pub fn strict_diagnostic_policy() -> DiagnosticPolicy {
  {
    fail_on_warning: true,
    max_scene_depth: 64,
    max_buffer_bytes: 64 * 1024 * 1024,
    require_default_scene: true,
    require_position_attribute: true,
  }
}

///|
fn severity_name(value : DiagnosticSeverity) -> String {
  match value {
    Info => "info"
    Warning => "warning"
    Error => "error"
  }
}

///|
fn code_name(value : DiagnosticCode) -> String {
  match value {
    EmptyDocument => "EMPTY_DOCUMENT"
    InvalidVersion => "INVALID_VERSION"
    MissingDefaultScene => "MISSING_DEFAULT_SCENE"
    InvalidReference => "INVALID_REFERENCE"
    InvalidAccessorLayout => "INVALID_ACCESSOR_LAYOUT"
    UnindexedGeometry => "UNINDEXED_GEOMETRY"
    MissingPosition => "MISSING_POSITION"
    MissingMaterial => "MISSING_MATERIAL"
    UnusedResource => "UNUSED_RESOURCE"
    DeepScene => "DEEP_SCENE"
    LargeBuffer => "LARGE_BUFFER"
    ExternalDependency => "EXTERNAL_DEPENDENCY"
    AnimationReference => "ANIMATION_REFERENCE"
    SkinReference => "SKIN_REFERENCE"
  }
}

///|
pub fn diagnostic_to_line(diagnostic : Diagnostic) -> String {
  let hint = match diagnostic.hint {
    Some(value) => " hint=\{value}"
    None => ""
  }
  "[\{severity_name(diagnostic.severity)}] \{code_name(diagnostic.code)} \{diagnostic.path}: \{diagnostic.message}\{hint}"
}

///|
pub fn diagnostics_to_lines(summary : DiagnosticSummary) -> Array[String] {
  summary.diagnostics.map(diagnostic_to_line)
}

///|
fn push_diagnostic(
  diagnostics : Array[Diagnostic],
  severity : DiagnosticSeverity,
  code : DiagnosticCode,
  path : String,
  message : String,
  hint : String?,
) -> Unit {
  diagnostics.push({ severity, code, path, message, hint })
}

///|
fn check_version(doc : GltfDocument, diagnostics : Array[Diagnostic]) -> Unit {
  if doc.version != "2.0" {
    push_diagnostic(
      diagnostics,
      Error,
      InvalidVersion,
      "asset.version",
      "only glTF version 2.0 is supported",
      Some("export the asset as glTF 2.0"),
    )
  }
}

///|
fn check_default_scene(
  doc : GltfDocument,
  policy : DiagnosticPolicy,
  diagnostics : Array[Diagnostic],
) -> Unit {
  if policy.require_default_scene && doc.default_scene is None {
    push_diagnostic(
      diagnostics,
      Warning,
      MissingDefaultScene,
      "scene",
      "asset has no default scene",
      Some("set the scene field in the exporter"),
    )
  }
  match doc.default_scene {
    Some(index) =>
      if index < 0 || index >= doc.scenes.length() {
        push_diagnostic(
          diagnostics,
          Error,
          InvalidReference,
          "scene",
          "default scene index is out of range",
          None,
        )
      }
    None => ()
  }
}

///|
fn check_scene_depth(
  doc : GltfDocument,
  policy : DiagnosticPolicy,
  diagnostics : Array[Diagnostic],
) -> Unit {
  let info = traverse_node_tree(doc)
  if info.max_depth > policy.max_scene_depth {
    push_diagnostic(
      diagnostics,
      Warning,
      DeepScene,
      "nodes",
      "scene depth exceeds the configured limit",
      Some("flatten unnecessary helper nodes"),
    )
  }
}

///|
fn check_buffers(
  doc : GltfDocument,
  policy : DiagnosticPolicy,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for index in 0.. policy.max_buffer_bytes {
      push_diagnostic(
        diagnostics,
        Warning,
        LargeBuffer,
        "buffers[\{index}]",
        "buffer is larger than the configured policy",
        Some("split or compress the asset"),
      )
    }
    match buffer.uri {
      Some(uri) =>
        if !uri.has_prefix("data:") {
          push_diagnostic(
            diagnostics,
            Info,
            ExternalDependency,
            "buffers[\{index}].uri",
            "asset depends on an external URI",
            Some("package the URI beside the asset"),
          )
        }
      None => ()
    }
  }
}

///|
fn check_buffer_views(
  doc : GltfDocument,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for index in 0..= doc.buffers.length() {
      push_diagnostic(
        diagnostics,
        Error,
        InvalidReference,
        "bufferViews[{index}].buffer",
        "buffer index is out of range",
        None,
      )
    } else if view.byte_offset < 0 ||
      view.byte_length < 0 ||
      buffer_view_end(view) > doc.buffers[view.buffer].byte_length {
      push_diagnostic(
        diagnostics,
        Error,
        InvalidReference,
        "bufferViews[{index}]",
        "view exceeds its parent buffer",
        None,
      )
    }
    if !buffer_view_is_aligned(view) {
      push_diagnostic(
        diagnostics,
        Warning,
        InvalidAccessorLayout,
        "bufferViews[{index}]",
        "view offset is not four-byte aligned",
        Some("align bufferView offsets to four bytes"),
      )
    }
  }
}

///|
fn check_accessors(doc : GltfDocument, diagnostics : Array[Diagnostic]) -> Unit {
  for issue in validate_accessor_layouts(doc) {
    push_diagnostic(
      diagnostics,
      Error,
      InvalidAccessorLayout,
      "accessors[{issue.accessor}]",
      issue.message,
      Some("check byteOffset, byteStride and count"),
    )
  }
  for index in 0.. Unit {
  for mesh_index in 0..
          if index < 0 || index >= doc.materials.length() {
            push_diagnostic(
              diagnostics,
              Error,
              InvalidReference,
              "meshes[{mesh_index}].material",
              "material index is out of range",
              None,
            )
          }
        None =>
          push_diagnostic(
            diagnostics,
            Info,
            MissingMaterial,
            "meshes[{mesh_index}].primitives[{primitive_index}]",
            "primitive uses the default material",
            None,
          )
      }
    }
  }
}

///|
fn check_nodes(doc : GltfDocument, diagnostics : Array[Diagnostic]) -> Unit {
  for index in 0..
        if mesh < 0 || mesh >= doc.meshes.length() {
          push_diagnostic(
            diagnostics,
            Error,
            InvalidReference,
            "nodes[\{index}].mesh",
            "mesh index is out of range",
            None,
          )
        }
      None => ()
    }
    match node.skin {
      Some(skin) =>
        if skin < 0 || skin >= doc.skins.length() {
          push_diagnostic(
            diagnostics,
            Error,
            SkinReference,
            "nodes[\{index}].skin",
            "skin index is out of range",
            None,
          )
        }
      None => ()
    }
    for child in node.children {
      if child < 0 || child >= doc.nodes.length() {
        push_diagnostic(
          diagnostics,
          Error,
          InvalidReference,
          "nodes[\{index}].children",
          "child node index is out of range",
          None,
        )
      }
    }
  }
}

///|
fn check_animations(
  doc : GltfDocument,
  diagnostics : Array[Diagnostic],
) -> Unit {
  for animation_index in 0..
          if node < 0 || node >= doc.nodes.length() {
            push_diagnostic(
              diagnostics,
              Error,
              AnimationReference,
              "animations[{animation_index}].channels[{channel_index}]",
              "target node index is out of range",
              None,
            )
          }
        None =>
          push_diagnostic(
            diagnostics,
            Warning,
            AnimationReference,
            "animations[{animation_index}].channels[{channel_index}]",
            "animation channel has no target node",
            None,
          )
      }
      if channel.sampler < 0 || channel.sampler >= animation.samplers.length() {
        push_diagnostic(
          diagnostics,
          Error,
          AnimationReference,
          "animations[{animation_index}].channels[{channel_index}].sampler",
          "sampler index is out of range",
          None,
        )
      }
    }
  }
}

///|
pub fn diagnose(
  doc : GltfDocument,
  policy : DiagnosticPolicy,
) -> DiagnosticSummary {
  let diagnostics : Array[Diagnostic] = []
  if doc.version == "" && doc.scenes.length() == 0 && doc.nodes.length() == 0 {
    push_diagnostic(
      diagnostics,
      Error,
      EmptyDocument,
      "asset",
      "document contains no scene data",
      None,
    )
  }
  check_version(doc, diagnostics)
  check_default_scene(doc, policy, diagnostics)
  check_scene_depth(doc, policy, diagnostics)
  check_buffers(doc, policy, diagnostics)
  check_buffer_views(doc, diagnostics)
  check_accessors(doc, diagnostics)
  check_meshes(doc, policy, diagnostics)
  check_nodes(doc, diagnostics)
  check_animations(doc, diagnostics)
  let mut infos = 0
  let mut warnings = 0
  let mut errors = 0
  for diagnostic in diagnostics {
    match diagnostic.severity {
      Info => infos += 1
      Warning => warnings += 1
      Error => errors += 1
    }
  }
  {
    infos,
    warnings,
    errors,
    diagnostics,
    passed: errors == 0 && (!policy.fail_on_warning || warnings == 0),
  }
}

///|
pub fn diagnose_default(doc : GltfDocument) -> DiagnosticSummary {
  diagnose(doc, default_diagnostic_policy())
}

///|
pub fn diagnose_strict(doc : GltfDocument) -> DiagnosticSummary {
  diagnose(doc, strict_diagnostic_policy())
}