/*
 * This file is part of the Fun programming language.
 * https://fun-lang.xyz/
 *
 * Copyright 2025 Johannes Findeisen <you@hanez.org>
 * Licensed under the terms of the Apache-2.0 license.
 * https://opensource.org/license/apache-2-0
 *
 * Added: 2025-10-01
 */

// lib/crypt/sha384.fun
// Pure Fun SHA-384 implementation operating on hex-string input.
// 64-bit words are represented as [hi, lo] (two uint32 parts).
//
// Public API (class):
//   s = SHA384()
//   s.sha384_hex(hexStr) -> digest hex string (lowercase, 96 hex chars)
//   s.sha384_str(str) -> digest hex string (lowercase, 96 hex chars)
//
// Known test vector ("abc"):
//   SHA-384("abc") =
//   cb00753f45a35e8bb5a03d699ac65007272c32ab0eded163
//   1a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7

#include <hex.fun>
#include <strings.fun>

class SHA384()
  // -------- hex helpers --------
  fun byte_from_hex_pair(this, hh)
    number hi = hex_val(substr(hh, 0, 1))
    number lo = hex_val(substr(hh, 1, 1))
    return hi * 16 + lo

  fun from_hex(this, hex)
    hex = to_string(hex)
    arr = []
    number i = 0
    number n = len(hex)
    while i + 1 < n
      number b = this.byte_from_hex_pair(substr(hex, i, 2))
      push(arr, b)
      i = i + 2
    return arr

  // -------- 32/64 helpers --------
  fun u32(this, x)
    m = 4294967296
    while x < 0
      x = x + m
    while x >= m
      x = x - m
    return x

  // 64-bit as [hi, lo] (each uint32)
  fun pack64_be(this, b0,b1,b2,b3,b4,b5,b6,b7)
    number hi = this.u32(b0 * 16777216 + b1 * 65536 + b2 * 256 + b3)
    number lo = this.u32(b4 * 16777216 + b5 * 65536 + b6 * 256 + b7)
    return [hi, lo]

  fun add64(this, a, b)
    number alo = this.u32(a[1])
    number ahi = this.u32(a[0])
    number blo = this.u32(b[1])
    number bhi = this.u32(b[0])
    number sum_lo = alo + blo
    number carry = sum_lo / 4294967296
    sum_lo = sum_lo % 4294967296
    number sum_hi = (ahi + bhi + carry) % 4294967296
    return [sum_hi, sum_lo]

  fun add64_5(this, a, b, c, d, e)
    return this.add64(this.add64(this.add64(this.add64(a, b), c), d), e)

  fun xor64(this, a, b)
    return [ bxor(a[0], b[0]), bxor(a[1], b[1]) ]

  fun and64(this, a, b)
    return [ band(a[0], b[0]), band(a[1], b[1]) ]

  fun or64(this, a, b)
    return [ bor(a[0], b[0]), bor(a[1], b[1]) ]

  fun not64(this, a)
    return [ bnot(a[0]), bnot(a[1]) ]

  // logical right shift by n (0..63)
  fun shr64(this, a, n)
    number s = n % 64
    number hi = a[0]
    number lo = a[1]
    if (s == 0)
      return [hi, lo]
    else if (s < 32)
      number new_lo = bor( shr(lo, s), shl(hi, 32 - s) )
      number new_hi = shr(hi, s)
      return [ new_hi, new_lo ]
    else
      number k = s - 32
      number new_lo2 = shr(hi, k)
      return [ 0, new_lo2 ]

  // rotate right by n (0..63)
  fun rotr64(this, a, n)
    number s = n % 64
    number hi = a[0]
    number lo = a[1]
    if (s == 0)
      return [hi, lo]
    else if (s < 32)
      number new_lo = bor( shr(lo, s), shl(hi, 32 - s) )
      number new_hi = bor( shr(hi, s), shl(lo, 32 - s) )
      return [ new_hi, new_lo ]
    else
      number k = s - 32
      // swap roles
      number new_lo2 = bor( shr(hi, k), shl(lo, 32 - k) )
      number new_hi2 = bor( shr(lo, k), shl(hi, 32 - k) )
      return [ new_hi2, new_lo2 ]

  // SHA-512 logical functions (used by SHA-384 rounds)
  fun Ch(this, x, y, z)
    // (x & y) ^ (~x & z)
    return this.xor64(this.and64(x, y), this.and64(this.not64(x), z))

  fun Maj(this, x, y, z)
    // (x & y) ^ (x & z) ^ (y & z)
    return this.xor64(this.xor64(this.and64(x, y), this.and64(x, z)), this.and64(y, z))

  fun Sigma0(this, x)
    return this.xor64(this.xor64(this.rotr64(x, 28), this.rotr64(x, 34)), this.rotr64(x, 39))

  fun Sigma1(this, x)
    return this.xor64(this.xor64(this.rotr64(x, 14), this.rotr64(x, 18)), this.rotr64(x, 41))

  fun sigma0(this, x)
    return this.xor64(this.xor64(this.rotr64(x, 1), this.rotr64(x, 8)), this.shr64(x, 7))

  fun sigma1(this, x)
    return this.xor64(this.xor64(this.rotr64(x, 19), this.rotr64(x, 61)), this.shr64(x, 6))

  // -------- constants --------
  // SHA-384 uses the same K constants as SHA-512
  K = [
    [0x428a2f98,0xd728ae22],[0x71374491,0x23ef65cd],[0xb5c0fbcf,0xec4d3b2f],[0xe9b5dba5,0x8189dbbc],
    [0x3956c25b,0xf348b538],[0x59f111f1,0xb605d019],[0x923f82a4,0xaf194f9b],[0xab1c5ed5,0xda6d8118],
    [0xd807aa98,0xa3030242],[0x12835b01,0x45706fbe],[0x243185be,0x4ee4b28c],[0x550c7dc3,0xd5ffb4e2],
    [0x72be5d74,0xf27b896f],[0x80deb1fe,0x3b1696b1],[0x9bdc06a7,0x25c71235],[0xc19bf174,0xcf692694],
    [0xe49b69c1,0x9ef14ad2],[0xefbe4786,0x384f25e3],[0x0fc19dc6,0x8b8cd5b5],[0x240ca1cc,0x77ac9c65],
    [0x2de92c6f,0x592b0275],[0x4a7484aa,0x6ea6e483],[0x5cb0a9dc,0xbd41fbd4],[0x76f988da,0x831153b5],
    [0x983e5152,0xee66dfab],[0xa831c66d,0x2db43210],[0xb00327c8,0x98fb213f],[0xbf597fc7,0xbeef0ee4],
    [0xc6e00bf3,0x3da88fc2],[0xd5a79147,0x930aa725],[0x06ca6351,0xe003826f],[0x14292967,0x0a0e6e70],
    [0x27b70a85,0x46d22ffc],[0x2e1b2138,0x5c26c926],[0x4d2c6dfc,0x5ac42aed],[0x53380d13,0x9d95b3df],
    [0x650a7354,0x8baf63de],[0x766a0abb,0x3c77b2a8],[0x81c2c92e,0x47edaee6],[0x92722c85,0x1482353b],
    [0xa2bfe8a1,0x4cf10364],[0xa81a664b,0xbc423001],[0xc24b8b70,0xd0f89791],[0xc76c51a3,0x0654be30],
    [0xd192e819,0xd6ef5218],[0xd6990624,0x5565a910],[0xf40e3585,0x5771202a],[0x106aa070,0x32bbd1b8],
    [0x19a4c116,0xb8d2d0c8],[0x1e376c08,0x5141ab53],[0x2748774c,0xdf8eeb99],[0x34b0bcb5,0xe19b48a8],
    [0x391c0cb3,0xc5c95a63],[0x4ed8aa4a,0xe3418acb],[0x5b9cca4f,0x7763e373],[0x682e6ff3,0xd6b2b8a3],
    [0x748f82ee,0x5defb2fc],[0x78a5636f,0x43172f60],[0x84c87814,0xa1f0ab72],[0x8cc70208,0x1a6439ec],
    [0x90befffa,0x23631e28],[0xa4506ceb,0xde82bde9],[0xbef9a3f7,0xb2c67915],[0xc67178f2,0xe372532b],
    [0xca273ece,0xea26619c],[0xd186b8c7,0x21c0c207],[0xeada7dd6,0xcde0eb1e],[0xf57d4f7f,0xee6ed178],
    [0x06f067aa,0x72176fba],[0x0a637dc5,0xa2c898a6],[0x113f9804,0xbef90dae],[0x1b710b35,0x131c471b],
    [0x28db77f5,0x23047d84],[0x32caab7b,0x40c72493],[0x3c9ebe0a,0x15c9bebc],[0x431d67c4,0x9c100d4c],
    [0x4cc5d4be,0xcb3e42b6],[0x597f299c,0xfc657e2a],[0x5fcb6fab,0x3ad6faec],[0x6c44198c,0x4a475817]
  ]

  // SHA-384 initial hash value (IV), 64-bit words split into [hi, lo]
  IV = [
    [0xcbbb9d5d, 0xc1059ed8], [0x629a292a, 0x367cd507], [0x9159015a, 0x3070dd17], [0x152fecd8, 0xf70e5939],
    [0x67332667, 0xffc00b31], [0x8eb44a87, 0x68581511], [0xdb0c2e0d, 0x64f98fa7], [0x47b5481d, 0xbefa4fa4]
  ]

  // -------- padding --------
  fun pad_bytes(this, bytes)
    number L = len(bytes)
    out = []
    // copy bytes
    number i = 0
    while i < L
      push(out, bytes[i])
      i = i + 1
    // append 0x80
    push(out, 128)
    // pad zeros until length % 128 == 112 (i.e., 896 bits)
    while (len(out) % 128) != 112
      push(out, 0)
    // append 128-bit length (we use upper 64 bits zero, lower 64 = L*8)
    number j = 0
    while j < 8
      push(out, 0)
      j = j + 1
    // lower 8 bytes big-endian
    number bits = L * 8
    number t = 7
    while t >= 0
      number b = (bits / pow(2, 8 * t)) % 256
      push(out, b)
      t = t - 1
    return out

  // -------- core --------
  fun process_block(this, H, block)
    // W ring buffer of 16 words ([hi, lo])
    W = []
    number i = 0
    while i < 16
      number j = i * 8
      push(W, this.pack64_be(block[j],block[j+1],block[j+2],block[j+3],
                             block[j+4],block[j+5],block[j+6],block[j+7]))
      i = i + 1

    a = H[0]
    b = H[1]
    c = H[2]
    d = H[3]
    e = H[4]
    f = H[5]
    g = H[6]
    h = H[7]

    number t = 0
    while t < 80
      number idx = t % 16
      if (t >= 16)
        w2  = W[(t - 2) % 16]
        w7  = W[(t - 7) % 16]
        w15 = W[(t - 15) % 16]
        w16 = W[(t - 16) % 16]
        s1  = this.sigma1(w2)
        s0  = this.sigma0(w15)
        tmp = this.add64(w16, s0)
        tmp = this.add64(tmp, w7)
        W[idx] = this.add64(tmp, s1)
      Wt = W[idx]

      // Precompute hot terms once per round
      Se1 = this.Sigma1(e)
      ch  = this.Ch(e, f, g)

      // T1 = h + Se1 + ch + K[t] + Wt
      tmp1 = this.add64(h, Se1)
      tmp1 = this.add64(tmp1, ch)
      tmp1 = this.add64(tmp1, this.K[t])
      T1   = this.add64(tmp1, Wt)

      // T2 = Sigma0(a) + Maj(a, b, c)
      T2 = this.add64(this.Sigma0(a), this.Maj(a, b, c))

      // State update
      h  = g
      g  = f
      f  = e
      e  = this.add64(d, T1)
      d  = c
      c  = b
      b  = a
      a  = this.add64(T1, T2)
      t  = t + 1

    H[0] = this.add64(H[0], a)
    H[1] = this.add64(H[1], b)
    H[2] = this.add64(H[2], c)
    H[3] = this.add64(H[3], d)
    H[4] = this.add64(H[4], e)
    H[5] = this.add64(H[5], f)
    H[6] = this.add64(H[6], g)
    H[7] = this.add64(H[7], h)
    return H

  fun sha384_bytes(this, bytes)
    data = this.pad_bytes(bytes)
    // init H (deep copy IV)
    H = [ this.IV[0], this.IV[1], this.IV[2], this.IV[3], this.IV[4], this.IV[5], this.IV[6], this.IV[7] ]
    number off = 0
    number N = len(data)
    while off < N
      block = []
      number i = 0
      while i < 128
        push(block, data[off + i])
        i = i + 1
      H = this.process_block(H, block)
      off = off + 128

    // output 48 bytes big-endian from H[0..5]
    out = []
    number j = 0
    while j < 6
      word = H[j]
      number hi = word[0]
      number lo = word[1]
      // hi
      push(out, (hi / 16777216) % 256)
      push(out, (hi / 65536) % 256)
      push(out, (hi / 256) % 256)
      push(out, hi % 256)
      // lo
      push(out, (lo / 16777216) % 256)
      push(out, (lo / 65536) % 256)
      push(out, (lo / 256) % 256)
      push(out, lo % 256)
      j = j + 1
    return out

  fun sha384_hex(this, hexStr)
    bytes = this.from_hex(hexStr)
    digest = this.sha384_bytes(bytes)
    return bytes_to_hex(digest)

  // Hash raw string bytes (printable ASCII)
  fun sha384_str(this, str)
    bytes = string_to_bytes_ascii(str)
    digest = this.sha384_bytes(bytes)
    return bytes_to_hex(digest)
