///|
/// A named sprite input for atlas packing.
pub(all) struct SpriteSource {
  name : String
  canvas : Canvas
  padding : Int
  extrude : Bool
} derive(Debug)

///|
pub fn SpriteSource::new(name : String, canvas : Canvas) -> SpriteSource {
  { name, canvas, padding: 0, extrude: false }
}

///|
pub fn SpriteSource::with_padding(
  self : SpriteSource,
  padding : Int,
) -> SpriteSource {
  { ..self, padding: if padding < 0 { 0 } else { padding } }
}

///|
pub fn SpriteSource::with_extrude(
  self : SpriteSource,
  enabled : Bool,
) -> SpriteSource {
  { ..self, extrude: enabled }
}

///|
pub fn SpriteSource::name(self : SpriteSource) -> String {
  self.name
}

///|
pub fn SpriteSource::canvas(self : SpriteSource) -> Canvas {
  self.canvas
}

///|
pub fn SpriteSource::padding(self : SpriteSource) -> Int {
  self.padding
}

///|
pub fn SpriteSource::extrude(self : SpriteSource) -> Bool {
  self.extrude
}

///|
/// A placement in a packed atlas page.
pub(all) struct SpritePlacement {
  name : String
  page : Int
  x : Int
  y : Int
  width : Int
  height : Int
  source_width : Int
  source_height : Int
  rotated : Bool
} derive(Debug)

///|
pub fn SpritePlacement::name(self : SpritePlacement) -> String {
  self.name
}

///|
pub fn SpritePlacement::page(self : SpritePlacement) -> Int {
  self.page
}

///|
pub fn SpritePlacement::rect(self : SpritePlacement) -> GifRect {
  GifRect::new(left=self.x, top=self.y, width=self.width, height=self.height)
}

///|
pub fn SpritePlacement::source_size(self : SpritePlacement) -> (Int, Int) {
  (self.source_width, self.source_height)
}

///|
pub fn SpritePlacement::rotated(self : SpritePlacement) -> Bool {
  self.rotated
}

///|
pub(all) struct AtlasPackOptions {
  page_width : Int
  page_height : Int
  padding : Int
  allow_rotation : Bool
  extrude : Bool
  background : Rgba
} derive(Debug)

///|
pub fn AtlasPackOptions::default() -> AtlasPackOptions {
  {
    page_width: 512,
    page_height: 512,
    padding: 1,
    allow_rotation: false,
    extrude: false,
    background: rgba(0, 0, 0, 0),
  }
}

///|
pub fn AtlasPackOptions::page_size(
  self : AtlasPackOptions,
  width : Int,
  height : Int,
) -> AtlasPackOptions {
  {
    ..self,
    page_width: if width < 1 {
      1
    } else {
      width
    },
    page_height: if height < 1 {
      1
    } else {
      height
    },
  }
}

///|
pub fn AtlasPackOptions::padding(
  self : AtlasPackOptions,
  value : Int,
) -> AtlasPackOptions {
  { ..self, padding: if value < 0 { 0 } else { value } }
}

///|
pub fn AtlasPackOptions::rotation(
  self : AtlasPackOptions,
  enabled : Bool,
) -> AtlasPackOptions {
  { ..self, allow_rotation: enabled }
}

///|
pub fn AtlasPackOptions::extrusion(
  self : AtlasPackOptions,
  enabled : Bool,
) -> AtlasPackOptions {
  { ..self, extrude: enabled }
}

///|
pub fn AtlasPackOptions::background(
  self : AtlasPackOptions,
  color : Rgba,
) -> AtlasPackOptions {
  { ..self, background: color }
}

///|
pub(all) struct AtlasPage {
  index : Int
  canvas : Canvas
  placements : Array[SpritePlacement]
} derive(Debug)

///|
pub fn AtlasPage::index(self : AtlasPage) -> Int {
  self.index
}

///|
pub fn AtlasPage::canvas(self : AtlasPage) -> Canvas {
  self.canvas
}

///|
pub fn AtlasPage::placements(self : AtlasPage) -> ArrayView[SpritePlacement] {
  self.placements[:]
}

///|
pub(all) struct SpriteAtlas {
  pages : Array[AtlasPage]
  placements : Array[SpritePlacement]
  page_width : Int
  page_height : Int
} derive(Debug)

///|
pub fn SpriteAtlas::pages(self : SpriteAtlas) -> ArrayView[AtlasPage] {
  self.pages[:]
}

///|
pub fn SpriteAtlas::placements(
  self : SpriteAtlas,
) -> ArrayView[SpritePlacement] {
  self.placements[:]
}

///|
pub fn SpriteAtlas::page_count(self : SpriteAtlas) -> Int {
  self.pages.length()
}

///|
pub fn SpriteAtlas::find(self : SpriteAtlas, name : String) -> SpritePlacement? {
  for placement in self.placements {
    if placement.name() == name {
      return Some(placement)
    }
  }
  None
}

///|
/// Extract a named sprite from its atlas page.
pub fn SpriteAtlas::extract(self : SpriteAtlas, name : String) -> Canvas? {
  match self.find(name) {
    None => None
    Some(placement) => {
      let page = self.pages[placement.page()]
      let rect = placement.rect()
      Some(
        page
        .canvas()
        .crop(
          x=rect.left(),
          y=rect.top(),
          width=rect.width(),
          height=rect.height(),
        ),
      )
    }
  }
}

///|
fn sprite_copy(
  source : Canvas,
  target : Canvas,
  x : Int,
  y : Int,
  extrude : Bool,
) -> Unit {
  ignore(target.blit(source, destination_x=x, destination_y=y))
  if extrude && source.width > 0 && source.height > 0 {
    for column in 0.. Array[SpritePlacement] {
  let result : Array[SpritePlacement] = []
  for value in values {
    result.push(value)
  }
  result
}

///|
pub fn pack_sprites(
  sources : ArrayView[SpriteSource],
  options : AtlasPackOptions,
) -> SpriteAtlas {
  let pages : Array[AtlasPage] = []
  let all_placements : Array[SpritePlacement] = []
  let page_width = if options.page_width < 1 { 1 } else { options.page_width }
  let page_height = if options.page_height < 1 {
    1
  } else {
    options.page_height
  }
  let mut page_index = 0
  let mut canvas = Canvas::new(
    width=page_width,
    height=page_height,
    fill=options.background,
  )
  let page_placements : Array[SpritePlacement] = []
  let mut cursor_x = 0
  let mut cursor_y = 0
  let mut row_height = 0
  let mut has_content = false
  for source in sources {
    let padding = options.padding + source.padding()
    let source_canvas = source.canvas()
    let normal_width = source_canvas.width + padding * 2
    let normal_height = source_canvas.height + padding * 2
    let rotated_width = source_canvas.height + padding * 2
    let rotated_height = source_canvas.width + padding * 2
    let use_rotation = options.allow_rotation &&
      normal_width > page_width - cursor_x &&
      rotated_width <= page_width - cursor_x &&
      rotated_height <= page_height - cursor_y
    let item_width = if use_rotation { rotated_width } else { normal_width }
    let item_height = if use_rotation { rotated_height } else { normal_height }
    if cursor_x + item_width > page_width {
      cursor_x = 0
      cursor_y += row_height
      row_height = 0
    }
    if cursor_y + item_height > page_height {
      if has_content {
        pages.push(AtlasPage::{
          index: page_index,
          canvas,
          placements: copy_sprite_placements(page_placements),
        })
      }
      page_index += 1
      canvas = Canvas::new(
        width=page_width,
        height=page_height,
        fill=options.background,
      )
      page_placements.clear()
      cursor_x = 0
      cursor_y = 0
      row_height = 0
    }
    if item_width > page_width || item_height > page_height {
      continue
    }
    let placed_canvas = if use_rotation {
      source_canvas.rotate90()
    } else {
      source_canvas
    }
    let placement = SpritePlacement::{
      name: source.name(),
      page: page_index,
      x: cursor_x + padding,
      y: cursor_y + padding,
      width: placed_canvas.width,
      height: placed_canvas.height,
      source_width: source_canvas.width,
      source_height: source_canvas.height,
      rotated: use_rotation,
    }
    sprite_copy(
      placed_canvas,
      canvas,
      placement.x,
      placement.y,
      options.extrude || source.extrude(),
    )
    page_placements.push(placement)
    all_placements.push(placement)
    has_content = true
    cursor_x += item_width
    if item_height > row_height {
      row_height = item_height
    }
  }
  if has_content {
    pages.push(AtlasPage::{
      index: page_index,
      canvas,
      placements: copy_sprite_placements(page_placements),
    })
  }
  SpriteAtlas::{ pages, placements: all_placements, page_width, page_height }
}

///|
/// Pack each animation frame as a named sprite.
pub fn Animation::to_sprite_atlas(
  self : Animation,
  options : AtlasPackOptions,
) -> SpriteAtlas {
  let sources : Array[SpriteSource] = []
  for i, frame in self.frames {
    sources.push(SpriteSource::new("frame-\{i}", frame.canvas()))
  }
  pack_sprites(sources[:], options)
}

///|
/// Convert an atlas page into a new animation in placement order.
pub fn SpriteAtlas::to_animation(
  self : SpriteAtlas,
  page_index : Int,
  delay_cs~ : Int,
) -> Animation {
  let safe_page = if page_index < 0 {
    0
  } else if page_index >= self.pages.length() {
    self.pages.length() - 1
  } else {
    page_index
  }
  if self.pages.length() == 0 {
    Animation::new(width=0, height=0)
  } else {
    let page = self.pages[safe_page]
    let frames : Array[Frame] = []
    for placement in page.placements {
      let frame_canvas = page.canvas.crop(
        x=placement.x,
        y=placement.y,
        width=placement.width,
        height=placement.height,
      )
      frames.push(
        Frame::new(canvas=frame_canvas, delay_cs~).with_offset(left=0, top=0),
      )
    }
    Animation::{
      width: page.canvas.width,
      height: page.canvas.height,
      frames,
      loop_count: Some(0),
      background: rgba(0, 0, 0, 0),
    }
  }
}

///|
/// Build a regular grid atlas without searching for free rectangles.
pub fn grid_atlas(
  frames : ArrayView[Canvas],
  columns : Int,
  cell_width : Int,
  cell_height : Int,
  padding : Int,
) -> SpriteAtlas {
  let safe_columns = if columns < 1 { 1 } else { columns }
  let safe_width = if cell_width < 1 { 1 } else { cell_width }
  let safe_height = if cell_height < 1 { 1 } else { cell_height }
  let safe_padding = if padding < 0 { 0 } else { padding }
  let rows = if frames.length() == 0 {
    0
  } else {
    (frames.length() + safe_columns - 1) / safe_columns
  }
  let page = Canvas::new(
    width=safe_columns * (safe_width + safe_padding * 2),
    height=rows * (safe_height + safe_padding * 2),
    fill=rgba(0, 0, 0, 0),
  )
  let placements : Array[SpritePlacement] = []
  for i, frame in frames {
    let column = i % safe_columns
    let row = i / safe_columns
    let x = column * (safe_width + safe_padding * 2) + safe_padding
    let y = row * (safe_height + safe_padding * 2) + safe_padding
    let clipped = frame.crop(
      x=0,
      y=0,
      width=if frame.width < safe_width { frame.width } else { safe_width },
      height=if frame.height < safe_height { frame.height } else { safe_height },
    )
    sprite_copy(clipped, page, x, y, false)
    placements.push(SpritePlacement::{
      name: "frame-\{i}",
      page: 0,
      x,
      y,
      width: clipped.width,
      height: clipped.height,
      source_width: frame.width,
      source_height: frame.height,
      rotated: false,
    })
  }
  let atlas_page = AtlasPage::{
    index: 0,
    canvas: page,
    placements: copy_sprite_placements(placements),
  }
  SpriteAtlas::{
    pages: [atlas_page],
    placements,
    page_width: page.width,
    page_height: page.height,
  }
}