///|
fn xml_text(value : String) -> String {
  let out = StringBuilder()
  for c in value.iter() {
    match c {
      '&' => out.write_string("&")
      '<' => out.write_string("<")
      '>' => out.write_string(">")
      '"' => out.write_string(""")
      '\'' => out.write_string("'")
      '\n' | '\r' | '\t' => out.write_char(' ')
      _ =>
        if c.to_int() < 32 || c == '\u{FFFE}' || c == '\u{FFFF}' {
          out.write_char('�')
        } else {
          out.write_char(c)
        }
    }
  }
  out.to_string()
}

///|
fn content_panel(
  rect : Rect,
  booklet : Booklet,
  slot : Slot,
  rotation : Int32,
) -> String {
  let (logical, label) = match slot {
    Blank => ("PADDING", "No source page")
    Page(n) =>
      (
        "Logical \{n}",
        match booklet.document.pages[n - 1] {
          Empty => "AUTHORED BLANK"
          Source(source, page) => "\{source} #\{page}"
        },
      )
  }
  let cx = rect.x + rect.width / 2
  let cy = rect.y + rect.height / 2
  "\{xml_text(logical)}\{xml_text(label)}"
}

///|
fn job_face_svg(
  job : PrintJob,
  sheet_index : Int32,
  back : Bool,
) -> Result[String, WorkflowError] {
  let sheet = job.booklet.sheets[sheet_index]
  let positions = match layout(job.paper, sheet.depth) {
    Ok(positions) => positions
    Err(_) =>
      return Err(
        workflow_error("paper", "invalid geometry for requested sheet"),
      )
  }
  let face = if back { sheet.back } else { sheet.front }
  let left = content_panel(
    positions.left,
    job.booklet,
    face.left,
    face.rotation,
  )
  let right = content_panel(
    positions.right,
    job.booklet,
    face.right,
    face.rotation,
  )
  let center = job.paper.width / 2
  Ok(
    "\{left}\{right}",
  )
}

///|
/// A dimensioned source-labelled proof; source artwork is not embedded.
pub fn job_proof(
  job : PrintJob,
  sheet_index : Int32,
  back : Bool,
) -> Result[String, WorkflowError] {
  match recover_booklet(job.booklet) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if sheet_index < 0 || sheet_index >= job.booklet.sheets.length() {
    return Err(workflow_error("sheet", "proof sheet index out of range"))
  }
  let content = match job_face_svg(job, sheet_index, back) {
    Ok(svg) => svg
    Err(e) => return Err(e)
  }
  let side = if back { "back" } else { "front" }
  Ok(
    "QuireLoom sheet \{sheet_index + 1} \{side}\{content}",
  )
}

///|
/// Paginate large jobs with first_sheet/count; each atlas is limited to 64 sheets.
pub fn proof_atlas(
  job : PrintJob,
  first_sheet : Int32,
  count : Int32,
) -> Result[String, WorkflowError] {
  match verify_schedule(job.booklet, job.schedule) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if count < 1 ||
    count > 64 ||
    first_sheet < 0 ||
    first_sheet > job.booklet.sheets.length() - count {
    return Err(
      workflow_error("atlas", "expected a valid range containing 1..64 sheets"),
    )
  }
  let width = job.paper.width * 2 + 12
  let row_height = job.paper.height + 16
  let height = row_height * count
  let fragments : Array[String] = []
  for index in first_sheet..<(first_sheet + count) {
    let sheet = job.booklet.sheets[index]
    let y = (index - first_sheet) * row_height
    for side in 0..<2 {
      let x = side * (job.paper.width + 12)
      let label = if side == 0 { "FRONT" } else { "BACK" }
      let content = match job_face_svg(job, index, side == 1) {
        Ok(svg) => svg
        Err(e) => return Err(e)
      }
      fragments.push(
        "Sheet \{index + 1} / gathering \{sheet.signature + 1} / depth \{sheet.depth} / \{label}\{content}",
      )
    }
  }
  Ok(
    "QuireLoom source-labelled proof atlas\{fragments.join("")}",
  )
}