///|
fn page_occupancy_permille(pages : ArrayView[Page]) -> Int {
  if pages.is_empty() {
    return 0
  }
  let mut used = 0
  let mut capacity = 0
  for page in pages {
    used = used + total_placed_area(page.layout.placements)
    capacity = capacity + bin_area(page.layout.bin)
  }
  if capacity <= 0 {
    0
  } else {
    used * 1000 / capacity
  }
}

///|
fn strip_unpacked(layout : Layout) -> Layout {
  make_layout(layout.bin, layout.placements, [], layout.algorithm)
}

///|
/// Pack rectangles across multiple pages with the same bin/options.
pub fn pack_pages(
  rects : ArrayView[Rect],
  options : PackOptions,
  max_pages : Int,
) -> Result[MultiLayout, PackError] {
  if max_pages <= 0 {
    return Err(InvalidPageLimit(limit=max_pages))
  }
  let pages : Array[Page] = []
  let mut remaining = rects.to_owned()
  let mut page_index = 0
  while page_index < max_pages && !remaining.is_empty() {
    let layout = match pack(remaining, options) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    if layout.placements.is_empty() {
      break
    }
    pages.push(Page::{ index: page_index, layout: strip_unpacked(layout) })
    remaining = layout.unpacked
    page_index = page_index + 1
  }
  Ok(MultiLayout::{
    pages,
    unpacked: remaining,
    total_occupancy_permille: page_occupancy_permille(pages),
  })
}

///|
/// Parse a spec and pack it across several pages.
pub fn pack_pages_spec(
  source : String,
  max_pages : Int,
) -> Result[MultiLayout, PackError] {
  match parse_spec(source) {
    Err(err) => Err(err)
    Ok(spec) => pack_pages(spec.rects, spec.options, max_pages)
  }
}

///|
/// Human-readable multipage report.
pub fn to_pages_report(result : MultiLayout) -> String {
  let out = StringBuilder()
  out.write_string(
    "rectloom pages=\{result.pages.length()} unpacked=\{result.unpacked.length()} total_occupancy=\{result.total_occupancy_permille}/1000\n",
  )
  for page in result.pages {
    out.write_string("\npage \{page.index}\n")
    out.write_string(to_report(page.layout))
  }
  if !result.unpacked.is_empty() {
    out.write_string("\nremaining unpacked:\n")
    for rect in result.unpacked {
      out.write_string("- \{rect.id}: \{rect.width}x\{rect.height}\n")
    }
  }
  out.to_string()
}

///|
/// Stable JSON manifest for multipage packing.
pub fn to_pages_json_manifest(result : MultiLayout) -> String {
  let out = StringBuilder()
  out.write_string("{\n")
  out.write_string(
    "  \"total_occupancy_permille\": \{result.total_occupancy_permille},\n",
  )
  out.write_string("  \"pages\": [\n")
  for i in 0..