///|
/// Error returned when decoding pixmap bytes.
pub(all) enum PixmapDecodeError {
  UnexpectedEndOfData(Int, Int, Int)
  InvalidRgbaDimensions(Int, Int)
  RgbaByteLengthMismatch(Int, Int)
  InvalidBmpSignature(Byte, Byte)
  UnsupportedBmpHeaderSize(Int)
  UnsupportedBmpColorPlanes(Int)
  UnsupportedBmpBitsPerPixel(Int)
  UnsupportedBmpCompression(Int)
  InvalidBmpDimensions(Int, Int)
  BmpPixelDataOutOfBounds(Int, Int, Int)
  InvalidPngSignature
  MissingPngImageHeader
  MissingPngImageData
  MissingPngImageEnd
  UnsupportedPngHeaderSize(Int)
  UnsupportedPngBitDepth(Int)
  UnsupportedPngColorType(Int)
  UnsupportedPngCompression(Int)
  UnsupportedPngFilter(Int)
  UnsupportedPngInterlace(Int)
  UnsupportedPngChunk(Int, Int, Int, Int)
  UnsupportedPngDeflateCompression(Int)
  UnsupportedPngDeflatePresetDictionary
  UnsupportedPngDeflateBlockType(Int)
  InvalidPngDeflateCode
  UnsupportedPngFilterType(Int)
  InvalidPngDimensions(Int, Int)
  InvalidPngZlibHeader(Int, Int)
  PngCrcMismatch(UInt, UInt)
  PngAdlerMismatch(UInt, UInt)
  PngStoredBlockLengthMismatch(Int, Int)
  PngInflateLengthMismatch(Int, Int)
  PngScanlineLengthMismatch(Int, Int)
} derive(Eq, Debug)

///|
pub extend PixmapDecodeError with Debug::{to_repr}

///|
pub extend PixmapDecodeError with Eq::{not_equal, equal}

///|
/// Error returned when encoding pixmap bytes.
pub(all) enum PixmapEncodeError {
  InvalidPngDimensions(Int, Int)
} derive(Eq, Debug)

///|
pub extend PixmapEncodeError with Debug::{to_repr}

///|
pub extend PixmapEncodeError with Eq::{not_equal, equal}

///|
/// Return an isolated copy of the raw RGBA pixel bytes in row-major order.
pub fn Pixmap::to_rgba_bytes(self : Pixmap) -> Bytes {
  rgba_bytes_from_pixels(self.pixels)
}

///|
/// Encode this pixmap as a binary PPM (P6) image, dropping the alpha channel.
/// The result is a deterministic, dependency-free fixture format for headless
/// render tests and simple CPU-rendered pixel dumps.
pub fn Pixmap::to_ppm_bytes(self : Pixmap) -> Bytes {
  let out : Array[Byte] = []
  append_ppm_header(out, self.width, self.height)
  for y in 0.. Bytes {
  let out : Array[Byte] = []
  append_bmp_header(out, self.width, self.height)
  for y_offset in 0.. Result[Pixmap, PixmapDecodeError] {
  guard width >= 0 && height >= 0 else {
    return Err(InvalidRgbaDimensions(width, height))
  }
  let expected_len = width * height * 4
  guard data.length() == expected_len else {
    return Err(RgbaByteLengthMismatch(expected_len, data.length()))
  }
  let pixmap = Pixmap::new(width, height)
  let pixels = pixmap.get_pixels()
  for i in 0.. Result[Pixmap, PixmapDecodeError] {
  match decode_bmp_info(data) {
    Err(error) => Err(error)
    Ok(info) => Ok(decode_bmp_pixmap(data, info))
  }
}

///|
fn rgba_bytes_from_pixels(pixels : FixedArray[Byte]) -> Bytes {
  let out : Array[Byte] = []
  for i in 0.. Unit {
  append_ppm_byte(out, 80) // P
  append_ppm_byte(out, 54) // 6
  append_ppm_byte(out, 10)
  append_ppm_decimal(out, width)
  append_ppm_byte(out, 32)
  append_ppm_decimal(out, height)
  append_ppm_byte(out, 10)
  append_ppm_byte(out, 50) // 2
  append_ppm_byte(out, 53) // 5
  append_ppm_byte(out, 53) // 5
  append_ppm_byte(out, 10)
}

///|
fn append_ppm_decimal(out : Array[Byte], value : Int) -> Unit {
  let mut n = value
  if n == 0 {
    append_ppm_byte(out, 48)
  } else {
    if n < 0 {
      append_ppm_byte(out, 45)
      n = 0 - n
    }
    let digits : Array[Byte] = []
    while n > 0 {
      digits.push((48 + n % 10).to_byte())
      n = n / 10
    }
    for i in 0.. Unit {
  out.push(value.land(0xFF).to_byte())
}

///|
fn append_bmp_header(out : Array[Byte], width : Int, height : Int) -> Unit {
  let pixel_byte_count = width * height * 4
  append_bmp_u16_le(out, 0x4D42) // BM
  append_bmp_u32_le(out, 54 + pixel_byte_count)
  append_bmp_u16_le(out, 0)
  append_bmp_u16_le(out, 0)
  append_bmp_u32_le(out, 54)

  append_bmp_u32_le(out, 40)
  append_bmp_u32_le(out, width)
  append_bmp_u32_le(out, height)
  append_bmp_u16_le(out, 1)
  append_bmp_u16_le(out, 32)
  append_bmp_u32_le(out, 0)
  append_bmp_u32_le(out, pixel_byte_count)
  append_bmp_u32_le(out, 0)
  append_bmp_u32_le(out, 0)
  append_bmp_u32_le(out, 0)
  append_bmp_u32_le(out, 0)
}

///|
fn append_bmp_u16_le(out : Array[Byte], value : Int) -> Unit {
  out.push(value.land(0xFF).to_byte())
  out.push((value / 256).land(0xFF).to_byte())
}

///|
fn append_bmp_u32_le(out : Array[Byte], value : Int) -> Unit {
  out.push(value.land(0xFF).to_byte())
  out.push((value / 256).land(0xFF).to_byte())
  out.push((value / 65536).land(0xFF).to_byte())
  out.push((value / 16777216).land(0xFF).to_byte())
}

///|
struct BmpInfo {
  pixel_offset : Int
  width : Int
  height : Int
  is_top_down : Bool
  bytes_per_pixel : Int
  source_row_bytes : Int
} derive(Debug)

///|
pub extend BmpInfo with Debug::{to_repr}

///|
fn decode_bmp_info(data : Bytes) -> Result[BmpInfo, PixmapDecodeError] {
  guard data.length() >= 54 else {
    return Err(UnexpectedEndOfData(0, 54, data.length()))
  }
  guard data[0] == 66 && data[1] == 77 else {
    return Err(InvalidBmpSignature(data[0], data[1]))
  }
  let pixel_offset = read_u32_le_at(data, 10)
  let header_size = read_u32_le_at(data, 14)
  guard header_size >= 40 else {
    return Err(UnsupportedBmpHeaderSize(header_size))
  }
  let width = read_i32_le_at(data, 18)
  let raw_height = read_i32_le_at(data, 22)
  guard width > 0 && raw_height != 0 else {
    return Err(InvalidBmpDimensions(width, raw_height))
  }
  let color_planes = read_u16_le_at(data, 26)
  guard color_planes == 1 else {
    return Err(UnsupportedBmpColorPlanes(color_planes))
  }
  let bits_per_pixel = read_u16_le_at(data, 28)
  guard bits_per_pixel == 24 || bits_per_pixel == 32 else {
    return Err(UnsupportedBmpBitsPerPixel(bits_per_pixel))
  }
  let compression = read_u32_le_at(data, 30)
  guard compression == 0 else {
    return Err(UnsupportedBmpCompression(compression))
  }
  let height = raw_height.abs()
  let bytes_per_pixel = bits_per_pixel / 8
  let source_row_bytes = (width * bits_per_pixel + 31) / 32 * 4
  let required_len = pixel_offset + source_row_bytes * height
  guard data.length() >= required_len else {
    return Err(
      BmpPixelDataOutOfBounds(pixel_offset, required_len, data.length()),
    )
  }
  Ok({
    pixel_offset,
    width,
    height,
    is_top_down: raw_height < 0,
    bytes_per_pixel,
    source_row_bytes,
  })
}

///|
fn decode_bmp_pixmap(data : Bytes, info : BmpInfo) -> Pixmap {
  let pixmap = Pixmap::new(info.width, info.height)
  let dst = pixmap.get_pixels()
  let dst_row_bytes = info.width * 4
  for y in 0.. Int {
  data[offset].to_int() + data[offset + 1].to_int() * 256
}

///|
fn read_u32_le_at(data : Bytes, offset : Int) -> Int {
  data[offset].to_int() +
  data[offset + 1].to_int() * 256 +
  data[offset + 2].to_int() * 65536 +
  data[offset + 3].to_int() * 16777216
}

///|
fn read_i32_le_at(data : Bytes, offset : Int) -> Int {
  let b0 = data[offset].to_int()
  let b1 = data[offset + 1].to_int()
  let b2 = data[offset + 2].to_int()
  let b3 = data[offset + 3].to_int()
  let signed_b3 = if b3 >= 128 { b3 - 256 } else { b3 }
  b0 + b1 * 256 + b2 * 65536 + signed_b3 * 16777216
}