///|
fn PdfDocument::pdf_fun_number_array(
  self : PdfDocument,
  object : @syntax.PdfObject,
) -> Array[Double] raise @core.PdfError {
  match self.direct(object) {
    PdfArray(values) => {
      let output : Array[Double] = []
      for value in values {
        output.push(self.get_number(value))
      }
      output
    }
    _ => raise NumberExpected
  }
}

///|
fn PdfDocument::pdf_fun_lookup_number_array_or(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  key : @core.PdfName,
  default : ArrayView[Double],
) -> Array[Double] raise @core.PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.pdf_fun_number_array(value)
    None => [ for value in default => value ]
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_number_array(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  key : @core.PdfName,
) -> Array[Double] raise @core.PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.pdf_fun_number_array(value)
    None => raise DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_number(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  key : @core.PdfName,
) -> Double raise @core.PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.get_number(value)
    None => raise DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_int_array(
  self : PdfDocument,
  object : @syntax.PdfObject,
) -> Array[Int] raise @core.PdfError {
  match self.direct(object) {
    PdfArray(values) => {
      let output : Array[Int] = []
      for value in values {
        match self.direct(value) {
          PdfInteger(integer) => output.push(integer)
          _ => raise NumberExpected
        }
      }
      output
    }
    _ => raise NumberExpected
  }
}

///|
fn PdfDocument::pdf_fun_object_array(
  self : PdfDocument,
  object : @syntax.PdfObject,
) -> Array[@syntax.PdfObject] raise @core.PdfError {
  match self.direct(object) {
    PdfArray(values) => [ for value in values => value ]
    _ => raise FunctionExpected
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_object_array(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  key : @core.PdfName,
) -> Array[@syntax.PdfObject] raise @core.PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.pdf_fun_object_array(value)
    None => raise DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_int_array(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  key : @core.PdfName,
) -> Array[Int] raise @core.PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(value) => self.pdf_fun_int_array(value)
    None => raise DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_required_int(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  key : @core.PdfName,
) -> Int raise @core.PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(PdfInteger(value)) => value
    Some(_) => raise NumberExpected
    None => raise DictionaryKeyNotFound(key)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_int_or(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  key : @core.PdfName,
  default : Int,
) -> Int raise @core.PdfError {
  match self.lookup_direct(key, dictionary) {
    Some(PdfInteger(value)) => value
    Some(_) => raise NumberExpected
    None => default
  }
}

///|
fn pdf_fun_default_encode(size : ArrayView[Int]) -> Array[Double] {
  let output : Array[Double] = []
  for value in size {
    output.push(0.0)
    output.push((value - 1).to_double())
  }
  output
}

///|
fn PdfDocument::pdf_fun_lookup_encode(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  size : ArrayView[Int],
) -> Array[Double] raise @core.PdfError {
  match self.lookup_direct(pdf_fun_encode_key(), dictionary) {
    Some(value) => {
      let encode = self.pdf_fun_number_array(value)
      if encode.length() == size.length() * 2 {
        encode
      } else {
        pdf_fun_default_encode(size)
      }
    }
    None => pdf_fun_default_encode(size)
  }
}

///|
fn PdfDocument::pdf_fun_lookup_decode(
  self : PdfDocument,
  dictionary : @syntax.PdfObject,
  range : Array[Double]?,
) -> Array[Double] raise @core.PdfError {
  match self.lookup_direct(pdf_fun_decode_key(), dictionary) {
    Some(value) => self.pdf_fun_number_array(value)
    None =>
      match range {
        Some(range) => [ for value in range => value ]
        None => raise DictionaryKeyNotFound(pdf_fun_range_key())
      }
  }
}

///|
fn pdf_fun_product(values : ArrayView[Int]) -> Int {
  let mut product = 1
  for value in values {
    product *= value
  }
  product
}

///|
fn PdfDocument::pdf_fun_stream_data(
  self : PdfDocument,
  object : @syntax.PdfObject,
) -> @core.PdfBytes raise @core.PdfError {
  match self.direct(object) {
    PdfStreamObject(stream) =>
      @syntax.pdf_stream_data_bytes(
        self.pdf_decode_stream_value_direct(stream).data,
      )
    _ => raise ParseStreamExpected
  }
}

///|
fn PdfDocument::pdf_fun_read_sampled(
  self : PdfDocument,
  function_object : @syntax.PdfObject,
  dictionary : @syntax.PdfObject,
  range : Array[Double]?,
) -> PdfSampledFunction raise @core.PdfError {
  let size = self.pdf_fun_lookup_required_int_array(
    dictionary,
    pdf_fun_size_key(),
  )
  let order = self.pdf_fun_lookup_int_or(dictionary, pdf_fun_order_key(), 1)
  let encode = self.pdf_fun_lookup_encode(dictionary, size)
  let decode = self.pdf_fun_lookup_decode(dictionary, range)
  if decode.length() % 2 != 0 {
    raise FunctionExpected
  }
  let bits_per_sample = self.pdf_fun_lookup_required_int(
    dictionary,
    pdf_fun_bits_per_sample_key(),
  )
  let sample_count = pdf_fun_product(size) * (decode.length() / 2)
  let bitstream = @core.bitstream_of_bytes(
    self.pdf_fun_stream_data(function_object),
  )
  let samples : Array[Int] = []
  for _ in 0.. PdfStitchingFunction raise @core.PdfError {
  let function_objects = self.pdf_fun_lookup_required_object_array(
    dictionary,
    pdf_fun_functions_key(),
  )
  let functions : Array[PdfFunction] = []
  for function_object in function_objects {
    functions.push(self.parse_function(function_object))
  }
  let bounds = self.pdf_fun_lookup_required_number_array(
    dictionary,
    pdf_fun_bounds_key(),
  )
  let stitch_encode = self.pdf_fun_lookup_required_number_array(
    dictionary,
    pdf_fun_encode_key(),
  )
  if functions.length() == 0 ||
    bounds.length() != functions.length() - 1 ||
    stitch_encode.length() != functions.length() * 2 {
    raise FunctionExpected
  }
  { functions, bounds, stitch_encode, }
}

///|
/// Parse a PDF function object from this document.
///
/// Supports PDF function types 0, 2, 3, and 4. Sampled and calculator functions
/// are read from decoded stream data; interpolation and stitching functions are
/// read from dictionaries. Raises for malformed dictionaries, invalid arities,
/// stream decoding failures, or unsupported function types.
pub fn PdfDocument::parse_function(
  self : PdfDocument,
  function_object : @syntax.PdfObject,
) -> PdfFunction raise @core.PdfError {
  let function_dictionary = self.direct(function_object)
  let domain = self.pdf_fun_lookup_required_number_array(
    function_dictionary,
    pdf_fun_domain_key(),
  )
  let range = match
    self.lookup_direct(pdf_fun_range_key(), function_dictionary) {
    Some(value) => Some(self.pdf_fun_number_array(value))
    None => None
  }
  let function_type = match
    self.lookup_direct(pdf_fun_function_type_key(), function_dictionary) {
    Some(PdfInteger(value)) => value
    Some(_) => raise NumberExpected
    None => raise DictionaryKeyNotFound(pdf_fun_function_type_key())
  }
  match function_type {
    0 =>
      {
        domain,
        range,
        func: PdfFunctionSampled(
          self.pdf_fun_read_sampled(function_object, function_dictionary, range),
        ),
      }
    2 => {
      let c0 = self.pdf_fun_lookup_number_array_or(
        function_dictionary,
        pdf_fun_c0_key(),
        [0.0],
      )
      let c1 = self.pdf_fun_lookup_number_array_or(
        function_dictionary,
        pdf_fun_c1_key(),
        [1.0],
      )
      if c0.length() != c1.length() {
        raise FunctionExpected
      }
      {
        domain,
        range,
        func: PdfFunctionInterpolation({
          c0,
          c1,
          n: self.pdf_fun_lookup_required_number(
            function_dictionary,
            pdf_fun_n_key(),
          ),
        }),
      }
    }
    3 =>
      {
        domain,
        range,
        func: PdfFunctionStitching(
          self.pdf_fun_read_stitching(function_dictionary),
        ),
      }
    4 =>
      {
        domain,
        range,
        func: PdfFunctionCalculator(
          pdf_fun_parse_calculator(self.pdf_fun_stream_data(function_object)),
        ),
      }
    other => raise FunctionNotSupported(other)
  }
}

///|
/// Compatibility wrapper for `PdfDocument::parse_function`.
pub fn pdf_parse_function(
  document : PdfDocument,
  function_object : @syntax.PdfObject,
) -> PdfFunction raise @core.PdfError {
  document.parse_function(function_object)
}