///|
pub(all) enum IssueSeverity {
  IssueError
  IssueWarning
} derive(Eq, @debug.Debug)

///|
pub fn IssueSeverity::to_string(self : IssueSeverity) -> String {
  match self {
    IssueError => "error"
    IssueWarning => "warning"
  }
}

///|
pub(all) struct ValidationIssue {
  severity : IssueSeverity
  path : String
  code : String
  message : String
} derive(Eq, @debug.Debug)

///|
pub fn ValidationIssue::error(
  message : String,
  path? : String = "$",
  code? : String = "error",
) -> ValidationIssue {
  { severity: IssueError, path, code, message }
}

///|
pub fn ValidationIssue::warning(
  message : String,
  path? : String = "$",
  code? : String = "warning",
) -> ValidationIssue {
  { severity: IssueWarning, path, code, message }
}

///|
pub(all) struct ValidationReport {
  ok : Bool
  issues : Array[ValidationIssue]
  error_count : Int
  warning_count : Int
} derive(Eq, @debug.Debug)

///|
pub fn ValidationReport::new(
  issues : Array[ValidationIssue],
) -> ValidationReport {
  let errors = for issue in issues; total = 0 {
    if issue.severity == IssueError {
      continue total + 1
    } else {
      continue total
    }
  } nobreak {
    total
  }
  let warnings = issues.length() - errors
  { ok: errors == 0, issues, error_count: errors, warning_count: warnings }
}

///|
pub fn ValidationReport::empty() -> ValidationReport {
  ValidationReport::new([])
}

///|
pub fn ValidationReport::summary(self : ValidationReport) -> String {
  if self.ok {
    "ok: \{self.warning_count} warning(s)"
  } else {
    "failed: \{self.error_count} error(s), \{self.warning_count} warning(s)"
  }
}

///|
pub fn validate_sprite_sheet(sheet : SpriteSheet) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  validate_sheet_basics(sheet, issues)
  validate_frames(sheet, issues)
  validate_tags(sheet, issues)
  validate_slices(sheet, issues)
  validate_layers(sheet, issues)
  validate_runtime_readiness(sheet, issues)
  ValidationReport::new(issues)
}

///|
fn validate_sheet_basics(
  sheet : SpriteSheet,
  issues : Array[ValidationIssue],
) -> Unit {
  if sheet.frames.length() == 0 {
    issues.push(
      ValidationIssue::error(
        "sprite sheet contains no frames",
        path="$.frames",
        code="empty-frames",
      ),
    )
  }
  if sheet.meta.image == "" {
    issues.push(
      ValidationIssue::warning(
        "meta.image is empty; runtime exporters cannot reference the texture",
        path="$.meta.image",
        code="missing-image",
      ),
    )
  }
  if !sheet.meta.size.is_positive() {
    issues.push(
      ValidationIssue::warning(
        "meta.size is missing or non-positive",
        path="$.meta.size",
        code="missing-atlas-size",
      ),
    )
  }
}

///|
fn validate_frames(
  sheet : SpriteSheet,
  issues : Array[ValidationIssue],
) -> Unit {
  for i, frame in sheet.frames {
    let path = json_index_path("$.frames", i)
    if frame.name == "" {
      issues.push(
        ValidationIssue::error(
          "frame name is empty",
          path~,
          code="empty-frame-name",
        ),
      )
    }
    if !frame.rect.is_positive() {
      issues.push(
        ValidationIssue::error(
          "frame rectangle must have positive width and height",
          path=json_field_path(path, "frame"),
          code="invalid-frame-rect",
        ),
      )
    }
    if frame.duration_ms <= 0 {
      issues.push(
        ValidationIssue::error(
          "frame duration must be greater than zero",
          path=json_field_path(path, "duration"),
          code="invalid-duration",
        ),
      )
    }
    if frame.rotated {
      issues.push(
        ValidationIssue::warning(
          "rotated frames require renderer-side UV rotation support",
          path~,
          code="rotated-frame",
        ),
      )
    }
    if sheet.meta.size.is_positive() &&
      !Rect::new(0, 0, sheet.meta.size.width, sheet.meta.size.height).contains_rect(
        frame.rect,
      ) {
      issues.push(
        ValidationIssue::error(
          "frame rectangle exceeds atlas bounds",
          path=json_field_path(path, "frame"),
          code="frame-out-of-atlas",
        ),
      )
    }
    validate_duplicate_frame_name(sheet.frames, i, issues)
  }
}

///|
fn validate_duplicate_frame_name(
  frames : Array[FrameInfo],
  index : Int,
  issues : Array[ValidationIssue],
) -> Unit {
  let name = frames[index].name
  if name != "" {
    for j in 0.. Unit {
  for i, tag in sheet.meta.frame_tags {
    let path = json_index_path("$.meta.frameTags", i)
    if tag.name == "" {
      issues.push(
        ValidationIssue::error(
          "tag name is empty",
          path~,
          code="empty-tag-name",
        ),
      )
    }
    if tag.from_index < 0 ||
      tag.to_index < tag.from_index ||
      tag.to_index >= sheet.frames.length() {
      issues.push(
        ValidationIssue::error(
          "tag range is outside available frame indices",
          path~,
          code="invalid-tag-range",
        ),
      )
    }
    validate_duplicate_tag_name(sheet.meta.frame_tags, i, issues)
  }
}

///|
fn validate_duplicate_tag_name(
  tags : Array[FrameTag],
  index : Int,
  issues : Array[ValidationIssue],
) -> Unit {
  let name = tags[index].name
  if name != "" {
    for j in 0.. Unit {
  for i, slice in sheet.meta.slices {
    let path = json_index_path("$.meta.slices", i)
    if slice.name == "" {
      issues.push(
        ValidationIssue::error(
          "slice name is empty",
          path~,
          code="empty-slice-name",
        ),
      )
    }
    if slice.keys.length() == 0 {
      issues.push(
        ValidationIssue::warning(
          "slice contains no keys",
          path=json_field_path(path, "keys"),
          code="empty-slice-keys",
        ),
      )
    }
    for k, key in slice.keys {
      let key_path = json_index_path(json_field_path(path, "keys"), k)
      if key.frame < 0 || key.frame >= sheet.frames.length() {
        issues.push(
          ValidationIssue::error(
            "slice key references a frame outside the sprite sheet",
            path=key_path,
            code="invalid-slice-frame",
          ),
        )
      }
      if !key.bounds.is_positive() {
        issues.push(
          ValidationIssue::error(
            "slice bounds must have positive width and height",
            path=json_field_path(key_path, "bounds"),
            code="invalid-slice-bounds",
          ),
        )
      }
    }
    validate_duplicate_slice_name(sheet.meta.slices, i, issues)
  }
}

///|
fn validate_duplicate_slice_name(
  slices : Array[SliceInfo],
  index : Int,
  issues : Array[ValidationIssue],
) -> Unit {
  let name = slices[index].name
  if name != "" {
    for j in 0.. Unit {
  for i, layer in sheet.meta.layers {
    let path = json_index_path("$.meta.layers", i)
    if layer.name == "" {
      issues.push(
        ValidationIssue::warning(
          "layer name is empty",
          path~,
          code="empty-layer-name",
        ),
      )
    }
    if layer.opacity < 0 || layer.opacity > 255 {
      issues.push(
        ValidationIssue::error(
          "layer opacity must be in 0..255",
          path=json_field_path(path, "opacity"),
          code="invalid-layer-opacity",
        ),
      )
    }
  }
}

///|
fn validate_runtime_readiness(
  sheet : SpriteSheet,
  issues : Array[ValidationIssue],
) -> Unit {
  if sheet.meta.frame_tags.length() == 0 {
    issues.push(
      ValidationIssue::warning(
        "no frameTags found; runtime will create a fallback clip",
        path="$.meta.frameTags",
        code="missing-tags",
      ),
    )
  }
  let mut has_box = false
  for slice in sheet.meta.slices {
    if string_is_box_name(slice.name) {
      has_box = true
    }
  }
  if !has_box {
    issues.push(
      ValidationIssue::warning(
        "no hitbox/hurtbox/collision slice found",
        path="$.meta.slices",
        code="missing-box-slices",
      ),
    )
  }
}

///|
pub fn validate_ase_file(file : AseFile) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  if file.header.width <= 0 || file.header.height <= 0 {
    issues.push(
      ValidationIssue::error(
        "ASE canvas size must be positive",
        path="header",
        code="invalid-canvas-size",
      ),
    )
  }
  if file.header.frame_count != file.frames.length() {
    issues.push(
      ValidationIssue::error(
        "ASE parsed frame count does not match header",
        path="frames",
        code="frame-count-mismatch",
      ),
    )
  }
  if file.header.color_depth != 32 &&
    file.header.color_depth != 16 &&
    file.header.color_depth != 8 {
    issues.push(
      ValidationIssue::warning(
        "uncommon color depth; renderer adapter may need custom handling",
        path="header.colorDepth",
        code="uncommon-color-depth",
      ),
    )
  }
  if ase_has_compressed_cels(file) {
    issues.push(
      ValidationIssue::warning(
        "compressed cel pixels are detected; v1 exposes metadata and leaves decompression to future zlib integration",
        path="chunks",
        code="compressed-cel",
      ),
    )
  }
  ValidationReport::new(issues)
}