///|
/// Read the canvas dimensions from the first simple or extended WebP header.
/// The caller supplies the full file size because this function receives only a
/// short prefix, not the compressed image data.
pub fn webp_dimensions(bytes : Bytes, file_size : Int64) -> (Int32, Int32)? {
  if bytes.length() < 20 || file_size < 20L {
    return None
  }
  if bytes[0] != b'R' ||
    bytes[1] != b'I' ||
    bytes[2] != b'F' ||
    bytes[3] != b'F' ||
    bytes[8] != b'W' ||
    bytes[9] != b'E' ||
    bytes[10] != b'B' ||
    bytes[11] != b'P' {
    return None
  }
  if read_webp_u32(bytes, 4) + 8L != file_size {
    return None
  }

  let chunk_size = read_webp_u32(bytes, 16)
  let padded_chunk_size = chunk_size + (chunk_size & 1L)
  if padded_chunk_size > file_size - 20L {
    return None
  }

  if webp_chunk_type_is(bytes, 12, b"VP8X") {
    if chunk_size != 10L || bytes.length() < 30 {
      return None
    }
    if (bytes[20].to_int() & 0xC1) != 0 ||
      bytes[21] != b'\x00' ||
      bytes[22] != b'\x00' ||
      bytes[23] != b'\x00' {
      return None
    }
    let width = read_webp_u24(bytes, 24) + 1
    let height = read_webp_u24(bytes, 27) + 1
    if Int32::to_int64(width) * Int32::to_int64(height) > 4_294_967_295L {
      return None
    }
    return Some((width, height))
  }

  if webp_chunk_type_is(bytes, 12, b"VP8L") {
    if chunk_size < 5L || bytes.length() < 25 || bytes[20] != b'\x2F' {
      return None
    }
    let width = bytes[21].to_int() + (bytes[22].to_int() & 0x3F) * 256 + 1
    let height = ((bytes[22].to_int() >> 6) & 0x03) +
      bytes[23].to_int() * 4 +
      (bytes[24].to_int() & 0x0F) * 1024 +
      1
    if bytes[24].to_int() >> 5 != 0 {
      return None
    }
    return Some((width, height))
  }

  if webp_chunk_type_is(bytes, 12, b"VP8 ") {
    if chunk_size < 10L || bytes.length() < 30 {
      return None
    }
    if (bytes[20].to_int() & 1) != 0 ||
      bytes[23] != b'\x9D' ||
      bytes[24] != b'\x01' ||
      bytes[25] != b'\x2A' {
      return None
    }
    let width = read_little_endian_u16(bytes, 26) & 0x3FFF
    let height = read_little_endian_u16(bytes, 28) & 0x3FFF
    if width > 0 && height > 0 {
      return Some((width, height))
    }
  }
  None
}

///|
async fn read_webp_dimensions(path : String, size : Int64) -> (Int32, Int32)? {
  if size < 20L {
    return None
  }
  let header_length = if size > 30L { 30 } else { Int64::to_int(size) }
  let file = @fs.open(path, mode=@fs.ReadOnly)
  defer file.close()
  let header = file.read_exactly_at(header_length, position=0L)
  match webp_dimensions(header, size) {
    Some(dimensions) => {
      let complete = webp_chunks_are_valid(file, size) catch { _ => false }
      if complete {
        Some(dimensions)
      } else {
        None
      }
    }
    None => None
  }
}

///|
/// Check RIFF chunk bounds and padding without decoding WebP image data.
async fn webp_chunks_are_valid(file : @fs.File, size : Int64) -> Bool {
  if size < 20L {
    return false
  }
  let header = file.read_exactly_at(12, position=0L)
  if header[0] != b'R' ||
    header[1] != b'I' ||
    header[2] != b'F' ||
    header[3] != b'F' ||
    header[8] != b'W' ||
    header[9] != b'E' ||
    header[10] != b'B' ||
    header[11] != b'P' ||
    read_webp_u32(header, 4) + 8L != size {
    return false
  }

  let mut offset = 12L
  let mut first_chunk = true
  let mut extended = false
  let mut has_image_data = false
  while offset < size {
    if size - offset < 8L {
      return false
    }
    let chunk_header = file.read_exactly_at(8, position=offset)
    let chunk_size = read_webp_u32(chunk_header, 4)
    let padded_chunk_size = chunk_size + (chunk_size & 1L)
    if padded_chunk_size > size - offset - 8L {
      return false
    }

    let is_extended = webp_chunk_type_is(chunk_header, 0, b"VP8X")
    let is_lossy = webp_chunk_type_is(chunk_header, 0, b"VP8 ")
    let is_lossless = webp_chunk_type_is(chunk_header, 0, b"VP8L")
    let is_animation_frame = webp_chunk_type_is(chunk_header, 0, b"ANMF")
    if first_chunk {
      extended = is_extended
      if !is_extended && !is_lossy && !is_lossless {
        return false
      }
      if is_extended && chunk_size != 10L {
        return false
      }
      if (is_lossy && chunk_size < 10L) || (is_lossless && chunk_size < 5L) {
        return false
      }
    } else if is_extended || !extended {
      return false
    }
    if (is_lossy && chunk_size >= 10L) || (is_lossless && chunk_size >= 5L) {
      has_image_data = true
    }
    if is_animation_frame && chunk_size >= 16L {
      has_image_data = true
    }

    if (chunk_size & 1L) != 0 {
      let padding = file.read_exactly_at(1, position=offset + 8L + chunk_size)
      if padding[0] != b'\x00' {
        return false
      }
    }
    offset += 8L + padded_chunk_size
    first_chunk = false
  }

  !first_chunk && has_image_data && (extended || offset == size)
}

///|
fn read_webp_u24(bytes : Bytes, offset : Int32) -> Int32 {
  bytes[offset].to_int() +
  bytes[offset + 1].to_int() * 256 +
  bytes[offset + 2].to_int() * 65_536
}

///|
fn read_webp_u32(bytes : Bytes, offset : Int32) -> Int64 {
  Int32::to_int64(bytes[offset].to_int()) +
  Int32::to_int64(bytes[offset + 1].to_int()) * 256L +
  Int32::to_int64(bytes[offset + 2].to_int()) * 65_536L +
  Int32::to_int64(bytes[offset + 3].to_int()) * 16_777_216L
}

///|
fn webp_chunk_type_is(bytes : Bytes, offset : Int32, name : Bytes) -> Bool {
  bytes[offset] == name[0] &&
  bytes[offset + 1] == name[1] &&
  bytes[offset + 2] == name[2] &&
  bytes[offset + 3] == name[3]
}

///|
test "reads WebP lossless image dimensions" {
  let webp : Bytes = b"RIFF\x12\x00\x00\x00WEBPVP8L\x05\x00\x00\x00\x2F\x03\x80\x00\x00\x00"
  assert_true(webp_dimensions(webp, 26L) == Some((4, 3)))
}

///|
test "reads WebP lossy key frame dimensions" {
  let webp : Bytes = b"RIFF\x1E\x00\x00\x00WEBPVP8 \x0A\x00\x00\x00\x00\x00\x00\x9D\x01\x2A\x10\x00\x09\x00"
  assert_true(webp_dimensions(webp, 38L) == Some((16, 9)))
}

///|
test "reads extended WebP canvas dimensions" {
  let webp : Bytes = b"RIFF\x16\x00\x00\x00WEBPVP8X\x0A\x00\x00\x00\x00\x00\x00\x00\xAF\x04\x00\x75\x02\x00"
  assert_true(webp_dimensions(webp, 30L) == Some((1200, 630)))
}

///|
test "rejects malformed WebP headers and unsupported lossless versions" {
  assert_true(webp_dimensions(b"not a WebP", 10L) is None)
  let bad_riff_length : Bytes = b"RIFF\x13\x00\x00\x00WEBPVP8X\x0A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  assert_true(webp_dimensions(bad_riff_length, 30L) is None)
  let bad_version : Bytes = b"RIFF\x12\x00\x00\x00WEBPVP8L\x05\x00\x00\x00\x2F\x03\x80\x00\x20\x00"
  assert_true(webp_dimensions(bad_version, 26L) is None)
}