// Copyright (c) 2025 lws
// BMP (Bitmap) image encoder
//
// Encodes images as uncompressed BMP files:
//   - 24-bit BGR when all pixels are fully opaque
//   - 32-bit BGRA when any pixel has transparency
//
// Output uses a negative height in the DIB header to signal top-down
// row order, which is simpler and avoids manual row reversal.

//-----------------------------------------------------------------------------
// Little-endian write helpers
//-----------------------------------------------------------------------------

///|
/// Write a 16-bit unsigned integer in little-endian byte order
fn write_u16_le(buf : Array[Byte], pos : Int, val : Int) -> Unit {
  buf[pos] = (val & 0xFF).to_byte()
  buf[pos + 1] = ((val >> 8) & 0xFF).to_byte()
}

///|
/// Write a 32-bit unsigned integer in little-endian byte order
fn write_u32_le(buf : Array[Byte], pos : Int, val : Int) -> Unit {
  buf[pos] = (val & 0xFF).to_byte()
  buf[pos + 1] = ((val >> 8) & 0xFF).to_byte()
  buf[pos + 2] = ((val >> 16) & 0xFF).to_byte()
  buf[pos + 3] = ((val >> 24) & 0xFF).to_byte()
}

//-----------------------------------------------------------------------------
// Alpha detection
//-----------------------------------------------------------------------------

///|
/// Check whether any pixel in an RGBA8 image has a non-opaque alpha value
fn has_alpha(rgba : Image) -> Bool {
  let data = rgba.data
  let pixel_count = rgba.width * rgba.height
  for i = 0; i < pixel_count; i = i + 1 {
    let a = data[i * 4 + 3].to_int()
    if a != 255 {
      return true
    }
  }
  false
}

//-----------------------------------------------------------------------------
// Row stride with 4-byte alignment
//-----------------------------------------------------------------------------

///|
/// Compute the BMP row stride in bytes, padded to a 4-byte boundary
fn bmp_encode_stride(width : Int, bpp : Int) -> Int {
  let row_bytes = width * bpp / 8
  (row_bytes + 3) / 4 * 4
}

//-----------------------------------------------------------------------------
// BMP Encoder
//-----------------------------------------------------------------------------

///|
/// Encode an Image as an uncompressed BMP file.
///
/// The image is converted to RGBA8 internally. If all pixels are fully opaque
/// (alpha = 255 everywhere), the output is 24-bit BGR. Otherwise it is 32-bit
/// BGRA to preserve transparency.
///
/// Pixel data is written top-down with a negative height in the DIB header,
/// so most BMP readers display the image correctly without additional flipping.
///
/// # Errors
///
/// Raises `Failure` if the image has zero width or height.
pub fn encode_bmp(image : Image) -> Bytes raise Failure {
  // Convert to RGBA8 for uniform pixel access
  let rgba = image.to_rgba8()

  // Reject empty images
  if rgba.width <= 0 || rgba.height <= 0 {
    raise Failure::Failure("BMP encode: image dimensions must be positive")
  }

  // Determine bit depth: 32-bit if any pixel has transparency, else 24-bit
  let use_alpha = has_alpha(rgba)
  let bpp = if use_alpha { 32 } else { 24 }
  let bytes_per_pixel = bpp / 8

  // Calculate sizes
  let header_size = 14 + 40
  let row_stride = bmp_encode_stride(rgba.width, bpp)
  let pixel_data_size = row_stride * rgba.height
  let file_size = header_size + pixel_data_size

  // Allocate output buffer
  let buf = Array::make(file_size, b'\x00')

  // === BMP File Header (14 bytes) ===

  // bytes 0-1: "BM" signature
  buf[0] = b'B'
  buf[1] = b'M'

  // bytes 2-5: total file size (little-endian)
  write_u32_le(buf, 2, file_size)

  // bytes 6-9: reserved (already zero-initialized)

  // bytes 10-13: offset from start of file to pixel data (54 = 14 + 40)
  write_u32_le(buf, 10, header_size)

  // === BITMAPINFOHEADER (40 bytes, starting at file offset 14) ===

  // bytes 0-3 (file 14-17): DIB header size (40)
  write_u32_le(buf, 14, 40)

  // bytes 4-7 (file 18-21): image width
  write_u32_le(buf, 18, rgba.width)

  // bytes 8-11 (file 22-25): image height
  // Negative value signals top-down row order, avoiding the need to flip rows
  write_u32_le(buf, 22, -rgba.height)

  // bytes 12-13 (file 26-27): color planes (must be 1)
  write_u16_le(buf, 26, 1)

  // bytes 14-15 (file 28-29): bits per pixel (24 or 32)
  write_u16_le(buf, 28, bpp)

  // bytes 16-19 (file 30-33): compression method (0 = BI_RGB, no compression)
  // already zero-initialized

  // bytes 20-23 (file 34-37): image size (may be 0 for BI_RGB)
  // already zero-initialized

  // bytes 24-27 (file 38-41): horizontal resolution in pixels per meter
  // 2835 ppm ≈ 72 DPI
  write_u32_le(buf, 38, 2835)

  // bytes 28-31 (file 42-45): vertical resolution in pixels per meter
  write_u32_le(buf, 42, 2835)

  // bytes 32-35 (file 46-49): number of colors in palette (0 = full palette)
  // already zero-initialized

  // bytes 36-39 (file 50-53): number of important colors (0 = all important)
  // already zero-initialized

  // === Pixel Data ===
  //
  // Written top-down (row 0 = top of image). The negative height in the DIB
  // header tells BMP readers not to flip the rows.
  //
  // BMP stores pixels in BGR / BGRA byte order (blue first, red third).
  // Each row is padded to a multiple of 4 bytes with zeros.

  let src_data = rgba.data
  let src_stride = rgba.stride()

  for row = 0; row < rgba.height; row = row + 1 {
    let dst_row_offset = header_size + row * row_stride

    for col = 0; col < rgba.width; col = col + 1 {
      let src = row * src_stride + col * 4
      let dst = dst_row_offset + col * bytes_per_pixel

      // BGR / BGRA byte order (reversed from the source RGBA order)
      buf[dst] = src_data[src + 2] // B
      buf[dst + 1] = src_data[src + 1] // G
      buf[dst + 2] = src_data[src] // R
      if use_alpha {
        buf[dst + 3] = src_data[src + 3] // A
      }
    }
  }

  Bytes::from_array(buf)
}