///|
pub(all) struct Booklet {
  document : Document
  gatherings : Array[Signature]
  sheets : Array[Sheet]
  binding : Binding
  duplex : Duplex
}

///|
fn oriented_sheet(sheet : Sheet, binding : Binding, duplex : Duplex) -> Sheet {
  let front = if binding == Right { mirror(sheet.front) } else { sheet.front }
  let back = if binding == Right { mirror(sheet.back) } else { sheet.back }
  let back = if duplex == ShortEdge {
    { left: back.right, right: back.left, rotation: 180, }
  } else {
    back
  }
  { signature: sheet.signature, depth: sheet.depth, front, back, }
}

///|
/// Impose logical positions, keeping the source map separate from padding.
pub fn impose_document(
  document : Document,
  gatherings : Array[Signature],
  binding : Binding,
  duplex : Duplex,
) -> Result[Booklet, WorkflowError] {
  match validate_document(document) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  match validate_gatherings(document.pages.length(), gatherings) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let sheets : Array[Sheet] = []
  for group in gatherings {
    let last = group.start + group.pages - 1
    for depth in 0..<(group.pages / 4) {
      let low = group.start + 2 * depth
      let high = last - 2 * depth
      let sheet : Sheet = {
        signature: group.index,
        depth,
        front: {
          left: slot(high, document.pages.length()),
          right: slot(low, document.pages.length()),
          rotation: 0,
        },
        back: {
          left: slot(low + 1, document.pages.length()),
          right: slot(high - 1, document.pages.length()),
          rotation: 0,
        },
      }
      sheets.push(oriented_sheet(sheet, binding, duplex))
    }
  }
  Ok({ document, gatherings, sheets, binding, duplex, })
}

///|
/// Consume the supplied faces instead of reimposing a fresh plan for comparison.
pub fn recover_booklet(
  booklet : Booklet,
) -> Result[Array[ContentPage], WorkflowError] {
  match validate_document(booklet.document) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let count = booklet.document.pages.length()
  match validate_gatherings(count, booklet.gatherings) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if booklet.sheets.length() != (count + 3) / 4 {
    return Err(workflow_error("sheets", "incorrect sheet count"))
  }
  let order : Array[Slot] = []
  let mut cursor = 0
  for group in booklet.gatherings {
    let fronts : Array[Face] = []
    let backs : Array[Face] = []
    for depth in 0..<(group.pages / 4) {
      let sheet = booklet.sheets[cursor]
      let field = "sheets[\{cursor}]"
      cursor += 1
      if sheet.signature != group.index || sheet.depth != depth {
        return Err(
          workflow_error(field, "incorrect gathering or nesting order"),
        )
      }
      let rotation = if booklet.duplex == ShortEdge { 180 } else { 0 }
      if sheet.front.rotation != 0 || sheet.back.rotation != rotation {
        return Err(workflow_error(field, "incorrect face rotation"))
      }
      let back = if booklet.duplex == ShortEdge {
        mirror(sheet.back)
      } else {
        sheet.back
      }
      fronts.push(
        if booklet.binding == Right {
          mirror(sheet.front)
        } else {
          sheet.front
        },
      )
      backs.push(if booklet.binding == Right { mirror(back) } else { back })
    }
    for depth in 0..= 0 {
      order.push(backs[depth].right)
      order.push(fronts[depth].left)
      depth -= 1
    }
  }
  let result : Array[ContentPage] = []
  for index, position in order {
    let expected = if index < count { Page(index + 1) } else { Blank }
    if position != expected {
      return Err(
        workflow_error(
          "folded[\{index + 1}]",
          "incorrect logical page or padding",
        ),
      )
    }
    result.push(
      match position {
        Page(n) => booklet.document.pages[n - 1]
        Blank => Empty
      },
    )
  }
  Ok(result)
}