///|
/// Cheap preflight checks for untrusted GIF data before full decoding.
pub(all) struct GifPreflight {
  signature : Bool
  header : GifHeader?
  bytes : Int
  dimensions_ok : Bool
  estimated_pixels : Int
  frame_count : Int
  extension_count : Int
  complete : Bool
  safe : Bool
  issues : Array[String]
} derive(Debug)

///|
pub fn GifPreflight::signature(self : GifPreflight) -> Bool {
  self.signature
}

///|
pub fn GifPreflight::header(self : GifPreflight) -> GifHeader? {
  self.header
}

///|
pub fn GifPreflight::bytes(self : GifPreflight) -> Int {
  self.bytes
}

///|
pub fn GifPreflight::dimensions_ok(self : GifPreflight) -> Bool {
  self.dimensions_ok
}

///|
pub fn GifPreflight::estimated_pixels(self : GifPreflight) -> Int {
  self.estimated_pixels
}

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

///|
pub fn GifPreflight::extension_count(self : GifPreflight) -> Int {
  self.extension_count
}

///|
pub fn GifPreflight::complete(self : GifPreflight) -> Bool {
  self.complete
}

///|
pub fn GifPreflight::safe(self : GifPreflight) -> Bool {
  self.safe
}

///|
pub fn GifPreflight::issues(self : GifPreflight) -> ArrayView[String] {
  self.issues[:]
}

///|
/// Inspect basic structure before allocating decoded pixel buffers.
pub fn preflight_gif(data : Bytes, limits : GifLimits) -> GifPreflight {
  let issues : Array[String] = []
  let signature = has_gif_signature(data)
  if !signature {
    issues.push("missing GIF signature")
  }
  let header : GifHeader? = try Some(parse_header(data)) catch {
    _ => None
  } noraise {
    _ => None
  }
  let dimensions_ok = match header {
    None => false
    Some(value) => limits.valid_dimensions(value.width(), value.height())
  }
  if signature && !dimensions_ok {
    issues.push("logical screen exceeds configured limits")
  }
  let index : GifByteIndex? = if signature {
    try Some(index_gif(data)) catch {
      _ => None
    } noraise {
      _ => None
    }
  } else {
    None
  }
  let frames = match index {
    None => 0
    Some(value) => value.frames().length()
  }
  let extensions = match index {
    None => 0
    Some(value) => value.extension_count()
  }
  let complete = match index {
    None => false
    Some(value) => value.is_complete()
  }
  if signature && !complete {
    issues.push("GIF stream is not complete")
  }
  if frames > limits.frames() {
    issues.push("frame count exceeds configured limit")
  }
  let pixels = match header {
    None => 0
    Some(value) => value.area() * frames
  }
  if pixels > 0 &&
    pixels > limits.dimension() * limits.dimension() * limits.frames() {
    issues.push("estimated decoded pixel count is too large")
  }
  GifPreflight::{
    signature,
    header,
    bytes: data.length(),
    dimensions_ok,
    estimated_pixels: pixels,
    frame_count: frames,
    extension_count: extensions,
    complete,
    safe: issues.length() == 0,
    issues,
  }
}

///|
/// Decide whether full decode is allowed under resource limits.
pub fn GifPreflight::allows_decode(self : GifPreflight) -> Bool {
  self.safe && self.complete && self.frame_count > 0
}

///|
/// Return a stable reason string for rejected data.
pub fn GifPreflight::reason(self : GifPreflight) -> String {
  if self.safe {
    "preflight passed"
  } else {
    let mut result = "preflight failed"
    for issue in self.issues {
      result = result + ": " + issue
    }
    result
  }
}

///|
/// Decode only after a successful preflight.
pub fn decode_after_preflight(data : Bytes, limits : GifLimits) -> Animation? {
  let report = preflight_gif(data, limits)
  if !report.allows_decode() {
    None
  } else {
    decode_checked(data)
  }
}

///|
/// Return a conservative byte budget for decoded RGBA storage.
pub fn GifPreflight::decoded_memory_bytes(self : GifPreflight) -> Int {
  self.estimated_pixels * 4
}