///|
fn read_u32le(data : Bytes, offset : Int) -> UInt {
  let b0 = data[offset].to_int() & 0xFF
  let b1 = data[offset + 1].to_int() & 0xFF
  let b2 = data[offset + 2].to_int() & 0xFF
  let b3 = data[offset + 3].to_int() & 0xFF
  (b3 << 24).lor(b2 << 16).lor(b1 << 8).lor(b0).reinterpret_as_uint()
}

///|
fn read_i32le(data : Bytes, offset : Int) -> Int {
  let b0 = data[offset].to_int() & 0xFF
  let b1 = data[offset + 1].to_int() & 0xFF
  let b2 = data[offset + 2].to_int() & 0xFF
  let b3 = data[offset + 3].to_int() & 0xFF
  b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
}

///|
fn read_u16le(data : Bytes, offset : Int) -> Int {
  let b0 = data[offset].to_int() & 0xFF
  let b1 = data[offset + 1].to_int() & 0xFF
  (b1 << 8) | b0
}

///|
priv struct BmpDecodeInfo {
  width : Int
  height : Int
  bpp : Int
  bytes_per_px : Int
  pixel_offset : Int
  padded_row_size : Int
  top_down : Bool
}

///|
fn parse_bmp_info(raw : Bytes) -> BmpDecodeInfo raise DecodeError {
  if raw.length() < 54 {
    raise CorruptData("BMP file too short")
  }
  if raw[0] != b'\x42' || raw[1] != b'\x4D' {
    raise InvalidSignature("not a BMP file")
  }
  let pixel_offset = read_u32le(raw, 10).reinterpret_as_int()
  let dib_size = read_u32le(raw, 14).reinterpret_as_int()
  if dib_size < 40 {
    raise UnsupportedFeature(
      "unsupported DIB header size: " + dib_size.to_string(),
    )
  }
  let width = read_i32le(raw, 18)
  let raw_height = read_i32le(raw, 22)
  let top_down = raw_height < 0
  let height = if top_down { 0 - raw_height } else { raw_height }
  if width <= 0 || height <= 0 {
    raise CorruptData("invalid BMP dimensions")
  }
  let bpp = read_u16le(raw, 28)
  let compression = read_u32le(raw, 30).reinterpret_as_int()
  if compression != 0 {
    raise UnsupportedFeature(
      "compressed BMP not supported (compression=" +
      compression.to_string() +
      ")",
    )
  }
  if bpp != 24 && bpp != 32 {
    raise UnsupportedFeature(
      "unsupported BMP bits per pixel: " + bpp.to_string(),
    )
  }
  let bytes_per_px = bpp / 8
  let row_size = width * bytes_per_px
  let padded_row_size = (row_size + 3) / 4 * 4
  let required_size = pixel_offset + padded_row_size * height
  if raw.length() < required_size {
    raise CorruptData("BMP file too short for pixel data")
  }

  { width, height, bpp, bytes_per_px, pixel_offset, padded_row_size, top_down }
}

///|
fn decode_bmp_rows(
  raw : Bytes,
  info : BmpDecodeInfo,
  on_row : (Int, Bytes) -> Unit,
) -> Unit {
  for y in 0.. Unit,
) -> (Int, Int) raise DecodeError {
  let info = parse_bmp_info(raw)
  decode_bmp_rows(raw, info, on_row)
  (info.width, info.height)
}

///|
pub fn decode_bmp(raw : Bytes) -> ImageData raise DecodeError {
  let info = parse_bmp_info(raw)
  let rgba_buf = FixedArray::make(info.width * info.height * 4, b'\x00')
  decode_bmp_rows(raw, info, fn(y, row) {
    let dst_offset = y * info.width * 4
    rgba_buf.blit_from_bytes(dst_offset, row, 0, info.width * 4)
  })
  { width: info.width, height: info.height, data: Bytes::from_array(rgba_buf) }
}