///|
fn double_min(a : Double, b : Double) -> Double {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn double_max(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}

///|
/// The object is resolved first. PDF real numbers are returned directly and PDF
/// integers are widened to `Double`; any other object raises `@core.PdfError::NumberExpected`.
pub fn PdfDocument::get_number(
  self : PdfDocument,
  object : @syntax.PdfObject,
) -> Double raise @core.PdfError {
  match self.direct(object) {
    PdfReal(value) | PdfExactReal(value) => value
    PdfInteger(value) => value.to_double()
    _ => raise NumberExpected
  }
}

///|
/// Parse and normalize a PDF rectangle array.
///
/// The input must resolve to an array of four numeric objects. Coordinates are
/// normalized so the result always has lower bounds in `min_x`/`min_y` and
/// upper bounds in `max_x`/`max_y`. Non-array inputs raise `@core.PdfError::RectangleExpected`;
/// malformed numeric entries raise `@core.PdfError::BadRectangle`.
pub fn PdfDocument::parse_rectangle(
  self : PdfDocument,
  object : @syntax.PdfObject,
) -> @geometry.PdfRectangle raise @core.PdfError {
  match self.direct(object) {
    PdfArray(values) if values.length() == 4 => {
      let x0 = try self.get_number(values[0]) catch {
        err => Err(err)
      } noraise {
        value => Ok(value)
      }
      let y0 = try self.get_number(values[1]) catch {
        err => Err(err)
      } noraise {
        value => Ok(value)
      }
      let x1 = try self.get_number(values[2]) catch {
        err => Err(err)
      } noraise {
        value => Ok(value)
      }
      let y1 = try self.get_number(values[3]) catch {
        err => Err(err)
      } noraise {
        value => Ok(value)
      }
      match (x0, y0, x1, y1) {
        (Ok(x0), Ok(y0), Ok(x1), Ok(y1)) =>
          {
            min_x: double_min(x0, x1),
            min_y: double_min(y0, y1),
            max_x: double_max(x0, x1),
            max_y: double_max(y0, y1),
          }
        _ => raise BadRectangle
      }
    }
    _ => raise RectangleExpected
  }
}

///|
/// Transform a rectangle and return the bounding PDF rectangle array.
///
/// All four corners are transformed with `matrix`, then the axis-aligned bounds
/// of those transformed points are written as `[minX minY maxX maxY]`.
pub fn PdfDocument::transform_rect(
  self : PdfDocument,
  matrix : @geometry.TransformMatrix,
  object : @syntax.PdfObject,
) -> @syntax.PdfObject raise @core.PdfError {
  let rectangle = self.parse_rectangle(object)
  let p0 = matrix.apply(@geometry.point2(rectangle.min_x, rectangle.min_y))
  let p1 = matrix.apply(@geometry.point2(rectangle.max_x, rectangle.max_y))
  let p2 = matrix.apply(@geometry.point2(rectangle.min_x, rectangle.max_y))
  let p3 = matrix.apply(@geometry.point2(rectangle.max_x, rectangle.min_y))
  PdfArray([
    PdfReal(double_min(double_min(p0.x, p1.x), double_min(p2.x, p3.x))),
    PdfReal(double_min(double_min(p0.y, p1.y), double_min(p2.y, p3.y))),
    PdfReal(double_max(double_max(p0.x, p1.x), double_max(p2.x, p3.x))),
    PdfReal(double_max(double_max(p0.y, p1.y), double_max(p2.y, p3.y))),
  ])
}

///|
fn PdfDocument::pdf_transform_number_or_none(
  self : PdfDocument,
  object : @syntax.PdfObject,
) -> Double? {
  try self.get_number(object) catch {
    _ => None
  } noraise {
    value => Some(value)
  }
}

///|
fn PdfDocument::pdf_transform_quadpoint_chunk(
  self : PdfDocument,
  matrix : @geometry.TransformMatrix,
  values : ArrayView[@syntax.PdfObject],
  start : Int,
) -> Array[@syntax.PdfObject] {
  let numbers : Array[Double] = []
  let mut valid = true
  for index in start..<(start + 8) {
    match self.pdf_transform_number_or_none(values[index]) {
      Some(value) => numbers.push(value)
      None => valid = false
    }
  }
  if valid {
    let output : Array[@syntax.PdfObject] = []
    for index in 0..<4 {
      let point = matrix.apply(
        @geometry.point2(numbers[index * 2], numbers[index * 2 + 1]),
      )
      output.push(PdfReal(point.x))
      output.push(PdfReal(point.y))
    }
    output
  } else {
    [
      for index in start..<(start + 8) => values[index]
    ]
  }
}

///|
/// Transform PDF annotation quadpoints in groups of eight coordinates.
///
/// Each complete group is interpreted as four `(x, y)` pairs and transformed by
/// `matrix`. A complete group containing a non-numeric value is preserved
/// unchanged, as is any trailing partial group. Non-array inputs are returned
/// unchanged.
pub fn PdfDocument::transform_quadpoints(
  self : PdfDocument,
  matrix : @geometry.TransformMatrix,
  object : @syntax.PdfObject,
) -> @syntax.PdfObject {
  match self.direct(object) {
    PdfArray(values) => {
      let output : Array[@syntax.PdfObject] = []
      let mut start = 0
      while start < values.length() {
        if start + 8 <= values.length() {
          for value in self.pdf_transform_quadpoint_chunk(matrix, values, start) {
            output.push(value)
          }
        } else {
          for index in start.. object
  }
}

///|
/// Read a transformation matrix from a dictionary entry.
///
/// If `key` is absent, the identity matrix is returned. Present values must be
/// arrays of six numbers in PDF matrix order `[a b c d e f]`; malformed arrays
/// raise `@core.PdfError::MatrixMalformed`, and non-numeric entries raise `@core.PdfError::NumberExpected`.
pub fn PdfDocument::parse_matrix(
  self : PdfDocument,
  key : @core.PdfName,
  object : @syntax.PdfObject,
) -> @geometry.TransformMatrix raise @core.PdfError {
  match self.lookup_direct(key, object) {
    None => @geometry.transform_identity_matrix
    Some(PdfArray(values)) if values.length() == 6 =>
      {
        a: self.get_number(values[0]),
        b: self.get_number(values[1]),
        c: self.get_number(values[2]),
        d: self.get_number(values[3]),
        e: self.get_number(values[4]),
        f: self.get_number(values[5]),
      }
    _ => raise MatrixMalformed
  }
}

///|
/// Convert a transformation matrix into its six-number PDF array form.
pub fn pdf_object_of_matrix(
  matrix : @geometry.TransformMatrix,
) -> @syntax.PdfObject {
  PdfArray([
    PdfReal(matrix.a),
    PdfReal(matrix.b),
    PdfReal(matrix.c),
    PdfReal(matrix.d),
    PdfReal(matrix.e),
    PdfReal(matrix.f),
  ])
}