/// Image container identification and lightweight metadata parsing.
///
/// This module deliberately does not decode compressed JPEG/WebP/AVIF/TIFF
/// pixels. It gives callers a deterministic, allocation-light way to inspect
/// uploads before selecting a backend decoder.

///|
pub(all) enum ImageFormat {
  Png
  Gif
  Qoi
  Bmp
  Jpeg
  Webp
  Avif
  Tiff
}

///|
pub fn ImageFormat::name(self : ImageFormat) -> String {
  match self {
    Png => "png"
    Gif => "gif"
    Qoi => "qoi"
    Bmp => "bmp"
    Jpeg => "jpeg"
    Webp => "webp"
    Avif => "avif"
    Tiff => "tiff"
  }
}

///|
/// Width, height and animation hint read from an image container header.
pub(all) struct ImageMetadata {
  format : ImageFormat
  width : Int
  height : Int
  animated : Bool
}

///|
fn be16(data : Array[Byte], p : Int) -> Int {
  (data[p].to_int() << 8) | data[p + 1].to_int()
}

///|
fn be32(data : Array[Byte], p : Int) -> Int {
  (data[p].to_int() << 24) |
  (data[p + 1].to_int() << 16) |
  (data[p + 2].to_int() << 8) |
  data[p + 3].to_int()
}

///|
fn le16(data : Array[Byte], p : Int) -> Int {
  data[p].to_int() | (data[p + 1].to_int() << 8)
}

///|
fn le32(data : Array[Byte], p : Int) -> Int {
  data[p].to_int() |
  (data[p + 1].to_int() << 8) |
  (data[p + 2].to_int() << 16) |
  (data[p + 3].to_int() << 24)
}

///|
fn ascii4(
  data : Array[Byte],
  p : Int,
  a : Int,
  b : Int,
  c : Int,
  d : Int,
) -> Bool {
  data.length() >= p + 4 &&
  data[p].to_int() == a &&
  data[p + 1].to_int() == b &&
  data[p + 2].to_int() == c &&
  data[p + 3].to_int() == d
}

///|
/// Identifies common raster containers from their signatures and brands.
pub fn detect_image_format(data : Array[Byte]) -> ImageFormat? {
  if data.length() >= 8 && data[0] == b'\x89' && ascii4(data, 1, 80, 78, 71, 13) {
    Some(Png)
  } else if data.length() >= 6 && ascii4(data, 0, 71, 73, 70, 56) {
    Some(Gif)
  } else if data.length() >= 4 && ascii4(data, 0, 113, 111, 105, 102) {
    Some(Qoi)
  } else if data.length() >= 2 && data[0] == b'B' && data[1] == b'M' {
    Some(Bmp)
  } else if data.length() >= 2 && data[0] == b'\xFF' && data[1] == b'\xD8' {
    Some(Jpeg)
  } else if data.length() >= 12 &&
    ascii4(data, 0, 82, 73, 70, 70) &&
    ascii4(data, 8, 87, 69, 66, 80) {
    Some(Webp)
  } else if bmff_avif_brand(data) is Some(_) {
    Some(Avif)
  } else if data.length() >= 4 &&
    (
      (
        data[0] == b'I' &&
        data[1] == b'I' &&
        (data[2] == b'*' || data[2] == b'+') &&
        data[3] == b'\x00'
      ) ||
      (
        data[0] == b'M' &&
        data[1] == b'M' &&
        data[2] == b'\x00' &&
        (data[3] == b'*' || data[3] == b'+')
      )
    ) {
    Some(Tiff)
  } else {
    None
  }
}

///|
priv struct BmffMetadataBox {
  kind : Int
  payload : Int
  end : Int
}

///|
// Bound each box by its parent before adding offsets. A 64-bit length larger
// than the input's Int-sized address space cannot describe an in-memory box.
fn bmff_metadata_box(
  data : Array[Byte],
  p : Int,
  end : Int,
) -> BmffMetadataBox? {
  if p < 0 || end > data.length() || end - p < 8 {
    return None
  }
  let mut size = be32(data, p)
  let mut header = 8
  if size == 1 {
    if end - p < 16 || be32(data, p + 8) != 0 {
      return None
    }
    size = be32(data, p + 12)
    header = 16
  } else if size == 0 {
    size = end - p
  }
  if size < header || size > end - p {
    return None
  }
  Some({ kind: be32(data, p + 4), payload: p + header, end: p + size, })
}

///|
fn bmff_metadata_boxes(
  data : Array[Byte],
  start : Int,
  end : Int,
) -> Array[BmffMetadataBox]? {
  let boxes = []
  let mut p = start
  while p < end {
    let box = match bmff_metadata_box(data, p, end) {
      Some(box) => box
      None => return None
    }
    boxes.push(box)
    p = box.end
  }
  Some(boxes)
}

///|
fn bmff_metadata_unique(
  boxes : Array[BmffMetadataBox],
  kind : Int,
) -> BmffMetadataBox? {
  let mut found = None
  for box in boxes {
    if box.kind == kind {
      if found is Some(_) {
        return None
      }
      found = Some(box)
    }
  }
  found
}

///|
// Some(animated) means an AVIF brand was declared. General HEIF brands such
// as mif1 and heic do not identify AVIF without an avif/avis compatible brand.
fn bmff_avif_brand(data : Array[Byte]) -> Bool? {
  let box = match bmff_metadata_box(data, 0, data.length()) {
    Some(box) => box
    None => return None
  }
  if box.kind != 0x66747970 ||
    box.end - box.payload < 8 ||
    (box.end - box.payload) % 4 != 0 {
    return None
  }
  let mut found = false
  let mut animated = false
  let mut p = box.payload
  while p < box.end {
    let brand = be32(data, p)
    if brand == 0x61766966 {
      found = true
    }
    if brand == 0x61766973 {
      found = true
      animated = true
    }
    // The word after major_brand is minor_version, not another brand.
    p += if p == box.payload { 8 } else { 4 }
  }
  if found {
    Some(animated)
  } else {
    None
  }
}

///|
/// Parses dimensions for supported headers. Returns `None` for truncated or
/// valid-but-unhandled variants whose dimensions are not present in the probe.
pub fn image_metadata(data : Array[Byte]) -> ImageMetadata? {
  let format = match detect_image_format(data) {
    Some(value) => value
    None => return None
  }
  match format {
    Png =>
      if data.length() >= 24 {
        Some({
          format,
          width: be32(data, 16),
          height: be32(data, 20),
          animated: false,
        })
      } else {
        None
      }
    Gif =>
      if data.length() >= 10 {
        Some({
          format,
          width: le16(data, 6),
          height: le16(data, 8),
          animated: gif_has_animation(data),
        })
      } else {
        None
      }
    Qoi =>
      if data.length() >= 14 {
        Some({
          format,
          width: be32(data, 4),
          height: be32(data, 8),
          animated: false,
        })
      } else {
        None
      }
    Bmp =>
      if data.length() >= 26 {
        let w = le32(data, 18)
        let h0 = le32(data, 22)
        let h = if h0 < 0 { -h0 } else { h0 }
        Some({ format, width: w, height: h, animated: false, })
      } else {
        None
      }
    Jpeg => jpeg_metadata(data, format)
    Webp => webp_metadata(data, format)
    Avif => bmff_metadata(data, format)
    Tiff => tiff_metadata(data, format)
  }
}

///|
fn gif_has_animation(data : Array[Byte]) -> Bool {
  let marker : Array[Byte] = [
    b'N', b'E', b'T', b'S', b'C', b'A', b'P', b'E', b'2', b'.', b'0',
  ]
  if data.length() < marker.length() {
    return false
  }
  for p in 0..<(data.length() - marker.length() + 1) {
    let mut found = true
    for i in 0.. ImageMetadata? {
  let mut p = 2
  while p + 9 < data.length() {
    if data[p] != b'\xFF' {
      p += 1
      continue
    }
    let marker = data[p + 1].to_int()
    if marker == 0xD9 || marker == 0xDA {
      return None
    }
    if (marker >= 0xC0 && marker <= 0xC3) ||
      (marker >= 0xC5 && marker <= 0xC7) ||
      (marker >= 0xC9 && marker <= 0xCB) ||
      (marker >= 0xCD && marker <= 0xCF) {
      let h = be16(data, p + 5)
      let w = be16(data, p + 7)
      return Some({ format, width: w, height: h, animated: false, })
    }
    if p + 4 > data.length() {
      return None
    }
    let segment = be16(data, p + 2)
    if segment < 2 {
      return None
    }
    p = p + 2 + segment
  }
  None
}

///|
fn webp_metadata(data : Array[Byte], format : ImageFormat) -> ImageMetadata? {
  if data.length() >= 30 && ascii4(data, 12, 86, 80, 56, 88) {
    let w = 1 +
      (data[24].to_int() | (data[25].to_int() << 8) | (data[26].to_int() << 16))
    let h = 1 +
      (data[27].to_int() | (data[28].to_int() << 8) | (data[29].to_int() << 16))
    Some({
      format,
      width: w,
      height: h,
      animated: (data[20].to_int() & 2) != 0,
    })
  } else {
    None
  }
}

///|
fn bmff_metadata(data : Array[Byte], format : ImageFormat) -> ImageMetadata? {
  let animated = match bmff_avif_brand(data) {
    Some(animated) => animated
    None => return None
  }
  let boxes = match bmff_metadata_boxes(data, 0, data.length()) {
    Some(boxes) => boxes
    None => return None
  }
  let meta = match bmff_metadata_unique(boxes, 0x6D657461) { // meta
    Some(box) => box
    None => return None
  }
  if meta.end - meta.payload < 4 || be32(data, meta.payload) != 0 {
    return None
  }
  let children = match bmff_metadata_boxes(data, meta.payload + 4, meta.end) {
    Some(boxes) => boxes
    None => return None
  }
  let pitm = match bmff_metadata_unique(children, 0x7069746D) { // pitm
    Some(box) => box
    None => return None
  }
  if pitm.end - pitm.payload < 6 {
    return None
  }
  let primary = match be32(data, pitm.payload) {
    0 => {
      if pitm.end - pitm.payload != 6 {
        return None
      }
      be16(data, pitm.payload + 4)
    }
    0x01000000 => {
      if pitm.end - pitm.payload != 8 {
        return None
      }
      be32(data, pitm.payload + 4)
    }
    _ => return None
  }
  if primary == 0 {
    return None
  }
  let iprp = match bmff_metadata_unique(children, 0x69707270) { // iprp
    Some(box) => box
    None => return None
  }
  let property_boxes = match bmff_metadata_boxes(data, iprp.payload, iprp.end) {
    Some(boxes) => boxes
    None => return None
  }
  let ipco = match bmff_metadata_unique(property_boxes, 0x6970636F) { // ipco
    Some(box) => box
    None => return None
  }
  let properties = match bmff_metadata_boxes(data, ipco.payload, ipco.end) {
    Some(boxes) => boxes
    None => return None
  }
  let mut dimensions = None
  for ipma in property_boxes {
    if ipma.kind != 0x69706D61 {
      continue
    } // ipma
    if ipma.end - ipma.payload < 8 {
      return None
    }
    let version_flags = be32(data, ipma.payload)
    let version = version_flags >> 24
    if (version != 0 && version != 1) || (version_flags & 0xFFFFFE) != 0 {
      return None
    }
    let item_bytes = if version == 0 { 2 } else { 4 }
    let association_bytes = if (version_flags & 1) == 0 { 1 } else { 2 }
    let count = be32(data, ipma.payload + 4)
    let mut p = ipma.payload + 8
    if count < 0 || count > (ipma.end - p) / (item_bytes + 1) {
      return None
    }
    for _ in 0.. (ipma.end - p) / association_bytes {
        return None
      }
      for _ in 0.. properties.length() {
          return None
        }
        if item != primary || index == 0 {
          continue
        }
        let property = properties[index - 1]
        if property.kind != 0x69737065 {
          continue
        } // ispe
        if dimensions is Some(_) {
          return None
        }
        if property.end - property.payload != 12 ||
          be32(data, property.payload) != 0 {
          return None
        }
        let width = be32(data, property.payload + 4)
        let height = be32(data, property.payload + 8)
        if width <= 0 || height <= 0 {
          return None
        }
        dimensions = Some((width, height))
      }
    }
    if p != ipma.end {
      return None
    }
  }
  match dimensions {
    Some((width, height)) => Some({ format, width, height, animated, })
    None => None
  }
}

///|
fn tiff_metadata(data : Array[Byte], format : ImageFormat) -> ImageMetadata? {
  if data.length() < 8 {
    return None
  }
  let little = data[0] == b'I'
  let big = tiff_u16(data, 2, little) == 43
  if big &&
    (
      data.length() < 16 ||
      tiff_u16(data, 4, little) != 8 ||
      tiff_u16(data, 6, little) != 0
    ) {
    return None
  }
  let ifd = if big {
    match tiff_u64_int(data, 8, little) {
      Some(value) => value
      None => return None
    }
  } else {
    tiff_u32(data, 4, little)
  }
  let count_size = if big { 8 } else { 2 }
  let entry_size = if big { 20 } else { 12 }
  let next_size = if big { 8 } else { 4 }
  if ifd < (if big { 16 } else { 8 }) ||
    !tiff_range(data, ifd, count_size + next_size) {
    return None
  }
  let count = if big {
    match tiff_u64_int(data, ifd, little) {
      Some(value) => value
      None => return None
    }
  } else {
    tiff_u16(data, ifd, little)
  }
  let start = ifd + count_size
  if count > (data.length() - start - next_size) / entry_size {
    return None
  }
  let mut w = 0
  let mut h = 0
  for i in 0.. value
        None => return None
      }
    } else {
      tiff_u32(data, p + 4, little)
    }
    if n != 1 {
      return None
    }
    let kind = tiff_u16(data, p + 2, little)
    let values = if big {
      tiff_big_values(data, p, kind, n, little)
    } else {
      tiff_values(data, p, kind, n, little)
    }
    let value = match values {
      Some(values) => values[0]
      None => return None
    }
    if tag == 256 {
      w = value
    } else {
      h = value
    }
  }
  if w > 0 && h > 0 {
    Some({ format, width: w, height: h, animated: false, })
  } else {
    None
  }
}