/*!
 * sha256.js - SHA256 implementation for bcoin
 * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 * Parts of this software based on hash.js.
 */

'use strict';

/**
 * @module crypto/sha256
 * @ignore
 */

/*
 * Constants
 */

var DESC = new Buffer(8);
var BUFFER64 = new Buffer(64);
var K, PADDING;
var ctx, mctx;

PADDING = new Buffer(64);
PADDING.fill(0);
PADDING[0] = 0x80;

K = [
  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
];

/**
 * SHA256
 * @alias module:crypto/sha256.SHA256
 * @constructor
 * @property {Number[]} s
 * @property {Number[]} w
 * @property {Buffer} block
 * @property {Number} bytes
 */

function SHA256() {
  if (!(this instanceof SHA256))
    return new SHA256();

  this.s = new Array(8);
  this.w = new Array(64);
  this.block = new Buffer(64);
  this.bytes = 0;
}

/**
 * Initialize SHA256 context.
 */

SHA256.prototype.init = function init() {
  this.s[0] = 0x6a09e667;
  this.s[1] = 0xbb67ae85;
  this.s[2] = 0x3c6ef372;
  this.s[3] = 0xa54ff53a;
  this.s[4] = 0x510e527f;
  this.s[5] = 0x9b05688c;
  this.s[6] = 0x1f83d9ab;
  this.s[7] = 0x5be0cd19;
  this.bytes = 0;
};

/**
 * Update SHA256 context.
 * @param {Buffer} data
 */

SHA256.prototype.update = function update(data) {
  return this._update(data, data.length);
};

/**
 * Finalize SHA256 context.
 * @returns {Buffer}
 */

SHA256.prototype.finish = function finish() {
  return this._finish(new Buffer(32));
};

/**
 * Update SHA256 context.
 * @private
 * @param {Buffer} data
 * @param {Number} len
 */

SHA256.prototype._update = function update(data, len) {
  var size = this.bytes & 0x3f;
  var pos = 0;
  var i, want;

  this.bytes += len;

  if (size > 0) {
    want = 64 - size;

    if (want > len)
      want = len;

    for (i = 0; i < want; i++)
      this.block[size + i] = data[i];

    size += want;
    len -= want;
    pos += want;

    if (size < 64)
      return;

    this.transform(this.block, 0);
  }

  while (len >= 64) {
    this.transform(data, pos);
    pos += 64;
    len -= 64;
  }

  for (i = 0; i < len; i++)
    this.block[i] = data[pos + i];
};

/**
 * Finalize SHA256 context.
 * @private
 * @param {Buffer} out
 * @returns {Buffer}
 */

SHA256.prototype._finish = function _finish(out) {
  var i;

  writeU32(DESC, this.bytes >>> 29, 0);
  writeU32(DESC, this.bytes << 3, 4);

  this._update(PADDING, 1 + ((119 - (this.bytes % 64)) % 64));
  this._update(DESC, 8);

  for (i = 0; i < 8; i++) {
    writeU32(out, this.s[i], i * 4);
    this.s[i] = 0;
  }

  return out;
};

/**
 * Transform SHA256 block.
 * @param {Buffer} chunk
 * @param {Number} pos
 */

SHA256.prototype.transform = function transform(chunk, pos) {
  var a = this.s[0];
  var b = this.s[1];
  var c = this.s[2];
  var d = this.s[3];
  var e = this.s[4];
  var f = this.s[5];
  var g = this.s[6];
  var h = this.s[7];
  var w = this.w;
  var i, t1, t2;

  for (i = 0; i < 16; i++)
    w[i] = readU32(chunk, pos + i * 4);

  for (; i < 64; i++)
    w[i] = sigma1(w[i - 2]) + w[i - 7] + sigma0(w[i - 15]) + w[i - 16];

  for (i = 0; i < 64; i++) {
    t1 = h + Sigma1(e);
    t1 += Ch(e, f, g);
    t1 += K[i] + w[i];

    t2 = Sigma0(a);
    t2 += Maj(a, b, c);

    h = g;
    g = f;
    f = e;

    e = d + t1;

    d = c;
    c = b;
    b = a;

    a = t1 + t2;
  }

  this.s[0] += a;
  this.s[1] += b;
  this.s[2] += c;
  this.s[3] += d;
  this.s[4] += e;
  this.s[5] += f;
  this.s[6] += g;
  this.s[7] += h;

  this.s[0] >>>= 0;
  this.s[1] >>>= 0;
  this.s[2] >>>= 0;
  this.s[3] >>>= 0;
  this.s[4] >>>= 0;
  this.s[5] >>>= 0;
  this.s[6] >>>= 0;
  this.s[7] >>>= 0;
};

/**
 * SHA256Hmac
 * @alias module:crypto/sha256.SHA256Hmac
 * @constructor
 * @property {SHA256} inner
 * @property {SHA256} outer
 */

function SHA256Hmac() {
  if (!(this instanceof SHA256Hmac))
    return new SHA256Hmac();

  this.inner = new SHA256();
  this.outer = new SHA256();
}

/**
 * Initialize HMAC context.
 * @param {Buffer} data
 */

SHA256Hmac.prototype.init = function init(data) {
  var key = BUFFER64;
  var i;

  if (data.length > 64) {
    this.inner.init();
    this.inner.update(data);
    this.inner._finish(key);
    key.fill(0, 32, 64);
  } else {
    data.copy(key, 0);
    key.fill(0, data.length, 64);
  }

  for (i = 0; i < key.length; i++)
    key[i] ^= 0x36;

  this.inner.init();
  this.inner.update(key);

  for (i = 0; i < key.length; i++)
    key[i] ^= 0x6a;

  this.outer.init();
  this.outer.update(key);
};

/**
 * Update HMAC context.
 * @param {Buffer} data
 */

SHA256Hmac.prototype.update = function update(data) {
  this.inner.update(data);
};

/**
 * Finalize HMAC context.
 * @returns {Buffer}
 */

SHA256Hmac.prototype.finish = function finish() {
  this.outer.update(this.inner.finish());
  return this.outer.finish();
};

/*
 * Helpers
 * @see https://github.com/bitcoin-core/secp256k1/blob/master/src/hash_impl.h
 */

function Sigma0(x) {
  return (x >>> 2 | x << 30) ^ (x >>> 13 | x << 19) ^ (x >>> 22 | x << 10);
}

function Sigma1(x) {
  return (x >>> 6 | x << 26) ^ (x >>> 11 | x << 21) ^ (x >>> 25 | x << 7);
}

function sigma0(x) {
  return (x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ (x >>> 3);
}

function sigma1(x) {
  return (x >>> 17 | x << 15) ^ (x >>> 19 | x << 13) ^ (x >>> 10);
}

function Ch(x, y, z) {
  return z ^ (x & (y ^ z));
}

function Maj(x, y, z) {
  return (x & y) | (z & (x | y));
}

function writeU32(buf, value, offset) {
  buf[offset] = value >>> 24;
  buf[offset + 1] = (value >> 16) & 0xff;
  buf[offset + 2] = (value >> 8) & 0xff;
  buf[offset + 3] = value & 0xff;
}

function readU32(buf, offset) {
  return ((buf[offset] & 0xff) * 0x1000000)
    + ((buf[offset + 1] & 0xff) << 16)
    | ((buf[offset + 2] & 0xff) << 8)
    | (buf[offset + 3] & 0xff);
}

/*
 * Context Helpers
 */

ctx = new SHA256();
mctx = new SHA256Hmac();

/**
 * Hash buffer with sha256.
 * @alias module:crypto/sha256.sha256
 * @param {Buffer} data
 * @returns {Buffer}
 */

function sha256(data) {
  ctx.init();
  ctx.update(data);
  return ctx.finish();
}

/**
 * Hash buffer with double sha256.
 * @alias module:crypto/sha256.hash256
 * @param {Buffer} data
 * @returns {Buffer}
 */

function hash256(data) {
  var out = new Buffer(32);
  ctx.init();
  ctx.update(data);
  ctx._finish(out);
  ctx.init();
  ctx.update(out);
  ctx._finish(out);
  return out;
}

/**
 * Create a sha256 HMAC from buffer and key.
 * @alias module:crypto/sha256.hmac
 * @param {Buffer} data
 * @param {Buffer} key
 * @returns {Buffer}
 */

function hmac(data, key) {
  mctx.init(key);
  mctx.update(data);
  return mctx.finish();
}

/*
 * Expose
 */

exports = SHA256;
exports.SHA256 = SHA256;
exports.SHA256Hmac = SHA256Hmac;
exports.digest = sha256;
exports.hmac = hmac;
exports.hash256 = hash256;

module.exports = exports;
