import '../../Data/DC.dart';
import '../../Data/BinaryList.dart';
import 'dart:typed_data';

class SHA256 {
  static int RROT(int n, int d) {
    return ZSHIFT(n, d) | (n << (32 - d));
  }

  // Zero-fill right shift
  static int ZSHIFT(int n, int d) {
    return (n & 0xFFFFFFFF) >> d;
  }

  static DC compute(DC msg) {
    /*
        Note 1: All variables are 32 bit unsigned integers and addition is calculated modulo 2^32
        Note 2: For each round, there is one round constant k[i] and one entry in the message schedule array w[i], 0 ≤ i ≤ 63
        Note 3: The compression function uses 8 working variables, a through h
        Note 4: Big-endian convention is used when expressing the constants in this pseudocode,
            and when parsing message block data from bytes to words, for example,
            the first word of the input message "abc" after padding is 0x61626380
        */

    // Initialize hash values:
    // (first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19):

    var hash = new Uint32List.fromList([
      0x6a09e667,
      0xbb67ae85,
      0x3c6ef372,
      0xa54ff53a,
      0x510e527f,
      0x9b05688c,
      0x1f83d9ab,
      0x5be0cd19
    ]);

    // Initialize array of round constants:
    // (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311):
    var k = new Uint32List.fromList([
      0x428a2f98,
      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
    ]);

    // Pre-processing:
    // begin with the original message of length L bits
    int L = msg.length * 8;

    // append a single '1' bit
    // append K '0' bits, where K is the minimum number >= 0 such that L + 1 + K + 64 is a multiple of 512

    var K = 512 - ((L + 1 + 64) % 512);

    if (K == 512) K = 0;

    var paddingLength = (K + 1) ~/ 8;
    var paddingBytes = new DC(paddingLength);
    paddingBytes[0] = 0x80;

    var data = (BinaryList()
          ..addDC(msg)
          ..addDC(paddingBytes)
          ..addUint64(L, Endian.big))
        .toDC();

    // append L as a 64-bit big-endian integer, making the total post-processed length a multiple of 512 bits

    //  Process the message in successive 512-bit chunks:
    // break message into 512-bit chunks
    // for each chunk

    for (var chunk = 0; chunk < data.length; chunk += 64) {
      // create a 64-entry message schedule array w[0..63] of 32-bit words
      // (The initial values in w[0..63] don't matter, so many implementations zero them here)
      // copy chunk into first 16 words w[0..15] of the message schedule array

      var w = new Uint32List(64); // new Uint64List(64); // uint[64];
      for (var i = 0; i < 16; i++)
        w[i] = data.getUint32(chunk + (i * 4), Endian.big);

      //for(var i = 16; i < 64; i++)
      //  w[i] = 0;

      // Extend the first 16 words into the remaining 48 words w[16..63] of the message schedule array:
      //    for i from 16 to 63
      //        s0 := (w[i-15] rightrotate 7) xor (w[i-15] rightrotate 18) xor (w[i-15] rightshift 3)
      //        s1 := (w[i-2] rightrotate 17) xor (w[i-2] rightrotate 19) xor (w[i-2] rightshift 10)
      //        w[i] := w[i-16] + s0 + w[i-7] + s1

      for (var i = 16; i < 64; i++) {
        var s0 = SHA256.RROT(w[i - 15], 7) ^
            SHA256.RROT(w[i - 15], 18) ^
            ZSHIFT(w[i - 15], 3);
        var s1 = SHA256.RROT(w[i - 2], 17) ^
            SHA256.RROT(w[i - 2], 19) ^
            ZSHIFT(w[i - 2], 10);
        w[i] = w[i - 16] + s0 + w[i - 7] + s1;
      }

      // Initialize working variables to current hash value:
      var a = hash[0];
      var b = hash[1];
      var c = hash[2];
      var d = hash[3];
      var e = hash[4];
      var f = hash[5];
      var g = hash[6];
      var h = hash[7];

      // Compression function main loop:
      for (var i = 0; i < 64; i++) {
        var S1 = SHA256.RROT(e, 6) ^ SHA256.RROT(e, 11) ^ SHA256.RROT(e, 25);
        var ch = (e & f) ^ ((~e) & g);
        var temp1 = h + S1 + ch + k[i] + w[i];
        var S0 = SHA256.RROT(a, 2) ^ SHA256.RROT(a, 13) ^ SHA256.RROT(a, 22);
        var maj = (a & b) ^ (a & c) ^ (b & c);
        int temp2 = S0 + maj;

        h = g;
        g = f;
        f = e;
        e = ZSHIFT(d + temp1, 0);
        d = c;
        c = b;
        b = a;
        a = ZSHIFT(temp1 + temp2, 0);
      }

      // Add the compressed chunk to the current hash value:

      hash[0] = ZSHIFT(hash[0] + a, 0);
      hash[1] = ZSHIFT(hash[1] + b, 0);
      hash[2] = ZSHIFT(hash[2] + c, 0);
      hash[3] = ZSHIFT(hash[3] + d, 0);
      hash[4] = ZSHIFT(hash[4] + e, 0);
      hash[5] = ZSHIFT(hash[5] + f, 0);
      hash[6] = ZSHIFT(hash[6] + g, 0);
      hash[7] = ZSHIFT(hash[7] + h, 0);
    }

    // Produce the final hash value (big-endian):
    //digest := hash := h0 append h1 append h2 append h3 append h4 append h5 append h6 append h7

    var results = new BinaryList();
    for (var i = 0; i < 8; i++) results.addUint32(hash[i], Endian.big);

    return results.toDC();
  }
}
