/*!
 * crypto.js - crypto for bcoin
 * Copyright (c) 2014-2015, Fedor Indutny (MIT License)
 * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var assert = require('assert');
var random = require('./random');
var scrypt = require('./scrypt');
var scryptAsync = require('./scrypt-async');
var utils = require('../utils/utils');
var co = require('../utils/co');
var native = require('../utils/native');
var lazy = require('../utils/lazy')(require, exports);
var nodeCrypto, hash, aes;

var isBrowser =
  (typeof process !== 'undefined' && process.browser)
  || typeof window !== 'undefined';

if (!isBrowser) {
  nodeCrypto = require('crypto');
} else {
  hash = require('hash.js');
  aes = require('./aes');
}

/**
 * @exports crypto
 */

var crypto = exports;

/**
 * Hash with chosen algorithm.
 * @param {String} alg
 * @param {Buffer} data
 * @returns {Buffer}
 */

crypto.hash = function _hash(alg, data) {
  if (!nodeCrypto)
    return new Buffer(hash[alg]().update(data).digest());

  return nodeCrypto.createHash(alg).update(data).digest();
};

if (native)
  crypto.hash = native.hash;

/**
 * Hash with ripemd160.
 * @param {Buffer} data
 * @returns {Buffer}
 */

crypto.ripemd160 = function ripemd160(data) {
  return crypto.hash('ripemd160', data);
};

/**
 * Hash with sha1.
 * @param {Buffer} data
 * @returns {Buffer}
 */

crypto.sha1 = function sha1(data) {
  return crypto.hash('sha1', data);
};

/**
 * Hash with sha256.
 * @param {Buffer} data
 * @returns {Buffer}
 */

crypto.sha256 = function sha256(data) {
  return crypto.hash('sha256', data);
};

if (native)
  crypto.sha256 = native.sha256;

/**
 * Hash with sha256 and ripemd160 (OP_HASH160).
 * @param {Buffer} data
 * @returns {Buffer}
 */

crypto.hash160 = function hash160(data) {
  return crypto.ripemd160(crypto.sha256(data));
};

if (native)
  crypto.hash160 = native.hash160;

/**
 * Hash with sha256 twice (OP_HASH256).
 * @param {Buffer} data
 * @returns {Buffer}
 */

crypto.hash256 = function hash256(data) {
  return crypto.sha256(crypto.sha256(data));
};

if (native)
  crypto.hash256 = native.hash256;

/**
 * Create a sha256 checksum (common in bitcoin).
 * @param {Buffer} data
 * @returns {Buffer}
 */

crypto.checksum = function checksum(data) {
  return crypto.hash256(data).slice(0, 4);
};

/**
 * Create an HMAC.
 * @param {String} alg
 * @param {Buffer} data
 * @param {Buffer} salt
 * @returns {Buffer} HMAC
 */

crypto.hmac = function hmac(alg, data, salt) {
  var hmac;

  if (!nodeCrypto) {
    hmac = hash.hmac(hash[alg], salt);
    return new Buffer(hmac.update(data).digest());
  }

  hmac = nodeCrypto.createHmac(alg, salt);
  return hmac.update(data).digest();
};

if (native)
  crypto.hmac = native.hmac;

/**
 * Perform key derivation using PBKDF2.
 * @param {Buffer} key
 * @param {Buffer} salt
 * @param {Number} iter
 * @param {Number} len
 * @param {String} alg
 * @returns {Buffer}
 */

crypto.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) {
  if (typeof key === 'string')
    key = new Buffer(key, 'utf8');

  if (typeof salt === 'string')
    salt = new Buffer(salt, 'utf8');

  if (nodeCrypto && nodeCrypto.pbkdf2Sync)
    return nodeCrypto.pbkdf2Sync(key, salt, iter, len, alg);

  return crypto._pbkdf2(key, salt, iter, len, alg);
};

/**
 * Execute pbkdf2 asynchronously.
 * @param {Buffer} key
 * @param {Buffer} salt
 * @param {Number} iter
 * @param {Number} len
 * @param {String} alg
 * @returns {Promise}
 */

crypto.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg) {
  var result;

  if (typeof key === 'string')
    key = new Buffer(key, 'utf8');

  if (typeof salt === 'string')
    salt = new Buffer(salt, 'utf8');

  if (nodeCrypto && nodeCrypto.pbkdf2) {
    return new Promise(function(resolve, reject) {
      nodeCrypto.pbkdf2(key, salt, iter, len, alg, co.wrap(resolve, reject));
    });
  }

  try {
    result = crypto._pbkdf2(key, salt, iter, len, alg);
  } catch (e) {
    return Promise.reject(e);
  }

  return Promise.resolve(result);
};

/**
 * Perform key derivation using scrypt.
 * @param {Buffer} passwd
 * @param {Buffer} salt
 * @param {Number} N
 * @param {Number} r
 * @param {Number} p
 * @param {Number} len
 * @returns {Buffer}
 */

crypto.scrypt = function _scrypt(passwd, salt, N, r, p, len) {
  if (typeof passwd === 'string')
    passwd = new Buffer(passwd, 'utf8');

  if (typeof salt === 'string')
    salt = new Buffer(salt, 'utf8');

  return scrypt(passwd, salt, N, r, p, len);
};

/**
 * Execute scrypt asynchronously.
 * @param {Buffer} passwd
 * @param {Buffer} salt
 * @param {Number} N
 * @param {Number} r
 * @param {Number} p
 * @param {Number} len
 * @returns {Promise}
 */

crypto.scryptAsync = function _scrypt(passwd, salt, N, r, p, len) {
  if (typeof passwd === 'string')
    passwd = new Buffer(passwd, 'utf8');

  if (typeof salt === 'string')
    salt = new Buffer(salt, 'utf8');

  return new Promise(function(resolve, reject) {
    scryptAsync(passwd, salt, N, r, p, len, co.wrap(resolve, reject));
  });
};

/**
 * Derive a key using pbkdf2 with 50,000 iterations.
 * @param {Buffer|String} passphrase
 * @returns {Promise}
 */

crypto.derive = function derive(passphrase) {
  return crypto.pbkdf2Async(passphrase, 'bcoin', 50000, 32, 'sha256');
};

/**
 * Encrypt with aes-256-cbc. Derives key with {@link crypto.derive}.
 * @param {Buffer} data
 * @param {Buffer|String} passphrase
 * @param {Buffer} iv - 128 bit initialization vector.
 * @returns {Promise}
 */

crypto.encrypt = co(function* encrypt(data, passphrase, iv) {
  var key;

  assert(Buffer.isBuffer(data));
  assert(passphrase, 'No passphrase.');
  assert(Buffer.isBuffer(iv));

  key = yield crypto.derive(passphrase);

  try {
    data = crypto.encipher(data, key, iv);
  } catch (e) {
    key.fill(0);
    throw e;
  }

  key.fill(0);

  return data;
});

/**
 * Encrypt with aes-256-cbc.
 * @param {Buffer} data
 * @param {Buffer} key - 256 bit key.
 * @param {Buffer} iv - 128 bit initialization vector.
 * @returns {Buffer}
 */

crypto.encipher = function encipher(data, key, iv) {
  var cipher;

  assert(Buffer.isBuffer(data));
  assert(Buffer.isBuffer(key));
  assert(Buffer.isBuffer(iv));
  assert(key.length === 32);
  assert(iv.length === 16);

  if (!nodeCrypto)
    return aes.cbc.encrypt(data, key, iv);

  cipher = nodeCrypto.createCipheriv('aes-256-cbc', key, iv);

  return utils.concat(cipher.update(data), cipher.final());
};

/**
 * Decrypt with aes-256-cbc. Derives key with {@link crypto.derive}.
 * @param {Buffer} data
 * @param {Buffer|String} passphrase
 * @param {Buffer} iv - 128 bit initialization vector.
 * @returns {Promise}
 */

crypto.decrypt = co(function* decrypt(data, passphrase, iv) {
  var key;

  assert(Buffer.isBuffer(data));
  assert(passphrase, 'No passphrase.');
  assert(Buffer.isBuffer(iv));

  key = yield crypto.derive(passphrase);

  try {
    data = crypto.decipher(data, key, iv);
  } catch (e) {
    key.fill(0);
    throw e;
  }

  key.fill(0);

  return data;
});

/**
 * Decrypt with aes-256-cbc.
 * @param {Buffer} data
 * @param {Buffer} key - 256 bit key.
 * @param {Buffer} iv - 128 bit initialization vector.
 * @returns {Buffer}
 */

crypto.decipher = function decipher(data, key, iv) {
  assert(Buffer.isBuffer(data));
  assert(Buffer.isBuffer(key));
  assert(Buffer.isBuffer(iv));
  assert(key.length === 32);
  assert(iv.length === 16);
  try {
    return crypto._decipher(data, key, iv);
  } catch (e) {
    throw new Error('Bad key for decryption.');
  }
};

/**
 * Decrypt with aes-256-cbc.
 * @private
 * @param {Buffer} data
 * @param {Buffer} key
 * @param {Buffer} iv
 * @returns {Buffer}
 */

crypto._decipher = function decipher(data, key, iv) {
  var decipher;

  if (!nodeCrypto)
    return aes.cbc.decrypt(data, key, iv);

  decipher = nodeCrypto.createDecipheriv('aes-256-cbc', key, iv);

  return utils.concat(decipher.update(data), decipher.final());
};

/**
 * Perform key derivation using PBKDF2.
 * @private
 * @param {Buffer} key
 * @param {Buffer} salt
 * @param {Number} iter
 * @param {Number} len
 * @param {String} alg
 * @returns {Buffer}
 */

crypto._pbkdf2 = function pbkdf2(key, salt, iter, len, alg) {
  var size = crypto.hash(alg, new Buffer(0)).length;
  var blocks = Math.ceil(len / size);
  var out = new Buffer(len);
  var buf = new Buffer(salt.length + 4);
  var block = new Buffer(size);
  var pos = 0;
  var i, j, k, mac;

  salt.copy(buf, 0);

  for (i = 0; i < blocks; i++) {
    buf.writeUInt32BE(i + 1, salt.length, true);
    mac = crypto.hmac(alg, buf, key);
    mac.copy(block, 0);
    for (j = 1; j < iter; j++) {
      mac = crypto.hmac(alg, mac, key);
      for (k = 0; k < size; k++)
        block[k] ^= mac[k];
    }
    block.copy(out, pos);
    pos += size;
  }

  return out;
};

/**
 * Perform hkdf extraction.
 * @param {Buffer} ikm
 * @param {Buffer} salt
 * @param {String} alg
 * @returns {Buffer}
 */

crypto.hkdfExtract = function hkdfExtract(ikm, salt, alg) {
  return crypto.hmac(alg, ikm, salt);
};

/**
 * Perform hkdf expansion.
 * @param {Buffer} prk
 * @param {Buffer} info
 * @param {Number} len
 * @param {String} alg
 * @returns {Buffer}
 */

crypto.hkdfExpand = function hkdfExpand(prk, info, len, alg) {
  var size = crypto.hash(alg, new Buffer(0)).length;
  var blocks = Math.ceil(len / size);
  var i, okm, buf, out;

  if (blocks > 255)
    throw new Error('Too many blocks.');

  okm = new Buffer(len);

  if (blocks === 0)
    return okm;

  buf = new Buffer(size + info.length + 1);

  // First round:
  info.copy(buf, size);
  buf[buf.length - 1] = 1;
  out = crypto.hmac(alg, buf.slice(size), prk);
  out.copy(okm, 0);

  for (i = 1; i < blocks; i++) {
    out.copy(buf, 0);
    buf[buf.length - 1]++;
    out = crypto.hmac(alg, buf, prk);
    out.copy(okm, i * size);
  }

  return okm;
};

/**
 * memcmp in constant time (can only return true or false).
 * This protects us against timing attacks when
 * comparing an input against a secret string.
 * @see https://cryptocoding.net/index.php/Coding_rules
 * @see `$ man 3 memcmp` (NetBSD's consttime_memequal)
 * @param {Buffer} a
 * @param {Buffer} b
 * @returns {Boolean}
 */

crypto.ccmp = function ccmp(a, b) {
  var i, res;

  if (!Buffer.isBuffer(a))
    return false;

  if (!Buffer.isBuffer(b))
    return false;

  if (b.length === 0)
    return a.length === 0;

  res = a.length ^ b.length;

  for (i = 0; i < a.length; i++)
    res |= a[i] ^ b[i % b.length];

  return res === 0;
};

/**
 * Compare two bytes in constant time.
 * @param {Number} a
 * @param {Number} b
 * @returns {Boolean}
 */

crypto.ceq = function ceq(a, b) {
  var r = ~(a ^ b) & 0xff;
  r &= r >>> 4;
  r &= r >>> 2;
  r &= r >>> 1;
  return r === 1;
};

/**
 * Build a merkle tree from leaves.
 * @param {Buffer[]} leaves
 * @returns {Buffer[]} Tree (in rare cases this may return null).
 */

crypto.buildMerkleTree = function buildMerkleTree(leaves) {
  var tree = leaves.slice();
  var size = leaves.length;
  var i, j, i2, hash, left, right, buf;

  if (size > 1)
    buf = new Buffer(64);

  for (j = 0; size > 1; size = ((size + 1) / 2) | 0) {
    for (i = 0; i < size; i += 2) {
      i2 = Math.min(i + 1, size - 1);
      left = tree[j + i];
      right = tree[j + i2];

      if (i2 === i + 1 && i2 + 1 === size
          && utils.cmp(left, right) === 0) {
        return;
      }

      left.copy(buf, 0);
      right.copy(buf, 32);
      hash = crypto.hash256(buf);

      tree.push(hash);
    }
    j += size;
  }

  if (tree.length === 0)
    return;

  return tree;
};

if (native)
  crypto.buildMerkleTree = native.buildMerkleTree;

/**
 * Calculate merkle root from leaves.
 * @param {Buffer[]} leaves
 * @returns {Buffer?} Merkle root.
 */

crypto.getMerkleRoot = function getMerkleRoot(leaves) {
  var tree = crypto.buildMerkleTree(leaves);
  if (!tree)
    return;

  return tree[tree.length - 1];
};

/**
 * Collect a merkle branch at vector index.
 * @param {Number} index
 * @param {Buffer[]} leaves
 * @returns {Buffer[]} branch
 */

crypto.getMerkleBranch = function getMerkleBranch(index, leaves) {
  var tree = crypto.buildMerkleTree(leaves);
  var size = leaves.length;
  var branch = [];
  var j = 0;
  var i;

  for (; size > 1; size = (size + 1) / 2 | 0) {
    i = Math.min(index ^ 1, size - 1);
    branch.push(tree[j + i]);
    index >>>= 1;
    j += size;
  }

  return branch;
};

/**
 * Check a merkle branch at vector index.
 * @param {Buffer} hash
 * @param {Buffer[]} branch
 * @param {Number} index
 * @returns {Buffer} Hash.
 */

crypto.checkMerkleBranch = function checkMerkleBranch(hash, branch, index) {
  var i, otherside, buf;

  if (branch.length === 0)
    return hash;

  buf = new Buffer(64);

  for (i = 0; i < branch.length; i++) {
    otherside = branch[i];

    if (index & 1) {
      otherside.copy(buf, 0);
      hash.copy(buf, 32);
    } else {
      hash.copy(buf, 0);
      otherside.copy(buf, 32);
    }

    hash = crypto.hash256(buf);
    index >>>= 1;
  }

  return hash;
};

if (native)
  crypto.checkMerkleBranch = native.checkMerkleBranch;

/**
 * Generate some random bytes.
 * @function
 * @param {Number} size
 * @returns {Buffer}
 */

crypto.randomBytes = random.randomBytes;

/**
 * Generate a random number within a range.
 * Probably more cryptographically sound than
 * `Math.random()`.
 * @function
 * @param {Number} min - Inclusive.
 * @param {Number} max - Exclusive.
 * @returns {Number}
 */

crypto.randomRange = random.randomRange;

/**
 * Generate a random uint32.
 * Probably more cryptographically sound than
 * `Math.random()`.
 * @function
 * @returns {Number}
 */

crypto.randomInt = random.randomInt;

/*
 * Expose other objects.
 */

lazy('aes', './aes');
lazy('chachapoly', './chachapoly');
lazy('ec', './ec');
lazy('schnorr', './schnorr');
lazy('siphash', './siphash');
