///|
pub fn fixture_name(kind : FixtureKind) -> String {
  match kind {
    SpriteSheet => "sprite-sheet"
    UiPanels => "ui-panels"
    DocumentBlocks => "document-blocks"
    MixedIcons => "mixed-icons"
    StressSlivers => "stress-slivers"
  }
}

///|
pub fn fixture_description(kind : FixtureKind) -> String {
  match kind {
    SpriteSheet =>
      "game-like sprite assets with square icons and medium animation frames"
    UiPanels => "wide interface panels, badges, tabs, and compact buttons"
    DocumentBlocks =>
      "document layout blocks with mixed headings, paragraphs, and figures"
    MixedIcons => "small icon-like rectangles with deliberate size variation"
    StressSlivers => "thin rectangles that stress row and split decisions"
  }
}

///|
fn fixture_count(count : Int) -> Int {
  if count <= 0 {
    1
  } else {
    count
  }
}

///|
fn cyclic(value : Int, modulo : Int) -> Int {
  if modulo <= 0 {
    0
  } else {
    value % modulo
  }
}

///|
fn sprite_fixture_rect(index : Int) -> Rect {
  let phase = cyclic(index, 6)
  if phase == 0 {
    Rect::make("sprite-idle-\{index}", 16, 16)
  } else if phase == 1 {
    Rect::make("sprite-run-\{index}", 24, 18)
  } else if phase == 2 {
    Rect::make("sprite-jump-\{index}", 18, 28)
  } else if phase == 3 {
    Rect::make("sprite-hit-\{index}", 12, 14)
  } else if phase == 4 {
    Rect::make("sprite-fx-\{index}", 32, 12)
  } else {
    Rect::make("sprite-shadow-\{index}", 20, 8)
  }
}

///|
fn ui_panel_fixture_rect(index : Int) -> Rect {
  let phase = cyclic(index, 7)
  if phase == 0 {
    Rect::make("panel-card-\{index}", 40, 18)
  } else if phase == 1 {
    Rect::make("panel-toolbar-\{index}", 52, 10)
  } else if phase == 2 {
    Rect::make("panel-tab-\{index}", 18, 8)
  } else if phase == 3 {
    Rect::make("panel-button-\{index}", 16, 9)
  } else if phase == 4 {
    Rect::make("panel-modal-\{index}", 36, 24)
  } else if phase == 5 {
    Rect::make("panel-badge-\{index}", 9, 7)
  } else {
    Rect::make("panel-column-\{index}", 14, 34)
  }
}

///|
fn document_block_fixture_rect(index : Int) -> Rect {
  let phase = cyclic(index, 8)
  if phase == 0 {
    Rect::make("doc-title-\{index}", 54, 9)
  } else if phase == 1 {
    Rect::make("doc-paragraph-\{index}", 48, 14)
  } else if phase == 2 {
    Rect::make("doc-figure-\{index}", 32, 26)
  } else if phase == 3 {
    Rect::make("doc-caption-\{index}", 28, 6)
  } else if phase == 4 {
    Rect::make("doc-sidebar-\{index}", 16, 38)
  } else if phase == 5 {
    Rect::make("doc-table-\{index}", 42, 20)
  } else if phase == 6 {
    Rect::make("doc-note-\{index}", 22, 12)
  } else {
    Rect::make("doc-rule-\{index}", 56, 4)
  }
}

///|
fn mixed_icon_fixture_rect(index : Int) -> Rect {
  let phase = cyclic(index, 10)
  if phase == 0 {
    Rect::make("icon-xs-\{index}", 6, 6)
  } else if phase == 1 {
    Rect::make("icon-sm-\{index}", 8, 8)
  } else if phase == 2 {
    Rect::make("icon-md-\{index}", 12, 12)
  } else if phase == 3 {
    Rect::make("icon-lg-\{index}", 18, 18)
  } else if phase == 4 {
    Rect::make("icon-wide-\{index}", 20, 8)
  } else if phase == 5 {
    Rect::make("icon-tall-\{index}", 8, 20)
  } else if phase == 6 {
    Rect::make("icon-pill-\{index}", 24, 10)
  } else if phase == 7 {
    Rect::make("icon-chip-\{index}", 10, 5)
  } else if phase == 8 {
    Rect::make("icon-emblem-\{index}", 14, 16)
  } else {
    Rect::make("icon-mark-\{index}", 9, 11)
  }
}

///|
fn stress_sliver_fixture_rect(index : Int) -> Rect {
  let phase = cyclic(index, 9)
  if phase == 0 {
    Rect::make("sliver-hairline-\{index}", 48, 2)
  } else if phase == 1 {
    Rect::make("sliver-rail-\{index}", 3, 42)
  } else if phase == 2 {
    Rect::make("sliver-strip-\{index}", 36, 4)
  } else if phase == 3 {
    Rect::make("sliver-post-\{index}", 4, 30)
  } else if phase == 4 {
    Rect::make("sliver-block-\{index}", 12, 12)
  } else if phase == 5 {
    Rect::make("sliver-cap-\{index}", 18, 5)
  } else if phase == 6 {
    Rect::make("sliver-riser-\{index}", 5, 18)
  } else if phase == 7 {
    Rect::make("sliver-plate-\{index}", 30, 6)
  } else {
    Rect::make("sliver-pin-\{index}", 2, 24)
  }
}

///|
fn fixture_rect_at(kind : FixtureKind, index : Int) -> Rect {
  match kind {
    SpriteSheet => sprite_fixture_rect(index)
    UiPanels => ui_panel_fixture_rect(index)
    DocumentBlocks => document_block_fixture_rect(index)
    MixedIcons => mixed_icon_fixture_rect(index)
    StressSlivers => stress_sliver_fixture_rect(index)
  }
}

///|
/// Generate deterministic sample rectangles for examples and benchmarks.
pub fn fixture_rects(kind : FixtureKind, count : Int) -> Array[Rect] {
  let rects : Array[Rect] = []
  let limit = fixture_count(count)
  for i in 0.. PackOptions {
  match kind {
    SpriteSheet =>
      PackOptions::{
        bin: Bin::make(128, 128),
        padding: Padding::make(1, 1),
        sort: AreaDesc,
        algorithm: GuillotineBestArea,
        allow_rotate: true,
      }
    UiPanels =>
      PackOptions::{
        bin: Bin::make(160, 96),
        padding: Padding::make(2, 2),
        sort: WidthDesc,
        algorithm: SkylineBottomLeft,
        allow_rotate: false,
      }
    DocumentBlocks =>
      PackOptions::{
        bin: Bin::make(144, 160),
        padding: Padding::make(2, 2),
        sort: HeightDesc,
        algorithm: ShelfFirstFit,
        allow_rotate: false,
      }
    MixedIcons =>
      PackOptions::{
        bin: Bin::make(96, 96),
        padding: Padding::make(1, 1),
        sort: AreaDesc,
        algorithm: GuillotineBestArea,
        allow_rotate: true,
      }
    StressSlivers =>
      PackOptions::{
        bin: Bin::make(96, 96),
        padding: Padding::make(1, 1),
        sort: PerimeterDesc,
        algorithm: SkylineBottomLeft,
        allow_rotate: true,
      }
  }
}

///|
fn write_rect_spec_line(out : StringBuilder, rect : Rect) -> Unit {
  out.write_string("rect ")
  out.write_string(rect.id)
  out.write_char(' ')
  out.write_string("\{rect.width}")
  out.write_char(' ')
  out.write_string("\{rect.height}")
  out.write_char('\n')
}

///|
fn write_options_spec(out : StringBuilder, options : PackOptions) -> Unit {
  out.write_string("bin \{options.bin.width} \{options.bin.height}\n")
  out.write_string(
    "padding \{options.padding.outer} \{options.padding.inner}\n",
  )
  out.write_string("algorithm ")
  out.write_string(algorithm_name(options.algorithm))
  out.write_char('\n')
  out.write_string("sort ")
  out.write_string(sort_name(options.sort))
  out.write_char('\n')
  out.write_string("rotate ")
  out.write_string(bool_name(options.allow_rotate))
  out.write_char('\n')
}

///|
/// Export any rectangle list as a rectloom text spec.
pub fn rects_to_spec(rects : ArrayView[Rect], options : PackOptions) -> String {
  let out = StringBuilder()
  write_options_spec(out, options)
  for rect in rects {
    write_rect_spec_line(out, rect)
  }
  out.to_string()
}

///|
/// Generate a ready-to-pack spec for a fixture family.
pub fn fixture_spec(kind : FixtureKind, count : Int) -> String {
  rects_to_spec(fixture_rects(kind, count), fixture_options(kind))
}

///|
/// Pack a fixture with its recommended options.
pub fn pack_fixture(
  kind : FixtureKind,
  count : Int,
) -> Result[Layout, PackError] {
  pack(fixture_rects(kind, count), fixture_options(kind))
}

///|
/// Run automatic comparison on a fixture family.
pub fn compare_fixture(
  kind : FixtureKind,
  count : Int,
) -> Result[PackComparison, PackError] {
  let options = fixture_options(kind)
  compare_packers(fixture_rects(kind, count), options)
}

///|
fn write_fixture_catalog_row(out : StringBuilder, kind : FixtureKind) -> Unit {
  let options = fixture_options(kind)
  out.write_string("- ")
  out.write_string(fixture_name(kind))
  out.write_string(": ")
  out.write_string(fixture_description(kind))
  out.write_string(" default=")
  out.write_string(options_key(options))
  out.write_char('\n')
}

///|
/// Human-readable list of built-in fixtures.
pub fn fixture_catalog() -> String {
  let out = StringBuilder()
  out.write_string("rectloom fixture catalog\n")
  write_fixture_catalog_row(out, SpriteSheet)
  write_fixture_catalog_row(out, UiPanels)
  write_fixture_catalog_row(out, DocumentBlocks)
  write_fixture_catalog_row(out, MixedIcons)
  write_fixture_catalog_row(out, StressSlivers)
  out.to_string()
}