///|
/// Asset contracts (image/shader/material/atlas).
///
/// Ebiten refs:
/// - internal/atlas/image.go
/// - internal/atlas/shader.go

///|
pub struct AssetKey {
  value : String
} derive(Debug)

///|
pub impl Show for AssetKey with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub fn new_asset_key(value : String) -> AssetKey {
  { value, }
}

///|
pub struct ImageSpec {
  width : Int
  height : Int
  format : String
  palette : ImagePalette2x2
  pixels_rgba8 : Array[Int]
} derive(Debug)

///|
pub impl Show for ImageSpec with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub struct ShaderSpec {
  debug_name : String
  source : String
} derive(Debug)

///|
pub impl Show for ShaderSpec with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub struct MaterialSpec {
  shader : AssetKey
  blend : @gfx.BlendMode
  filter : @gfx.FilterMode
  uniform_keys : Array[String]
} derive(Debug)

///|
pub impl Show for MaterialSpec with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub struct AtlasRegion {
  x : Int
  y : Int
  width : Int
  height : Int
  atlas_id : Int
} derive(Debug)

///|
pub impl Show for AtlasRegion with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub fn new_atlas_region(
  x : Int,
  y : Int,
  width : Int,
  height : Int,
  atlas_id : Int,
) -> AtlasRegion {
  { x, y, width, height, atlas_id }
}

///|
pub struct ImagePaletteBinding {
  image_id : Int
  palette : ImagePalette2x2
} derive(Debug)

///|
pub impl Show for ImagePaletteBinding with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub struct SourceImageDirtyRect {
  x : Int
  y : Int
  width : Int
  height : Int
} derive(Debug)

///|
pub impl Show for SourceImageDirtyRect with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub struct SourceImageBinding {
  image_id : Int
  width : Int
  height : Int
  generation : Int
  dirty_rect : SourceImageDirtyRect?
  palette : ImagePalette2x2
  pixels_rgba8 : Array[Int]
} derive(Debug)

///|
pub impl Show for SourceImageBinding with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub struct AtlasDrawSource {
  page_image_id : Int
  region : AtlasRegion
  u0 : Double
  v0 : Double
  u1 : Double
  v1 : Double
} derive(Debug)

///|
pub impl Show for AtlasDrawSource with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub fn new_source_image_binding(
  image_id : Int,
  width : Int,
  height : Int,
  generation : Int,
  dirty_rect : SourceImageDirtyRect?,
  palette : ImagePalette2x2,
  pixels_rgba8 : Array[Int],
) -> SourceImageBinding {
  { image_id, width, height, generation, dirty_rect, palette, pixels_rgba8 }
}

///|
pub fn new_source_image_dirty_rect(
  x : Int,
  y : Int,
  width : Int,
  height : Int,
) -> SourceImageDirtyRect {
  { x, y, width, height }
}

///|
pub fn new_unmanaged_atlas_draw_source(image_id : Int) -> AtlasDrawSource {
  let safe_image_id = if image_id < 0 { 0 } else { image_id }
  {
    page_image_id: safe_image_id,
    region: { x: 0, y: 0, width: 1, height: 1, atlas_id: safe_image_id },
    u0: 0.0,
    v0: 0.0,
    u1: 1.0,
    v1: 1.0,
  }
}

///|
pub trait ImageRepository {
  create_image(Self, key : AssetKey, spec : ImageSpec) -> @gfx.ImageHandle raise
  get_image(Self, key : AssetKey) -> @gfx.ImageHandle?
  remove_image(Self, key : AssetKey) -> Unit raise
}

///|
pub trait ShaderRepository {
  create_shader(Self, key : AssetKey, spec : ShaderSpec) -> @gfx.ShaderHandle raise
  get_shader(Self, key : AssetKey) -> @gfx.ShaderHandle?
  remove_shader(Self, key : AssetKey) -> Unit raise
}

///|
pub trait MaterialRepository {
  register_material(Self, key : AssetKey, spec : MaterialSpec) -> Unit raise
  get_material(Self, key : AssetKey) -> MaterialSpec?
}

///|
pub trait AtlasAllocator {
  allocate(Self, width : Int, height : Int) -> AtlasRegion?
  release(Self, region : AtlasRegion) -> Unit
}

///|
pub fn default_image_spec(width : Int, height : Int) -> ImageSpec {
  let seed = if width > 0 && height > 0 { width * 65537 + height } else { 0 }
  {
    width,
    height,
    format: "rgba8unorm",
    palette: checker_palette_from_seed(seed),
    pixels_rgba8: [],
  }
}

///|
pub fn image_spec_with_palette(
  width : Int,
  height : Int,
  palette : ImagePalette2x2,
) -> ImageSpec {
  { width, height, format: "rgba8unorm", palette, pixels_rgba8: [] }
}

///|
fn clamp_u8_channel(channel : Int) -> Int {
  if channel < 0 {
    0
  } else if channel > 255 {
    255
  } else {
    channel
  }
}

///|
fn normalized_rgba8_channels(
  pixels_rgba8 : Array[Int],
  width : Int,
  height : Int,
) -> Array[Int] {
  let safe_width = if width <= 0 { 1 } else { width }
  let safe_height = if height <= 0 { 1 } else { height }
  let expected = safe_width * safe_height * 4
  let normalized : Array[Int] = []
  if expected <= 0 || pixels_rgba8.length() < expected {
    normalized
  } else {
    for i in 0.. Int {
  let base = (y * width + x) * 4 + channel_index
  if base < 0 || base >= pixels_rgba8.length() {
    0
  } else {
    clamp_u8_channel(pixels_rgba8[base])
  }
}

///|
fn palette_from_rgba8_channels(
  width : Int,
  height : Int,
  pixels_rgba8 : Array[Int],
) -> ImagePalette2x2 {
  let safe_width = if width <= 0 { 1 } else { width }
  let safe_height = if height <= 0 { 1 } else { height }
  let expected = safe_width * safe_height * 4
  if expected <= 0 || pixels_rgba8.length() < expected {
    checker_palette_from_seed(safe_width * 65537 + safe_height)
  } else {
    let x1 = safe_width - 1
    let y1 = safe_height - 1
    new_image_palette_2x2(
      new_rgba(
        sample_rgba8_channel(pixels_rgba8, safe_width, 0, 0, 0),
        sample_rgba8_channel(pixels_rgba8, safe_width, 0, 0, 1),
        sample_rgba8_channel(pixels_rgba8, safe_width, 0, 0, 2),
        sample_rgba8_channel(pixels_rgba8, safe_width, 0, 0, 3),
      ),
      new_rgba(
        sample_rgba8_channel(pixels_rgba8, safe_width, x1, 0, 0),
        sample_rgba8_channel(pixels_rgba8, safe_width, x1, 0, 1),
        sample_rgba8_channel(pixels_rgba8, safe_width, x1, 0, 2),
        sample_rgba8_channel(pixels_rgba8, safe_width, x1, 0, 3),
      ),
      new_rgba(
        sample_rgba8_channel(pixels_rgba8, safe_width, 0, y1, 0),
        sample_rgba8_channel(pixels_rgba8, safe_width, 0, y1, 1),
        sample_rgba8_channel(pixels_rgba8, safe_width, 0, y1, 2),
        sample_rgba8_channel(pixels_rgba8, safe_width, 0, y1, 3),
      ),
      new_rgba(
        sample_rgba8_channel(pixels_rgba8, safe_width, x1, y1, 0),
        sample_rgba8_channel(pixels_rgba8, safe_width, x1, y1, 1),
        sample_rgba8_channel(pixels_rgba8, safe_width, x1, y1, 2),
        sample_rgba8_channel(pixels_rgba8, safe_width, x1, y1, 3),
      ),
    )
  }
}

///|
pub fn image_spec_with_rgba8(
  width : Int,
  height : Int,
  pixels_rgba8 : Array[Int],
) -> ImageSpec {
  let normalized = normalized_rgba8_channels(pixels_rgba8, width, height)
  {
    width,
    height,
    format: "rgba8unorm",
    palette: palette_from_rgba8_channels(width, height, normalized),
    pixels_rgba8: normalized,
  }
}

///|
fn expected_rgba8_channel_count(width : Int, height : Int) -> Int {
  let safe_width = if width <= 0 { 1 } else { width }
  let safe_height = if height <= 0 { 1 } else { height }
  safe_width * safe_height * 4
}

///|
fn has_valid_rgba8_payload(
  pixels_rgba8 : Array[Int],
  width : Int,
  height : Int,
) -> Bool {
  let expected = expected_rgba8_channel_count(width, height)
  expected > 0 && pixels_rgba8.length() >= expected
}

///|
fn image_palette_eq(
  lhs : ImagePalette2x2,
  rhs : ImagePalette2x2,
) -> Bool {
  let mut ok = true
  for pixel_index in 0..<4 {
    for channel_index in 0..<4 {
      if palette_channel(lhs, pixel_index, channel_index) !=
        palette_channel(rhs, pixel_index, channel_index) {
        ok = false
      }
    }
  }
  ok
}

///|
fn image_pixels_eq(
  lhs : Array[Int],
  rhs : Array[Int],
  width : Int,
  height : Int,
) -> Bool {
  if !has_valid_rgba8_payload(lhs, width, height) ||
    !has_valid_rgba8_payload(rhs, width, height) {
    lhs.length() == 0 && rhs.length() == 0
  } else {
    let expected = expected_rgba8_channel_count(width, height)
    let mut ok = true
    for i in 0.. SourceImageDirtyRect {
  {
    x: 0,
    y: 0,
    width: if width <= 0 {
      1
    } else {
      width
    },
    height: if height <= 0 {
      1
    } else {
      height
    },
  }
}

///|
fn diff_dirty_rect_from_pixels(
  old_pixels : Array[Int],
  new_pixels : Array[Int],
  width : Int,
  height : Int,
) -> SourceImageDirtyRect? {
  if !has_valid_rgba8_payload(old_pixels, width, height) ||
    !has_valid_rgba8_payload(new_pixels, width, height) {
    if old_pixels.length() == 0 && new_pixels.length() == 0 {
      None
    } else {
      Some(source_dirty_rect_full(width, height))
    }
  } else {
    let mut min_x = width
    let mut min_y = height
    let mut max_x = -1
    let mut max_y = -1
    for y in 0.. max_x {
            max_x = x
          }
          if y > max_y {
            max_y = y
          }
        }
      }
    }
    if max_x < 0 || max_y < 0 {
      None
    } else {
      Some({
        x: min_x,
        y: min_y,
        width: max_x - min_x + 1,
        height: max_y - min_y + 1,
      })
    }
  }
}

///|
fn merge_dirty_rects(
  lhs : SourceImageDirtyRect?,
  rhs : SourceImageDirtyRect?,
) -> SourceImageDirtyRect? {
  match lhs {
    None => rhs
    Some(a) =>
      match rhs {
        None => Some(a)
        Some(b) => {
          let x0 = if a.x < b.x { a.x } else { b.x }
          let y0 = if a.y < b.y { a.y } else { b.y }
          let ax1 = a.x + a.width
          let bx1 = b.x + b.width
          let ay1 = a.y + a.height
          let by1 = b.y + b.height
          let x1 = if ax1 > bx1 { ax1 } else { bx1 }
          let y1 = if ay1 > by1 { ay1 } else { by1 }
          Some({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 })
        }
      }
  }
}

///|
struct SimpleImageEntry {
  key : AssetKey
  handle : @gfx.ImageHandle
  palette : ImagePalette2x2
  pixels_rgba8 : Array[Int]
  generation : Int
  dirty_rect : SourceImageDirtyRect?
} derive(Debug)

///|
pub impl Show for SimpleImageEntry with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
struct SimpleShaderEntry {
  key : AssetKey
  handle : @gfx.ShaderHandle
} derive(Debug)

///|
pub impl Show for SimpleShaderEntry with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
struct SimpleMaterialEntry {
  key : AssetKey
  spec : MaterialSpec
} derive(Debug)

///|
pub impl Show for SimpleMaterialEntry with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub struct SimpleImageRepository {
  mut next_image_id : Int
  mut images : Array[SimpleImageEntry]
}

///|
pub struct SimpleShaderRepository {
  mut next_shader_id : Int
  mut shaders : Array[SimpleShaderEntry]
}

///|
pub struct SimpleMaterialRepository {
  mut materials : Array[SimpleMaterialEntry]
}

///|
fn asset_key_eq(lhs : AssetKey, rhs : AssetKey) -> Bool {
  lhs.value == rhs.value
}

///|
fn find_image_handle(
  images : Array[SimpleImageEntry],
  key : AssetKey,
) -> @gfx.ImageHandle? {
  let mut out : @gfx.ImageHandle? = None
  for entry in images {
    if asset_key_eq(entry.key, key) {
      out = Some(entry.handle)
    }
  }
  out
}

///|
fn find_image_palette_by_id(
  images : Array[SimpleImageEntry],
  image_id : Int,
) -> ImagePalette2x2? {
  let mut out : ImagePalette2x2? = None
  for entry in images {
    if entry.handle.id == image_id {
      out = Some(entry.palette)
    }
  }
  out
}

///|
fn find_image_pixels_by_id(
  images : Array[SimpleImageEntry],
  image_id : Int,
) -> Array[Int]? {
  let mut out : Array[Int]? = None
  for entry in images {
    if entry.handle.id == image_id {
      out = Some(entry.pixels_rgba8)
    }
  }
  out
}

///|
fn find_image_generation_by_id(
  images : Array[SimpleImageEntry],
  image_id : Int,
) -> Int? {
  let mut out : Int? = None
  for entry in images {
    if entry.handle.id == image_id {
      out = Some(entry.generation)
    }
  }
  out
}

///|
fn find_shader_handle(
  shaders : Array[SimpleShaderEntry],
  key : AssetKey,
) -> @gfx.ShaderHandle? {
  let mut out : @gfx.ShaderHandle? = None
  for entry in shaders {
    if asset_key_eq(entry.key, key) {
      out = Some(entry.handle)
    }
  }
  out
}

///|
fn find_material_spec(
  materials : Array[SimpleMaterialEntry],
  key : AssetKey,
) -> MaterialSpec? {
  let mut out : MaterialSpec? = None
  for entry in materials {
    if asset_key_eq(entry.key, key) {
      out = Some(entry.spec)
    }
  }
  out
}

///|
pub fn new_simple_image_repository() -> SimpleImageRepository {
  { next_image_id: 1, images: [] }
}

///|
pub fn get_image_palette_by_id(
  repo : SimpleImageRepository,
  image_id : Int,
) -> ImagePalette2x2? {
  find_image_palette_by_id(repo.images, image_id)
}

///|
pub fn list_image_palette_bindings(
  repo : SimpleImageRepository,
) -> Array[ImagePaletteBinding] {
  let bindings : Array[ImagePaletteBinding] = []
  for entry in repo.images {
    bindings.push({ image_id: entry.handle.id, palette: entry.palette })
  }
  bindings
}

///|
pub fn get_image_pixels_by_id(
  repo : SimpleImageRepository,
  image_id : Int,
) -> Array[Int]? {
  find_image_pixels_by_id(repo.images, image_id)
}

///|
pub fn get_image_generation_by_id(
  repo : SimpleImageRepository,
  image_id : Int,
) -> Int {
  match find_image_generation_by_id(repo.images, image_id) {
    Some(generation) => if generation <= 0 { 1 } else { generation }
    None => 0
  }
}

///|
pub fn list_source_image_bindings(
  repo : SimpleImageRepository,
) -> Array[SourceImageBinding] {
  let bindings : Array[SourceImageBinding] = []
  for entry in repo.images {
    bindings.push({
      image_id: entry.handle.id,
      width: entry.handle.width,
      height: entry.handle.height,
      generation: entry.generation,
      dirty_rect: entry.dirty_rect,
      palette: entry.palette,
      pixels_rgba8: entry.pixels_rgba8,
    })
  }
  bindings
}

///|
pub fn list_dirty_source_image_bindings(
  repo : SimpleImageRepository,
) -> Array[SourceImageBinding] {
  let bindings : Array[SourceImageBinding] = []
  for entry in repo.images {
    match entry.dirty_rect {
      Some(rect) =>
        bindings.push({
          image_id: entry.handle.id,
          width: entry.handle.width,
          height: entry.handle.height,
          generation: entry.generation,
          dirty_rect: Some(rect),
          palette: entry.palette,
          pixels_rgba8: entry.pixels_rgba8,
        })
      None => ()
    }
  }
  bindings
}

///|
pub fn clear_source_image_dirty_flags(repo : SimpleImageRepository) -> Int {
  let next : Array[SimpleImageEntry] = []
  let mut cleared = 0
  for entry in repo.images {
    match entry.dirty_rect {
      Some(_) => {
        next.push({
          key: entry.key,
          handle: entry.handle,
          palette: entry.palette,
          pixels_rgba8: entry.pixels_rgba8,
          generation: entry.generation,
          dirty_rect: None,
        })
        cleared = cleared + 1
      }
      None => next.push(entry)
    }
  }
  repo.images = next
  cleared
}

///|
fn next_image_generation(current : Int) -> Int {
  if current <= 0 {
    1
  } else {
    current + 1
  }
}

///|
pub fn update_image_spec(
  repo : SimpleImageRepository,
  key : AssetKey,
  spec : ImageSpec,
) -> @gfx.ImageHandle? {
  let width = if spec.width <= 0 { 1 } else { spec.width }
  let height = if spec.height <= 0 { 1 } else { spec.height }
  let pixels_rgba8 = normalized_rgba8_channels(spec.pixels_rgba8, width, height)
  let next : Array[SimpleImageEntry] = []
  let mut updated : @gfx.ImageHandle? = None
  for entry in repo.images {
    if asset_key_eq(entry.key, key) {
      if entry.handle.width == width && entry.handle.height == height {
        let old_has_pixels = has_valid_rgba8_payload(
          entry.pixels_rgba8,
          width,
          height,
        )
        let new_has_pixels = has_valid_rgba8_payload(
          pixels_rgba8, width, height,
        )
        let pixels_changed = !image_pixels_eq(
          entry.pixels_rgba8,
          pixels_rgba8,
          width,
          height,
        )
        let palette_changed = !image_palette_eq(entry.palette, spec.palette)
        let diff_rect = if pixels_changed {
          diff_dirty_rect_from_pixels(
            entry.pixels_rgba8,
            pixels_rgba8,
            width,
            height,
          )
        } else {
          None
        }
        let palette_rect = if palette_changed &&
          !(old_has_pixels && new_has_pixels) {
          Some(source_dirty_rect_full(width, height))
        } else {
          None
        }
        let dirty_rect = merge_dirty_rects(
          entry.dirty_rect,
          merge_dirty_rects(diff_rect, palette_rect),
        )
        if pixels_changed || palette_changed {
          next.push({
            key: entry.key,
            handle: entry.handle,
            palette: spec.palette,
            pixels_rgba8,
            generation: next_image_generation(entry.generation),
            dirty_rect,
          })
        } else {
          next.push(entry)
        }
        updated = Some(entry.handle)
      } else {
        next.push(entry)
      }
    } else {
      next.push(entry)
    }
  }
  repo.images = next
  updated
}

///|
pub fn new_simple_shader_repository() -> SimpleShaderRepository {
  { next_shader_id: 1, shaders: [] }
}

///|
pub fn new_simple_material_repository() -> SimpleMaterialRepository {
  { materials: [] }
}

///|
pub impl ImageRepository for SimpleImageRepository with create_image(
  self,
  key,
  spec,
) {
  match find_image_handle(self.images, key) {
    Some(handle) => handle
    None => {
      let width = if spec.width <= 0 { 1 } else { spec.width }
      let height = if spec.height <= 0 { 1 } else { spec.height }
      let pixels_rgba8 = normalized_rgba8_channels(
        spec.pixels_rgba8,
        width,
        height,
      )
      let handle = @gfx.new_image_handle(self.next_image_id, width, height)
      self.next_image_id = self.next_image_id + 1
      self.images.push({
        key,
        handle,
        palette: spec.palette,
        pixels_rgba8,
        generation: 1,
        dirty_rect: Some(source_dirty_rect_full(width, height)),
      })
      handle
    }
  }
}

///|
pub impl ImageRepository for SimpleImageRepository with get_image(self, key) {
  find_image_handle(self.images, key)
}

///|
pub impl ImageRepository for SimpleImageRepository with remove_image(self, key) {
  let next : Array[SimpleImageEntry] = []
  for entry in self.images {
    if !asset_key_eq(entry.key, key) {
      next.push(entry)
    }
  }
  self.images = next
}

///|
pub impl ShaderRepository for SimpleShaderRepository with create_shader(
  self,
  key,
  spec,
) {
  match find_shader_handle(self.shaders, key) {
    Some(handle) => handle
    None => {
      let handle = @gfx.new_shader_handle(self.next_shader_id, spec.source)
      self.next_shader_id = self.next_shader_id + 1
      self.shaders.push({ key, handle })
      handle
    }
  }
}

///|
pub impl ShaderRepository for SimpleShaderRepository with get_shader(self, key) {
  find_shader_handle(self.shaders, key)
}

///|
pub impl ShaderRepository for SimpleShaderRepository with remove_shader(
  self,
  key,
) {
  let next : Array[SimpleShaderEntry] = []
  for entry in self.shaders {
    if !asset_key_eq(entry.key, key) {
      next.push(entry)
    }
  }
  self.shaders = next
}

///|
pub impl MaterialRepository for SimpleMaterialRepository with register_material(
  self,
  key,
  spec,
) {
  let next : Array[SimpleMaterialEntry] = []
  let mut replaced = false
  for entry in self.materials {
    if asset_key_eq(entry.key, key) {
      next.push({ key, spec })
      replaced = true
    } else {
      next.push(entry)
    }
  }
  if !replaced {
    next.push({ key, spec })
  }
  self.materials = next
}

///|
pub impl MaterialRepository for SimpleMaterialRepository with get_material(
  self,
  key,
) {
  find_material_spec(self.materials, key)
}

///|
pub struct SimpleAtlasAllocator {
  atlas_width : Int
  atlas_height : Int
  atlas_id : Int
  mut cursor_x : Int
  mut cursor_y : Int
  mut row_height : Int
  mut allocated : Array[AtlasRegion]
}

///|
fn atlas_region_eq(lhs : AtlasRegion, rhs : AtlasRegion) -> Bool {
  lhs.x == rhs.x &&
  lhs.y == rhs.y &&
  lhs.width == rhs.width &&
  lhs.height == rhs.height &&
  lhs.atlas_id == rhs.atlas_id
}

///|
pub fn new_simple_atlas_allocator(
  atlas_width : Int,
  atlas_height : Int,
  atlas_id : Int,
) -> SimpleAtlasAllocator {
  {
    atlas_width: if atlas_width <= 0 {
      1
    } else {
      atlas_width
    },
    atlas_height: if atlas_height <= 0 {
      1
    } else {
      atlas_height
    },
    atlas_id,
    cursor_x: 0,
    cursor_y: 0,
    row_height: 0,
    allocated: [],
  }
}

///|
pub impl AtlasAllocator for SimpleAtlasAllocator with allocate(
  self,
  width,
  height,
) {
  if width <= 0 || height <= 0 {
    None
  } else if width > self.atlas_width || height > self.atlas_height {
    None
  } else {
    if self.cursor_x + width > self.atlas_width {
      self.cursor_x = 0
      self.cursor_y = self.cursor_y + self.row_height
      self.row_height = 0
    }
    if self.cursor_y + height > self.atlas_height {
      None
    } else {
      let region : AtlasRegion = {
        x: self.cursor_x,
        y: self.cursor_y,
        width,
        height,
        atlas_id: self.atlas_id,
      }
      self.cursor_x = self.cursor_x + width
      if height > self.row_height {
        self.row_height = height
      }
      self.allocated.push(region)
      Some(region)
    }
  }
}

///|
pub impl AtlasAllocator for SimpleAtlasAllocator with release(self, region) {
  let next : Array[AtlasRegion] = []
  for current in self.allocated {
    if !atlas_region_eq(current, region) {
      next.push(current)
    }
  }
  self.allocated = next
}

///|
pub fn atlas_allocator_is_empty(allocator : SimpleAtlasAllocator) -> Bool {
  allocator.allocated.length() == 0
}

///|
pub fn atlas_allocator_reset_if_empty(allocator : SimpleAtlasAllocator) -> Bool {
  if allocator.allocated.length() == 0 {
    allocator.cursor_x = 0
    allocator.cursor_y = 0
    allocator.row_height = 0
    true
  } else {
    false
  }
}

///|
struct SimpleAtlasImageEntry {
  key : AssetKey
  handle : @gfx.ImageHandle
  region : AtlasRegion
  palette : ImagePalette2x2
  pixels_rgba8 : Array[Int]
} derive(Debug)

///|
pub impl Show for SimpleAtlasImageEntry with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
///
/// Atlas image repository with page-level dirty tracking.
///
/// Ebiten refs:
/// - internal/atlas/image.go
/// - internal/atlas/image.go (writePixels / region updates)
pub struct SimpleAtlasImageRepository {
  atlas_width : Int
  atlas_height : Int
  atlas_id : Int
  mut next_image_id : Int
  allocator : SimpleAtlasAllocator
  mut images : Array[SimpleAtlasImageEntry]
  mut page_generation : Int
  mut page_dirty_rect : SourceImageDirtyRect?
}

///|
fn atlas_region_to_dirty_rect(region : AtlasRegion) -> SourceImageDirtyRect {
  { x: region.x, y: region.y, width: region.width, height: region.height }
}

///|
fn atlas_local_dirty_to_page_dirty(
  region : AtlasRegion,
  local_rect : SourceImageDirtyRect?,
) -> SourceImageDirtyRect? {
  match local_rect {
    Some(rect) =>
      Some({
        x: region.x + rect.x,
        y: region.y + rect.y,
        width: rect.width,
        height: rect.height,
      })
    None => None
  }
}

///|
fn clamp_atlas_coord(value : Int, bound : Int) -> Int {
  if value < 0 {
    0
  } else if value > bound {
    bound
  } else {
    value
  }
}

///|
fn atlas_uv_from_region(
  region : AtlasRegion,
  atlas_width : Int,
  atlas_height : Int,
) -> AtlasDrawSource {
  let safe_width = if atlas_width <= 0 { 1 } else { atlas_width }
  let safe_height = if atlas_height <= 0 { 1 } else { atlas_height }
  let x0 = clamp_atlas_coord(region.x, safe_width)
  let y0 = clamp_atlas_coord(region.y, safe_height)
  let x1 = clamp_atlas_coord(region.x + region.width, safe_width)
  let y1 = clamp_atlas_coord(region.y + region.height, safe_height)
  {
    page_image_id: region.atlas_id,
    region,
    u0: x0.to_double() / safe_width.to_double(),
    v0: y0.to_double() / safe_height.to_double(),
    u1: x1.to_double() / safe_width.to_double(),
    v1: y1.to_double() / safe_height.to_double(),
  }
}

///|
fn mark_atlas_page_dirty(
  repo : SimpleAtlasImageRepository,
  dirty_rect : SourceImageDirtyRect?,
) -> Unit {
  match dirty_rect {
    Some(rect) => {
      repo.page_dirty_rect = merge_dirty_rects(repo.page_dirty_rect, Some(rect))
      repo.page_generation = next_image_generation(repo.page_generation)
    }
    None => ()
  }
}

///|
pub fn new_simple_atlas_image_repository(
  atlas_width : Int,
  atlas_height : Int,
  atlas_id : Int,
) -> SimpleAtlasImageRepository {
  let safe_width = if atlas_width <= 0 { 1 } else { atlas_width }
  let safe_height = if atlas_height <= 0 { 1 } else { atlas_height }
  {
    atlas_width: safe_width,
    atlas_height: safe_height,
    atlas_id,
    next_image_id: 1,
    allocator: new_simple_atlas_allocator(safe_width, safe_height, atlas_id),
    images: [],
    page_generation: 0,
    page_dirty_rect: None,
  }
}

///|
pub fn create_atlas_image(
  repo : SimpleAtlasImageRepository,
  key : AssetKey,
  spec : ImageSpec,
) -> @gfx.ImageHandle? {
  for entry in repo.images {
    if asset_key_eq(entry.key, key) {
      return Some(entry.handle)
    }
  }
  let width = if spec.width <= 0 { 1 } else { spec.width }
  let height = if spec.height <= 0 { 1 } else { spec.height }
  let pixels_rgba8 = normalized_rgba8_channels(spec.pixels_rgba8, width, height)
  match repo.allocator.allocate(width, height) {
    Some(region) => {
      let handle = @gfx.new_image_handle(repo.next_image_id, width, height)
      repo.next_image_id = repo.next_image_id + 1
      repo.images.push({
        key,
        handle,
        region,
        palette: spec.palette,
        pixels_rgba8,
      })
      mark_atlas_page_dirty(repo, Some(atlas_region_to_dirty_rect(region)))
      Some(handle)
    }
    None => None
  }
}

///|
/// Register an image with a precomputed atlas region (bypasses allocator).
/// Used by sprite_packer to register MaxRects-packed results directly.
pub fn register_precomputed_image(
  repo : SimpleAtlasImageRepository,
  key : AssetKey,
  region : AtlasRegion,
  spec : ImageSpec,
) -> @gfx.ImageHandle {
  for entry in repo.images {
    if asset_key_eq(entry.key, key) {
      return entry.handle
    }
  }
  let width = if spec.width <= 0 { 1 } else { spec.width }
  let height = if spec.height <= 0 { 1 } else { spec.height }
  let pixels_rgba8 = normalized_rgba8_channels(spec.pixels_rgba8, width, height)
  let handle = @gfx.new_image_handle(repo.next_image_id, width, height)
  repo.next_image_id = repo.next_image_id + 1
  repo.images.push({ key, handle, region, palette: spec.palette, pixels_rgba8 })
  mark_atlas_page_dirty(repo, Some(atlas_region_to_dirty_rect(region)))
  handle
}

///|
pub fn get_atlas_image_region(
  repo : SimpleAtlasImageRepository,
  key : AssetKey,
) -> AtlasRegion? {
  let mut out : AtlasRegion? = None
  for entry in repo.images {
    if asset_key_eq(entry.key, key) {
      out = Some(entry.region)
    }
  }
  out
}

///|
pub fn get_atlas_draw_source(
  repo : SimpleAtlasImageRepository,
  key : AssetKey,
) -> AtlasDrawSource? {
  let mut out : AtlasDrawSource? = None
  for entry in repo.images {
    if asset_key_eq(entry.key, key) {
      out = Some(
        atlas_uv_from_region(entry.region, repo.atlas_width, repo.atlas_height),
      )
    }
  }
  out
}

///|
pub fn update_atlas_image_spec(
  repo : SimpleAtlasImageRepository,
  key : AssetKey,
  spec : ImageSpec,
) -> @gfx.ImageHandle? {
  let width = if spec.width <= 0 { 1 } else { spec.width }
  let height = if spec.height <= 0 { 1 } else { spec.height }
  let pixels_rgba8 = normalized_rgba8_channels(spec.pixels_rgba8, width, height)
  let next : Array[SimpleAtlasImageEntry] = []
  let mut updated : @gfx.ImageHandle? = None
  let mut page_dirty_rect : SourceImageDirtyRect? = None
  for entry in repo.images {
    if asset_key_eq(entry.key, key) {
      if entry.handle.width == width && entry.handle.height == height {
        let old_has_pixels = has_valid_rgba8_payload(
          entry.pixels_rgba8,
          width,
          height,
        )
        let new_has_pixels = has_valid_rgba8_payload(
          pixels_rgba8, width, height,
        )
        let pixels_changed = !image_pixels_eq(
          entry.pixels_rgba8,
          pixels_rgba8,
          width,
          height,
        )
        let palette_changed = !image_palette_eq(entry.palette, spec.palette)
        let diff_rect = if pixels_changed {
          diff_dirty_rect_from_pixels(
            entry.pixels_rgba8,
            pixels_rgba8,
            width,
            height,
          )
        } else {
          None
        }
        let palette_rect = if palette_changed &&
          !(old_has_pixels && new_has_pixels) {
          Some(source_dirty_rect_full(width, height))
        } else {
          None
        }
        let local_dirty_rect = merge_dirty_rects(diff_rect, palette_rect)
        page_dirty_rect = merge_dirty_rects(
          page_dirty_rect,
          atlas_local_dirty_to_page_dirty(entry.region, local_dirty_rect),
        )
        if pixels_changed || palette_changed {
          next.push({
            key: entry.key,
            handle: entry.handle,
            region: entry.region,
            palette: spec.palette,
            pixels_rgba8,
          })
        } else {
          next.push(entry)
        }
        updated = Some(entry.handle)
      } else {
        next.push(entry)
      }
    } else {
      next.push(entry)
    }
  }
  repo.images = next
  mark_atlas_page_dirty(repo, page_dirty_rect)
  updated
}

///|
pub fn remove_atlas_image(
  repo : SimpleAtlasImageRepository,
  key : AssetKey,
) -> Unit {
  let next : Array[SimpleAtlasImageEntry] = []
  let mut released_dirty_rect : SourceImageDirtyRect? = None
  for entry in repo.images {
    if asset_key_eq(entry.key, key) {
      repo.allocator.release(entry.region)
      released_dirty_rect = merge_dirty_rects(
        released_dirty_rect,
        Some(atlas_region_to_dirty_rect(entry.region)),
      )
    } else {
      next.push(entry)
    }
  }
  repo.images = next
  mark_atlas_page_dirty(repo, released_dirty_rect)
  let _ = atlas_allocator_reset_if_empty(repo.allocator)
}

///|
fn new_blank_rgba8_channels(width : Int, height : Int) -> Array[Int] {
  let safe_width = if width <= 0 { 1 } else { width }
  let safe_height = if height <= 0 { 1 } else { height }
  let out : Array[Int] = []
  for _ in 0..<(safe_width * safe_height * 4) {
    out.push(0)
  }
  out
}

///|
fn fill_atlas_page_pixels_from_entry(
  pixels_rgba8 : Array[Int],
  atlas_width : Int,
  atlas_height : Int,
  entry : SimpleAtlasImageEntry,
) -> Unit {
  let image_width = if entry.handle.width <= 0 { 1 } else { entry.handle.width }
  let image_height = if entry.handle.height <= 0 {
    1
  } else {
    entry.handle.height
  }
  let has_pixels = has_valid_rgba8_payload(
    entry.pixels_rgba8,
    image_width,
    image_height,
  )
  let fallback_r = palette_channel(entry.palette, 0, 0)
  let fallback_g = palette_channel(entry.palette, 0, 1)
  let fallback_b = palette_channel(entry.palette, 0, 2)
  let fallback_a = palette_channel(entry.palette, 0, 3)
  for local_y in 0..= atlas_height {
      continue
    }
    for local_x in 0..= atlas_width {
        continue
      }
      let dst_base = (atlas_y * atlas_width + atlas_x) * 4
      if has_pixels {
        let src_base = (local_y * image_width + local_x) * 4
        pixels_rgba8[dst_base] = entry.pixels_rgba8[src_base]
        pixels_rgba8[dst_base + 1] = entry.pixels_rgba8[src_base + 1]
        pixels_rgba8[dst_base + 2] = entry.pixels_rgba8[src_base + 2]
        pixels_rgba8[dst_base + 3] = entry.pixels_rgba8[src_base + 3]
      } else {
        pixels_rgba8[dst_base] = fallback_r
        pixels_rgba8[dst_base + 1] = fallback_g
        pixels_rgba8[dst_base + 2] = fallback_b
        pixels_rgba8[dst_base + 3] = fallback_a
      }
    }
  }
}

///|
fn compose_atlas_page_rgba8(repo : SimpleAtlasImageRepository) -> Array[Int] {
  let pixels_rgba8 = new_blank_rgba8_channels(
    repo.atlas_width,
    repo.atlas_height,
  )
  for entry in repo.images {
    fill_atlas_page_pixels_from_entry(
      pixels_rgba8,
      repo.atlas_width,
      repo.atlas_height,
      entry,
    )
  }
  pixels_rgba8
}

///|
pub fn list_dirty_atlas_page_bindings(
  repo : SimpleAtlasImageRepository,
) -> Array[SourceImageBinding] {
  let bindings : Array[SourceImageBinding] = []
  match repo.page_dirty_rect {
    Some(rect) => {
      let page_pixels_rgba8 = compose_atlas_page_rgba8(repo)
      bindings.push({
        image_id: repo.atlas_id,
        width: repo.atlas_width,
        height: repo.atlas_height,
        generation: if repo.page_generation <= 0 {
          1
        } else {
          repo.page_generation
        },
        dirty_rect: Some(rect),
        palette: palette_from_rgba8_channels(
          repo.atlas_width,
          repo.atlas_height,
          page_pixels_rgba8,
        ),
        pixels_rgba8: page_pixels_rgba8,
      })
    }
    None => ()
  }
  bindings
}

///|
pub fn clear_dirty_atlas_page_flags(repo : SimpleAtlasImageRepository) -> Int {
  match repo.page_dirty_rect {
    Some(_) => {
      repo.page_dirty_rect = None
      1
    }
    None => 0
  }
}

///|
/// Multi-page atlas repository that auto-creates new pages when
/// the current page runs out of space.
///
/// Ebiten refs:
/// - internal/atlas/image.go (multi-backend page management)
pub struct MultiPageAtlasRepository {
  page_width : Int
  page_height : Int
  mut next_page_id : Int
  pages : Array[SimpleAtlasImageRepository]
  mut key_page_index : Array[(String, Int)]
}

///|
pub fn new_multi_page_atlas_repository(
  page_width : Int,
  page_height : Int,
) -> MultiPageAtlasRepository {
  let safe_width = if page_width <= 0 { 1 } else { page_width }
  let safe_height = if page_height <= 0 { 1 } else { page_height }
  {
    page_width: safe_width,
    page_height: safe_height,
    next_page_id: 100,
    pages: [],
    key_page_index: [],
  }
}

///|
pub fn multi_page_count(repo : MultiPageAtlasRepository) -> Int {
  repo.pages.length()
}

///|
pub fn multi_page_image_count(repo : MultiPageAtlasRepository) -> Int {
  let mut total = 0
  for page in repo.pages {
    total = total + page.images.length()
  }
  total
}

///|
fn find_key_page_index(index : Array[(String, Int)], key : AssetKey) -> Int? {
  for entry in index {
    if entry.0 == key.value {
      return Some(entry.1)
    }
  }
  None
}

///|
fn add_key_page_index(
  repo : MultiPageAtlasRepository,
  key : AssetKey,
  page_index : Int,
) -> Unit {
  repo.key_page_index.push((key.value, page_index))
}

///|
fn remove_key_page_index(
  repo : MultiPageAtlasRepository,
  key : AssetKey,
) -> Unit {
  let next : Array[(String, Int)] = []
  for entry in repo.key_page_index {
    if entry.0 != key.value {
      next.push(entry)
    }
  }
  repo.key_page_index = next
}

///|
fn ensure_page(repo : MultiPageAtlasRepository) -> Int {
  if repo.pages.length() == 0 {
    let page_id = repo.next_page_id
    repo.next_page_id = repo.next_page_id + 1
    repo.pages.push(
      new_simple_atlas_image_repository(
        repo.page_width,
        repo.page_height,
        page_id,
      ),
    )
  }
  repo.pages.length() - 1
}

///|
fn add_new_page(repo : MultiPageAtlasRepository) -> Int {
  let page_id = repo.next_page_id
  repo.next_page_id = repo.next_page_id + 1
  repo.pages.push(
    new_simple_atlas_image_repository(
      repo.page_width,
      repo.page_height,
      page_id,
    ),
  )
  repo.pages.length() - 1
}

///|
pub fn multi_page_create_image(
  repo : MultiPageAtlasRepository,
  key : AssetKey,
  spec : ImageSpec,
) -> @gfx.ImageHandle? {
  // Check if key already exists
  match find_key_page_index(repo.key_page_index, key) {
    Some(page_idx) =>
      if page_idx < repo.pages.length() {
        for entry in repo.pages[page_idx].images {
          if asset_key_eq(entry.key, key) {
            return Some(entry.handle)
          }
        }
      }
    None => ()
  }
  // Try current (last) page first
  let last_idx = ensure_page(repo)
  match create_atlas_image(repo.pages[last_idx], key, spec) {
    Some(handle) => {
      add_key_page_index(repo, key, last_idx)
      Some(handle)
    }
    None => {
      // Current page full, create new page and retry
      let new_idx = add_new_page(repo)
      match create_atlas_image(repo.pages[new_idx], key, spec) {
        Some(handle) => {
          add_key_page_index(repo, key, new_idx)
          Some(handle)
        }
        None => None // Image too large for a single page
      }
    }
  }
}

///|
pub fn multi_page_get_draw_source(
  repo : MultiPageAtlasRepository,
  key : AssetKey,
) -> AtlasDrawSource? {
  match find_key_page_index(repo.key_page_index, key) {
    Some(page_idx) =>
      if page_idx < repo.pages.length() {
        get_atlas_draw_source(repo.pages[page_idx], key)
      } else {
        None
      }
    None => None
  }
}

///|
pub fn multi_page_get_region(
  repo : MultiPageAtlasRepository,
  key : AssetKey,
) -> AtlasRegion? {
  match find_key_page_index(repo.key_page_index, key) {
    Some(page_idx) =>
      if page_idx < repo.pages.length() {
        get_atlas_image_region(repo.pages[page_idx], key)
      } else {
        None
      }
    None => None
  }
}

///|
pub fn multi_page_update_image_spec(
  repo : MultiPageAtlasRepository,
  key : AssetKey,
  spec : ImageSpec,
) -> @gfx.ImageHandle? {
  match find_key_page_index(repo.key_page_index, key) {
    Some(page_idx) =>
      if page_idx < repo.pages.length() {
        update_atlas_image_spec(repo.pages[page_idx], key, spec)
      } else {
        None
      }
    None => None
  }
}

///|
pub fn multi_page_remove_image(
  repo : MultiPageAtlasRepository,
  key : AssetKey,
) -> Unit {
  match find_key_page_index(repo.key_page_index, key) {
    Some(page_idx) =>
      if page_idx < repo.pages.length() {
        remove_atlas_image(repo.pages[page_idx], key)
        remove_key_page_index(repo, key)
      }
    None => ()
  }
}

///|
pub fn multi_page_list_dirty_bindings(
  repo : MultiPageAtlasRepository,
) -> Array[SourceImageBinding] {
  let bindings : Array[SourceImageBinding] = []
  for page in repo.pages {
    let page_bindings = list_dirty_atlas_page_bindings(page)
    for binding in page_bindings {
      bindings.push(binding)
    }
  }
  bindings
}

///|
pub fn multi_page_clear_dirty_flags(repo : MultiPageAtlasRepository) -> Int {
  let mut cleared = 0
  for page in repo.pages {
    cleared = cleared + clear_dirty_atlas_page_flags(page)
  }
  cleared
}

///|
pub fn multi_page_get_page_ids(repo : MultiPageAtlasRepository) -> Array[Int] {
  let ids : Array[Int] = []
  for page in repo.pages {
    ids.push(page.atlas_id)
  }
  ids
}

///|
pub fn atlas_page_is_empty(repo : SimpleAtlasImageRepository) -> Bool {
  repo.images.length() == 0
}

///|
pub fn atlas_page_image_count(repo : SimpleAtlasImageRepository) -> Int {
  repo.images.length()
}

///|
pub struct AtlasCompactResult {
  removed_page_ids : Array[Int]
  removed_page_count : Int
  remaining_page_count : Int
} derive(Debug)

///|
pub impl Show for AtlasCompactResult with output(self, logger) {
  logger.write_object(to_repr(self))
}

///|
pub fn multi_page_compact(
  repo : MultiPageAtlasRepository,
) -> AtlasCompactResult {
  let removed_page_ids : Array[Int] = []
  // Collect indices of empty pages (reverse order for safe removal)
  let empty_indices : Array[Int] = []
  for i in 0..= 0 {
    let _ = repo.pages.remove(empty_indices[i])
    i = i - 1
  }
  // Rebuild key_page_index
  let next_index : Array[(String, Int)] = []
  for page_idx in 0.. AtlasPageStats {
  {
    atlas_id: repo.atlas_id,
    image_count: repo.images.length(),
    allocated_region_count: repo.allocator.allocated.length(),
    atlas_width: repo.atlas_width,
    atlas_height: repo.atlas_height,
    is_empty: repo.images.length() == 0,
  }
}

///|
pub fn multi_page_stats(
  repo : MultiPageAtlasRepository,
) -> Array[AtlasPageStats] {
  let stats : Array[AtlasPageStats] = []
  for page in repo.pages {
    stats.push(atlas_page_stats(page))
  }
  stats
}