///|
fn pdf_chop_rectangle_object(
  min_x : Double,
  min_y : Double,
  max_x : Double,
  max_y : Double,
) -> @syntax.PdfObject {
  PdfArray([PdfReal(min_x), PdfReal(min_y), PdfReal(max_x), PdfReal(max_y)])
}

///|
fn pdf_chop_box(
  document : PdfDocument,
  page : @page.PdfPage,
) -> @geometry.PdfRectangle raise @core.PdfError {
  match document.lookup_direct(pdf_chop_cropbox_key(), page.rest) {
    Some(cropbox) => document.parse_rectangle(cropbox)
    None => document.parse_rectangle(page.mediabox)
  }
}

///|
fn pdf_chop_erase_boxes(
  rest : @syntax.PdfObject,
) -> @syntax.PdfObject raise @core.PdfError {
  rest
  .remove_dict_entry(pdf_chop_cropbox_key())
  .remove_dict_entry(pdf_chop_bleedbox_key())
  .remove_dict_entry(pdf_chop_trimbox_key())
  .remove_dict_entry(pdf_chop_artbox_key())
  .remove_dict_entry(pdf_chop_annots_key())
}

///|
fn pdf_chop_page_with_box(
  page : @page.PdfPage,
  min_x : Double,
  min_y : Double,
  max_x : Double,
  max_y : Double,
) -> @page.PdfPage raise @core.PdfError {
  {
    ..page,
    mediabox: pdf_chop_rectangle_object(min_x, min_y, max_x, max_y),
    rest: pdf_chop_erase_boxes(page.rest),
  }
}

///|
fn pdf_chop_index_order(count : Int, reverse : Bool) -> Array[Int] {
  let output : Array[Int] = Array(capacity=count)
  for index in 0.. Array[@page.PdfPage] raise @core.PdfError {
  let width = (bounds.max_x - bounds.min_x) / x.to_double()
  let height = (bounds.max_y - bounds.min_y) / y.to_double()
  let pages : Array[@page.PdfPage] = Array(capacity=x * y)
  let x_order = pdf_chop_index_order(x, rtl)
  let y_order = pdf_chop_index_order(y, !btt)
  if columns {
    for tx in x_order {
      for ty in y_order {
        let min_x = bounds.min_x + width * tx.to_double()
        let min_y = bounds.min_y + height * ty.to_double()
        pages.push(
          pdf_chop_page_with_box(
            page,
            min_x,
            min_y,
            min_x + width,
            min_y + height,
          ),
        )
      }
    }
  } else {
    for ty in y_order {
      for tx in x_order {
        let min_x = bounds.min_x + width * tx.to_double()
        let min_y = bounds.min_y + height * ty.to_double()
        pages.push(
          pdf_chop_page_with_box(
            page,
            min_x,
            min_y,
            min_x + width,
            min_y + height,
          ),
        )
      }
    }
  }
  pages
}

///|
fn pdf_chop_pair_pages(
  first : @page.PdfPage,
  second : @page.PdfPage,
  columns : Bool,
) -> Array[@page.PdfPage] {
  let pages : Array[@page.PdfPage] = Array(capacity=2)
  if columns {
    pages.push(second)
    pages.push(first)
  } else {
    pages.push(first)
    pages.push(second)
  }
  pages
}

///|
fn pdf_chop_page_boxes(
  document : PdfDocument,
  page : @page.PdfPage,
  line : Double,
  x : Int,
  y : Int,
  columns : Bool,
  btt : Bool,
  rtl : Bool,
) -> Array[@page.PdfPage] raise @core.PdfError {
  let bounds = pdf_chop_box(document, page)
  if x == 0 {
    let top = pdf_chop_page_with_box(
      page,
      bounds.min_x,
      line,
      bounds.max_x,
      bounds.max_y,
    )
    let bottom = pdf_chop_page_with_box(
      page,
      bounds.min_x,
      bounds.min_y,
      bounds.max_x,
      line,
    )
    pdf_chop_pair_pages(top, bottom, columns)
  } else if y == 0 {
    let left = pdf_chop_page_with_box(
      page,
      bounds.min_x,
      bounds.min_y,
      line,
      bounds.max_y,
    )
    let right = pdf_chop_page_with_box(
      page,
      line,
      bounds.min_y,
      bounds.max_x,
      bounds.max_y,
    )
    pdf_chop_pair_pages(left, right, columns)
  } else {
    pdf_chop_grid_pages(page, bounds, x, y, columns, btt, rtl)
  }
}

///|
fn pdf_chop_validate_page_numbers(
  page_numbers : ArrayView[Int],
  end_page : Int,
  context : String,
) -> Unit raise @core.PdfError {
  for page_number in page_numbers {
    if page_number < 1 || page_number > end_page {
      raise BadPageSpecification(
        context + ": range contains pages not present in pdf",
      )
    }
  }
}

///|
fn pdf_chop_validate_grid(x : Int, y : Int) -> Unit raise @core.PdfError {
  if x <= 0 || y <= 0 {
    raise BadNumberArgument("chop: x and y must be positive")
  }
}

///|
fn pdf_chop_selected_page_count(
  original_count : Int,
  selected_pages : @core.PdfNumberSet,
) -> Int {
  let mut count = 0
  for page_number in 1..<=original_count {
    if selected_pages.contains(page_number) {
      count += 1
    }
  }
  count
}

///|
fn pdf_chop_change_table(
  original_count : Int,
  selected_pages : @core.PdfNumberSet,
  selected_count : Int,
  pieces_per_selected_page : Int,
) -> Array[(Int, Int)] {
  let changes : Array[(Int, Int)] = Array(
    capacity=original_count + selected_count * (pieces_per_selected_page - 1),
  )
  let mut next_page = 1
  for old_page in 1..<=original_count {
    if selected_pages.contains(old_page) {
      for new_page in next_page..<(next_page + pieces_per_selected_page) {
        changes.push((old_page, new_page))
      }
      next_page += pieces_per_selected_page
    } else {
      changes.push((old_page, next_page))
      next_page += 1
    }
  }
  changes
}

///|
fn PdfDocument::pdf_chop_inner(
  self : PdfDocument,
  line : Double,
  x : Int,
  y : Int,
  columns : Bool,
  btt : Bool,
  rtl : Bool,
  page_numbers : ArrayView[Int],
  context : String,
) -> PdfDocument raise @core.PdfError {
  let end_page = self.endpage()
  pdf_chop_validate_page_numbers(page_numbers, end_page, context)
  let pages = self.pages_of_pagetree()
  let pieces = if x == 0 || y == 0 { 2 } else { x * y }
  let selected_pages = @core.pdf_number_set(page_numbers)
  let selected_count = pdf_chop_selected_page_count(
    pages.length(),
    selected_pages,
  )
  let output : Array[@page.PdfPage] = Array(
    capacity=pages.length() + selected_count * (pieces - 1),
  )
  for index in 0..