///|
/// Layout helpers for contact sheets, sticker boards, and sprite previews.
pub(all) enum LayoutDirection {
  Horizontal
  Vertical
  Grid(columns~ : Int)
} derive(Eq, Debug)

///|
pub(all) struct LayoutItem {
  canvas : Canvas
  left : Int
  top : Int
  width : Int
  height : Int
} derive(Debug)

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

///|
pub fn LayoutItem::position(self : LayoutItem) -> (Int, Int) {
  (self.left, self.top)
}

///|
pub fn LayoutItem::size(self : LayoutItem) -> (Int, Int) {
  (self.width, self.height)
}

///|
pub(all) struct LayoutResult {
  width : Int
  height : Int
  items : Array[LayoutItem]
} derive(Debug)

///|
pub fn LayoutResult::width(self : LayoutResult) -> Int {
  self.width
}

///|
pub fn LayoutResult::height(self : LayoutResult) -> Int {
  self.height
}

///|
pub fn LayoutResult::items(self : LayoutResult) -> ArrayView[LayoutItem] {
  self.items[:]
}

///|
/// Lay out canvases in a row, column, or fixed-column grid.
pub fn layout_canvases(
  canvases : ArrayView[Canvas],
  direction : LayoutDirection,
  gap : Int,
  background : Rgba,
) -> LayoutResult {
  let safe_gap = if gap < 0 { 0 } else { gap }
  let items : Array[LayoutItem] = []
  let mut total_width = 0
  let mut total_height = 0
  match direction {
    Horizontal => {
      let mut left = 0
      for canvas in canvases {
        items.push(LayoutItem::{
          canvas,
          left,
          top: 0,
          width: canvas.width,
          height: canvas.height,
        })
        left += canvas.width + safe_gap
        if canvas.height > total_height {
          total_height = canvas.height
        }
      }
      total_width = if left > 0 { left - safe_gap } else { 0 }
    }
    Vertical => {
      let mut top = 0
      for canvas in canvases {
        items.push(LayoutItem::{
          canvas,
          left: 0,
          top,
          width: canvas.width,
          height: canvas.height,
        })
        top += canvas.height + safe_gap
        if canvas.width > total_width {
          total_width = canvas.width
        }
      }
      total_height = if top > 0 { top - safe_gap } else { 0 }
    }
    Grid(columns~) => {
      let columns = if columns < 1 { 1 } else { columns }
      let mut row_height = 0
      let mut left = 0
      let mut top = 0
      for index, canvas in canvases {
        if index > 0 && index % columns == 0 {
          top += row_height + safe_gap
          left = 0
          row_height = 0
        }
        items.push(LayoutItem::{
          canvas,
          left,
          top,
          width: canvas.width,
          height: canvas.height,
        })
        left += canvas.width + safe_gap
        if canvas.height > row_height {
          row_height = canvas.height
        }
        if left > total_width {
          total_width = left - safe_gap
        }
      }
      total_height = if canvases.length() == 0 { 0 } else { top + row_height }
    }
  }
  ignore(background)
  LayoutResult::{ width: total_width, height: total_height, items }
}

///|
/// Render a layout with clipped placement and alpha compositing.
pub fn LayoutResult::render(self : LayoutResult, background : Rgba) -> Canvas {
  let result = Canvas::new(
    width=self.width,
    height=self.height,
    fill=background,
  )
  for item in self.items {
    ignore(
      result.blit(item.canvas, destination_x=item.left, destination_y=item.top),
    )
  }
  result
}

///|
/// Place a canvas inside a safe rectangle with letterboxing.
pub fn contain_layout(
  canvas : Canvas,
  width : Int,
  height : Int,
  background : Rgba,
) -> Canvas {
  let result = Canvas::new(width~, height~, fill=background)
  let contained = canvas.contain(width~, height~, fill=background)
  let left = (width - contained.width) / 2
  let top = (height - contained.height) / 2
  ignore(result.blit(contained, destination_x=left, destination_y=top))
  result
}

///|
/// Crop a nine-patch panel and fill its center without scaling borders.
pub fn nine_patch(
  source : Canvas,
  width : Int,
  height : Int,
  border : Int,
) -> Canvas {
  let safe_border = if border < 0 { 0 } else { border }
  let result = Canvas::new(width~, height~, fill=rgba(0, 0, 0, 0))
  if source.width == 0 || source.height == 0 {
    result
  } else {
    for y in 0..= width - safe_border {
          source.width - (width - x)
        } else {
          safe_border +
          (x - safe_border) *
          (source.width - 2 * safe_border) /
          (width - 2 * safe_border + 1)
        }
        let sy = if y < safe_border {
          y
        } else if y >= height - safe_border {
          source.height - (height - y)
        } else {
          safe_border +
          (y - safe_border) *
          (source.height - 2 * safe_border) /
          (height - 2 * safe_border + 1)
        }
        let cx = if sx < 0 {
          0
        } else if sx >= source.width {
          source.width - 1
        } else {
          sx
        }
        let cy = if sy < 0 {
          0
        } else if sy >= source.height {
          source.height - 1
        } else {
          sy
        }
        result.pixels[x + y * width] = source.pixels[cx + cy * source.width]
      }
    }
    result
  }
}

///|
/// Render a frame sequence as a centered board with a fixed cell size.
pub fn board_animation(
  animation : Animation,
  columns : Int,
  cell_width : Int,
  cell_height : Int,
  gap : Int,
  background : Rgba,
) -> Canvas {
  let selected = animation.sample_with_policy(EveryFrame)
  let canvases : Array[Canvas] = []
  for frame in selected.frames {
    canvases.push(
      contain_layout(frame.canvas(), cell_width, cell_height, background),
    )
  }
  layout_canvases(canvases[:], Grid(columns~), gap, background).render(
    background,
  )
}