///|
fn pdf_read_reader_revision_target(revision : Int?) -> Int? raise PdfError {
  match revision {
    Some(value) if value < 1 => raise PdfError::BadRevision
    _ => revision
  }
}

///|
fn pdf_reader_revision_reads_section(
  revision : Int?,
  current_revision : Int,
) -> Bool {
  match revision {
    Some(target) => target <= current_revision
    None => true
  }
}

///|
fn pdf_reader_revision_selects_trailer(
  revision : Int?,
  current_revision : Int,
  depth : Int,
) -> Bool {
  match revision {
    Some(target) => target == current_revision
    None => depth == 0
  }
}

///|
fn pdf_read_classic_document_from_view_at_revision(
  data : BytesView,
  revision? : Int? = None,
  user_password? : BytesView? = None,
  owner_password? : BytesView? = None,
) -> PdfDocument raise PdfError {
  let revision = pdf_read_reader_revision_target(revision)
  let was_linearized = pdf_read_was_linearized(data)
  let cursor = byte_cursor_of_view(data)
  let version = cursor.read_pdf_header()
  let xref_position = pdf_read_startxref_position(cursor.data)
  let xrefs : Array[PdfClassicXRefEntry] = []
  let mut trailer = PdfDictionary([])
  let xref_stream_refs : Array[(Int, Int)] = []
  let mut current_xref = xref_position
  let mut done = false
  let mut depth = 0
  while !done {
    if depth >= 1024 {
      raise PdfError::XRefExpected
    }
    cursor.seek_pdf_absolute(current_xref)
    let section = cursor.read_pdf_xref_section()
    let current_revision = depth + 1
    let read_section = pdf_reader_revision_reads_section(
      revision, current_revision,
    )
    if read_section {
      pdf_classic_xrefs_add_newer_first(xrefs, section.entries)
      match section.stream_object_number {
        Some(number) => xref_stream_refs.push((number, current_xref))
        None => ()
      }
    }
    let current_trailer = section.trailer
    if pdf_reader_revision_selects_trailer(revision, current_revision, depth) {
      trailer = current_trailer
    }
    if read_section {
      match pdf_trailer_xref_stream_position(current_trailer) {
        Some(xref_stream_position) => {
          cursor.seek_pdf_absolute(xref_stream_position)
          let stream_section = cursor.read_pdf_xref_stream_section()
          pdf_classic_xrefs_add_newer_first(xrefs, stream_section.entries)
          xref_stream_refs.push(
            (stream_section.stream_object_number.unwrap(), xref_stream_position),
          )
        }
        None => ()
      }
    }
    match pdf_trailer_prev_position(current_trailer) {
      Some(previous) => current_xref = previous
      None =>
        if read_section {
          done = true
        } else {
          raise PdfError::BadRevision
        }
    }
    depth += 1
  }
  let document = pdf_document_empty()
  document.set_version(version.major, version.minor)
  document.set_first_xref(xref_position)
  document.set_was_linearized(was_linearized)
  match revision {
    Some(value) => document.set_revision_read(value)
    None => ()
  }
  document.set_trailer_and_root(trailer)
  let xref_stream_ref_set = pdf_reader_xref_stream_ref_set(xref_stream_refs)
  let xref_index = pdf_classic_xref_index(xrefs)
  for xref in xrefs {
    match xref.object_stream {
      Some(_) => ()
      None =>
        if xref.in_use &&
          xref.object_number != 0 &&
          !xref_stream_ref_set.contains(xref.object_number, xref.offset) {
          cursor.seek_pdf_absolute(xref.offset)
          let parsed = cursor.read_first_indirect_object_at_current(
            xrefs,
            xref_index=Some(xref_index),
          )
          if parsed.number != xref.object_number ||
            parsed.generation != xref.generation {
            raise PdfError::XRefEntryExpected
          }
          document.load_object_given_number(
            parsed.number,
            parsed.object,
            generation=parsed.generation,
          )
        }
    }
  }
  for group in pdf_reader_object_stream_groups(xrefs) {
    let object_stream_number = group.0
    let (context, already_decrypted) = document.read_object_stream_context_with_passwords(
      object_stream_number, user_password, owner_password,
    )
    document.load_embedded_objects_from_context(
      group.1,
      object_stream_number,
      context,
      already_decrypted,
    )
  }
  document.set_trailer_dict(
    pdf_reader_sanitize_trailer(
      document.trailer_dict(),
      document.objects.max_object_number() + 1,
    ),
  )
  document
}

///|
/// Read a PDF using classic xref metadata.
///
/// This strict reader follows `startxref`, xref tables/streams, and trailer
/// `/Prev` links, loads referenced objects, resolves object streams, and records
/// document metadata. It does not use reconstruction fallback.
pub fn pdf_read_classic_document_from_view(
  data : BytesView,
) -> PdfDocument raise PdfError {
  pdf_read_classic_document_from_view_at_revision(data)
}

///|
/// Read a classic-xref PDF from owned bytes.
pub fn pdf_read_classic_document_from_bytes(
  data : PdfBytes,
) -> PdfDocument raise PdfError {
  pdf_read_classic_document_from_view(data)
}

///|
/// Read one classic-xref document revision.
///
/// Revision `1` is the newest revision; larger revision numbers select older
/// revisions by following trailer `/Prev` links. Invalid or unavailable
/// revisions raise `PdfError::BadRevision`.
pub fn pdf_read_classic_document_revision_from_view(
  data : BytesView,
  revision : Int,
) -> PdfDocument raise PdfError {
  pdf_read_classic_document_from_view_at_revision(data, revision=Some(revision))
}

///|
/// Read one classic-xref document revision from owned bytes.
pub fn pdf_read_classic_document_revision_from_bytes(
  data : PdfBytes,
  revision : Int,
) -> PdfDocument raise PdfError {
  pdf_read_classic_document_revision_from_view(data, revision)
}

///|
/// Read a PDF document from a borrowed byte view.
///
/// This is the high-level non-password reader. It first tries the strict classic
/// xref reader and probes the page tree; for selected structural errors it falls
/// back to reconstruction by scanning indirect objects.
pub fn pdf_read_document_from_view(
  data : BytesView,
) -> PdfDocument raise PdfError {
  pdf_read_probe_page_tree_or_reconstruct(
    data,
    pdf_read_classic_document_from_view(data),
  ) catch {
    PdfError::StartXRefExpected =>
      pdf_read_reconstructed_document_from_view(data)
    PdfError::XRefExpected =>
      pdf_read_reconstructed_or_raise(
        data,
        PdfError::XRefExpected,
        include_xref_stream_trailers=true,
      )
    PdfError::XRefEntryExpected =>
      if pdf_reader_startxref_root_uses_object_stream_or_false(data) {
        raise PdfError::XRefEntryExpected
      } else {
        pdf_read_reconstructed_or_raise(data, PdfError::XRefEntryExpected)
      }
    PdfError::TrailerExpected =>
      pdf_read_reconstructed_or_raise(data, PdfError::TrailerExpected)
    PdfError::RootExpected =>
      pdf_read_reconstructed_or_raise(data, PdfError::RootExpected)
    PdfError::NumberExpected =>
      pdf_read_reconstructed_or_raise(data, PdfError::NumberExpected)
    PdfError::ParseIndirectObjectExpected =>
      pdf_read_reconstructed_or_raise(
        data,
        PdfError::ParseIndirectObjectExpected,
      )
    PdfError::ParseStreamExpected =>
      pdf_read_reconstructed_or_raise(data, PdfError::ParseStreamExpected)
    PdfError::ParseEndStreamExpected =>
      pdf_read_reconstructed_or_raise(data, PdfError::ParseEndStreamExpected)
    PdfError::ParseEndObjectExpected =>
      pdf_read_reconstructed_or_raise(data, PdfError::ParseEndObjectExpected)
    PdfError::EndOfInput =>
      pdf_read_reconstructed_or_raise(data, PdfError::EndOfInput)
    PdfError::ParseObjectExpected =>
      pdf_read_reconstructed_or_raise(data, PdfError::ParseObjectExpected)
    PdfError::InvalidHexEscape =>
      pdf_read_reconstructed_or_raise(data, PdfError::InvalidHexEscape)
    PdfError::StreamDataExpected =>
      if pdf_reader_startxref_root_uses_object_stream_or_false(data) {
        pdf_read_reconstructed_or_raise(
          data,
          PdfError::StreamDataExpected,
          include_xref_stream_trailers=true,
        )
      } else {
        raise PdfError::StreamDataExpected
      }
    PdfError::InvalidCursorPosition(position) =>
      pdf_read_reconstructed_or_raise(
        data,
        PdfError::InvalidCursorPosition(position),
        include_xref_stream_trailers=true,
      )
    PdfError::FilterNotSupported(filter) =>
      if pdf_reader_xref_metadata_error_allows_reconstruction(
          data,
          PdfError::FilterNotSupported(filter),
        ) {
        pdf_read_reconstructed_or_raise(
          data,
          PdfError::FilterNotSupported(filter),
          include_xref_stream_trailers=true,
        )
      } else {
        raise PdfError::FilterNotSupported(filter)
      }
    PdfError::PredictorExpected =>
      if pdf_reader_xref_metadata_error_allows_reconstruction(
          data,
          PdfError::PredictorExpected,
        ) {
        pdf_read_reconstructed_or_raise(
          data,
          PdfError::PredictorExpected,
          include_xref_stream_trailers=true,
        )
      } else {
        raise PdfError::PredictorExpected
      }
    PdfError::PredictorNotSupported(predictor) =>
      if pdf_reader_xref_metadata_error_allows_reconstruction(
          data,
          PdfError::PredictorNotSupported(predictor),
        ) {
        pdf_read_reconstructed_or_raise(
          data,
          PdfError::PredictorNotSupported(predictor),
          include_xref_stream_trailers=true,
        )
      } else {
        raise PdfError::PredictorNotSupported(predictor)
      }
    error => raise error
  }
}

///|
/// Read one document revision from a borrowed byte view.
///
/// Revision selection uses strict classic-xref parsing and does not use the
/// reconstruction fallback used by `pdf_read_document_from_view`.
pub fn pdf_read_document_revision_from_view(
  data : BytesView,
  revision : Int,
) -> PdfDocument raise PdfError {
  pdf_read_classic_document_revision_from_view(data, revision)
}

///|
/// Read one document revision from owned PDF bytes.
pub fn pdf_read_document_revision_from_bytes(
  data : PdfBytes,
  revision : Int,
) -> PdfDocument raise PdfError {
  pdf_read_document_revision_from_view(data, revision)
}

///|
/// Read a PDF document from owned PDF bytes.
pub fn pdf_read_document_from_bytes(
  data : PdfBytes,
) -> PdfDocument raise PdfError {
  pdf_read_document_from_view(data)
}

///|
/// Read and decrypt one PDF revision using optional passwords.
///
/// Revision `1` is the newest revision. The returned value is `None` when the
/// document is encrypted and neither supplied password can decrypt it; otherwise
/// it contains the decrypted document and permission information.
pub fn pdf_read_document_revision_from_view_with_passwords(
  data : BytesView,
  revision : Int,
  user_password? : BytesView? = None,
  owner_password? : BytesView? = None,
) -> PdfDecryptionResult? raise PdfError {
  let document = pdf_read_classic_document_from_view_at_revision(
    data,
    revision=Some(revision),
    user_password~,
    owner_password~,
  )
  document.decrypt_with_passwords(user_password~, owner_password~)
}

///|
/// Read and decrypt one PDF revision from owned bytes.
pub fn pdf_read_document_revision_from_bytes_with_passwords(
  data : PdfBytes,
  revision : Int,
  user_password? : BytesView? = None,
  owner_password? : BytesView? = None,
) -> PdfDecryptionResult? raise PdfError {
  pdf_read_document_revision_from_view_with_passwords(
    data,
    revision,
    user_password~,
    owner_password~,
  )
}

///|
/// Read and decrypt a PDF document using optional passwords.
///
/// This is the high-level password-aware reader. It uses the same reconstruction
/// fallback policy as `pdf_read_document_from_view` before attempting
/// decryption. Unencrypted documents return a copied document inside
/// `Some(...)`.
pub fn pdf_read_document_from_view_with_passwords(
  data : BytesView,
  user_password? : BytesView? = None,
  owner_password? : BytesView? = None,
) -> PdfDecryptionResult? raise PdfError {
  let document = pdf_read_probe_page_tree_or_reconstruct(
    data,
    pdf_read_classic_document_from_view_at_revision(
      data,
      user_password~,
      owner_password~,
    ),
    user_password~,
    owner_password~,
  ) catch {
    error =>
      if pdf_reader_password_error_allows_reconstruction(data, error) {
        pdf_read_reconstructed_document_from_view(
          data,
          include_xref_stream_trailers=true,
          user_password~,
          owner_password~,
        )
      } else {
        raise error
      }
  }
  document.decrypt_with_passwords(user_password~, owner_password~)
}

///|
/// Read and decrypt a PDF document from owned PDF bytes.
pub fn pdf_read_document_from_bytes_with_passwords(
  data : PdfBytes,
  user_password? : BytesView? = None,
  owner_password? : BytesView? = None,
) -> PdfDecryptionResult? raise PdfError {
  pdf_read_document_from_view_with_passwords(
    data,
    user_password~,
    owner_password~,
  )
}