// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// A rectangle of samples, the format they are in, and how far apart the rows are.
//
// The row stride is separate from the width because a caller uploading to a GPU
// needs rows padded to a multiple of 256 bytes, and because a decoder that has
// to crop by moving a window would rather not copy. Every reader in this library
// produces tight rows; the stride is there for the caller that needs otherwise.
//
// Pixels are immutable once an image is built. Operations return a new image
// rather than writing into the old one, which costs a copy and buys the property
// that an image a caller is holding never changes under it.

///|
/// An image: its shape, its layout, and the bytes.
pub(all) struct Image {
  /// How many pixels across.
  width : Int32
  /// How many pixels down.
  height : Int32
  /// How each pixel is laid out.
  format : PixelFormat
  /// How many bytes from the start of one row to the start of the next.
  stride : Int32
  /// The samples, row after row, `height * stride` bytes or a little less when
  /// the last row is tight.
  pixels : Bytes
  /// Whether the colour channels have already been multiplied by alpha.
  premultiplied : Bool
  /// What the file said about itself.
  metadata : Metadata
}

///|
/// A black image of this shape, with tight rows.
///
/// `stride` is only needed when the rows must be padded; leave it out and the
/// rows are as wide as the pixels.
pub fn Image::new(
  width : Int32,
  height : Int32,
  format : PixelFormat,
  stride? : Int32,
  premultiplied? : Bool,
  metadata? : Metadata,
) -> Image {
  let tight = width * format.bytes()
  let stride = stride.unwrap_or(tight)
  let length = if height == 0 { 0 } else { stride * (height - 1) + tight }
  {
    width,
    height,
    format,
    stride,
    pixels: Bytes::new(length),
    premultiplied: premultiplied.unwrap_or(false),
    metadata: metadata.unwrap_or(Metadata::default()),
  }
}

///|
/// An image over bytes that already exist.
///
/// Raises when the bytes cannot hold the shape: a stride narrower than a row, or
/// fewer bytes than the last row needs.
pub fn Image::of(
  width : Int32,
  height : Int32,
  format : PixelFormat,
  pixels : Bytes,
  stride? : Int32,
  premultiplied? : Bool,
  metadata? : Metadata,
) -> Image raise Refused {
  if width < 0 || height < 0 {
    raise Unexpected(at=0, got="negative size", want="a width and a height")
  }
  let tight = width * format.bytes()
  let stride = stride.unwrap_or(tight)
  if stride < tight {
    raise Unexpected(at=0, got="stride \{stride}", want="at least \{tight}")
  }
  let needed = if height == 0 { 0 } else { stride * (height - 1) + tight }
  if pixels.length() < needed {
    raise Truncated(at=pixels.length(), want=needed - pixels.length())
  }
  {
    width,
    height,
    format,
    stride,
    pixels,
    premultiplied: premultiplied.unwrap_or(false),
    metadata: metadata.unwrap_or(Metadata::default()),
  }
}

///|
/// One row, without its padding.
pub fn Image::row(self : Image, y : Int32) -> BytesView raise Refused {
  if y < 0 || y >= self.height {
    raise OverLimit(limit=self.height, got=y)
  }
  let start = y * self.stride
  self.pixels.exact_view(start~, end=start + self.width * self.format.bytes())
}

///|
/// The pixel at a coordinate, in the units every operation works in.
///
/// Straight alpha, as stored: an image that says it is premultiplied returns
/// premultiplied samples, because pretending otherwise would be a conversion the
/// caller did not ask for.
pub fn Image::pixel(self : Image, x : Int32, y : Int32) -> Pixel raise Refused {
  if x < 0 || x >= self.width {
    raise OverLimit(limit=self.width, got=x)
  }
  if y < 0 || y >= self.height {
    raise OverLimit(limit=self.height, got=y)
  }
  let offset = y * self.stride + x * self.format.bytes()
  read_pixel(self.pixels, offset, self.format)
}

///|
/// The image over a buffer that has just been filled.
///
/// The buffer must hold `width * height` tight rows; every operation that
/// builds an image builds it that way, so the check `Image::of` does would only
/// ever pass.
fn assemble(
  width : Int32,
  height : Int32,
  format : PixelFormat,
  buffer : FixedArray[Byte],
  premultiplied : Bool,
  metadata : Metadata,
) -> Image {
  {
    width,
    height,
    format,
    stride: width * format.bytes(),
    pixels: Bytes::from_array(buffer.clamped_view()),
    premultiplied,
    metadata,
  }
}