///|
fn pdf_aes_valid_key_length(length : Int) -> Bool {
  length == 16 || length == 24 || length == 32
}

///|
/// Validate the key length, mapping an invalid key onto the PDF error
/// type. The official `x/crypto` AES routines raise a shared
/// `CryptoError` for both key and data length problems, so the length
/// checks happen here before delegating.
fn pdf_aes_check_key(key : BytesView) -> Unit raise @core.PdfError {
  guard pdf_aes_valid_key_length(key.length()) else { raise CryptoKeyExpected }
}

///|
fn pdf_aes_block_length(data : BytesView) -> Unit raise @core.PdfError {
  guard data.length() == 16 else {
    raise InvalidCryptoDataLength(data.length())
  }
}

///|
/// Map the official AES error onto the PDF error type defensively. The
/// caller has already checked key and block lengths, so any remaining
/// `CryptoError` is treated as a data-length failure.
fn pdf_aes_map_crypto_error(
  action : () -> Bytes raise @crypto.CryptoError,
  actual : Int,
) -> @core.PdfBytes raise @core.PdfError {
  action() catch {
    _ => raise InvalidCryptoDataLength(actual)
  }
}

///|
/// Encrypt one 16-byte AES block with a 128-, 192-, or 256-bit key.
pub fn pdf_aes_encrypt_block(
  key : BytesView,
  block : BytesView,
) -> @core.PdfBytes raise @core.PdfError {
  pdf_aes_check_key(key)
  pdf_aes_block_length(block)
  pdf_aes_map_crypto_error(
    () => @crypto.aes_ecb_encrypt(key, block),
    block.length(),
  )
}

///|
/// Decrypt one 16-byte AES block with a 128-, 192-, or 256-bit key.
pub fn pdf_aes_decrypt_block(
  key : BytesView,
  block : BytesView,
) -> @core.PdfBytes raise @core.PdfError {
  pdf_aes_check_key(key)
  pdf_aes_block_length(block)
  pdf_aes_map_crypto_error(
    () => @crypto.aes_ecb_decrypt(key, block),
    block.length(),
  )
}

///|
fn pdf_aes_remove_padding(data : BytesView) -> @core.PdfBytes {
  let length = data.length()
  let padding = data[length - 1].to_int()
  guard padding >= 1 && padding <= 16 else { return data.to_owned() }
  for index in (length - padding).. @core.PdfBytes raise @core.PdfError {
  guard data.length() > 16 else { return Bytes::new(0) }
  guard data.length() % 16 == 0 else {
    raise InvalidCryptoDataLength(data.length())
  }
  pdf_aes_check_key(key)
  let decrypted = pdf_aes_map_crypto_error(
    () => @crypto.aes_cbc_decrypt(key, data[:16], data[16:]),
    data.length() - 16,
  )
  if remove_padding {
    pdf_aes_remove_padding(decrypted)
  } else {
    decrypted
  }
}

///|
/// Encrypt bytes with AES-CBC and an explicit IV.
///
/// PKCS-style padding is added and the returned bytes include the IV as the
/// first block, matching PDF AES string and stream data layout.
pub fn pdf_aes_cbc_encrypt_with_iv(
  key : BytesView,
  iv : BytesView,
  data : BytesView,
) -> @core.PdfBytes raise @core.PdfError {
  pdf_aes_check_key(key)
  pdf_aes_block_length(iv)
  let overflow = data.length() % 16
  let padding = 16 - overflow
  let padded : Array[Byte] = data.to_array()
  for _ in 0.. @crypto.aes_cbc_encrypt(key, iv, Bytes::from_array(padded)),
    data.length() + padding,
  )
  let output = Array::make(16 + encrypted.length(), b'\x00')
  for index in 0..<16 {
    output[index] = iv[index]
  }
  for index in 0.. @core.PdfBytes raise @core.PdfError {
  guard data.length() > 0 else { return Bytes::new(0) }
  guard data.length() % 16 == 0 else {
    raise InvalidCryptoDataLength(data.length())
  }
  pdf_aes_check_key(key)
  let decrypted = pdf_aes_map_crypto_error(
    () => @crypto.aes_ecb_decrypt(key, data),
    data.length(),
  )
  if remove_padding {
    pdf_aes_remove_padding(decrypted)
  } else {
    decrypted
  }
}

///|
/// Encrypt AES-ECB data without adding padding.
///
/// The input length must be a non-zero multiple of 16 unless it is empty.
pub fn pdf_aes_ecb_encrypt(
  key : BytesView,
  data : BytesView,
) -> @core.PdfBytes raise @core.PdfError {
  guard data.length() % 16 == 0 else {
    raise InvalidCryptoDataLength(data.length())
  }
  guard data.length() > 0 else { return Bytes::new(0) }
  pdf_aes_check_key(key)
  pdf_aes_map_crypto_error(
    () => @crypto.aes_ecb_encrypt(key, data),
    data.length(),
  )
}