///|
/// Cross-object semantic validation for production importers.
///
/// Structural parsing only proves that JSON has the expected shape. These
/// rules validate relationships between arrays and the invariants that a
/// runtime loader depends on: reference bounds, scene reachability, accessor
/// ranges, material chains, animation targets, skin joints, and safe resource
/// names.

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

///|
pub(all) struct SemanticIssue {
  severity : SemanticSeverity
  rule : String
  path : String
  message : String
  hint : String?
} derive(Debug, ToJson, Eq)

///|
pub(all) struct SemanticValidationReport {
  valid : Bool
  notes : Int
  warnings : Int
  errors : Int
  issues : Array[SemanticIssue]
  checked_references : Int
  checked_accessors : Int
  checked_primitives : Int
  checked_resources : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct SemanticValidationPolicy {
  require_asset_version_2 : Bool
  require_scene : Bool
  require_scene_reachability : Bool
  reject_duplicate_names : Bool
  reject_negative_indices : Bool
  require_position : Bool
  require_material_reference : Bool
  reject_unsafe_uris : Bool
  reject_non_monotonic_animation_times : Bool
  max_name_length : Int
} derive(Debug, ToJson, Eq)

///|
pub fn default_semantic_policy() -> SemanticValidationPolicy {
  {
    require_asset_version_2: true,
    require_scene: false,
    require_scene_reachability: false,
    reject_duplicate_names: false,
    reject_negative_indices: true,
    require_position: true,
    require_material_reference: false,
    reject_unsafe_uris: true,
    reject_non_monotonic_animation_times: true,
    max_name_length: 256,
  }
}

///|
pub fn strict_semantic_policy() -> SemanticValidationPolicy {
  {
    require_asset_version_2: true,
    require_scene: true,
    require_scene_reachability: true,
    reject_duplicate_names: true,
    reject_negative_indices: true,
    require_position: true,
    require_material_reference: true,
    reject_unsafe_uris: true,
    reject_non_monotonic_animation_times: true,
    max_name_length: 128,
  }
}

///|
fn semantic_issue(
  severity : SemanticSeverity,
  rule : String,
  path : String,
  message : String,
  hint : String?,
) -> SemanticIssue {
  { severity, rule, path, message, hint }
}

///|
pub fn semantic_severity_name(severity : SemanticSeverity) -> String {
  match severity {
    Note => "note"
    Warning => "warning"
    Error => "error"
  }
}

///|
pub fn semantic_issue_line(issue : SemanticIssue) -> String {
  let hint = match issue.hint {
    Some(value) => " hint=\{value}"
    None => ""
  }
  "[\{semantic_severity_name(issue.severity)}] \{issue.rule} \{issue.path}: \{issue.message}\{hint}"
}

///|
fn check_index(
  issues : Array[SemanticIssue],
  value : Int,
  upper : Int,
  path : String,
  rule : String,
  policy : SemanticValidationPolicy,
) -> Unit {
  if (policy.reject_negative_indices && value < 0) || value >= upper {
    issues.push(
      semantic_issue(
        Error,
        rule,
        path,
        "index \{value} is outside the valid range 0..\{upper}",
        None,
      ),
    )
  }
}

///|
fn check_name(
  issues : Array[SemanticIssue],
  name : String?,
  path : String,
  policy : SemanticValidationPolicy,
) -> Unit {
  match name {
    Some(value) => {
      if value == "" {
        issues.push(
          semantic_issue(
            Warning,
            "EMPTY_NAME",
            path,
            "name is empty",
            Some("omit the name or provide a stable editor-facing name"),
          ),
        )
      }
      if value.length() > policy.max_name_length {
        issues.push(
          semantic_issue(
            Warning,
            "NAME_TOO_LONG",
            path,
            "name exceeds the configured maximum length",
            None,
          ),
        )
      }
    }
    None => ()
  }
}

///|
fn validate_asset(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 1
  if policy.require_asset_version_2 && doc.version != "2.0" {
    issues.push(
      semantic_issue(
        Error,
        "ASSET_VERSION",
        "asset.version",
        "only glTF 2.0 assets are accepted by this pipeline",
        Some("export the asset with glTF version 2.0"),
      ),
    )
  }
  if doc.scenes.length() == 0 {
    issues.push(
      if policy.require_scene {
        semantic_issue(
          Error,
          "EMPTY_SCENES",
          "scenes",
          "asset contains no scenes",
          Some("export at least one scene"),
        )
      } else {
        semantic_issue(
          Note,
          "EMPTY_SCENES",
          "scenes",
          "asset contains no scenes",
          None,
        )
      },
    )
  }
  checked += doc.scenes.length()
  checked
}

///|
fn validate_scene_references(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 0
  match doc.default_scene {
    Some(scene) => {
      checked += 1
      check_index(
        issues,
        scene,
        doc.scenes.length(),
        "scene",
        "DEFAULT_SCENE",
        policy,
      )
    }
    None =>
      if policy.require_scene && doc.scenes.length() > 0 {
        issues.push(
          semantic_issue(
            Error,
            "MISSING_DEFAULT_SCENE",
            "scene",
            "asset has scenes but no default scene",
            Some("set the scene field for deterministic playback"),
          ),
        )
      }
  }
  for scene_index, scene in doc.scenes {
    check_name(issues, scene.name, "scenes[\{scene_index}].name", policy)
    for root, node in scene.nodes {
      checked += 1
      check_index(
        issues,
        node,
        doc.nodes.length(),
        "scenes[\{scene_index}].nodes[\{root}]",
        "SCENE_NODE_REFERENCE",
        policy,
      )
    }
  }
  if policy.require_scene_reachability {
    let orphan = orphan_node_indices(doc)
    if orphan.length() > 0 {
      issues.push(
        semantic_issue(
          Error,
          "ORPHAN_NODES",
          "nodes",
          "scene graph contains \{orphan.length()} unreachable nodes",
          Some("attach nodes to a scene or remove editor-only leftovers"),
        ),
      )
    }
  }
  checked
}

///|
fn validate_nodes(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 0
  for node_index, node in doc.nodes {
    check_name(issues, node.name, "nodes[\{node_index}].name", policy)
    let local_children : Map[Int, Bool] = Map([])
    for child, child_index in node.children {
      checked += 1
      check_index(
        issues,
        child_index,
        doc.nodes.length(),
        "nodes[\{node_index}].children[\{child}]",
        "NODE_CHILD_REFERENCE",
        policy,
      )
      if local_children.contains(child_index) {
        issues.push(
          semantic_issue(
            Error,
            "DUPLICATE_CHILD",
            "nodes[\{node_index}].children[\{child}]",
            "node lists the same child more than once",
            Some("deduplicate child references"),
          ),
        )
      } else {
        local_children[child_index] = true
      }
      if child_index == node_index {
        issues.push(
          semantic_issue(
            Error,
            "SELF_CYCLE",
            "nodes[\{node_index}].children[\{child}]",
            "node directly references itself",
            None,
          ),
        )
      }
    }
    match node.mesh {
      Some(mesh) => {
        checked += 1
        check_index(
          issues,
          mesh,
          doc.meshes.length(),
          "nodes[\{node_index}].mesh",
          "NODE_MESH_REFERENCE",
          policy,
        )
      }
      None => ()
    }
    match node.skin {
      Some(skin) => {
        checked += 1
        check_index(
          issues,
          skin,
          doc.skins.length(),
          "nodes[\{node_index}].skin",
          "NODE_SKIN_REFERENCE",
          policy,
        )
      }
      None => ()
    }
  }
  if scene_cycle_count(doc) > 0 {
    issues.push(
      semantic_issue(
        Error,
        "SCENE_CYCLE",
        "nodes",
        "scene graph contains a cycle",
        Some("repair parent-child links before runtime import"),
      ),
    )
  }
  checked
}

///|
fn validate_meshes(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> (Int, Int) {
  let mut checked = 0
  let mut primitives = 0
  for mesh_index, mesh in doc.meshes {
    check_name(issues, mesh.name, "meshes[\{mesh_index}].name", policy)
    for primitive_index, primitive in mesh.primitives {
      primitives += 1
      checked += primitive.attributes.length()
      if policy.require_position && primitive.attributes.get("POSITION") is None {
        issues.push(
          semantic_issue(
            Error,
            "MISSING_POSITION",
            "meshes[\{mesh_index}].primitives[\{primitive_index}]",
            "primitive has no POSITION attribute",
            Some("export vertex positions for every renderable primitive"),
          ),
        )
      }
      for name, accessor in primitive.attributes {
        check_index(
          issues,
          accessor,
          doc.accessors.length(),
          "meshes[\{mesh_index}].primitives[\{primitive_index}].attributes.\{name}",
          "ATTRIBUTE_REFERENCE",
          policy,
        )
      }
      match primitive.indices {
        Some(accessor) =>
          check_index(
            issues,
            accessor,
            doc.accessors.length(),
            "meshes[\{mesh_index}].primitives[\{primitive_index}].indices",
            "INDEX_REFERENCE",
            policy,
          )
        None => ()
      }
      match primitive.material {
        Some(material) =>
          check_index(
            issues,
            material,
            doc.materials.length(),
            "meshes[\{mesh_index}].primitives[\{primitive_index}].material",
            "MATERIAL_REFERENCE",
            policy,
          )
        None =>
          if policy.require_material_reference {
            issues.push(
              semantic_issue(
                Warning,
                "MISSING_MATERIAL",
                "meshes[\{mesh_index}].primitives[\{primitive_index}]",
                "primitive has no material reference",
                Some(
                  "assign a material or make the missing-material policy explicit",
                ),
              ),
            )
          }
      }
      if primitive.mode == Triangles &&
        primitive.indices is None &&
        primitive.attributes.get("POSITION") is Some(accessor) &&
        doc.accessors.get(accessor) is Some(value) &&
        value.count % 3 != 0 {
        issues.push(
          semantic_issue(
            Warning,
            "TRAILING_VERTICES",
            "meshes[\{mesh_index}].primitives[\{primitive_index}]",
            "non-indexed triangle primitive has trailing vertices",
            Some("ensure POSITION count is divisible by three"),
          ),
        )
      }
    }
  }
  (checked, primitives)
}

///|
fn validate_materials(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 0
  for material_index, material in doc.materials {
    checked += 1
    check_name(
      issues,
      material.name,
      "materials[\{material_index}].name",
      policy,
    )
    if material_pbr_parameter_error_count(material) > 0 {
      issues.push(
        semantic_issue(
          Error,
          "MATERIAL_RANGE",
          "materials[\{material_index}]",
          "material parameters are outside legal PBR ranges",
          Some("validate metallic, roughness, alpha cutoff, and color factors"),
        ),
      )
    }
    for semantic in material_texture_semantics(material) {
      match material_texture_info(material, semantic) {
        Some(info) => {
          checked += 1
          check_index(
            issues,
            info.index,
            doc.textures.length(),
            "materials[\{material_index}].\{texture_semantic_name(semantic)}.index",
            "TEXTURE_REFERENCE",
            policy,
          )
          if info.tex_coord < 0 {
            issues.push(
              semantic_issue(
                Error,
                "TEXCOORD_RANGE",
                "materials[\{material_index}]",
                "texture coordinate set cannot be negative",
                None,
              ),
            )
          }
        }
        None => ()
      }
    }
  }
  checked
}

///|
fn validate_textures(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 0
  for texture_index, texture in doc.textures {
    match texture.source {
      Some(image) => {
        checked += 1
        check_index(
          issues,
          image,
          doc.images.length(),
          "textures[\{texture_index}].source",
          "IMAGE_REFERENCE",
          policy,
        )
      }
      None => ()
    }
    match texture.sampler {
      Some(sampler) => {
        checked += 1
        check_index(
          issues,
          sampler,
          doc.samplers.length(),
          "textures[\{texture_index}].sampler",
          "SAMPLER_REFERENCE",
          policy,
        )
      }
      None => ()
    }
  }
  for image_index, image in doc.images {
    check_name(issues, image.name, "images[\{image_index}].name", policy)
    match image.buffer_view {
      Some(view) => {
        checked += 1
        check_index(
          issues,
          view,
          doc.buffer_views.length(),
          "images[\{image_index}].bufferView",
          "IMAGE_BUFFER_VIEW",
          policy,
        )
      }
      None => ()
    }
    match image.uri {
      Some(uri) =>
        if policy.reject_unsafe_uris && !uri_is_safe_external(uri) {
          issues.push(
            semantic_issue(
              Error,
              "UNSAFE_URI",
              "images[\{image_index}].uri",
              "image URI is not safe for package resolution",
              Some("use a relative path without parent traversal"),
            ),
          )
        }
      None =>
        if image.buffer_view is None {
          issues.push(
            semantic_issue(
              Error,
              "IMAGE_SOURCE_MISSING",
              "images[\{image_index}]",
              "image has neither a URI nor a bufferView",
              None,
            ),
          )
        }
    }
  }
  checked
}

///|
fn validate_buffers(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 0
  for index, buffer in doc.buffers {
    checked += 1
    check_name(issues, buffer.name, "buffers[\{index}].name", policy)
    if buffer.byte_length < 0 {
      issues.push(
        semantic_issue(
          Error,
          "BUFFER_LENGTH",
          "buffers[\{index}].byteLength",
          "buffer byteLength cannot be negative",
          None,
        ),
      )
    }
    match buffer.uri {
      Some(uri) =>
        if policy.reject_unsafe_uris && !uri_is_safe_external(uri) {
          issues.push(
            semantic_issue(
              Error,
              "UNSAFE_URI",
              "buffers[\{index}].uri",
              "buffer URI is not safe for package resolution",
              None,
            ),
          )
        }
      None => ()
    }
  }
  for index, view in doc.buffer_views {
    checked += 1
    check_index(
      issues,
      view.buffer,
      doc.buffers.length(),
      "bufferViews[\{index}].buffer",
      "BUFFER_VIEW_BUFFER",
      policy,
    )
    if view.byte_offset < 0 || view.byte_length < 0 {
      issues.push(
        semantic_issue(
          Error,
          "BUFFER_VIEW_RANGE",
          "bufferViews[\{index}]",
          "bufferView offsets and lengths cannot be negative",
          None,
        ),
      )
    }
    if view.byte_stride is Some(stride) &&
      (stride < 4 || stride > 252 || stride % 4 != 0) {
      issues.push(
        semantic_issue(
          Error,
          "BUFFER_VIEW_STRIDE",
          "bufferViews[\{index}].byteStride",
          "byteStride must be aligned and within glTF limits",
          None,
        ),
      )
    }
  }
  checked
}

///|
fn validate_accessors(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 0
  for index, accessor in doc.accessors {
    checked += 1
    check_name(issues, accessor.name, "accessors[\{index}].name", policy)
    if accessor.count < 0 || accessor.byte_offset < 0 {
      issues.push(
        semantic_issue(
          Error,
          "ACCESSOR_RANGE",
          "accessors[\{index}]",
          "accessor count and byteOffset must be non-negative",
          None,
        ),
      )
    }
    if accessor.min_values.length() > 0 &&
      accessor.min_values.length() !=
      accessor_component_count(accessor.accessor_type) {
      issues.push(
        semantic_issue(
          Error,
          "ACCESSOR_MIN_SHAPE",
          "accessors[\{index}].min",
          "min array length does not match accessor type",
          None,
        ),
      )
    }
    if accessor.max_values.length() > 0 &&
      accessor.max_values.length() !=
      accessor_component_count(accessor.accessor_type) {
      issues.push(
        semantic_issue(
          Error,
          "ACCESSOR_MAX_SHAPE",
          "accessors[\{index}].max",
          "max array length does not match accessor type",
          None,
        ),
      )
    }
    for component in 0.. accessor.max_values[component] {
        issues.push(
          semantic_issue(
            Error,
            "ACCESSOR_BOUNDS",
            "accessors[\{index}]",
            "accessor min value exceeds max value",
            None,
          ),
        )
      }
    }
    match accessor.buffer_view {
      Some(view) =>
        check_index(
          issues,
          view,
          doc.buffer_views.length(),
          "accessors[\{index}].bufferView",
          "ACCESSOR_BUFFER_VIEW",
          policy,
        )
      None =>
        if accessor.count > 0 {
          issues.push(
            semantic_issue(
              Warning,
              "SPARSE_UNSUPPORTED",
              "accessors[\{index}]",
              "accessor has no bufferView and requires sparse data support",
              Some("provide a regular bufferView for this importer"),
            ),
          )
        }
    }
    let layout = accessor_layout(doc, index)
    if !layout.valid {
      for problem in layout.problems {
        issues.push(
          semantic_issue(
            Error,
            "ACCESSOR_LAYOUT",
            "accessors[\{index}]",
            problem,
            None,
          ),
        )
      }
    }
  }
  checked
}

///|
fn validate_animations(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 0
  for animation_index, animation in doc.animations {
    check_name(
      issues,
      animation.name,
      "animations[\{animation_index}].name",
      policy,
    )
    for sampler_index, sampler in animation.samplers {
      checked += 1
      check_index(
        issues,
        sampler.input,
        doc.accessors.length(),
        "animations[\{animation_index}].samplers[\{sampler_index}].input",
        "ANIMATION_INPUT",
        policy,
      )
      check_index(
        issues,
        sampler.output,
        doc.accessors.length(),
        "animations[\{animation_index}].samplers[\{sampler_index}].output",
        "ANIMATION_OUTPUT",
        policy,
      )
    }
    for channel_index, channel in animation.channels {
      checked += 1
      check_index(
        issues,
        channel.sampler,
        animation.samplers.length(),
        "animations[\{animation_index}].channels[\{channel_index}].sampler",
        "ANIMATION_SAMPLER",
        policy,
      )
      match channel.target.node {
        Some(node) =>
          check_index(
            issues,
            node,
            doc.nodes.length(),
            "animations[\{animation_index}].channels[\{channel_index}].target.node",
            "ANIMATION_NODE",
            policy,
          )
        None =>
          issues.push(
            semantic_issue(
              Warning,
              "ANIMATION_GLOBAL_TARGET",
              "animations[\{animation_index}].channels[\{channel_index}]",
              "animation channel has no target node",
              None,
            ),
          )
      }
    }
  }
  checked
}

///|
fn validate_skins(
  doc : GltfDocument,
  issues : Array[SemanticIssue],
  policy : SemanticValidationPolicy,
) -> Int {
  let mut checked = 0
  for skin_index, skin in doc.skins {
    check_name(issues, skin.name, "skins[\{skin_index}].name", policy)
    for joint, node in skin.joints {
      checked += 1
      check_index(
        issues,
        node,
        doc.nodes.length(),
        "skins[\{skin_index}].joints[\{joint}]",
        "SKIN_JOINT",
        policy,
      )
    }
    match skin.skeleton {
      Some(node) =>
        check_index(
          issues,
          node,
          doc.nodes.length(),
          "skins[\{skin_index}].skeleton",
          "SKIN_SKELETON",
          policy,
        )
      None => ()
    }
    match skin.inverse_bind_matrices {
      Some(accessor) =>
        check_index(
          issues,
          accessor,
          doc.accessors.length(),
          "skins[\{skin_index}].inverseBindMatrices",
          "SKIN_INVERSE_BIND",
          policy,
        )
      None => ()
    }
    if skin_duplicate_joint_count(doc, skin_index) > 0 {
      issues.push(
        semantic_issue(
          Error,
          "SKIN_DUPLICATE_JOINT",
          "skins[\{skin_index}].joints",
          "skin joint list contains duplicates",
          None,
        ),
      )
    }
  }
  checked
}

///|
pub fn semantic_validate(
  doc : GltfDocument,
  policy : SemanticValidationPolicy,
) -> SemanticValidationReport {
  let issues : Array[SemanticIssue] = []
  let mut references = 0
  let mut accessors = 0
  let mut primitives = 0
  let mut resources = 0
  ignore(validate_asset(doc, issues, policy))
  references += validate_scene_references(doc, issues, policy)
  references += validate_nodes(doc, issues, policy)
  let (mesh_refs, primitive_count) = validate_meshes(doc, issues, policy)
  references += mesh_refs
  primitives += primitive_count
  references += validate_materials(doc, issues, policy)
  resources += validate_textures(doc, issues, policy)
  resources += validate_buffers(doc, issues, policy)
  accessors += validate_accessors(doc, issues, policy)
  references += validate_animations(doc, issues, policy)
  references += validate_skins(doc, issues, policy)
  if policy.reject_duplicate_names && named_node_duplicate_count(doc) > 0 {
    issues.push(
      semantic_issue(
        Error,
        "DUPLICATE_NODE_NAMES",
        "nodes",
        "multiple nodes share the same name",
        Some("use stable unique names for editor and runtime lookup"),
      ),
    )
  }
  let notes = issues.fold(init=0, (count, issue) => {
    if issue.severity == Note {
      count + 1
    } else {
      count
    }
  })
  let warnings = issues.fold(init=0, (count, issue) => {
    if issue.severity == Warning {
      count + 1
    } else {
      count
    }
  })
  let errors = issues.fold(init=0, (count, issue) => {
    if issue.severity == Error {
      count + 1
    } else {
      count
    }
  })
  {
    valid: errors == 0,
    notes,
    warnings,
    errors,
    issues,
    checked_references: references,
    checked_accessors: accessors,
    checked_primitives: primitives,
    checked_resources: resources,
  }
}

///|
pub fn semantic_validate_default(
  doc : GltfDocument,
) -> SemanticValidationReport {
  semantic_validate(doc, default_semantic_policy())
}

///|
pub fn semantic_validate_strict(doc : GltfDocument) -> SemanticValidationReport {
  semantic_validate(doc, strict_semantic_policy())
}

///|
pub fn semantic_issue_lines(report : SemanticValidationReport) -> Array[String] {
  report.issues.map(semantic_issue_line)
}

///|
pub fn semantic_error_issues(
  report : SemanticValidationReport,
) -> Array[SemanticIssue] {
  report.issues.filter(issue => issue.severity == Error)
}

///|
pub fn semantic_warning_issues(
  report : SemanticValidationReport,
) -> Array[SemanticIssue] {
  report.issues.filter(issue => issue.severity == Warning)
}

///|
pub fn semantic_rule_count(
  report : SemanticValidationReport,
  rule : String,
) -> Int {
  report.issues.fold(init=0, (count, issue) => {
    if issue.rule == rule {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn semantic_report_summary_lines(
  report : SemanticValidationReport,
) -> Array[String] {
  [
    "valid=\{report.valid}, errors=\{report.errors}, warnings=\{report.warnings}, notes=\{report.notes}",
    "references=\{report.checked_references}, accessors=\{report.checked_accessors}",
    "primitives=\{report.checked_primitives}, resources=\{report.checked_resources}",
  ]
}