///|
fn pdf_add_filter_to_dictionary(
  dictionary : @syntax.PdfObject,
  filter : @core.PdfName,
) -> @syntax.PdfObject raise @core.PdfError {
  let filter_object = @syntax.PdfNameObject(filter)
  match dictionary.lookup_immediate(pdf_filter_key()) {
    None => dictionary.add_dict_entry(pdf_filter_key(), filter_object)
    Some(PdfNameObject(existing)) =>
      dictionary.replace_dict_entry(
        pdf_filter_key(),
        PdfArray([filter_object, PdfNameObject(existing)]),
      )
    Some(PdfArray(filters)) =>
      dictionary.replace_dict_entry(
        pdf_filter_key(),
        PdfArray([filter_object, ..filters]),
      )
    _ => raise FilterExpected
  }
}

///|
/// Encode a stream object with one filter.
///
/// The new filter is prepended to existing filters and `/Length` is refreshed.
/// When `only_if_smaller` is true, the original stream is returned if encoded
/// data is not smaller than the original stream data.
pub fn pdf_encode_stream_with_filter(
  filter : @core.PdfName,
  object : @syntax.PdfObject,
  only_if_smaller? : Bool = false,
  flate_level? : Int? = None,
) -> @syntax.PdfObject raise @core.PdfError {
  match object {
    PdfStreamObject(stream) => {
      let data = @syntax.pdf_stream_data_bytes(stream.data)
      let encoded = match flate_level {
        Some(level) =>
          pdf_encode_filter_bytes_with_flate_level(filter, data, level)
        None => pdf_encode_filter_bytes(filter, data)
      }
      if only_if_smaller && !pdf_encoded_stream_is_smaller(encoded, data) {
        object
      } else {
        let dictionary = pdf_stream_replace_length(
          pdf_add_filter_to_dictionary(stream.dictionary, filter),
          encoded.length(),
        )
        @syntax.pdf_stream(dictionary, StreamGot(encoded))
      }
    }
    _ => raise ParseStreamExpected
  }
}

///|
fn pdf_encoded_stream_is_smaller(
  encoded : @core.PdfBytes,
  data : @core.PdfBytes,
) -> Bool {
  encoded.length() + 20 < data.length()
}

///|
fn pdf_predictor_decodeparms(
  predictor : Int,
  colors : Int,
  bits_per_component : Int,
  columns : Int,
) -> @syntax.PdfObject {
  @syntax.pdf_dictionary([
    (pdf_predictor_key(), PdfInteger(predictor)),
    (pdf_colors_key(), PdfInteger(colors)),
    (pdf_bits_per_component_key(), PdfInteger(bits_per_component)),
    (pdf_columns_key(), PdfInteger(columns)),
  ])
}

///|
fn pdf_ccitt_encoding_decodeparms(
  k : Int,
  columns : Int,
  rows : Int,
) -> @syntax.PdfObject {
  @syntax.pdf_dictionary([
    (pdf_ccitt_k_key(), PdfInteger(k)),
    (pdf_columns_key(), PdfInteger(columns)),
    (pdf_ccitt_rows_key(), PdfInteger(rows)),
  ])
}

///|
fn pdf_add_decodeparms_to_dictionary(
  dictionary : @syntax.PdfObject,
  decodeparms : @syntax.PdfObject,
) -> @syntax.PdfObject raise @core.PdfError {
  match dictionary.lookup_immediate(pdf_decodeparms_key()) {
    None => dictionary.add_dict_entry(pdf_decodeparms_key(), decodeparms)
    Some(PdfDictionary(_) as existing) =>
      dictionary.replace_dict_entry(
        pdf_decodeparms_key(),
        PdfArray([decodeparms, existing]),
      )
    Some(PdfArray(values)) =>
      dictionary.replace_dict_entry(
        pdf_decodeparms_key(),
        PdfArray([decodeparms, ..values]),
      )
    _ => raise PredictorExpected
  }
}

///|
fn pdf_encode_stream_with_ccitt(
  columns : Int,
  rows : Int,
  k : Int,
  object : @syntax.PdfObject,
  only_if_smaller : Bool,
) -> @syntax.PdfObject raise @core.PdfError {
  match object {
    PdfStreamObject(stream) => {
      let data = @syntax.pdf_stream_data_bytes(stream.data)
      let encoded = if k < 0 {
        pdf_ccitt_encode_g4_view(data, columns, rows)
      } else {
        pdf_ccitt_encode_g3_1d_view(data, columns, rows)
      }
      if only_if_smaller && !pdf_encoded_stream_is_smaller(encoded, data) {
        object
      } else {
        let dictionary = pdf_stream_replace_length(
          pdf_add_filter_to_dictionary(
            pdf_add_decodeparms_to_dictionary(
              stream.dictionary,
              pdf_ccitt_encoding_decodeparms(k, columns, rows),
            ),
            pdf_filter_ccitt_fax_decode_name,
          ),
          encoded.length(),
        )
        @syntax.pdf_stream(dictionary, StreamGot(encoded))
      }
    }
    _ => raise ParseStreamExpected
  }
}

///|
/// Encode a stream after first applying predictor encoding.
///
/// `/DecodeParms` are added for the predictor, the filter is prepended, and
/// `/Length` is refreshed. `only_if_smaller` compares encoded data against the
/// original stream data.
pub fn pdf_encode_stream_with_filter_and_predictor(
  filter : @core.PdfName,
  predictor : Int,
  colors : Int,
  bits_per_component : Int,
  columns : Int,
  object : @syntax.PdfObject,
  only_if_smaller? : Bool = false,
  flate_level? : Int? = None,
) -> @syntax.PdfObject raise @core.PdfError {
  match object {
    PdfStreamObject(stream) => {
      let data = @syntax.pdf_stream_data_bytes(stream.data)
      let predicted = pdf_encode_predictor(
        predictor, colors, bits_per_component, columns, data,
      )
      let encoded = match flate_level {
        Some(level) =>
          pdf_encode_filter_bytes_with_flate_level(filter, predicted, level)
        None => pdf_encode_filter_bytes(filter, predicted)
      }
      if only_if_smaller && !pdf_encoded_stream_is_smaller(encoded, data) {
        object
      } else {
        let dictionary = pdf_stream_replace_length(
          pdf_add_filter_to_dictionary(
            pdf_add_decodeparms_to_dictionary(
              stream.dictionary,
              pdf_predictor_decodeparms(
                predictor, colors, bits_per_component, columns,
              ),
            ),
            filter,
          ),
          encoded.length(),
        )
        @syntax.pdf_stream(dictionary, StreamGot(encoded))
      }
    }
    _ => raise ParseStreamExpected
  }
}

///|
/// Encode a stream using a typed encoding description.
///
/// CCITT encodings write fax `/DecodeParms`; other encodings may optionally
/// apply a predictor before the filter. CCITT does not accept predictors.
pub fn pdf_encode_stream_with_encoding(
  encoding : PdfStreamEncoding,
  object : @syntax.PdfObject,
  only_if_smaller? : Bool = false,
  predictor? : PdfStreamPredictor? = None,
  predictor_columns? : Int = 1,
  flate_level? : Int? = None,
) -> @syntax.PdfObject raise @core.PdfError {
  let filter = encoding.filter_name()
  match encoding {
    PdfStreamCCITT(columns, rows) =>
      match predictor {
        None =>
          pdf_encode_stream_with_ccitt(
            columns, rows, 0, object, only_if_smaller,
          )
        Some(_) => raise PredictorExpected
      }
    PdfStreamCCITTG4(columns, rows) =>
      match predictor {
        None =>
          pdf_encode_stream_with_ccitt(
            columns, rows, -1, object, only_if_smaller,
          )
        Some(_) => raise PredictorExpected
      }
    _ =>
      match predictor {
        None =>
          pdf_encode_stream_with_filter(
            filter,
            object,
            only_if_smaller~,
            flate_level~,
          )
        Some(predictor) =>
          pdf_encode_stream_with_filter_and_predictor(
            filter,
            predictor.number(),
            1,
            8,
            predictor_columns,
            object,
            only_if_smaller~,
            flate_level~,
          )
      }
  }
}