///|
fn pad_int(value : Int, width : Int) -> String {
  let s = value.to_string()
  if s.length() >= width {
    s
  } else {
    "0".repeat(width - s.length()) + s
  }
}

///|
fn write_header(pdf : @pdf.Pdf, output : @pdfio.Output) -> Unit {
  (output.output_string)("%PDF-\{pdf.major}.\{pdf.minor}\n%")
  let bytes = [128, 129, 130, 131]
  for b in bytes {
    (output.output_byte)(b)
  }
  (output.output_char)('\n')
}

///|
fn bytes_required(value : Int) -> Int {
  if value <= 0 {
    return 1
  }
  let mut v = value
  let mut bytes = 0
  while v > 0 {
    bytes = bytes + 1
    v = v >> 8
  }
  bytes
}

///|
fn write_int_be(output : @pdfio.Output, value : Int, width : Int) -> Unit {
  for pos in (width - 1)>=..0 {
    let shift = pos * 8
    let byte = (value >> shift) & 0xff
    (output.output_byte)(byte)
  }
}

///|
fn build_xref_stream(
  pdf : @pdf.Pdf,
  size : Int,
  offsets : Array[Int],
  gens : Array[Int],
  used : Array[Bool],
  object_stream_entries : Map[Int, (Int, Int)],
) -> @pdf.PdfObject raise {
  let mut max_type = 1
  let mut max_field2 = 0
  let mut max_field3 = 0
  for i in 0.. (2, stream_id, index)
      None =>
        if i == 0 || !used[i] {
          (0, 0, 65535)
        } else {
          (1, offsets[i], gens[i])
        }
    }
    if typ > max_type {
      max_type = typ
    }
    if field2 > max_field2 {
      max_field2 = field2
    }
    if field3 > max_field3 {
      max_field3 = field3
    }
  }
  let w1 = bytes_required(max_type)
  let w2 = bytes_required(max_field2)
  let w3 = bytes_required(max_field3)
  let (output, buffer) = @pdfio.Output::of_bytes(256)
  for i in 0.. (2, stream_id, index)
      None =>
        if i == 0 || !used[i] {
          (0, 0, 65535)
        } else {
          (1, offsets[i], gens[i])
        }
    }
    write_int_be(output, typ, w1)
    write_int_be(output, field2, w2)
    write_int_be(output, field3, w3)
  }
  let data = output.extract_bytes(buffer)
  let dict_with_type = pdf.trailerdict.add_entry(
    "/Type",
    PdfObject::Name(@pdf.PdfName::of_string("/XRef")),
  )
  let dict_with_size = dict_with_type.add_entry(
    "/Size",
    PdfObject::Integer(size),
  )
  let dict_with_root = dict_with_size.add_entry(
    "/Root",
    PdfObject::Indirect(pdf.root),
  )
  let dict_with_w = dict_with_root.add_entry(
    "/W",
    PdfObject::Array([
      PdfObject::Integer(w1),
      PdfObject::Integer(w2),
      PdfObject::Integer(w3),
    ]),
  )
  let dict_with_length = dict_with_w.add_entry(
    "/Length",
    PdfObject::Integer(@pdfio.bytes_size(data)),
  )
  let stream_obj = PdfObject::Stream(
    Ref::new((dict_with_length, @pdf.Stream::Got(data))),
  )
  stream_obj
}

///|
/// Write a PDF to an output.
pub fn PdfWrite::pdf_to_output(
  self : PdfWrite,
  preserve_objstm? : Bool = false,
  generate_objstm? : Bool = false,
  compress_objstm? : Bool = false,
  recrypt? : String,
  encryption : Encryption?,
  build_new_id : Bool,
  pdf : @pdf.Pdf,
  output : @pdfio.Output,
) -> Unit raise {
  let mut pdf = pdf
  if build_new_id {
    pdf.change_id("")
  }
  let recrypting = match recrypt {
    Some(_) => true
    None => false
  }
  let mut encryption_value = encryption
  if recrypting {
    encryption_value = Some(dummy_encryption)
  }
  let encrypting = match encryption_value {
    Some(_) => true
    None => false
  }
  if encrypting {
    pdf.objects.object_stream_ids.remove(pdf.root)
  }
  let mut object_stream_entries : Map[Int, (Int, Int)] = Map::new()
  if preserve_objstm || generate_objstm {
    if generate_objstm {
      generate_object_stream_hints(pdf, preserve_objstm, encrypting)
    }
    let groups = build_object_stream_groups(pdf)
    if groups.length() > 0 {
      object_stream_entries = bake_object_streams(pdf, compress_objstm, groups)
    }
  }
  if !preserve_objstm && !generate_objstm {
    match encryption_value {
      Some(enc) =>
        match enc.encryption_method {
          @pdfcrypt.EncryptionMethod::AlreadyEncrypted => ()
          _ => pdf = pdf.renumber(pdf.changes())
        }
      None => ()
    }
  }
  match recrypt {
    Some(password) =>
      pdf = @pdfcrypt.PdfCrypt::new().recrypt_pdf(
        pdf,
        password,
        renumber=!(preserve_objstm || generate_objstm),
      )
    None => ()
  }
  pdf = crypt_if_necessary(pdf, encryption_value)
  let use_xref_stream = preserve_objstm || generate_objstm
  write_header(pdf, output)
  let entries = @pdf.pdfobjmap_bindings(pdf.objects.objects)
  let objnums = entries.map(pair => pair.0)
  objnums.sort_by((a, b) => a - b)
  let max_obj = pdf.objects.max_obj_num
  let size = if use_xref_stream { max_obj + 2 } else { max_obj + 1 }
  let xref_objnum = max_obj + 1
  let offsets = Array::make(size, 0)
  let gens = Array::make(size, 0)
  let used = Array::make(size, false)
  for objnum in objnums {
    if objnum == 0 || (use_xref_stream && objnum == xref_objnum) {
      continue
    }
    match pdf.objects.objects.get(objnum) {
      None => ()
      Some((_obj_ref, gen)) => {
        let offset = (output.pos_out)()
        offsets[objnum] = offset
        gens[objnum] = gen
        used[objnum] = true
        (output.output_string)("\{objnum} \{gen} obj\n")
        let obj = pdf.lookup_obj(objnum)
        write_pdf_object_to_output(self, obj, output, true, false)
        (output.output_string)("\nendobj\n")
      }
    }
  }
  let xref_start = (output.pos_out)()
  if use_xref_stream {
    offsets[xref_objnum] = xref_start
    gens[xref_objnum] = 0
    used[xref_objnum] = true
    let xref_stream = build_xref_stream(
      pdf, size, offsets, gens, used, object_stream_entries,
    )
    (output.output_string)("\{xref_objnum} 0 obj\n")
    write_pdf_object_to_output(self, xref_stream, output, true, true)
    (output.output_string)("\nendobj\n")
    (output.output_string)("startxref\n")
    (output.output_string)("\{xref_start}\n%%EOF\n")
  } else {
    (output.output_string)("xref\n")
    (output.output_string)("0 \{size}\n")
    for i in 0..