///|
fn pdf_chop_name(text : String) -> PdfName {
  pdf_name_of_bytes(@ascii.encode(text))
}

///|
let pdf_chop_cropbox_key_name : PdfName = pdf_chop_name("/CropBox")

///|
fn pdf_chop_cropbox_key() -> PdfName {
  pdf_chop_cropbox_key_name
}

///|
let pdf_chop_bleedbox_key_name : PdfName = pdf_chop_name("/BleedBox")

///|
fn pdf_chop_bleedbox_key() -> PdfName {
  pdf_chop_bleedbox_key_name
}

///|
let pdf_chop_trimbox_key_name : PdfName = pdf_chop_name("/TrimBox")

///|
fn pdf_chop_trimbox_key() -> PdfName {
  pdf_chop_trimbox_key_name
}

///|
let pdf_chop_artbox_key_name : PdfName = pdf_chop_name("/ArtBox")

///|
fn pdf_chop_artbox_key() -> PdfName {
  pdf_chop_artbox_key_name
}

///|
let pdf_chop_annots_key_name : PdfName = pdf_chop_name("/Annots")

///|
fn pdf_chop_annots_key() -> PdfName {
  pdf_chop_annots_key_name
}

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

///|
fn pdf_chop_box(
  document : PdfDocument,
  page : PdfPage,
) -> PdfRectangle raise 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 : PdfObject) -> PdfObject raise 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 : PdfPage,
  min_x : Double,
  min_y : Double,
  max_x : Double,
  max_y : Double,
) -> PdfPage raise 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] {
  [
    for index in 0.. if reverse { count - index - 1 } else { index }
  ]
}

///|
fn pdf_chop_grid_pages(
  page : PdfPage,
  bounds : PdfRectangle,
  x : Int,
  y : Int,
  columns : Bool,
  btt : Bool,
  rtl : Bool,
) -> Array[PdfPage] raise 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[PdfPage] = []
  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 : PdfPage,
  second : PdfPage,
  columns : Bool,
) -> Array[PdfPage] {
  if columns {
    [second, first]
  } else {
    [first, second]
  }
}

///|
fn pdf_chop_page_boxes(
  document : PdfDocument,
  page : PdfPage,
  line : Double,
  x : Int,
  y : Int,
  columns : Bool,
  btt : Bool,
  rtl : Bool,
) -> Array[PdfPage] raise 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 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 PdfError {
  if x <= 0 || y <= 0 {
    raise BadNumberArgument("chop: x and y must be positive")
  }
}

///|
fn pdf_chop_change_table(
  original_count : Int,
  selected_pages : @number_set.PdfNumberSet,
  pieces_per_selected_page : Int,
) -> Array[(Int, Int)] {
  let changes : Array[(Int, Int)] = Array::new(capacity=original_count)
  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 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 = @number_set.pdf_number_set(page_numbers)
  let output : Array[PdfPage] = Array::new(
    capacity=pages.length() + page_numbers.length() * (pieces - 1),
  )
  for index in 0.. PdfDocument raise PdfError {
  pdf_chop_validate_grid(x, y)
  self.pdf_chop_inner(0.0, x, y, columns, btt, rtl, page_numbers, "chop")
}

///|
/// Chop selected pages horizontally or vertically at `line`.
pub fn PdfDocument::chop_hv(
  self : PdfDocument,
  is_h : Bool,
  line : Double,
  columns : Bool,
  page_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
  self.pdf_chop_inner(
    line,
    if is_h {
      0
    } else {
      1
    },
    if is_h {
      1
    } else {
      0
    },
    columns,
    false,
    false,
    page_numbers,
    if is_h {
      "chop_h"
    } else {
      "chop_v"
    },
  )
}

///|
/// Compatibility wrapper for `PdfDocument::chop`.
pub fn pdf_chop(
  document : PdfDocument,
  x : Int,
  y : Int,
  columns : Bool,
  btt : Bool,
  rtl : Bool,
  page_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
  document.chop(x, y, columns, btt, rtl, page_numbers)
}

///|
/// Compatibility wrapper matching cpdfchop's source argument order.
pub fn pdf_chop_cpdf_order(
  x : Int,
  y : Int,
  columns : Bool,
  btt : Bool,
  rtl : Bool,
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
  document.chop(x, y, columns, btt, rtl, page_numbers)
}

///|
/// Compatibility wrapper for `PdfDocument::chop_hv`.
pub fn pdf_chop_hv(
  document : PdfDocument,
  is_h : Bool,
  line : Double,
  columns : Bool,
  page_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
  document.chop_hv(is_h, line, columns, page_numbers)
}

///|
/// Compatibility wrapper matching cpdfchop's source argument order.
pub fn pdf_chop_hv_cpdf_order(
  is_h : Bool,
  line : Double,
  columns : Bool,
  document : PdfDocument,
  page_numbers : ArrayView[Int],
) -> PdfDocument raise PdfError {
  document.chop_hv(is_h, line, columns, page_numbers)
}