///|
/// Impose pages into cpdf-style sheets.
///
/// In non-fit mode, `x` and `y` are grid dimensions; one of them may be zero to
/// stack all pages in the other direction. In fit mode, `x` and `y` are the
/// output page width and height, and the grid is calculated from the current
/// page size.
pub fn PdfDocument::impose(
  self : PdfDocument,
  x : Double,
  y : Double,
  fit? : Bool = false,
  columns? : Bool = false,
  rtl? : Bool = false,
  btt? : Bool = false,
  center? : Bool = false,
  margin? : Double = 0.0,
  spacing? : Double = 0.0,
  line_width? : Double = 0.0,
  fast? : Bool = false,
  process_struct_tree? : Bool = false,
) -> PdfDocument raise @core.PdfError {
  let source = if process_struct_tree {
    self.mark_all_as_artifact()
  } else {
    self
  }
  let original_page_count = source.endpage()
  if original_page_count == 0 {
    raise BadNumberArgument("impose: pages must not be empty")
  }
  let prepared = source.pdf_impose_preprocess(fit, spacing, line_width, fast)
  let prepared_pages = prepared.pages_of_pagetree()
  let layout = prepared.pdf_impose_layout(
    fit,
    x,
    y,
    margin,
    original_page_count,
    prepared_pages[0].mediabox,
  )
  let imposed_pages : Array[@page.PdfPage] = []
  for page_set in pdf_impose_split_pages(prepared_pages, layout.pages_per_sheet) {
    imposed_pages.push(
      prepared.pdf_impose_pages_core(
        prepared.renumber_pages(page_set),
        fit,
        layout.x_slots,
        layout.y_slots,
        columns,
        rtl,
        btt,
        center,
        margin,
        layout.output_mediabox,
        fast,
        layout.fit_extra_hspace,
        layout.fit_extra_vspace,
      ),
    )
  }
  let imposed = prepared.change_pages(
    imposed_pages,
    change_references=true,
    changes=pdf_impose_page_reference_changes(
      original_page_count,
      layout.pages_per_sheet,
    ),
  )
  if fit {
    imposed
  } else {
    let output_count = imposed.endpage()
    imposed.shift_pages(
      pdf_impose_all_pages(output_count),
      pdf_impose_repeated_offsets(output_count, (margin, margin)),
      fast~,
    )
  }
}

///|
/// Compatibility wrapper for `PdfDocument::impose`.
pub fn pdf_impose(
  document : PdfDocument,
  x : Double,
  y : Double,
  fit? : Bool = false,
  columns? : Bool = false,
  rtl? : Bool = false,
  btt? : Bool = false,
  center? : Bool = false,
  margin? : Double = 0.0,
  spacing? : Double = 0.0,
  line_width? : Double = 0.0,
  fast? : Bool = false,
  process_struct_tree? : Bool = false,
) -> PdfDocument raise @core.PdfError {
  document.impose(
    x,
    y,
    fit~,
    columns~,
    rtl~,
    btt~,
    center~,
    margin~,
    spacing~,
    line_width~,
    fast~,
    process_struct_tree~,
  )
}

///|
/// Legacy cpdf two-up stack operation.
///
/// This first imposes source pages in a 2x1 non-fit grid, then rotates the
/// imposed sheets by -90 degrees and uprights them into ordinary page geometry.
pub fn PdfDocument::twoup_stack(
  self : PdfDocument,
  fast? : Bool = false,
  process_struct_tree? : Bool = false,
) -> PdfDocument raise @core.PdfError {
  let imposed = self.impose(2.0, 1.0, fast~, process_struct_tree~)
  let all_pages = pdf_impose_all_pages(imposed.endpage())
  imposed.rotate_pages(-90, all_pages).upright_pages(all_pages, fast~)
}

///|
/// Compatibility wrapper for `PdfDocument::twoup_stack`.
pub fn pdf_twoup_stack(
  document : PdfDocument,
  fast? : Bool = false,
  process_struct_tree? : Bool = false,
) -> PdfDocument raise @core.PdfError {
  document.twoup_stack(fast~, process_struct_tree~)
}

///|
/// Legacy cpdf two-up operation.
///
/// This scales source pages down, imposes them 2x1, rotates/uprights the
/// imposed sheets, then scales each sheet back to the first page's original
/// size.
pub fn PdfDocument::twoup(
  self : PdfDocument,
  fast? : Bool = false,
  process_struct_tree? : Bool = false,
) -> PdfDocument raise @core.PdfError {
  let pages = self.pages_of_pagetree()
  if pages.length() == 0 {
    raise BadNumberArgument("twoup: pages must not be empty")
  }
  let bounds = self.parse_rectangle(pages[0].mediabox)
  let width = bounds.max_x - bounds.min_x
  let height = bounds.max_y - bounds.min_y
  let scale = if width > height {
    let page_ratio = height / width
    let sheet_ratio = width / 2.0 / height
    if page_ratio < sheet_ratio {
      page_ratio
    } else {
      sheet_ratio
    }
  } else {
    let page_ratio = width / height
    let sheet_ratio = height / 2.0 / width
    if page_ratio < sheet_ratio {
      page_ratio
    } else {
      sheet_ratio
    }
  }
  let source_count = self.endpage()
  let source_pages = pdf_impose_all_pages(source_count)
  let scaled = self.scale_pages(
    source_pages,
    pdf_impose_repeated_scales(source_count, (scale, scale)),
    fast~,
  )
  let imposed = scaled.impose(
    2.0,
    1.0,
    center=true,
    fast~,
    process_struct_tree~,
  )
  let output_count = imposed.endpage()
  let output_pages = pdf_impose_all_pages(output_count)
  let upright = imposed
    .rotate_pages(-90, output_pages)
    .upright_pages(output_pages, fast~)
  upright.scale_to_fit_pages(
    output_pages,
    pdf_impose_repeated_scales(output_count, (width, height)),
    position=PdfPositionDiagonal,
    fast~,
  )
}

///|
/// Compatibility wrapper for `PdfDocument::twoup`.
pub fn pdf_twoup(
  document : PdfDocument,
  fast? : Bool = false,
  process_struct_tree? : Bool = false,
) -> PdfDocument raise @core.PdfError {
  document.twoup(fast~, process_struct_tree~)
}