///|
/// Argon2id (RFC 9106) with version 0x13, unkeyed BLAKE2b, and empty secret
/// and associated data — exactly the configuration RFC 9580 §3.7.1.4 pins
/// for the S2K specifier type 4.
///
/// The implementation is single-threaded: filling slice by slice, lane by
/// lane, is equivalent to the threaded reference because slices are the
/// only synchronisation barrier and a lane never references blocks of the
/// current slice in another lane during pass 0 slice 0 (RFC 9106 §3.4).

///|
/// Hard ceiling on Argon2 memory, in KiB: 2^21 KiB = 2 GiB, the first
/// RFC 9106 §4 recommended Argon2id option and what RFC 9580 Appendix A.5
/// and A.12 use. A 20-octet S2K specifier can ask for up to 2^31 KiB, so
/// callers must never allocate an attacker-chosen size unchecked.
pub const ARGON2_MAX_MEMORY_KIB : Int32 = 2097152

///|
/// Default Argon2 memory policy (KiB) for the public decrypt entry points:
/// 2^16 KiB = 64 MiB (S7).
///
/// An Argon2 S2K specifier is attacker-controlled and the derivation runs
/// *before* any authentication, so the cap decides how much memory one
/// unauthenticated message can cost — and a malicious SKESK may pair the
/// full cap with up to 255 passes, multiplying both memory and CPU. The
/// 64 MiB default matches the profile every mainstream writer defaults to
/// (go-crypto/gopenpgp, OpenPGP.js), so ordinary interop never needs to
/// raise it, while GnuPG cannot produce an Argon2 S2K at all.
///
/// Messages that genuinely ask for more — RFC 9580 Appendix A.5/A.12 use
/// the RFC 9106 first recommended option (2 GiB, `ARGON2_MAX_MEMORY_KIB`)
/// — must pass the wider policy explicitly:
///
///   decrypt_with_password(msg, pass, argon2_max_memory_kib=@primitives.ARGON2_MAX_MEMORY_KIB)
///
/// `scripts/acceptance.sh` and the `slow:*` tests do exactly that, so the
/// widened call is exercised by the release gate.
pub const DEFAULT_ARGON2_MAX_MEMORY_KIB : Int32 = 65536

///|
/// Argon2 version pinned by RFC 9580 (the current 1.3 version).
const ARGON2_VERSION : Int32 = 0x13

///|
/// Argon2 type id for Argon2id (0=d, 1=i, 2=id).
const ARGON2_TYPE_ID : Int32 = 2

///|
/// One Argon2 block is 128 64-bit words (1024 octets).
const ARGON2_BLOCK_WORDS : Int32 = 128

///|
/// Words between regenerations of the Argon2i address block.
const ARGON2_ADDRESSES_IN_BLOCK : Int32 = 128

///|
/// Number of slices (synchronisation points) per pass.
const ARGON2_SYNC_POINTS : Int32 = 4

///|
/// The mutable working set of one Argon2 invocation.
priv struct Argon2Instance {
  /// All `m'` blocks, laid out block-major (`block * 128 + word`).
  mem : FixedArray[UInt64]
  /// 128-word scratch for the compression function.
  scratch : FixedArray[UInt64]
  /// Argon2i address block.
  addr : FixedArray[UInt64]
  /// Argon2i input block.
  input : FixedArray[UInt64]
  /// All-zero block used by `next_addresses`.
  zero : FixedArray[UInt64]
  lanes : Int32
  /// Rounded block count actually allocated (a multiple of 4*lanes).
  m_prime : Int32
  /// Blocks per lane.
  lane_len : Int32
  /// Blocks per slice (a quarter of a lane).
  segment_len : Int32
}

///|
fn argon2_instance(lanes : Int32, m_prime : Int32) -> Argon2Instance {
  let lane_len = m_prime / lanes
  {
    mem: FixedArray::make(m_prime * ARGON2_BLOCK_WORDS, 0UL),
    scratch: FixedArray::make(ARGON2_BLOCK_WORDS, 0UL),
    addr: FixedArray::make(ARGON2_BLOCK_WORDS, 0UL),
    input: FixedArray::make(ARGON2_BLOCK_WORDS, 0UL),
    zero: FixedArray::make(ARGON2_BLOCK_WORDS, 0UL),
    lanes,
    m_prime,
    lane_len,
    segment_len: lane_len / ARGON2_SYNC_POINTS,
  }
}

///|
/// The BlaMka mixing function GB (RFC 9106 §3.6): the BLAKE2b G with the
/// `2 * trunc(a) * trunc(b)` term added to each addition, operating on four
/// words of `b` in place.
fn blamka_gb(
  b : FixedArray[UInt64],
  i : Int32,
  j : Int32,
  k : Int32,
  l : Int32,
) -> Unit {
  let mut a = b[i]
  let mut bb = b[j]
  let mut c = b[k]
  let mut d = b[l]
  a = a + bb + 2UL * (a & 0xFFFFFFFFUL) * (bb & 0xFFFFFFFFUL)
  d = rotr64(d ^ a, 32)
  c = c + d + 2UL * (c & 0xFFFFFFFFUL) * (d & 0xFFFFFFFFUL)
  bb = rotr64(bb ^ c, 24)
  a = a + bb + 2UL * (a & 0xFFFFFFFFUL) * (bb & 0xFFFFFFFFUL)
  d = rotr64(d ^ a, 16)
  c = c + d + 2UL * (c & 0xFFFFFFFFUL) * (d & 0xFFFFFFFFUL)
  bb = rotr64(bb ^ c, 63)
  b[i] = a
  b[j] = bb
  b[k] = c
  b[l] = d
}

///|
/// The Argon2 permutation P on 16 words identified by their indices
/// (RFC 9106 §3.6).
fn argon2_round(
  b : FixedArray[UInt64],
  i0 : Int32,
  i1 : Int32,
  i2 : Int32,
  i3 : Int32,
  i4 : Int32,
  i5 : Int32,
  i6 : Int32,
  i7 : Int32,
  i8 : Int32,
  i9 : Int32,
  i10 : Int32,
  i11 : Int32,
  i12 : Int32,
  i13 : Int32,
  i14 : Int32,
  i15 : Int32,
) -> Unit {
  blamka_gb(b, i0, i4, i8, i12)
  blamka_gb(b, i1, i5, i9, i13)
  blamka_gb(b, i2, i6, i10, i14)
  blamka_gb(b, i3, i7, i11, i15)
  blamka_gb(b, i0, i5, i10, i15)
  blamka_gb(b, i1, i6, i11, i12)
  blamka_gb(b, i2, i7, i8, i13)
  blamka_gb(b, i3, i4, i9, i14)
}

///|
/// Apply P to all eight rows (contiguous groups of 16 words) and then to
/// all eight columns of the 8x16 word matrix.
fn argon2_permute(b : FixedArray[UInt64]) -> Unit {
  for i in 0..<8 {
    let base = i * 16
    argon2_round(
      b,
      base + 0,
      base + 1,
      base + 2,
      base + 3,
      base + 4,
      base + 5,
      base + 6,
      base + 7,
      base + 8,
      base + 9,
      base + 10,
      base + 11,
      base + 12,
      base + 13,
      base + 14,
      base + 15,
    )
  }
  for i in 0..<8 {
    let c = 2 * i
    argon2_round(
      b,
      c,
      c + 1,
      c + 16,
      c + 17,
      c + 32,
      c + 33,
      c + 48,
      c + 49,
      c + 64,
      c + 65,
      c + 80,
      c + 81,
      c + 96,
      c + 97,
      c + 112,
      c + 113,
    )
  }
}

///|
/// The Argon2 compression function G over blocks in the main memory array:
/// `G(prev, ref) = P(prev XOR ref) XOR (prev XOR ref)`. For version 0x13 and
/// any pass after the first, RFC 9106 §3.4 XORs the block being replaced
/// into the result, so `B[i][j] = G(B[i][j-1], B[l][z]) XOR B[i][j](old)`.
///
/// `out` never aliases `prev` or `ref` here, so the old value can be read
/// back in place just before it is overwritten.
fn argon2_fill_block(
  inst : Argon2Instance,
  prev_off : Int32,
  ref_off : Int32,
  out_off : Int32,
  with_xor : Bool,
) -> Unit {
  let mem = inst.mem
  let s = inst.scratch
  for k in 0.. Unit {
  let s = inst.scratch
  let old : FixedArray[UInt64] = FixedArray::make(ARGON2_BLOCK_WORDS, 0UL)
  for k in 0.. Unit {
  inst.input[6] = inst.input[6] + 1UL
  argon2_fill_block_sep(inst, inst.zero, inst.input, inst.addr, false)
  argon2_fill_block_sep(inst, inst.zero, inst.addr, inst.addr, false)
}

///|
/// The reference-block index selection (RFC 9106 §3.4.1.3, `index_alpha`).
fn argon2_index_alpha(
  inst : Argon2Instance,
  pass : Int32,
  slice : Int32,
  index : Int32,
  j1 : UInt64,
  same_lane : Bool,
) -> Int32 {
  let seg = inst.segment_len
  let lane_len = inst.lane_len
  let reference_area_size : Int32 = if pass == 0 {
    if slice == 0 {
      index - 1
    } else if same_lane {
      slice * seg + index - 1
    } else {
      slice * seg + (if index == 0 { -1 } else { 0 })
    }
  } else if same_lane {
    lane_len - seg + index - 1
  } else {
    lane_len - seg + (if index == 0 { -1 } else { 0 })
  }
  let area = reference_area_size.to_uint64()
  let mut relative_position = j1
  relative_position = (relative_position * relative_position) >> 32
  relative_position = area - 1UL - ((area * relative_position) >> 32)
  let start_position : Int32 = if pass == 0 {
    0
  } else if slice == ARGON2_SYNC_POINTS - 1 {
    0
  } else {
    (slice + 1) * seg
  }
  ((start_position.to_uint64() + relative_position) % lane_len.to_uint64()).to_int()
}

///|
/// Fill one segment: the blocks of `slice` in `lane` for `pass`
/// (RFC 9106 §3.4.1.1).
fn argon2_fill_segment(
  inst : Argon2Instance,
  pass : Int32,
  lane : Int32,
  slice : Int32,
  passes : Int32,
) -> Unit {
  let lanes = inst.lanes
  let seg = inst.segment_len
  let lane_len = inst.lane_len
  // Argon2id: data-independent addressing for the first two slices of the
  // first pass, data-dependent afterwards (RFC 9106 §3.4.1.1).
  let data_independent = pass == 0 && slice < 2
  if data_independent {
    for k in 0..> 32) % lanes.to_uint64()).to_int()
    if pass == 0 && slice == 0 {
      // No other lane has usable data yet.
      ref_lane = lane
    }
    let ref_index = argon2_index_alpha(
      inst,
      pass,
      slice,
      i,
      j1,
      ref_lane == lane,
    )
    argon2_fill_block(
      inst,
      prev * ARGON2_BLOCK_WORDS,
      (ref_lane * lane_len + ref_index) * ARGON2_BLOCK_WORDS,
      curr * ARGON2_BLOCK_WORDS,
      pass > 0,
    )
    curr = curr + 1
    prev = prev + 1
  }
}

///|
/// Append a 32-bit little-endian integer.
fn push_le32(out : Array[Byte], v : Int32) -> Unit {
  out.push((v & 0xFF).to_byte())
  out.push(((v >> 8) & 0xFF).to_byte())
  out.push(((v >> 16) & 0xFF).to_byte())
  out.push(((v >> 24) & 0xFF).to_byte())
}

///|
/// Append a length-prefixed octet string (`LE32(len) || bytes`).
fn push_sized(out : Array[Byte], data : BytesView) -> Unit {
  push_le32(out, data.length())
  for b in data {
    out.push(b)
  }
}

///|
/// Store 128 little-endian words of `data` (1024 octets) at block `off`.
fn argon2_store_block(
  mem : FixedArray[UInt64],
  off : Int32,
  data : BytesView,
) -> Unit {
  let arr = data.to_array()
  let view : ArrayView[Byte] = arr[:]
  for i in 0.. Bytes {
  let out : Array[Byte] = Array(capacity=len * 8)
  for i in off..<(off + len) {
    for b in words[i].to_le_bytes() {
      out.push(b)
    }
  }
  Bytes::from_array(out)
}

///|
/// The Argon2 variable-length hash H' (RFC 9106 §3.3).
fn argon2_h_prime(input : BytesView, out_len : Int32) -> Bytes raise {
  let prefix : Array[Byte] = []
  push_le32(prefix, out_len)
  let head = concat2(Bytes::from_array(prefix), input)
  if out_len <= 64 {
    return blake2b(head, out_len)
  }
  let r = (out_len + 31) / 32 - 2
  let mut v = blake2b(head, 64)
  let out : Array[Byte] = Array(capacity=out_len)
  for i in 0.. Bytes raise {
  let buf : Array[Byte] = []
  push_le32(buf, lanes)
  push_le32(buf, tag_len)
  push_le32(buf, m_kib)
  push_le32(buf, passes)
  push_le32(buf, ARGON2_VERSION)
  push_le32(buf, ARGON2_TYPE_ID)
  push_sized(buf, pass)
  push_sized(buf, salt)
  push_sized(buf, secret)
  push_sized(buf, ad)
  blake2b(Bytes::from_array(buf), 64)
}

///|
/// Argon2id with a caller-chosen secret and associated data. OpenPGP never
/// uses `secret`/`ad`, but RFC 9106's own known-answer test does, and this
/// is the entry point that reproduces it.
pub fn argon2id_with_secret_ad(
  pass : BytesView,
  salt : BytesView,
  secret : BytesView,
  ad : BytesView,
  passes : Int32,
  m_kib : Int32,
  parallelism : Int32,
  tag_len : Int32,
  max_memory_kib~ : Int32 = ARGON2_MAX_MEMORY_KIB,
) -> Bytes raise {
  guard passes >= 1 else { @error.fail("argon2: passes must be non-zero") }
  guard parallelism >= 1 else {
    @error.fail("argon2: parallelism must be non-zero")
  }
  guard tag_len >= 1 else { @error.fail("argon2: tag length must be non-zero") }
  guard m_kib >= 8 * parallelism else {
    @error.fail("argon2: memory must be at least 8*parallelism KiB")
  }
  guard m_kib <= max_memory_kib else {
    @error.fail(
      "argon2: requested memory \{m_kib} KiB exceeds the cap \{max_memory_kib} KiB",
    )
  }
  guard m_kib <= ARGON2_MAX_MEMORY_KIB else {
    @error.fail(
      "argon2: memory \{m_kib} KiB exceeds the implementation limit \{ARGON2_MAX_MEMORY_KIB} KiB",
    )
  }
  // RFC 9106 §3.1: round the block count down to a multiple of 4*lanes.
  let m_prime = ARGON2_SYNC_POINTS *
    parallelism *
    (m_kib / (ARGON2_SYNC_POINTS * parallelism))
  let inst = argon2_instance(parallelism, m_prime)
  let h0 = argon2_h0(
    pass, salt, secret, ad, tag_len, m_kib, passes, parallelism,
  )
  // B[i][0] and B[i][1] are the H' expansion of H0 with the block index.
  for lane in 0.. Bytes raise {
  argon2id_with_secret_ad(
    pass,
    salt,
    Bytes::new(0),
    Bytes::new(0),
    passes,
    m_kib,
    parallelism,
    tag_len,
    max_memory_kib~,
  )
}