///|
pub struct PdfRead {
  // Keep IO-read configuration on the instance instead of global `Ref` state.
  read_debug : Bool
  error_on_malformed : Bool
  debug_always_treat_malformed : Bool
  logger : (String) -> Unit
}

///|
pub fn PdfRead::new(
  read_debug? : Bool = @pdfe.read_debug.val,
  error_on_malformed? : Bool = false,
  debug_always_treat_malformed? : Bool = false,
  logger? : (String) -> Unit = @pdfe.logger.val,
) -> PdfRead {
  { read_debug, error_on_malformed, debug_always_treat_malformed, logger }
}

///|
/// Read a PDF from an input.
pub fn PdfRead::pdf_of_input(
  self : PdfRead,
  revision? : Int,
  user_password : String?,
  owner_password : String?,
  input : @pdfio.Input,
) -> @pdf.Pdf raise {
  read_pdf_internal(self, revision, user_password, owner_password, input, true)
}

///|
/// Read a PDF from an input, lazily.
pub fn PdfRead::pdf_of_input_lazy(
  self : PdfRead,
  revision? : Int,
  user_password : String?,
  owner_password : String?,
  input : @pdfio.Input,
) -> @pdf.Pdf raise {
  read_pdf_internal(self, revision, user_password, owner_password, input, false)
}

///|
/// Return number of revisions.
pub fn PdfRead::revisions(self : PdfRead, input : @pdfio.Input) -> Int raise {
  try {
    let _ = read_pdf_internal(self, Some(-1), None, None, input, false)
    0
  } catch {
    ReadPdfError::Revisions(n) => n
    err => raise err
  }
}

///|
/// Return encryption method in use.
pub fn PdfRead::what_encryption(
  _self : PdfRead,
  pdf : @pdf.Pdf,
) -> @pdfcrypt.EncryptionMethod? {
  if !@pdfcrypt.PdfCrypt::new().is_encrypted(pdf) {
    return None
  }
  let values : Result[
    (
      @pdfcryptprimitives.Encryption,
      String,
      String,
      Int,
      String,
      String?,
      String?,
    ),
    Error,
  ] = try? @pdfcrypt.PdfCrypt::new().get_encryption_values(pdf)
  match values {
    Ok((crypt, _, _, _, _, _, _)) => {
      let metadata = match pdf.lookup_direct("/Encrypt", pdf.trailerdict) {
        Some(encrypt_dict) =>
          match pdf.lookup_direct("/EncryptMetadata", encrypt_dict) {
            Some(@pdf.PdfObject::Boolean(false)) => false
            _ => true
          }
        None => true
      }
      match crypt {
        @pdfcryptprimitives.Encryption::ARC4(bits, _) =>
          match bits {
            40 => Some(@pdfcrypt.EncryptionMethod::PDF40bit)
            128 => Some(@pdfcrypt.EncryptionMethod::PDF128bit)
            _ => None
          }
        @pdfcryptprimitives.Encryption::AESV2 =>
          Some(@pdfcrypt.EncryptionMethod::AES128bit(metadata))
        @pdfcryptprimitives.Encryption::AESV3(is_iso) =>
          if is_iso {
            Some(@pdfcrypt.EncryptionMethod::AES256bitISO(metadata))
          } else {
            Some(@pdfcrypt.EncryptionMethod::AES256bit(metadata))
          }
      }
    }
    Err(_) => None
  }
}

///|
/// Return list of permissions.
pub fn PdfRead::permissions(
  _self : PdfRead,
  pdf : @pdf.Pdf,
) -> Array[@pdfcrypt.Permission] {
  if !@pdfcrypt.PdfCrypt::new().is_encrypted(pdf) {
    return []
  }
  let values : Result[
    (
      @pdfcryptprimitives.Encryption,
      String,
      String,
      Int,
      String,
      String?,
      String?,
    ),
    Error,
  ] = try? @pdfcrypt.PdfCrypt::new().get_encryption_values(pdf)
  match values {
    Ok((_, _, _, p, _, _, _)) => @pdfcrypt.PdfCrypt::new().banlist_of_p(p)
    Err(_) => []
  }
}

///|
/// Check if input is linearized.
pub fn PdfRead::is_linearized(self : PdfRead, input : @pdfio.Input) -> Bool {
  try {
    let _ = read_header(self, input)
    let lexemes = @pdfsyntax.PdfSyntax::new().lex_dictionary(false, input)
    let (_, parsed) = @pdfsyntax.PdfSyntax::new().parse(
      lexemes,
      failure_is_ok=true,
    )
    match @pdf.Pdf::empty().lookup_direct("/Linearized", parsed) {
      Some(@pdf.PdfObject::Integer(_)) | Some(@pdf.PdfObject::Real(_)) => true
      _ => false
    }
  } catch {
    _ => false
  }
}

///|
/// Read a PDF header.
fn read_header(ctx : PdfRead, input : @pdfio.Input) -> (Int, Int) raise {
  fn get8chars(input : @pdfio.Input) -> Array[Char] {
    let out = Array::new(capacity=8)
    let mut ok = true
    let mut i = 0
    while ok && i < 8 {
      match (input.input_char)() {
        Some(ch) => out.push(ch)
        None => ok = false
      }
      i = i + 1
    }
    if ok {
      out
    } else {
      Array::new()
    }
  }

  fn digits_to_int(digits : Array[Char]) -> Int {
    let mut value = 0
    for d in digits {
      value = value * 10 + (d.to_int() - '0'.to_int())
    }
    value
  }

  let mut pos = 0
  while pos <= 1024 {
    (input.seek_in)(pos)
    let chars = get8chars(input)
    if chars.length() == 8 &&
      chars[0] == '%' &&
      chars[1] == 'P' &&
      chars[2] == 'D' &&
      chars[3] == 'F' &&
      chars[4] == '-' &&
      chars[6] == '.' {
      let major_char = chars[5]
      if !(major_char is ('0'..='9')) {
        return (2, 0)
      }
      let minor_digits = Array::new()
      let mut idx = 7
      while idx < chars.length() {
        let c = chars[idx]
        if c is ('0'..='9') {
          minor_digits.push(c)
          idx = idx + 1
        } else {
          idx = chars.length()
        }
      }
      if minor_digits.length() == 0 {
        let message = @pdf.input_pdferror(input, "Malformed PDF header")
        raise @pdf.PdfError::Msg(message)
      }
      if ctx.read_debug {
        (ctx.logger)("setting offset to \{pos}\n")
      }
      (input.set_offset)(pos)
      return (major_char.to_int() - '0'.to_int(), digits_to_int(minor_digits))
    }
    pos = pos + 1
  }
  (2, 0)
}