///|
fn find_key(
  no_encrypt_metadata : Bool,
  password : String,
  r : Int,
  o : String,
  p : Int,
  id : String,
  keylength : Int,
) -> Array[Int] {
  let prims = @pdfcryptprimitives.PdfCryptPrimitives::new()
  let password_ints = @pdfio.int_array_of_string(password)
  let o_ints = @pdfio.int_array_of_string(o)
  let id_ints = @pdfio.int_array_of_string(id)
  let pw = pad_password(password_ints)
  let from_p = [
    Int::land(p, 0xff),
    Int::land(p >> 8, 0xff),
    Int::land(p >> 16, 0xff),
    Int::land(p >> 24, 0xff),
  ]
  let rev4_no_metadata = if r >= 4 && no_encrypt_metadata {
    [255, 255, 255, 255]
  } else {
    []
  }
  let digest_input = bytes_of_int_arrays([
    pw, o_ints, from_p, id_ints, rev4_no_metadata,
  ])
  let h = prims.md5(digest_input)
  let key_len = keylength / 8
  if r >= 3 {
    let mut i = 1
    let mut h_bytes = h
    while i <= 50 {
      let digest = prims.md5(h_bytes)
      h_bytes = bytes_prefix(digest, key_len)
      i = i + 1
    }
    @pdfio.int_array_of_bytes(h_bytes)
  } else {
    @pdfio.int_array_of_bytes(bytes_prefix(h, key_len))
  }
}

///|
fn authenticate_user(
  no_encrypt_metadata : Bool,
  password : String,
  r : Int,
  u : String,
  o : String,
  p : Int,
  id : String,
  keylength : Int,
) -> Bool {
  let prims = @pdfcryptprimitives.PdfCryptPrimitives::new()
  let u_ints = @pdfio.int_array_of_string(u)
  let key = find_key(no_encrypt_metadata, password, r, o, p, id, keylength)
  if r >= 3 {
    let id_ints = @pdfio.int_array_of_string(id)
    let digest_input = bytes_of_int_arrays([paddings, id_ints])
    let hashed = prims.md5(digest_input)
    let encrypted = prims.crypt(key, hashed)
    let mut u_prime = @pdfio.int_array_of_bytes(encrypted)
    let mut x = 1
    while x <= 19 {
      let key_prime = Array::make(keylength / 8, 0)
      let mut k = 0
      while k < keylength / 8 {
        key_prime[k] = key[k] ^ x
        k = k + 1
      }
      let data_bytes = @pdfio.bytes_of_int_array(u_prime)
      let new_bytes = prims.crypt(key_prime, data_bytes)
      u_prime = @pdfio.int_array_of_bytes(new_bytes)
      x = x + 1
    }
    array_equal(int_array_prefix(u_ints, 16), u_prime)
  } else {
    let encrypted = prims.crypt(key, @pdfio.bytes_of_int_array(paddings))
    array_equal(u_ints, @pdfio.int_array_of_bytes(encrypted))
  }
}

///|
fn owner_key(padded_owner : Array[Int], keylength : Int, r : Int) -> Array[Int] {
  let prims = @pdfcryptprimitives.PdfCryptPrimitives::new()
  let mut d = prims.md5(@pdfio.bytes_of_int_array(padded_owner))
  if r >= 3 {
    let mut i = 1
    while i <= 50 {
      d = prims.md5(d)
      i = i + 1
    }
  }
  @pdfio.int_array_of_bytes(bytes_prefix(d, keylength / 8))
}

///|
fn mk_owner(
  r : Int,
  owner_pw : String,
  user_pw : String,
  keylength : Int,
) -> String {
  let prims = @pdfcryptprimitives.PdfCryptPrimitives::new()
  let source = if owner_pw == "" { user_pw } else { owner_pw }
  let padded_owner = pad_password(@pdfio.int_array_of_string(source))
  let key = owner_key(padded_owner, keylength, r)
  let padded_user = pad_password(@pdfio.int_array_of_string(user_pw))
  if r == 2 {
    @pdfio.string_of_bytes(
      prims.crypt(key, @pdfio.bytes_of_int_array(padded_user)),
    )
  } else {
    let mut acc = prims.crypt(key, @pdfio.bytes_of_int_array(padded_user))
    let mut x = 1
    while x <= 19 {
      acc = prims.crypt(mkkey(key, x), acc)
      x = x + 1
    }
    @pdfio.string_of_bytes(acc)
  }
}

///|
fn mk_user(
  no_encrypt_metadata : Bool,
  user_pw : String,
  o : String,
  p : Int,
  id : String,
  r : Int,
  keylength : Int,
) -> String {
  let prims = @pdfcryptprimitives.PdfCryptPrimitives::new()
  let key = find_key(no_encrypt_metadata, user_pw, r, o, p, id, keylength)
  if r == 2 {
    @pdfio.string_of_bytes(
      prims.crypt(key, @pdfio.bytes_of_int_array(paddings)),
    )
  } else {
    let digest_input = bytes_of_int_arrays([
      paddings,
      @pdfio.int_array_of_string(id),
    ])
    let d = prims.md5(digest_input)
    let mut acc = prims.crypt(key, d)
    let mut x = 1
    while x <= 19 {
      acc = prims.crypt(mkkey(key, x), acc)
      x = x + 1
    }
    let zeros = @pdfio.mkbytes(16)
    @pdfio.string_of_bytes(acc) + @pdfio.string_of_bytes(zeros)
  }
}