///|
/// Symmetric block cipher abstraction. OpenPGP modes (CFB variants, AEAD)
/// only need single-block encrypt/decrypt with a fixed key, so this is all
/// the rest of the module is allowed to know about the primitive.
///
/// Block size is always 16 for every algorithm OpenPGP 0/1 modes use with
/// AES; still, callers must consult `block_size` rather than assume.
pub(open) trait BlockCipher {
  fn block_size(Self) -> Int32
  fn encrypt_block(Self, BytesView) -> Bytes
  fn decrypt_block(Self, BytesView) -> Bytes

  /// Best-effort scrub of expanded key material (GC languages cannot
  /// guarantee it; see project README).
  fn wipe(Self) -> Unit
}

///|
/// Symmetric cipher algorithms, RFC 9580 §9.3 numbers. TripleDES (2) and
/// CAST5 (3) exist for the read side only — RFC 4880 §9.2 makes 3DES a
/// MUST-implement and CAST5 was GnuPG's pre-AES default; the generation
/// paths always pick AES.
pub(all) enum CipherAlgo {
  Tripledes
  Cast5
  Aes128
  Aes192
  Aes256
} derive(Eq, Debug)

///|
pub fn CipherAlgo::number(self : CipherAlgo) -> Int32 {
  match self {
    Tripledes => 2
    Cast5 => 3
    Aes128 => 7
    Aes192 => 8
    Aes256 => 9
  }
}

///|
pub fn cipher_algo_of_number(n : Int32) -> CipherAlgo? {
  match n {
    2 => Some(Tripledes)
    3 => Some(Cast5)
    7 => Some(Aes128)
    8 => Some(Aes192)
    9 => Some(Aes256)
    _ => None
  }
}

///|
pub fn CipherAlgo::key_size(self : CipherAlgo) -> Int32 {
  match self {
    Tripledes => 24
    Cast5 => 16
    Aes128 => 16
    Aes192 => 24
    Aes256 => 32
  }
}

///|
/// Block size used by every supported algorithm.
pub const BLOCK_SIZE : Int32 = 16

///|
/// Display name of a symmetric cipher algorithm, for error messages.
fn cipher_algo_name(algo : CipherAlgo) -> String {
  match algo {
    Tripledes => "TripleDES"
    Cast5 => "CAST5"
    Aes128 => "AES-128"
    Aes192 => "AES-192"
    Aes256 => "AES-256"
  }
}

///|
/// Construct the block cipher for `algo` under `key`.
pub fn new_block_cipher(
  algo : CipherAlgo,
  key : BytesView,
) -> &BlockCipher raise {
  guard key.length() == algo.key_size() else {
    @error.fail(
      "key length \{key.length()} does not match \{cipher_algo_name(algo)} key size \{algo.key_size()}",
    )
  }
  match algo {
    Tripledes => new_des_ede3(key)
    Cast5 => new_cast5(key)
    _ => new_aes(key)
  }
}

///|
pub extend CipherAlgo with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend CipherAlgo with Eq::{not_equal, equal}