///|
/// CAST5 (CAST-128, RFC 2144), self-implemented for OpenPGP symmetric
/// algorithm 3 — a read-side requirement: it was GnuPG's default cipher
/// before AES, so a large share of legacy payloads and passphrase-
/// protected secret keys are wrapped in it. OpenPGP never *generates*
/// CAST5 here; the dispatch in `blockcipher.mbt` only routes legacy
/// payloads. OpenPGP always uses the full 16-octet key (16 rounds).
///
/// 32-bit arithmetic is done on `UInt` (mod 2^32 is natural there); the
/// helpers keep the add/sub/xor/rotate forms of the RFC literal.

///|
fn cast5_rotl(x : UInt32, n : Int32) -> UInt32 {
  (x << n) | (x >> (32 - n))
}

///|
fn cast5_add(a : UInt32, b : UInt32) -> UInt32 {
  (a.to_uint64() + b.to_uint64()).to_uint()
}

///|
fn cast5_sub(a : UInt32, b : UInt32) -> UInt32 {
  (a.to_uint64() - b.to_uint64()).to_uint()
}

///|
/// Overall byte n of a little array of 32-bit words: byte 0 is the most
/// significant byte of word 0 (RFC notation x0..xF, z0..zF).
fn cast5_byte(words : Array[UInt32], n : Int32) -> Int32 {
  ((words[n / 4] >> (24 - 8 * (n % 4))) & 0xFFU).reinterpret_as_int()
}

///|
priv struct Cast5State {
  /// 16 masking subkeys Km (one per round).
  km : Array[UInt32]
  /// 16 rotation subkeys Kr (5 bits used per round).
  kr : Array[UInt32]
}

///|
/// RFC 2144 §2.4 key schedule. The body (z from x, four K, x' from z,
/// four K) repeats four times; the K equations alternate between two
/// patterns each, selected by the round parity.
fn cast5_schedule(key : BytesView) -> Cast5State raise {
  guard key.length() == 16 else { @error.fail("cast5: key must be 16 octets") }
  let mut x : Array[UInt32] = Array::makei(4, i => {
    (key[4 * i].to_int().reinterpret_as_uint() << 24) |
    (key[4 * i + 1].to_int().reinterpret_as_uint() << 16) |
    (key[4 * i + 2].to_int().reinterpret_as_uint() << 8) |
    key[4 * i + 3].to_int().reinterpret_as_uint()
  })
  let km : Array[UInt32] = []
  let kr : Array[UInt32] = []
  for round in 0..<4 {
    // z0z1z2z3 .. zCzDzEzF from x — identical equations on every pass.
    let z : Array[UInt32] = Array::make(4, 0U)
    z[0] = x[0] ^
      cast5_s5[cast5_byte(x, 13)] ^
      cast5_s6[cast5_byte(x, 15)] ^
      cast5_s7[cast5_byte(x, 12)] ^
      cast5_s8[cast5_byte(x, 14)] ^
      cast5_s7[cast5_byte(x, 8)]
    z[1] = x[2] ^
      cast5_s5[cast5_byte(z, 0)] ^
      cast5_s6[cast5_byte(z, 2)] ^
      cast5_s7[cast5_byte(z, 1)] ^
      cast5_s8[cast5_byte(z, 3)] ^
      cast5_s8[cast5_byte(x, 10)]
    z[2] = x[3] ^
      cast5_s5[cast5_byte(z, 7)] ^
      cast5_s6[cast5_byte(z, 6)] ^
      cast5_s7[cast5_byte(z, 5)] ^
      cast5_s8[cast5_byte(z, 4)] ^
      cast5_s5[cast5_byte(x, 9)]
    z[3] = x[1] ^
      cast5_s5[cast5_byte(z, 10)] ^
      cast5_s6[cast5_byte(z, 9)] ^
      cast5_s7[cast5_byte(z, 11)] ^
      cast5_s8[cast5_byte(z, 8)] ^
      cast5_s6[cast5_byte(x, 11)]
    if round % 2 == 0 {
      // K1..K4 pattern.
      km.push(
        cast5_s5[cast5_byte(z, 8)] ^
        cast5_s6[cast5_byte(z, 9)] ^
        cast5_s7[cast5_byte(z, 7)] ^
        cast5_s8[cast5_byte(z, 6)] ^
        cast5_s5[cast5_byte(z, 2)],
      )
      km.push(
        cast5_s5[cast5_byte(z, 10)] ^
        cast5_s6[cast5_byte(z, 11)] ^
        cast5_s7[cast5_byte(z, 5)] ^
        cast5_s8[cast5_byte(z, 4)] ^
        cast5_s6[cast5_byte(z, 6)],
      )
      km.push(
        cast5_s5[cast5_byte(z, 12)] ^
        cast5_s6[cast5_byte(z, 13)] ^
        cast5_s7[cast5_byte(z, 3)] ^
        cast5_s8[cast5_byte(z, 2)] ^
        cast5_s7[cast5_byte(z, 9)],
      )
      km.push(
        cast5_s5[cast5_byte(z, 14)] ^
        cast5_s6[cast5_byte(z, 15)] ^
        cast5_s7[cast5_byte(z, 1)] ^
        cast5_s8[cast5_byte(z, 0)] ^
        cast5_s8[cast5_byte(z, 12)],
      )
    } else {
      // K9..K12 pattern.
      km.push(
        cast5_s5[cast5_byte(z, 3)] ^
        cast5_s6[cast5_byte(z, 2)] ^
        cast5_s7[cast5_byte(z, 12)] ^
        cast5_s8[cast5_byte(z, 13)] ^
        cast5_s5[cast5_byte(z, 9)],
      )
      km.push(
        cast5_s5[cast5_byte(z, 1)] ^
        cast5_s6[cast5_byte(z, 0)] ^
        cast5_s7[cast5_byte(z, 14)] ^
        cast5_s8[cast5_byte(z, 15)] ^
        cast5_s6[cast5_byte(z, 12)],
      )
      km.push(
        cast5_s5[cast5_byte(z, 7)] ^
        cast5_s6[cast5_byte(z, 6)] ^
        cast5_s7[cast5_byte(z, 8)] ^
        cast5_s8[cast5_byte(z, 9)] ^
        cast5_s7[cast5_byte(z, 2)],
      )
      km.push(
        cast5_s5[cast5_byte(z, 5)] ^
        cast5_s6[cast5_byte(z, 4)] ^
        cast5_s7[cast5_byte(z, 10)] ^
        cast5_s8[cast5_byte(z, 11)] ^
        cast5_s8[cast5_byte(z, 6)],
      )
    }
    // x0x1x2x3 .. xCxDxExF from z — identical equations on every pass.
    let nx : Array[UInt32] = Array::make(4, 0U)
    nx[0] = z[2] ^
      cast5_s5[cast5_byte(z, 5)] ^
      cast5_s6[cast5_byte(z, 7)] ^
      cast5_s7[cast5_byte(z, 4)] ^
      cast5_s8[cast5_byte(z, 6)] ^
      cast5_s7[cast5_byte(z, 0)]
    nx[1] = z[0] ^
      cast5_s5[cast5_byte(nx, 0)] ^
      cast5_s6[cast5_byte(nx, 2)] ^
      cast5_s7[cast5_byte(nx, 1)] ^
      cast5_s8[cast5_byte(nx, 3)] ^
      cast5_s8[cast5_byte(z, 2)]
    nx[2] = z[1] ^
      cast5_s5[cast5_byte(nx, 7)] ^
      cast5_s6[cast5_byte(nx, 6)] ^
      cast5_s7[cast5_byte(nx, 5)] ^
      cast5_s8[cast5_byte(nx, 4)] ^
      cast5_s5[cast5_byte(z, 1)]
    nx[3] = z[3] ^
      cast5_s5[cast5_byte(nx, 10)] ^
      cast5_s6[cast5_byte(nx, 9)] ^
      cast5_s7[cast5_byte(nx, 11)] ^
      cast5_s8[cast5_byte(nx, 8)] ^
      cast5_s6[cast5_byte(z, 3)]
    if round % 2 == 0 {
      // K5..K8 pattern.
      km.push(
        cast5_s5[cast5_byte(nx, 3)] ^
        cast5_s6[cast5_byte(nx, 2)] ^
        cast5_s7[cast5_byte(nx, 12)] ^
        cast5_s8[cast5_byte(nx, 13)] ^
        cast5_s5[cast5_byte(nx, 8)],
      )
      km.push(
        cast5_s5[cast5_byte(nx, 1)] ^
        cast5_s6[cast5_byte(nx, 0)] ^
        cast5_s7[cast5_byte(nx, 14)] ^
        cast5_s8[cast5_byte(nx, 15)] ^
        cast5_s6[cast5_byte(nx, 13)],
      )
      km.push(
        cast5_s5[cast5_byte(nx, 7)] ^
        cast5_s6[cast5_byte(nx, 6)] ^
        cast5_s7[cast5_byte(nx, 8)] ^
        cast5_s8[cast5_byte(nx, 9)] ^
        cast5_s7[cast5_byte(nx, 3)],
      )
      km.push(
        cast5_s5[cast5_byte(nx, 5)] ^
        cast5_s6[cast5_byte(nx, 4)] ^
        cast5_s7[cast5_byte(nx, 10)] ^
        cast5_s8[cast5_byte(nx, 11)] ^
        cast5_s8[cast5_byte(nx, 7)],
      )
    } else {
      // K13..K16 pattern.
      km.push(
        cast5_s5[cast5_byte(nx, 8)] ^
        cast5_s6[cast5_byte(nx, 9)] ^
        cast5_s7[cast5_byte(nx, 7)] ^
        cast5_s8[cast5_byte(nx, 6)] ^
        cast5_s5[cast5_byte(nx, 3)],
      )
      km.push(
        cast5_s5[cast5_byte(nx, 10)] ^
        cast5_s6[cast5_byte(nx, 11)] ^
        cast5_s7[cast5_byte(nx, 5)] ^
        cast5_s8[cast5_byte(nx, 4)] ^
        cast5_s6[cast5_byte(nx, 7)],
      )
      km.push(
        cast5_s5[cast5_byte(nx, 12)] ^
        cast5_s6[cast5_byte(nx, 13)] ^
        cast5_s7[cast5_byte(nx, 3)] ^
        cast5_s8[cast5_byte(nx, 2)] ^
        cast5_s7[cast5_byte(nx, 8)],
      )
      km.push(
        cast5_s5[cast5_byte(nx, 14)] ^
        cast5_s6[cast5_byte(nx, 15)] ^
        cast5_s7[cast5_byte(nx, 1)] ^
        cast5_s8[cast5_byte(nx, 0)] ^
        cast5_s8[cast5_byte(nx, 13)],
      )
    }
    x = nx
  }
  // §2.4.1: Kmi = Ki, Kri = K16+i (5 bits).
  for i in 0..<16 {
    kr.push(km[16 + i] & 0x1FU)
  }
  Cast5State::{ km, kr, }
}

///|
/// The three round functions (RFC 2144 §2.2). Ia..Id are the bytes of I,
/// most significant first.
fn cast5_f(state : Cast5State, round : Int32, d : UInt32) -> UInt32 {
  let km = state.km[round]
  let kr = state.kr[round].reinterpret_as_int()
  let i : UInt32 = match round % 3 {
    0 => cast5_rotl(cast5_add(km, d), kr)
    1 => cast5_rotl(km ^ d, kr)
    _ => cast5_rotl(cast5_sub(km, d), kr)
  }
  let ia = (i >> 24).reinterpret_as_int() & 0xFF
  let ib = ((i >> 16) & 0xFFU).reinterpret_as_int()
  let ic = ((i >> 8) & 0xFFU).reinterpret_as_int()
  let id = (i & 0xFFU).reinterpret_as_int()
  match round % 3 {
    // ((cast5_s1 ^ cast5_s2) - cast5_s3) + cast5_s4
    0 =>
      cast5_add(
        cast5_sub(cast5_s1[ia] ^ cast5_s2[ib], cast5_s3[ic]),
        cast5_s4[id],
      )
    // ((cast5_s1 - cast5_s2) + cast5_s3) ^ cast5_s4
    1 =>
      cast5_add(cast5_sub(cast5_s1[ia], cast5_s2[ib]), cast5_s3[ic]) ^
      cast5_s4[id]
    // ((cast5_s1 + cast5_s2) ^ cast5_s3) - cast5_s4
    _ =>
      cast5_sub(
        cast5_add(cast5_s1[ia], cast5_s2[ib]) ^ cast5_s3[ic],
        cast5_s4[id],
      )
  }
}

///|
fn Cast5State::crypt(
  self : Cast5State,
  src : BytesView,
  decrypt : Bool,
) -> Bytes {
  if src.length() != 8 {
    abort("cast5: block must be 8 octets")
  }
  let mut l : UInt32 = (src[0].to_int().reinterpret_as_uint() << 24) |
    (src[1].to_int().reinterpret_as_uint() << 16) |
    (src[2].to_int().reinterpret_as_uint() << 8) |
    src[3].to_int().reinterpret_as_uint()
  let mut r : UInt32 = (src[4].to_int().reinterpret_as_uint() << 24) |
    (src[5].to_int().reinterpret_as_uint() << 16) |
    (src[6].to_int().reinterpret_as_uint() << 8) |
    src[7].to_int().reinterpret_as_uint()
  // Feistel with the half swap; decryption is the same rounds with the
  // subkey order reversed (the ciphertext is loaded as R16||L16, which
  // the wire's first half already is).
  for i in 0..<16 {
    let round = if decrypt { 15 - i } else { i }
    let f = cast5_f(self, round, r)
    let nl = r
    r = l ^ f
    l = nl
  }
  // Ciphertext = R16 || L16.
  let out : Array[Byte] = [
    ((r >> 24) & 0xFFU).to_byte(),
    ((r >> 16) & 0xFFU).to_byte(),
    ((r >> 8) & 0xFFU).to_byte(),
    (r & 0xFFU).to_byte(),
    ((l >> 24) & 0xFFU).to_byte(),
    ((l >> 16) & 0xFFU).to_byte(),
    ((l >> 8) & 0xFFU).to_byte(),
    (l & 0xFFU).to_byte(),
  ]
  Bytes::from_array(out)
}

///|
impl BlockCipher for Cast5State with fn block_size(_self) -> Int32 {
  8
}

///|
impl BlockCipher for Cast5State with fn encrypt_block(self, block) -> Bytes {
  self.crypt(block, false)
}

///|
impl BlockCipher for Cast5State with fn decrypt_block(self, block) -> Bytes {
  self.crypt(block, true)
}

///|
impl BlockCipher for Cast5State with fn wipe(self) -> Unit {
  for i in 0.. &BlockCipher raise {
  cast5_schedule(key)
}