///|
pub enum Severity {
  Error
  Warning
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct Diagnostic {
  severity : Severity
  code : String
  message : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct DiagnosticSummary {
  errors : Int
  warnings : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
fn error(code : String, message : String) -> Diagnostic {
  { severity: Error, code, message }
}

///|
fn warning(code : String, message : String) -> Diagnostic {
  { severity: Warning, code, message }
}

///|
pub fn validate_sheet(sheet : SpriteSheet) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  let sheet_bounds = { x: 0, y: 0, w: sheet.size.w, h: sheet.size.h }
  if sheet.frames.length() == 0 {
    diagnostics.push(error("empty_frames", "sprite sheet contains no frames"))
  }
  if sheet.size.w <= 0 || sheet.size.h <= 0 {
    diagnostics.push(error("invalid_sheet_size", "sheet size must be positive"))
  }
  for frame in sheet.frames {
    if frame.duration <= 0 {
      diagnostics.push(
        error(
          "invalid_duration",
          "frame duration must be positive: \{frame.filename}",
        ),
      )
    }
    if frame.source_size.w <= 0 || frame.source_size.h <= 0 {
      diagnostics.push(
        error(
          "invalid_source_size",
          "frame source size must be positive: \{frame.filename}",
        ),
      )
    }
    let source_bounds = {
      x: 0,
      y: 0,
      w: frame.source_size.w,
      h: frame.source_size.h,
    }
    if frame.frame.is_empty() || !sheet_bounds.contains_rect(frame.frame) {
      diagnostics.push(
        error(
          "frame_out_of_bounds", "frame rectangle is outside sheet: \\{frame.filename}",
        ),
      )
    }
    if frame.sprite_source_size.is_empty() ||
      !source_bounds.contains_rect(frame.sprite_source_size) {
      diagnostics.push(
        error(
          "source_rect_out_of_bounds", "sprite source rectangle is outside source size: \\{frame.filename}",
        ),
      )
    }
    for box in frame.boxes {
      if box.rect.is_empty() || !source_bounds.contains_rect(box.rect) {
        diagnostics.push(
          warning(
            "collision_box_out_of_bounds", "collision box is outside source size: \\{frame.filename}",
          ),
        )
      }
    }
  }
  for tag in sheet.tags {
    if tag.from < 0 || tag.to < tag.from || tag.to >= sheet.frames.length() {
      diagnostics.push(
        error("tag_out_of_range", "tag range is outside frames: \{tag.name}"),
      )
    }
    if tag.name == "" {
      diagnostics.push(warning("empty_tag_name", "frame tag has an empty name"))
    }
  }
  for i in 0..
          diagnostics.push(
            warning(
              "nine_patch_center_out_of_bounds", "nine-patch center is outside slice: \\{slice.name}",
            ),
          )
        _ => ()
      }
    }
  }
  diagnostics
}

///|
pub fn has_errors(diagnostics : ArrayView[Diagnostic]) -> Bool {
  for item in diagnostics {
    if item.severity == Error {
      return true
    }
  }
  false
}

///|
pub fn summarize_diagnostics(
  diagnostics : ArrayView[Diagnostic],
) -> DiagnosticSummary {
  let mut errors = 0
  let mut warnings = 0
  for item in diagnostics {
    match item.severity {
      Error => errors += 1
      Warning => warnings += 1
    }
  }
  { errors, warnings }
}

///|
pub fn diagnostics_to_json(
  diagnostics : Array[Diagnostic],
  indent? : Int = 2,
) -> String {
  diagnostics.to_json().stringify(indent~)
}