///|
fn pdf_impose_validate_transform_inputs(
  x : Int,
  y : Int,
  count : Int,
) -> Unit raise @core.PdfError {
  if x <= 0 || y <= 0 {
    raise BadNumberArgument("impose: x and y must be positive")
  }
  if count < 0 {
    raise BadNumberArgument("impose: page count must be non-negative")
  }
}

///|
fn pdf_impose_order(
  row : Int,
  col : Int,
  x : Int,
  y : Int,
  rtl : Bool,
  btt : Bool,
) -> (Int, Int) {
  (if btt { y - row - 1 } else { row }, if rtl { x - col - 1 } else { col })
}

///|
fn pdf_impose_adjusted_row(
  original_row : Int,
  y : Int,
  final_full_rows : Int,
  remaining : Int,
  center : Bool,
) -> Int {
  if center && remaining <= final_full_rows {
    let final_empty_rows = y - final_full_rows
    original_row + (y - 1 - 1 - final_empty_rows / 2)
  } else {
    original_row
  }
}

///|
fn pdf_impose_adjusted_col(
  original_col : Int,
  x : Int,
  final_full_cols : Int,
  remaining : Int,
  center : Bool,
) -> Int {
  if center && remaining <= final_full_cols {
    let final_empty_cols = x - final_full_cols
    original_col + (x - 1 - 1 - final_empty_cols / 2)
  } else {
    original_col
  }
}

///|
fn pdf_impose_translate(
  x : Int,
  rtl : Bool,
  btt : Bool,
  cent_extra_x : Double,
  cent_extra_y : Double,
  fit_extra_hspace : Double,
  fit_extra_vspace : Double,
  row : Int,
  col : Int,
  px : Double,
  py : Double,
) -> @geometry.TransformMatrix {
  let cex = if rtl { -cent_extra_x } else { cent_extra_x }
  let cey = if btt { -cent_extra_y } else { cent_extra_y }
  let space_col = if rtl { x - col - 1 } else { col }
  let total_fit_extra_hspace = fit_extra_hspace * (space_col.to_double() + 1.0)
  let total_fit_extra_vspace = fit_extra_vspace * (row.to_double() + 1.0)
  @geometry.transform_translate(
    px + cex + total_fit_extra_hspace,
    py + cey + total_fit_extra_vspace,
  )
}

///|
fn PdfDocument::pdf_impose_make_margin(
  self : PdfDocument,
  output_mediabox : @syntax.PdfObject,
  margin : Double,
  matrix : @geometry.TransformMatrix,
) -> @geometry.TransformMatrix raise @core.PdfError {
  if margin == 0.0 {
    matrix
  } else {
    let box = self.parse_rectangle(output_mediabox)
    let width = box.max_x - box.min_x
    let height = box.max_y - box.min_y
    if margin > width / 2.0 || margin > height / 2.0 {
      raise BadNumberArgument("impose: margin would fill whole page")
    }
    let hfactor = (width - margin - margin) / width
    let vfactor = (height - margin - margin) / height
    let factor = if hfactor < vfactor { hfactor } else { vfactor }
    let scale = @geometry.transform_scale(
      @geometry.point2(0.0, 0.0),
      factor,
      factor,
    )
    let shift = @geometry.transform_translate(
      (width - width * factor) / 2.0,
      (height - height * factor) / 2.0,
    )
    @geometry.transform_matrix_compose(
      shift,
      @geometry.transform_matrix_compose(scale, matrix),
    )
  }
}

///|
fn PdfDocument::pdf_impose_transform_for_slot(
  self : PdfDocument,
  fit : Bool,
  x : Int,
  rtl : Bool,
  btt : Bool,
  margin : Double,
  output_mediabox : @syntax.PdfObject,
  fit_extra_hspace : Double,
  fit_extra_vspace : Double,
  cent_extra_x : Double,
  cent_extra_y : Double,
  row : Int,
  col : Int,
  px : Double,
  py : Double,
) -> @geometry.TransformMatrix raise @core.PdfError {
  let matrix = pdf_impose_translate(
    x, rtl, btt, cent_extra_x, cent_extra_y, fit_extra_hspace, fit_extra_vspace,
    row, col, px, py,
  )
  if fit {
    self.pdf_impose_make_margin(output_mediabox, margin, matrix)
  } else {
    matrix
  }
}

///|
#warnings("-unused_field")
priv struct PdfImposeLayout {
  pages_per_sheet : Int
  x_slots : Int
  y_slots : Int
  output_mediabox : @syntax.PdfObject
  fit_extra_hspace : Double
  fit_extra_vspace : Double
}

///|
fn pdf_impose_output_box(width : Double, height : Double) -> @syntax.PdfObject {
  PdfArray([PdfReal(0.0), PdfReal(0.0), PdfReal(width), PdfReal(height)])
}

///|
fn PdfDocument::pdf_impose_fit_layout(
  self : PdfDocument,
  x : Double,
  y : Double,
  page_width : Double,
  page_height : Double,
) -> PdfImposeLayout raise @core.PdfError {
  ignore(self)
  let across = @math.floor(x / page_width + 0.001).to_int()
  let down = @math.floor(y / page_height + 0.001).to_int()
  if across < 1 || down < 1 {
    raise BadNumberArgument("impose: not even a single page would fit")
  }
  {
    pages_per_sheet: across * down,
    x_slots: across,
    y_slots: down,
    output_mediabox: pdf_impose_output_box(x, y),
    fit_extra_hspace: (x - across.to_double() * page_width) /
    (across.to_double() + 1.0),
    fit_extra_vspace: (y - down.to_double() * page_height) /
    (down.to_double() + 1.0),
  }
}

///|
fn PdfDocument::pdf_impose_xy_layout(
  self : PdfDocument,
  x : Double,
  y : Double,
  margin : Double,
  page_count : Int,
  page_width : Double,
  page_height : Double,
) -> PdfImposeLayout raise @core.PdfError {
  ignore(self)
  let ix = x.to_int()
  let iy = y.to_int()
  if ix == 0 && iy == 0 {
    raise BadNumberArgument("impose-xy: both dimensions cannot be zero")
  }
  let (pages_per_sheet, x_slots, y_slots) = if ix == 0 {
    (page_count, page_count, 1)
  } else if iy == 0 {
    (page_count, 1, page_count)
  } else {
    (ix * iy, ix, iy)
  }
  let margin2 = margin * 2.0
  let output_mediabox = if x == 0.0 {
    pdf_impose_output_box(
      page_width * page_count.to_double() + margin2,
      page_height + margin2,
    )
  } else if y == 0.0 {
    pdf_impose_output_box(
      page_width + margin2,
      page_height * page_count.to_double() + margin2,
    )
  } else {
    pdf_impose_output_box(page_width * x + margin2, page_height * y + margin2)
  }
  {
    pages_per_sheet,
    x_slots,
    y_slots,
    output_mediabox,
    fit_extra_hspace: 0.0,
    fit_extra_vspace: 0.0,
  }
}

///|
#warnings("-unused_value")
fn PdfDocument::pdf_impose_layout(
  self : PdfDocument,
  fit : Bool,
  x : Double,
  y : Double,
  margin : Double,
  page_count : Int,
  mediabox : @syntax.PdfObject,
) -> PdfImposeLayout raise @core.PdfError {
  let bounds = self.parse_rectangle(mediabox)
  let page_width = bounds.max_x - bounds.min_x
  let page_height = bounds.max_y - bounds.min_y
  if fit {
    self.pdf_impose_fit_layout(x, y, page_width, page_height)
  } else {
    self.pdf_impose_xy_layout(x, y, margin, page_count, page_width, page_height)
  }
}

///|
#warnings("-unused_value")
fn PdfDocument::pdf_impose_transforms(
  self : PdfDocument,
  fit : Bool,
  x : Int,
  y : Int,
  columns : Bool,
  rtl : Bool,
  btt : Bool,
  center : Bool,
  margin : Double,
  mediabox : @syntax.PdfObject,
  output_mediabox : @syntax.PdfObject,
  fit_extra_hspace : Double,
  fit_extra_vspace : Double,
  count : Int,
) -> Array[@geometry.TransformMatrix] raise @core.PdfError {
  pdf_impose_validate_transform_inputs(x, y, count)
  let page_box = self.parse_rectangle(mediabox)
  let width = page_box.max_x - page_box.min_x
  let height = page_box.max_y - page_box.min_y
  let final_full_cols = count % x
  let final_full_rows = count % y
  let output : Array[@geometry.TransformMatrix] = []
  let mut remaining = count
  let mut cent_extra_x = 0.0
  let mut cent_extra_y = 0.0
  if columns {
    for col in 0.. 0 {
          output.push(
            self.pdf_impose_transform_for_slot(
              fit,
              x,
              rtl,
              btt,
              margin,
              output_mediabox,
              fit_extra_hspace,
              fit_extra_vspace,
              cent_extra_x,
              cent_extra_y,
              adjusted_row,
              ordered_col,
              width * ordered_col.to_double(),
              height * ordered_row.to_double(),
            ),
          )
        }
        remaining -= 1
      }
    }
  } else {
    for row_offset in 0.. 0 {
          output.push(
            self.pdf_impose_transform_for_slot(
              fit,
              x,
              rtl,
              btt,
              margin,
              output_mediabox,
              fit_extra_hspace,
              fit_extra_vspace,
              cent_extra_x,
              cent_extra_y,
              ordered_row,
              adjusted_col,
              width * ordered_col.to_double(),
              height * ordered_row.to_double(),
            ),
          )
        }
        remaining -= 1
      }
    }
  }
  output
}