///|
/// An 8-bit RGBA raster image.
///
/// Pixels are stored row-major in `data`, four bytes per pixel in R, G, B, A
/// order, so `data.length() == width * height * 4`. This layout matches the
/// browser `ImageData.data` buffer, which lets a `` hand its pixels to
/// the library without any conversion step.
pub(all) struct Image {
  width : Int
  height : Int
  data : FixedArray[Byte]
}

// Keep this limit in sync with the codec decoders.  Besides bounding memory
// use, checking the product before multiplying prevents hostile dimensions
// from wrapping an `Int` and reaching `FixedArray::make`.

///|
const MAX_IMAGE_PIXELS : Int = 100000000

///|
/// Computes the RGBA buffer length after validating image dimensions.
///
/// Image dimensions are non-negative and may contain at most
/// `MAX_IMAGE_PIXELS` pixels.  Empty images (where either dimension is zero)
/// are valid and use an empty backing buffer.  The checks are deliberately
/// performed before either multiplication so this helper is safe for
/// untrusted decoder input.
fn checked_buffer_len(width : Int, height : Int) -> Int {
  if width < 0 || height < 0 {
    abort("Image: width and height must be non-negative")
  }
  if width == 0 || height == 0 {
    return 0
  }
  if width > MAX_IMAGE_PIXELS / height {
    abort("Image: dimensions exceed the maximum pixel count")
  }
  let pixels = width * height
  // MAX_IMAGE_PIXELS is chosen so that this multiplication is safe on all
  // supported MoonBit Int targets (including 32-bit targets).
  pixels * 4
}

///|
/// Creates a fully transparent black image of the given size.
pub fn Image::new(width : Int, height : Int) -> Image {
  let len = checked_buffer_len(width, height)
  { width, height, data: FixedArray::make(len, b'\x00'), }
}

///|
/// Wraps an existing RGBA buffer. Aborts if `data` does not hold exactly
/// `width * height * 4` bytes, so a malformed buffer fails fast instead of
/// producing silently corrupt output.
pub fn Image::from_bytes(
  width : Int,
  height : Int,
  data : FixedArray[Byte],
) -> Image {
  let len = checked_buffer_len(width, height)
  if data.length() != len {
    abort("Image::from_bytes: buffer length must equal width * height * 4")
  }
  { width, height, data, }
}

///|
/// The number of pixels (not bytes) in the image.
pub fn Image::pixel_count(self : Image) -> Int {
  // Validate dimensions even when this method is called on a struct assembled
  // by a low-level host.  This keeps malformed public records from silently
  // propagating an overflowing pixel count.
  checked_buffer_len(self.width, self.height) / 4
}

///|
/// Returns a deep copy that shares no mutable state with the original.
pub fn Image::copy(self : Image) -> Image {
  { width: self.width, height: self.height, data: self.data.copy(), }
}

///|
/// Reads the (R, G, B, A) channels of the pixel at (`x`, `y`).
///
/// Aborts when the coordinates are outside the image bounds or when a host
/// supplies an `Image` record whose backing buffer does not match its size.
pub fn Image::get_pixel(
  self : Image,
  x : Int,
  y : Int,
) -> (Byte, Byte, Byte, Byte) {
  let i = self.pixel_offset(x, y)
  (self.data[i], self.data[i + 1], self.data[i + 2], self.data[i + 3])
}

///|
/// Writes the (R, G, B, A) channels of the pixel at (`x`, `y`).
///
/// Aborts when the coordinates are outside the image bounds or when a host
/// supplies an `Image` record whose backing buffer does not match its size.
pub fn Image::set_pixel(
  self : Image,
  x : Int,
  y : Int,
  r : Byte,
  g : Byte,
  b : Byte,
  a : Byte,
) -> Unit {
  let i = self.pixel_offset(x, y)
  self.data[i] = r
  self.data[i + 1] = g
  self.data[i + 2] = b
  self.data[i + 3] = a
}

///|
/// Returns the byte offset for one pixel after checking the image invariant
/// and coordinate bounds.  Keeping this in one place makes all public pixel
/// accessors fail with a deterministic message instead of a raw array panic.
fn Image::pixel_offset(self : Image, x : Int, y : Int) -> Int {
  let len = checked_buffer_len(self.width, self.height)
  if self.data.length() != len {
    abort("Image: backing buffer length does not match dimensions")
  }
  if x < 0 || x >= self.width || y < 0 || y >= self.height {
    abort("Image: pixel coordinates are out of bounds")
  }
  (y * self.width + x) * 4
}

///|
/// Clamps an integer into the `[0, 255]` range and narrows it to a `Byte`.
/// Channel math is done in `Int` to avoid wrap-around, then funnelled through
/// this helper before being stored back into a pixel buffer.
pub fn clamp_byte(value : Int) -> Byte {
  if value < 0 {
    b'\x00'
  } else if value > 255 {
    b'\xFF'
  } else {
    value.to_byte()
  }
}