///|
/// A validation level suitable for command-line diagnostics and CI reports.
pub(all) enum DiagnosticLevel {
  Info
  Warning
  Error
} derive(Eq, Debug)

///|
pub(all) struct GifDiagnostic {
  level : DiagnosticLevel
  code : String
  message : String
  frame_index : Int?
} derive(Debug)

///|
pub fn GifDiagnostic::level(self : GifDiagnostic) -> DiagnosticLevel {
  self.level
}

///|
pub fn GifDiagnostic::code(self : GifDiagnostic) -> String {
  self.code
}

///|
pub fn GifDiagnostic::message(self : GifDiagnostic) -> String {
  self.message
}

///|
pub fn GifDiagnostic::frame(self : GifDiagnostic) -> Int? {
  self.frame_index
}

///|
pub(all) struct GifValidationReport {
  diagnostics : Array[GifDiagnostic]
  frame_count : Int
  duration_cs : Int
  pixel_count : Int
} derive(Debug)

///|
pub fn GifValidationReport::diagnostics(
  self : GifValidationReport,
) -> ArrayView[GifDiagnostic] {
  self.diagnostics[:]
}

///|
pub fn GifValidationReport::frame_count(self : GifValidationReport) -> Int {
  self.frame_count
}

///|
pub fn GifValidationReport::duration_cs(self : GifValidationReport) -> Int {
  self.duration_cs
}

///|
pub fn GifValidationReport::pixel_count(self : GifValidationReport) -> Int {
  self.pixel_count
}

///|
pub fn GifValidationReport::errors(self : GifValidationReport) -> Int {
  let mut count = 0
  for diagnostic in self.diagnostics {
    if diagnostic.level == Error {
      count += 1
    }
  }
  count
}

///|
pub fn GifValidationReport::warnings(self : GifValidationReport) -> Int {
  let mut count = 0
  for diagnostic in self.diagnostics {
    if diagnostic.level == Warning {
      count += 1
    }
  }
  count
}

///|
pub fn GifValidationReport::is_valid(self : GifValidationReport) -> Bool {
  self.errors() == 0
}

///|
fn add_diagnostic(
  list : Array[GifDiagnostic],
  level : DiagnosticLevel,
  code : String,
  message : String,
  frame_index : Int?,
) -> Unit {
  list.push(GifDiagnostic::{ level, code, message, frame_index })
}

///|
fn check_frame_geometry(
  animation : Animation,
  frame : Frame,
  index : Int,
  diagnostics : Array[GifDiagnostic],
) -> Unit {
  let canvas = frame.canvas()
  let (left, top) = frame.offset()
  if canvas.width <= 0 || canvas.height <= 0 {
    add_diagnostic(
      diagnostics,
      Error,
      "FRAME_EMPTY",
      "frame dimensions must be positive",
      Some(index),
    )
  }
  if left < 0 ||
    top < 0 ||
    left + canvas.width > animation.width ||
    top + canvas.height > animation.height {
    add_diagnostic(
      diagnostics,
      Error,
      "FRAME_BOUNDS",
      "frame rectangle exceeds the logical screen",
      Some(index),
    )
  }
  if frame.delay_cs() < 0 {
    add_diagnostic(
      diagnostics,
      Error,
      "FRAME_DELAY",
      "frame delay cannot be negative",
      Some(index),
    )
  } else if frame.delay_cs() == 0 {
    add_diagnostic(
      diagnostics,
      Warning,
      "FRAME_ZERO_DELAY",
      "zero delay is legal but may be clamped by viewers",
      Some(index),
    )
  }
  if frame.transparent is Some(color) && color.alpha() != 0 {
    add_diagnostic(
      diagnostics,
      Warning,
      "TRANSPARENT_ALPHA",
      "transparent key colors are normally encoded with alpha zero",
      Some(index),
    )
  }
}

///|
/// Validate an in-memory animation before it reaches the encoder.
pub fn validate_animation(animation : Animation) -> GifValidationReport {
  let diagnostics : Array[GifDiagnostic] = []
  if animation.width <= 0 || animation.height <= 0 {
    add_diagnostic(
      diagnostics,
      Error,
      "SCREEN_DIMENSIONS",
      "logical screen dimensions must be positive",
      None,
    )
  }
  if animation.width > 65535 || animation.height > 65535 {
    add_diagnostic(
      diagnostics,
      Error,
      "SCREEN_LIMIT",
      "GIF logical screen dimensions exceed 16-bit limits",
      None,
    )
  }
  if animation.frames.length() == 0 {
    add_diagnostic(
      diagnostics,
      Error,
      "NO_FRAMES",
      "animation must contain at least one frame",
      None,
    )
  }
  match animation.loop_count {
    None => ()
    Some(loop_count) if loop_count < 0 =>
      add_diagnostic(
        diagnostics,
        Error,
        "LOOP_COUNT",
        "loop count cannot be negative",
        None,
      )
    Some(_) => ()
  }
  let mut duration = 0
  let mut pixels = 0
  for index, frame in animation.frames {
    check_frame_geometry(animation, frame, index, diagnostics)
    duration += frame.delay_cs()
    pixels += frame.canvas().len()
  }
  if duration > 0 && duration < 2 {
    add_diagnostic(
      diagnostics,
      Info,
      "SHORT_ANIMATION",
      "total duration is shorter than two centiseconds",
      None,
    )
  }
  GifValidationReport::{
    diagnostics,
    frame_count: animation.frames.length(),
    duration_cs: duration,
    pixel_count: pixels,
  }
}

///|
/// Validate encoded bytes and preserve a concise diagnostic report on failure.
pub fn validate_gif_bytes(data : Bytes) -> GifValidationReport {
  let diagnostics : Array[GifDiagnostic] = []
  try {
    let info = inspect(data)
    GifValidationReport::{
      diagnostics: [],
      frame_count: info.frame_count(),
      duration_cs: 0,
      pixel_count: info.width() * info.height() * info.frame_count(),
    }
  } catch {
    error => {
      let (code, message) = match error {
        InvalidSignature => ("SIGNATURE", "GIF signature is missing or invalid")
        _ => ("MALFORMED", "GIF contains a truncated or invalid block")
      }
      add_diagnostic(diagnostics, Error, code, message, None)
      GifValidationReport::{
        diagnostics,
        frame_count: 0,
        duration_cs: 0,
        pixel_count: 0,
      }
    }
  } noraise {
    _ =>
      GifValidationReport::{
        diagnostics,
        frame_count: 0,
        duration_cs: 0,
        pixel_count: 0,
      }
  }
}

///|
/// Produce a normalized animation suitable for a best-effort export.
pub fn repair_animation(animation : Animation) -> Animation {
  let width = if animation.width < 1 {
    1
  } else if animation.width > 65535 {
    65535
  } else {
    animation.width
  }
  let height = if animation.height < 1 {
    1
  } else if animation.height > 65535 {
    65535
  } else {
    animation.height
  }
  let result = Animation::new(width~, height~)
  let loop_count = match animation.loop_count {
    None => None
    Some(value) =>
      Some(if value < 0 { 0 } else if value > 65535 { 65535 } else { value })
  }
  let repaired = result
    .with_loop_count(loop_count~)
    .with_background(background=animation.background)
  for frame in animation.frames {
    let source = frame.canvas()
    let left = if frame.left < 0 { 0 } else { frame.left }
    let top = if frame.top < 0 { 0 } else { frame.top }
    let available_width = width - left
    let available_height = height - top
    if available_width > 0 &&
      available_height > 0 &&
      source.width > 0 &&
      source.height > 0 {
      let crop_width = if source.width > available_width {
        available_width
      } else {
        source.width
      }
      let crop_height = if source.height > available_height {
        available_height
      } else {
        source.height
      }
      let cropped = source.crop(x=0, y=0, width=crop_width, height=crop_height)
      let safe_delay = if frame.delay_cs < 0 {
        0
      } else if frame.delay_cs > 65535 {
        65535
      } else {
        frame.delay_cs
      }
      ignore(
        repaired.add_frame(
          frame
          .with_canvas(cropped)
          .with_offset(left~, top~)
          .with_delay(delay_cs=safe_delay),
        ),
      )
    }
  }
  if repaired.frames.length() == 0 {
    ignore(
      repaired.add_frame(
        Frame::new(
          canvas=Canvas::new(width~, height~, fill=animation.background),
          delay_cs=0,
        ),
      ),
    )
  }
  repaired
}

///|
/// Return the first error code, if any, for simple command-line integrations.
pub fn GifValidationReport::first_error(self : GifValidationReport) -> String? {
  for diagnostic in self.diagnostics {
    if diagnostic.level == Error {
      return Some(diagnostic.code())
    }
  }
  None
}

///|
/// Count diagnostics for one frame, useful for editor overlays.
pub fn GifValidationReport::frame_diagnostics(
  self : GifValidationReport,
  frame_index : Int,
) -> Int {
  let mut count = 0
  for diagnostic in self.diagnostics {
    if diagnostic.frame() == Some(frame_index) {
      count += 1
    }
  }
  count
}

///|
/// Validate, repair, and return both versions for an import pipeline.
pub fn validate_and_repair(
  animation : Animation,
) -> (GifValidationReport, Animation, GifValidationReport) {
  let before = validate_animation(animation)
  let repaired = repair_animation(animation)
  let after = validate_animation(repaired)
  (before, repaired, after)
}