// Copyright (c) 2025 lws
// BMP (Bitmap) image decoder
//
// Supported BMP formats:
//   - 32-bit BGRA (uncompressed + BITFIELDS)
//   - 24-bit BGR (uncompressed)
//   - 8-bit indexed with RLE8 compression
//   - 4-bit indexed with RLE4 compression
//   - 1-bit monochrome (with palette / B&W)
//   - Top-down and bottom-up scan order

// Compression types

///|
const BI_RLE8 : Int = 1

///|
const BI_RLE4 : Int = 2

//-----------------------------------------------------------------------------
// BMP Decoder
//-----------------------------------------------------------------------------

///|
/// Decode a BMP image from raw bytes
pub fn decode_bmp(data : Bytes) -> Image raise Failure {
  let decoder = BmpDecoder::new(data)
  decoder.decode()
}

///|
/// BMP decoder state machine
priv struct BmpDecoder {
  data : Bytes
  width : Int
  height : Int
  top_down : Bool
  bpp : Int
  compression : Int
  data_offset : Int
  data_end : Int
  dib_size : Int
  palette_count : Int
}

///|
fn BmpDecoder::new(data : Bytes) -> BmpDecoder raise Failure {
  // Verify BMP signature
  if data.length() < 2 {
    raise Failure::Failure("BMP: file too small for signature")
  }
  if data[0] != b'B' || data[1] != b'M' {
    raise Failure::Failure("BMP: invalid signature, expected 'BM'")
  }
  if data.length() < 54 {
    raise Failure::Failure("BMP: file too small for DIB header")
  }
  let data_offset = read_u32_le(data, 10)
  let dib_size = read_u32_le(data, 14)
  if dib_size != 40 &&
    dib_size != 52 &&
    dib_size != 56 &&
    dib_size != 108 &&
    dib_size != 124 {
    raise Failure::Failure("BMP: unsupported DIB header")
  }
  if dib_size > data.length() - 14 ||
    data_offset < 14 + dib_size ||
    data_offset > data.length() {
    raise Failure::Failure("BMP: invalid header or pixel offset")
  }
  let width_raw = read_u32_le(data, 18)
  let height_raw = read_u32_le(data, 22)
  // Height can be signed: positive = bottom-up, negative = top-down
  let top_down = height_raw < 0
  let height = if top_down { -height_raw } else { height_raw }
  let width = width_raw
  if width <= 0 || height <= 0 {
    raise Failure::Failure("BMP: invalid dimensions")
  }
  let bpp = read_u16_le(data, 28)
  let compression = read_u32_le(data, 30)
  if read_u16_le(data, 26) != 1 {
    raise Failure::Failure("BMP: invalid planes")
  }
  if bpp != 1 && bpp != 4 && bpp != 8 && bpp != 24 && bpp != 32 {
    raise Failure::Failure("BMP: unsupported bit depth: \{bpp}")
  }
  if !(compression == 0 ||
    (compression == BI_RLE8 && bpp == 8) ||
    (compression == BI_RLE4 && bpp == 4) ||
    (compression == 3 && bpp == 32)) {
    raise Failure::Failure("BMP: unsupported compression or bit depth")
  }
  if top_down && (compression == BI_RLE8 || compression == BI_RLE4) {
    raise Failure::Failure("BMP: top-down RLE is invalid")
  }
  let _ = checked_image_size(width, height, if bpp == 24 { 3 } else { 4 })
  if compression == 3 {
    // This decoder supports the byte-aligned BGRA layout, not arbitrary masks.
    if data_offset < 66 {
      raise Failure::Failure("BMP: truncated color masks")
    }
    if read_u32_le(data, 54) != 0x00FF0000 ||
      read_u32_le(data, 58) != 0x0000FF00 ||
      read_u32_le(data, 62) != 0x000000FF {
      raise Failure::Failure("BMP: unsupported color masks")
    }
    if dib_size >= 56 {
      let alpha_mask = read_u32_le(data, 66)
      if alpha_mask != 0 && alpha_mask != 0xFF000000 {
        raise Failure::Failure("BMP: unsupported alpha mask")
      }
    }
  }
  let palette_count = if bpp <= 8 {
    let used = read_u32_le(data, 46)
    let count = if used == 0 { 1 << bpp } else { used }
    if count <= 0 ||
      count > 1 << bpp ||
      count > (data_offset - 14 - dib_size) / 4 {
      raise Failure::Failure("BMP: invalid or truncated palette")
    }
    count
  } else {
    0
  }
  let data_end = if compression == BI_RLE8 || compression == BI_RLE4 {
    let size = read_u32_le(data, 34)
    if size < 0 || size > data.length() - data_offset {
      raise Failure::Failure("BMP: invalid compressed data size")
    }
    if size == 0 {
      data.length()
    } else {
      data_offset + size
    }
  } else {
    data.length()
  }
  {
    data,
    width,
    height,
    top_down,
    bpp,
    compression,
    data_offset,
    data_end,
    dib_size,
    palette_count,
  }
}

///|
fn BmpDecoder::decode(self : BmpDecoder) -> Image raise Failure {
  match self.bpp {
    32 => self.decode_rgba()
    24 => self.decode_24bit()
    8 =>
      if self.compression == BI_RLE8 {
        self.decode_rle8()
      } else {
        self.decode_8bit()
      }
    4 =>
      if self.compression == BI_RLE4 {
        self.decode_rle4()
      } else {
        self.decode_4bit()
      }
    1 => self.decode_1bit()
    _ => raise Failure::Failure("BMP: unsupported bit depth: \{self.bpp}")
  }
}

///|
/// Read palette from BMP data (after DIB header)
fn BmpDecoder::read_palette(self : BmpDecoder) -> Array[Color] {
  let palette_offset = 14 + self.dib_size // file header + DIB header
  let palette = Array::make(self.palette_count, Color::default())
  for i = 0; i < self.palette_count; i = i + 1 {
    let offset = palette_offset + i * 4
    let b = self.data[offset].to_int()
    let g = self.data[offset + 1].to_int()
    let r = self.data[offset + 2].to_int()
    palette[i] = Color::new(r, g, b, 255)
  }
  palette
}

///|
/// Pre-compute palette as RGBA byte quads for fast indexed decoding
fn palette_to_rgba_bytes(palette : Array[Color]) -> Array[Array[Byte]] {
  let result = Array::make(palette.length(), [
    b'\x00', b'\x00', b'\x00', b'\xFF',
  ])
  for i = 0; i < palette.length(); i = i + 1 {
    let c = palette[i]
    result[i] = [c.r.to_byte(), c.g.to_byte(), c.b.to_byte(), c.a.to_byte()]
  }
  result
}

///|
/// Row stride in bytes (padded to 4-byte boundary)
fn bmp_row_stride(width : Int, bpp : Int) -> Int raise Failure {
  let stride = (width.to_int64() * bpp.to_int64() + 31L) / 32L * 4L
  if stride > 0x7FFFFFFFL {
    raise Failure::Failure("BMP: row stride too large")
  }
  stride.to_int()
}

///|
/// Write a palette color to the output buffer at a pixel position
fn write_palette_pixel(
  _buf : Array[Byte],
  dst : Int,
  idx : Int,
  num_colors : Int,
  palette_rgba : Array[Array[Byte]],
) -> Unit raise Failure {
  if idx >= 0 && idx < num_colors {
    let c = palette_rgba[idx]
    _buf[dst] = c[0]
    _buf[dst + 1] = c[1]
    _buf[dst + 2] = c[2]
    _buf[dst + 3] = c[3]
  } else {
    raise Failure::Failure("BMP: palette index out of range")
  }
}

///|
/// Decode 32-bit BGRA
fn BmpDecoder::decode_rgba(self : BmpDecoder) -> Image raise Failure {
  let stride = bmp_row_stride(self.width, 32)
  // Upfront bounds validation: total pixel data region must fit
  if self.height > (self.data.length() - self.data_offset) / stride {
    raise Failure::Failure("BMP: unexpected end of pixel data")
  }
  let out_size = checked_image_size(self.width, self.height, 4)
  let _buf = Array::make(out_size, Byte::default())

  for row = 0; row < self.height; row = row + 1 {
    let src_row = if self.top_down { row } else { self.height - 1 - row }
    let row_offset = self.data_offset + src_row * stride
    for col = 0; col < self.width; col = col + 1 {
      let src = row_offset + col * 4
      let dst = (row * self.width + col) * 4
      // BMP stores BGRA in little-endian: byte order is B, G, R, A
      _buf[dst] = self.data[src + 2] // R
      _buf[dst + 1] = self.data[src + 1] // G
      _buf[dst + 2] = self.data[src] // B
      _buf[dst + 3] = self.data[src + 3] // A
    }
  }
  Image::new(
    self.width,
    self.height,
    PixelFormat::RGBA8,
    Bytes::from_array(_buf),
  )
}

///|
/// Decode 24-bit BGR
fn BmpDecoder::decode_24bit(self : BmpDecoder) -> Image raise Failure {
  let stride = bmp_row_stride(self.width, 24)
  // Upfront bounds validation: total pixel data region must fit
  if self.height > (self.data.length() - self.data_offset) / stride {
    raise Failure::Failure("BMP: unexpected end of pixel data")
  }
  let out_size = checked_image_size(self.width, self.height, 3)
  let _buf = Array::make(out_size, Byte::default())

  for row = 0; row < self.height; row = row + 1 {
    let src_row = if self.top_down { row } else { self.height - 1 - row }
    let row_offset = self.data_offset + src_row * stride
    for col = 0; col < self.width; col = col + 1 {
      let src = row_offset + col * 3
      let dst = (row * self.width + col) * 3
      // BMP stores BGR
      _buf[dst] = self.data[src + 2] // R
      _buf[dst + 1] = self.data[src + 1] // G
      _buf[dst + 2] = self.data[src] // B
    }
  }
  Image::new(
    self.width,
    self.height,
    PixelFormat::RGB8,
    Bytes::from_array(_buf),
  )
}

///|
/// Decode 8-bit indexed (uncompressed) — uses pre-computed palette
fn BmpDecoder::decode_8bit(self : BmpDecoder) -> Image raise Failure {
  let palette = self.read_palette()
  let palette_rgba = palette_to_rgba_bytes(palette)
  let stride = bmp_row_stride(self.width, 8)
  // Upfront bounds validation
  if self.height > (self.data.length() - self.data_offset) / stride {
    raise Failure::Failure("BMP: unexpected end of pixel data")
  }
  let out_size = checked_image_size(self.width, self.height, 4)
  let _buf = Array::make(out_size, Byte::default())

  for row = 0; row < self.height; row = row + 1 {
    let src_row = if self.top_down { row } else { self.height - 1 - row }
    let row_offset = self.data_offset + src_row * stride
    for col = 0; col < self.width; col = col + 1 {
      let src = row_offset + col
      let idx = self.data[src].to_int()
      let dst = (row * self.width + col) * 4
      write_palette_pixel(_buf, dst, idx, self.palette_count, palette_rgba)
    }
  }
  Image::new(
    self.width,
    self.height,
    PixelFormat::RGBA8,
    Bytes::from_array(_buf),
  )
}

///|
/// Decode 4-bit indexed (uncompressed)
fn BmpDecoder::decode_4bit(self : BmpDecoder) -> Image raise Failure {
  let palette = self.read_palette()
  let palette_rgba = palette_to_rgba_bytes(palette)
  let stride = bmp_row_stride(self.width, 4)
  // Upfront bounds validation
  if self.height > (self.data.length() - self.data_offset) / stride {
    raise Failure::Failure("BMP: unexpected end of pixel data")
  }
  let out_size = checked_image_size(self.width, self.height, 4)
  let _buf = Array::make(out_size, Byte::default())

  for row = 0; row < self.height; row = row + 1 {
    let src_row = if self.top_down { row } else { self.height - 1 - row }
    let row_offset = self.data_offset + src_row * stride
    for col = 0; col < self.width; col = col + 1 {
      let src = row_offset + col / 2
      let byte_val = self.data[src].to_int()
      // High nibble first (leftmost pixel)
      let idx = if col % 2 == 0 {
        (byte_val >> 4) & 0xF
      } else {
        byte_val & 0xF
      }
      let dst = (row * self.width + col) * 4
      write_palette_pixel(_buf, dst, idx, self.palette_count, palette_rgba)
    }
  }
  Image::new(
    self.width,
    self.height,
    PixelFormat::RGBA8,
    Bytes::from_array(_buf),
  )
}

///|
/// Decode 1-bit monochrome
fn BmpDecoder::decode_1bit(self : BmpDecoder) -> Image raise Failure {
  let palette = self.read_palette()
  let palette_rgba = palette_to_rgba_bytes(palette)
  let stride = bmp_row_stride(self.width, 1)
  // Upfront bounds validation
  if self.height > (self.data.length() - self.data_offset) / stride {
    raise Failure::Failure("BMP: unexpected end of pixel data")
  }
  let out_size = checked_image_size(self.width, self.height, 4)
  let _buf = Array::make(out_size, Byte::default())

  for row = 0; row < self.height; row = row + 1 {
    let src_row = if self.top_down { row } else { self.height - 1 - row }
    let row_offset = self.data_offset + src_row * stride
    for col = 0; col < self.width; col = col + 1 {
      let src = row_offset + col / 8
      let byte_val = self.data[src].to_int()
      // MSB first (leftmost pixel is bit 7)
      let bit = (byte_val >> (7 - col % 8)) & 1
      let dst = (row * self.width + col) * 4
      write_palette_pixel(_buf, dst, bit, self.palette_count, palette_rgba)
    }
  }
  Image::new(
    self.width,
    self.height,
    PixelFormat::RGBA8,
    Bytes::from_array(_buf),
  )
}

//-----------------------------------------------------------------------------
// BMP RLE8 Decoder
//-----------------------------------------------------------------------------

///|
/// Decode RLE8 compressed 8-bit BMP
fn BmpDecoder::decode_rle8(self : BmpDecoder) -> Image raise Failure {
  let palette = self.read_palette()
  let palette_rgba = palette_to_rgba_bytes(palette)
  let out_size = checked_image_size(self.width, self.height, 4)
  let _buf = Array::make(out_size, Byte::default())

  let row_step = if self.top_down { 1 } else { -1 }
  let start_row = if self.top_down { 0 } else { self.height - 1 }

  for pos = self.data_offset, row = start_row, col = 0, valid_row = true {
    // Use mutable locals for complex state mutations within one iteration
    let mut p = pos
    let mut r = row
    let mut c = col
    let mut vr = valid_row

    if self.data_end - p < 2 {
      raise Failure::Failure("BMP RLE8: unexpected end of data")
    }
    let count = self.data[p].to_int()
    let value = self.data[p + 1].to_int()
    p = p + 2

    if count == 0 {
      if value == 0 {
        // End of line
        if !vr {
          raise Failure::Failure("BMP RLE8: row out of bounds")
        }
        r = r + row_step
        vr = r >= 0 && r < self.height
        c = 0
        continue p, r, c, vr
      } else if value == 1 {
        // End of bitmap
        break
      } else if value == 2 {
        // Delta: move to new position
        if self.data_end - p < 2 {
          raise Failure::Failure("BMP RLE8: truncated delta")
        }
        let dx = self.data[p].to_int()
        let dy = self.data[p + 1].to_int()
        p = p + 2
        c = c + dx
        r = r + row_step * dy
        vr = r >= 0 && r < self.height
        if !vr || c > self.width {
          raise Failure::Failure("BMP RLE8: delta out of bounds")
        }
        continue p, r, c, vr
      } else {
        // Absolute run: next `value` bytes are literal indices
        let run_len = value
        if !vr || run_len > self.width - c {
          raise Failure::Failure("BMP RLE8: run exceeds row")
        }
        if run_len + run_len % 2 > self.data_end - p {
          raise Failure::Failure("BMP RLE8: truncated absolute run")
        }
        for i = 0; i < run_len; i = i + 1 {
          if vr {
            let idx = self.data[p].to_int()
            let dst = (r * self.width + c) * 4
            write_palette_pixel(
              _buf,
              dst,
              idx,
              self.palette_count,
              palette_rgba,
            )
          }
          c = c + 1
          p = p + 1
        }
        // Word-align: skip padding byte if run_len is odd
        if run_len % 2 == 1 {
          p = p + 1
        }
        continue p, r, c, vr
      }
    } else {
      // Encoded run: repeat `value` for `count` pixels
      if !vr || count > self.width - c {
        raise Failure::Failure("BMP RLE8: run exceeds row")
      }
      for _i = 0; _i < count; _i = _i + 1 {
        if vr {
          let dst = (r * self.width + c) * 4
          write_palette_pixel(
            _buf,
            dst,
            value,
            self.palette_count,
            palette_rgba,
          )
        }
        c = c + 1
      }
      continue p, r, c, vr
    }
  }

  Image::new(
    self.width,
    self.height,
    PixelFormat::RGBA8,
    Bytes::from_array(_buf),
  )
}

//-----------------------------------------------------------------------------
// BMP RLE4 Decoder
//-----------------------------------------------------------------------------

///|
/// Decode RLE4 compressed 4-bit BMP
fn BmpDecoder::decode_rle4(self : BmpDecoder) -> Image raise Failure {
  let palette = self.read_palette()
  let palette_rgba = palette_to_rgba_bytes(palette)
  let out_size = checked_image_size(self.width, self.height, 4)
  let _buf = Array::make(out_size, Byte::default())

  let row_step = if self.top_down { 1 } else { -1 }
  let start_row = if self.top_down { 0 } else { self.height - 1 }

  for pos = self.data_offset, row = start_row, col = 0, valid_row = true {
    // Use mutable locals for complex state mutations within one iteration
    let mut p = pos
    let mut r = row
    let mut c = col
    let mut vr = valid_row

    if self.data_end - p < 2 {
      raise Failure::Failure("BMP RLE4: unexpected end of data")
    }
    let count = self.data[p].to_int()
    let value = self.data[p + 1].to_int()
    p = p + 2

    if count == 0 {
      if value == 0 {
        // End of line
        if !vr {
          raise Failure::Failure("BMP RLE4: row out of bounds")
        }
        r = r + row_step
        vr = r >= 0 && r < self.height
        c = 0
        continue p, r, c, vr
      } else if value == 1 {
        // End of bitmap
        break
      } else if value == 2 {
        // Delta
        if self.data_end - p < 2 {
          raise Failure::Failure("BMP RLE4: truncated delta")
        }
        let dx = self.data[p].to_int()
        let dy = self.data[p + 1].to_int()
        p = p + 2
        c = c + dx
        r = r + row_step * dy
        vr = r >= 0 && r < self.height
        if !vr || c > self.width {
          raise Failure::Failure("BMP RLE4: delta out of bounds")
        }
        continue p, r, c, vr
      } else {
        // Absolute run
        let num_pixels = value
        let num_bytes = (num_pixels + 1) / 2
        if !vr || num_pixels > self.width - c {
          raise Failure::Failure("BMP RLE4: run exceeds row")
        }
        if num_bytes + num_bytes % 2 > self.data_end - p {
          raise Failure::Failure("BMP RLE4: truncated absolute run")
        }
        let mut pixels_read = 0
        for i = 0; i < num_bytes; i = i + 1 {
          let byte_val = self.data[p].to_int()
          p = p + 1
          // High nibble first
          let idx_hi = (byte_val >> 4) & 0x0F
          if pixels_read < num_pixels && vr {
            let dst = (r * self.width + c) * 4
            write_palette_pixel(
              _buf,
              dst,
              idx_hi,
              self.palette_count,
              palette_rgba,
            )
          }
          c = c + 1
          pixels_read = pixels_read + 1
          // Low nibble second
          let idx_lo = byte_val & 0x0F
          if pixels_read < num_pixels && vr {
            let dst = (r * self.width + c) * 4
            write_palette_pixel(
              _buf,
              dst,
              idx_lo,
              self.palette_count,
              palette_rgba,
            )
            c = c + 1
            pixels_read = pixels_read + 1
          }
        }
        // Word-align
        if num_bytes % 2 == 1 {
          p = p + 1
        }
        continue p, r, c, vr
      }
    } else {
      // Encoded run: alternate between two nibbles
      let hi_nib = (value >> 4) & 0x0F
      let lo_nib = value & 0x0F
      if !vr || count > self.width - c {
        raise Failure::Failure("BMP RLE4: run exceeds row")
      }
      for i = 0; i < count; i = i + 1 {
        let idx = if i % 2 == 0 { hi_nib } else { lo_nib }
        if vr {
          let dst = (r * self.width + c) * 4
          write_palette_pixel(_buf, dst, idx, self.palette_count, palette_rgba)
        }
        c = c + 1
      }
      continue p, r, c, vr
    }
  }

  Image::new(
    self.width,
    self.height,
    PixelFormat::RGBA8,
    Bytes::from_array(_buf),
  )
}