///|
/// This package is based on the Go implementation found here:
/// https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/crypto/sha256/sha256.go
/// which has the copyright notice:
/// Copyright 2009 The Go Authors. All rights reserved.
/// Use of this source code is governed by a BSD-style
/// license that can be found in the LICENSE file.

// The size of a sha256 checksum in bytes.
let size = 32

///|
// The blocksize of sha256 in bytes.
let block_size = 64

///|
let chunk = 64

///|
let init0 = 0x6A09E667U

///|
let init1 = 0xBB67AE85U

///|
let init2 = 0x3C6EF372U

///|
let init3 = 0xA54FF53AU

///|
let init4 = 0x510E527FU

///|
let init5 = 0x9B05688CU

///|
let init6 = 0x1F83D9ABU

///|
let init7 = 0x5BE0CD19U

///|
/// `Digest` represents the partial evaluation of a checksum.
struct Digest {
  h : FixedArray[UInt] // 8
  x : FixedArray[Byte] // chunk
  mut nx : Int
  mut len : UInt64
  // workaround to get rid of compiler warning:
  // See: https://discord.com/channels/1135479745207869510/1272985768498958387/1275082247484997693
  name : String
}

///|
/// `Digest::new` returns a new, reset Digest, ready to sum.
pub fn Digest::new() -> Digest {
  {
    h: FixedArray::from_array([
      init0, init1, init2, init3, init4, init5, init6, init7,
    ]),
    x: FixedArray::make(chunk, b'\x00'),
    nx: 0,
    len: 0,
    // workaround to get rid of compiler warning:
    name: "sha256",
  }
}

///|
let _trait : &HashFunc = Digest::new()

///|
pub impl HashFunc for Digest with fn name(self) {
  self.name
}

///|
/// `check_sum` returns the final sha256sum as a hex string.
pub impl HashFunc for Digest with fn check_sum(self) {
  let digest = self.sum()
  let result = Buffer(size_hint=2 * size)
  for b in digest {
    result.write_char_utf16le(to_hex((b.to_int() >> 4) & 0xf))
    result.write_char_utf16le(to_hex(b.to_int() & 0xf))
  }
  result.contents().to_unchecked_string()
}

///|
/// `sum` returns the final sha256sum as a `FixedArray[Byte]`.
fn Digest::sum(self : Digest) -> FixedArray[Byte] {
  // Padding. Add a 1 bit and 0 bits until 56 bytes mod 64.
  let tmp = FixedArray::make(64 + 8, b'\x00')
  tmp[0] = b'\x80'
  let t = if self.len % 64 < 56 {
    (56UL - self.len % 64).to_int()
  } else {
    (64UL + 56UL - self.len % 64).to_int()
  }

  // Length in bits.
  let len = self.len << 3
  be_put_uint64(tmp, t, len) // append length in bits
  for i = 0; i < t + 8; i = i + 1 {
    self.write(tmp[i])
  }

  // The previous write ensures that a whole number of
  // blocks (i.e. a multiple of 64 bytes) have been hashed.
  if self.nx != 0 {
    panic()
  }

  // Generate digest
  let digest = FixedArray::make(size, b'\x00')
  be_put_uint32(digest, 0, self.h[0])
  be_put_uint32(digest, 4, self.h[1])
  be_put_uint32(digest, 8, self.h[2])
  be_put_uint32(digest, 12, self.h[3])
  be_put_uint32(digest, 16, self.h[4])
  be_put_uint32(digest, 20, self.h[5])
  be_put_uint32(digest, 24, self.h[6])
  be_put_uint32(digest, 28, self.h[7])
  digest
}

///|
/// `reset` resets a digest for re-use.
pub fn Digest::reset(self : Digest) -> Unit {
  self.h[0] = init0
  self.h[1] = init1
  self.h[2] = init2
  self.h[3] = init3
  self.h[4] = init4
  self.h[5] = init5
  self.h[6] = init6
  self.h[7] = init7
  self.nx = 0
  self.len = 0
}

///|
/// `write` writes a byte to the digest.
pub impl HashFunc for Digest with fn write(self, b) {
  self.len += 1
  self.x[self.nx] = b
  self.nx += 1
  if self.nx == block_size {
    self.block_generic()
    self.nx = 0
  }
}

///|
let _K : ReadOnlyArray[UInt] = [
  0x428a2f98U, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
  0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
  0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
  0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
  0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
  0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
  0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
  0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
  0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
  0xc67178f2,
]

///|
fn Digest::block_generic(self : Digest) -> Unit {
  let w = FixedArray::make(64, 0U)

  //
  let p = self.x
  for i = 0; i < 16; i = i + 1 {
    let j = i * 4
    w[i] = (p[j].to_uint() << 24) |
      (p[j + 1].to_uint() << 16) |
      (p[j + 2].to_uint() << 8) |
      p[j + 3].to_uint()
  }
  for i = 16; i < 64; i = i + 1 {
    let v1 = w[i - 2]
    let t1 = rotl(v1, -17) ^ rotl(v1, -19) ^ (v1 >> 10)
    let v2 = w[i - 15]
    let t2 = rotl(v2, -7) ^ rotl(v2, -18) ^ (v2 >> 3)
    w[i] = t1 + w[i - 7] + t2 + w[i - 16]
  }
  //
  let mut a = self.h[0]
  let mut b = self.h[1]
  let mut c = self.h[2]
  let mut d = self.h[3]
  let mut e = self.h[4]
  let mut f = self.h[5]
  let mut g = self.h[6]
  let mut h = self.h[7]
  //
  for i = 0; i < 64; i = i + 1 {
    let t1 = h +
      (rotl(e, -6) ^ rotl(e, -11) ^ rotl(e, -25)) +
      ((e & f) ^ (e.lnot() & g)) +
      _K[i] +
      w[i]
    let t2 = (rotl(a, -2) ^ rotl(a, -13) ^ rotl(a, -22)) +
      ((a & b) ^ (a & c) ^ (b & c))
    h = g
    g = f
    f = e
    e = d + t1
    d = c
    c = b
    b = a
    a = t1 + t2
  }

  // save state
  self.h[0] += a
  self.h[1] += b
  self.h[2] += c
  self.h[3] += d
  self.h[4] += e
  self.h[5] += f
  self.h[6] += g
  self.h[7] += h
}

///|
fn be_put_uint32(b : FixedArray[Byte], offset : Int, value : UInt) -> Unit {
  b[offset + 3] = (value & 0xff).to_byte()
  b[offset + 2] = ((value >> 8) & 0xff).to_byte()
  b[offset + 1] = ((value >> 16) & 0xff).to_byte()
  b[offset] = ((value >> 24) & 0xff).to_byte()
}

///|
fn be_put_uint64(b : FixedArray[Byte], offset : Int, value : UInt64) -> Unit {
  be_put_uint32(b, offset + 4, (value & 0xffffffff).to_uint())
  be_put_uint32(b, offset, ((value >> 32) & 0xffffffff).to_uint())
}

///|
fn rotl(x : UInt, r : Int) -> UInt {
  (x << r) | (x >> (32 - r))
}

///|
fn to_hex(v : Int) -> Char {
  if v < 10 {
    Int::unsafe_to_char(v + b'0'.to_int())
  } else {
    Int::unsafe_to_char(v - 10 + b'a'.to_int())
  }
}