/*!
 * private.js - hd private keys for bcoin
 * Copyright (c) 2015-2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var assert = require('assert');
var util = require('../utils/util');
var crypto = require('../crypto/crypto');
var ec = require('../crypto/ec');
var networks = require('../protocol/networks');
var Network = require('../protocol/network');
var StaticWriter = require('../utils/staticwriter');
var BufferReader = require('../utils/reader');
var StaticWriter = require('../utils/staticwriter');
var base58 = require('../utils/base58');
var encoding = require('../utils/encoding');
var common = require('./common');
var Mnemonic = require('./mnemonic');
var HDPublicKey = require('./public');

/**
 * HDPrivateKey
 * @alias module:hd.PrivateKey
 * @constructor
 * @param {Object|Base58String} options
 * @param {Base58String?} options.xkey - Serialized base58 key.
 * @param {Mnemonic?} options.mnemonic
 * @param {Number?} options.depth
 * @param {Buffer?} options.parentFingerPrint
 * @param {Number?} options.childIndex
 * @param {Buffer?} options.chainCode
 * @param {Buffer?} options.privateKey
 * @property {Network} network
 * @property {Mnemonic?} mnemonic
 * @property {Number} depth
 * @property {Buffer} parentFingerPrint
 * @property {Number} childIndex
 * @property {Buffer} chainCode
 * @property {Buffer} privateKey
 */

function HDPrivateKey(options) {
  if (!(this instanceof HDPrivateKey))
    return new HDPrivateKey(options);

  this.network = Network.primary;
  this.depth = 0;
  this.parentFingerPrint = encoding.ZERO_U32;
  this.childIndex = 0;
  this.chainCode = encoding.ZERO_HASH;
  this.privateKey = encoding.ZERO_HASH;

  this.publicKey = encoding.ZERO_KEY;
  this.fingerPrint = null;

  this.mnemonic = null;

  this._xprivkey = null;

  this._hdPublicKey = null;

  if (options)
    this.fromOptions(options);
}

/**
 * Inject properties from options object.
 * @private
 * @param {Object} options
 */

HDPrivateKey.prototype.fromOptions = function fromOptions(options) {
  assert(options, 'No options for HD private key.');
  assert(util.isNumber(options.depth));
  assert(Buffer.isBuffer(options.parentFingerPrint));
  assert(util.isNumber(options.childIndex));
  assert(Buffer.isBuffer(options.chainCode));
  assert(Buffer.isBuffer(options.privateKey));
  assert(options.depth <= 0xff, 'Depth is too high.');

  if (options.network)
    this.network = Network.get(options.network);

  this.depth = options.depth;
  this.parentFingerPrint = options.parentFingerPrint;
  this.childIndex = options.childIndex;
  this.chainCode = options.chainCode;
  this.privateKey = options.privateKey;
  this.publicKey = ec.publicKeyCreate(options.privateKey, true);

  if (options.mnemonic) {
    assert(options.mnemonic instanceof Mnemonic);
    this.mnemonic = options.mnemonic;
  }

  if (options.xprivkey) {
    assert(typeof options.xprivkey === 'string');
    this._xprivkey = options.xprivkey;
  }

  return this;
};

/**
 * Instantiate HD private key from options object.
 * @param {Object} options
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromOptions = function fromOptions(options) {
  return new HDPrivateKey().fromOptions(options);
};

/**
 * Get HD public key.
 * @returns {HDPublicKey}
 */

HDPrivateKey.prototype.toPublic = function toPublic() {
  var key = this._hdPublicKey;

  if (!key) {
    key = new HDPublicKey();
    key.network = this.network;
    key.depth = this.depth;
    key.parentFingerPrint = this.parentFingerPrint;
    key.childIndex = this.childIndex;
    key.chainCode = this.chainCode;
    key.publicKey = this.publicKey;
    this._hdPublicKey = key;
  }

  return key;
};

/**
 * Get cached base58 xprivkey.
 * @returns {Base58String}
 */

HDPrivateKey.prototype.xprivkey = function xprivkey() {
  if (!this._xprivkey)
    this._xprivkey = this.toBase58();
  return this._xprivkey;
};

/**
 * Get cached base58 xpubkey.
 * @returns {Base58String}
 */

HDPrivateKey.prototype.xpubkey = function xpubkey() {
  return this.toPublic().xpubkey();
};

/**
 * Verify network.
 * @param {(NetworkType|Network)} network
 * @returns {Boolean}
 */

HDPrivateKey.prototype.verifyNetwork = function verifyNetwork(network) {
  network = Network.get(network);
  return this.network.keyPrefix.xprivkey === network.keyPrefix.xprivkey
    && this.network.keyPrefix.coinType === network.keyPrefix.coinType;
};

/**
 * Destroy the key (zeroes chain code, privkey, and pubkey).
 * @param {Boolean} pub - Destroy hd public key as well.
 */

HDPrivateKey.prototype.destroy = function destroy(pub) {
  this.depth = 0;
  this.childIndex = 0;

  crypto.cleanse(this.parentFingerPrint);
  crypto.cleanse(this.chainCode);
  crypto.cleanse(this.privateKey);
  crypto.cleanse(this.publicKey);

  if (this.fingerPrint) {
    crypto.cleanse(this.fingerPrint);
    this.fingerPrint = null;
  }

  if (this._hdPublicKey) {
    if (pub)
      this._hdPublicKey.destroy();
    this._hdPublicKey = null;
  }

  this._xprivkey = null;

  if (this.mnemonic) {
    this.mnemonic.destroy();
    this.mnemonic = null;
  }
};

/**
 * Derive a child key.
 * @param {Number|String} - Child index or path.
 * @param {Boolean?} hardened - Whether the derivation should be hardened.
 * @returns {HDPrivateKey}
 */

HDPrivateKey.prototype.derive = function derive(index, hardened, cache) {
  var bw, id, data, hash, left, right, key, child;

  if (typeof hardened !== 'boolean') {
    cache = hardened;
    hardened = false;
  }

  if (!cache)
    cache = common.cache;

  if (typeof index === 'string')
    return this.derivePath(index, cache);

  hardened = index >= common.HARDENED ? true : hardened;

  if (index < common.HARDENED && hardened)
    index += common.HARDENED;

  if (!(index >= 0 && index < common.MAX_INDEX))
    throw new Error('Index out of range.');

  if (this.depth >= 0xff)
    throw new Error('Depth too high.');

  if (cache) {
    id = this.getID(index);
    child = cache.get(id);
    if (child)
      return child;
  }

  bw = new StaticWriter(37);

  if (hardened) {
    bw.writeU8(0);
    bw.writeBytes(this.privateKey);
    bw.writeU32BE(index);
  } else {
    bw.writeBytes(this.publicKey);
    bw.writeU32BE(index);
  }

  data = bw.render();

  hash = crypto.hmac('sha512', data, this.chainCode);
  left = hash.slice(0, 32);
  right = hash.slice(32, 64);

  try {
    key = ec.privateKeyTweakAdd(this.privateKey, left);
  } catch (e) {
    return this.derive(index + 1, cache);
  }

  if (!this.fingerPrint)
    this.fingerPrint = crypto.hash160(this.publicKey).slice(0, 4);

  child = new HDPrivateKey();
  child.network = this.network;
  child.depth = this.depth + 1;
  child.parentFingerPrint = this.fingerPrint;
  child.childIndex = index;
  child.chainCode = right;
  child.privateKey = key;
  child.publicKey = ec.publicKeyCreate(key, true);

  if (cache)
    cache.set(id, child);

  return child;
};

/**
 * Unique HD key ID.
 * @private
 * @param {Number} index
 * @returns {String}
 */

HDPrivateKey.prototype.getID = function getID(index) {
  return this.network.keyPrefix.xprivkey58
    + this.publicKey.toString('hex')
    + index;
};

/**
 * Derive a BIP44 account key.
 * @param {Number} accountIndex
 * @returns {HDPrivateKey}
 * @throws Error if key is not a master key.
 */

HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(accountIndex, cache) {
  assert(util.isNumber(accountIndex), 'Account index must be a number.');
  assert(this.isMaster(), 'Cannot derive account index.');
  return this
    .derive(44, true, cache)
    .derive(this.network.keyPrefix.coinType, true, cache)
    .derive(accountIndex, true, cache);
};

/**
 * Derive a BIP45 purpose key.
 * @returns {HDPrivateKey}
 */

HDPrivateKey.prototype.derivePurpose45 = function derivePurpose45(cache) {
  assert(this.isMaster(), 'Cannot derive purpose 45.');
  return this.derive(45, true, cache);
};

/**
 * Test whether the key is a master key.
 * @returns {Boolean}
 */

HDPrivateKey.prototype.isMaster = function isMaster() {
  return common.isMaster(this);
};

/**
 * Test whether the key is (most likely) a BIP44 account key.
 * @param {Number?} accountIndex
 * @returns {Boolean}
 */

HDPrivateKey.prototype.isAccount44 = function isAccount44(accountIndex) {
  return common.isAccount44(this, accountIndex);
};

/**
 * Test whether the key is a BIP45 purpose key.
 * @returns {Boolean}
 */

HDPrivateKey.prototype.isPurpose45 = function isPurpose45() {
  return common.isPurpose45(this);
};

/**
 * Test whether an object is in the form of a base58 xprivkey.
 * @param {String} data
 * @returns {Boolean}
 */

HDPrivateKey.isBase58 = function isBase58(data) {
  var i, type, prefix;

  if (typeof data !== 'string')
    return false;

  for (i = 0; i < networks.types.length; i++) {
    type = networks.types[i];
    prefix = networks[type].keyPrefix.xprivkey58;
    if (data.indexOf(prefix) === 0)
      return true;
  }

  return false;
};

/**
 * Test whether a buffer has a valid network prefix.
 * @param {Buffer} data
 * @returns {NetworkType}
 */

HDPrivateKey.isRaw = function isRaw(data) {
  var i, version, prefix, type;

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

  if (data.length < 4)
    return false;

  version = data.readUInt32BE(0, true);

  for (i = 0; i < networks.types.length; i++) {
    type = networks.types[i];
    prefix = networks[type].keyPrefix.xprivkey;
    if (version === prefix)
      return type;
  }

  return false;
};

/**
 * Test whether a string is a valid path.
 * @param {String} path
 * @param {Boolean?} hardened
 * @returns {Boolean}
 */

HDPrivateKey.isValidPath = function isValidPath(path) {
  if (typeof path !== 'string')
    return false;

  try {
    common.parsePath(path, common.MAX_INDEX);
    return true;
  } catch (e) {
    return false;
  }
};

/**
 * Derive a key from a derivation path.
 * @param {String} path
 * @returns {HDPrivateKey}
 * @throws Error if `path` is not a valid path.
 */

HDPrivateKey.prototype.derivePath = function derivePath(path, cache) {
  var indexes = common.parsePath(path, common.MAX_INDEX);
  var key = this;
  var i;

  for (i = 0; i < indexes.length; i++)
    key = key.derive(indexes[i], cache);

  return key;
};

/**
 * Compare a key against an object.
 * @param {Object} obj
 * @returns {Boolean}
 */

HDPrivateKey.prototype.equal = function equal(obj) {
  if (!HDPrivateKey.isHDPrivateKey(obj))
    return false;

  return this.network === obj.network
    && this.depth === obj.depth
    && util.equal(this.parentFingerPrint, obj.parentFingerPrint)
    && this.childIndex === obj.childIndex
    && util.equal(this.chainCode, obj.chainCode)
    && util.equal(this.privateKey, obj.privateKey);
};

/**
 * Compare a key against an object.
 * @param {Object} obj
 * @returns {Boolean}
 */

HDPrivateKey.prototype.compare = function compare(key) {
  var cmp;

  if (!HDPrivateKey.isHDPrivateKey(key))
    return 1;

  cmp = this.depth - key.depth;

  if (cmp !== 0)
    return cmp;

  cmp = util.cmp(this.parentFingerPrint, key.parentFingerPrint);

  if (cmp !== 0)
    return cmp;

  cmp = this.childIndex - key.childIndex;

  if (cmp !== 0)
    return cmp;

  cmp = util.cmp(this.chainCode, key.chainCode);

  if (cmp !== 0)
    return cmp;

  cmp = util.cmp(this.privateKey, key.privateKey);

  if (cmp !== 0)
    return cmp;

  return 0;
};

/**
 * Inject properties from seed.
 * @private
 * @param {Buffer} seed
 * @param {(Network|NetworkType)?} network
 */

HDPrivateKey.prototype.fromSeed = function fromSeed(seed, network) {
  var hash, left, right;

  assert(Buffer.isBuffer(seed));

  if (!(seed.length * 8 >= common.MIN_ENTROPY
      && seed.length * 8 <= common.MAX_ENTROPY)) {
    throw new Error('Entropy not in range.');
  }

  hash = crypto.hmac('sha512', seed, common.SEED_SALT);

  left = hash.slice(0, 32);
  right = hash.slice(32, 64);

  // Only a 1 in 2^127 chance of happening.
  if (!ec.privateKeyVerify(left))
    throw new Error('Master private key is invalid.');

  this.network = Network.get(network);
  this.depth = 0;
  this.parentFingerPrint = new Buffer([0, 0, 0, 0]);
  this.childIndex = 0;
  this.chainCode = right;
  this.privateKey = left;
  this.publicKey = ec.publicKeyCreate(left, true);

  return this;
};

/**
 * Instantiate an hd private key from a 512 bit seed.
 * @param {Buffer} seed
 * @param {(Network|NetworkType)?} network
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromSeed = function fromSeed(seed, network) {
  return new HDPrivateKey().fromSeed(seed, network);
};

/**
 * Inject properties from a mnemonic.
 * @private
 * @param {Mnemonic|Object} mnemonic
 * @param {(Network|NetworkType)?} network
 */

HDPrivateKey.prototype.fromMnemonic = function fromMnemonic(mnemonic, network) {
  if (!(mnemonic instanceof Mnemonic))
    mnemonic = new Mnemonic(mnemonic);
  this.fromSeed(mnemonic.toSeed(), network);
  this.mnemonic = mnemonic;
  return this;
};

/**
 * Instantiate an hd private key from a mnemonic.
 * @param {Mnemonic|Object} mnemonic
 * @param {(Network|NetworkType)?} network
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromMnemonic = function fromMnemonic(mnemonic, network) {
  return new HDPrivateKey().fromMnemonic(mnemonic, network);
};

/**
 * Inject properties from privateKey and entropy.
 * @private
 * @param {Buffer} key
 * @param {Buffer} entropy
 * @param {(Network|NetworkType)?} network
 */

HDPrivateKey.prototype.fromKey = function fromKey(key, entropy, network) {
  assert(Buffer.isBuffer(key) && key.length === 32);
  assert(Buffer.isBuffer(entropy) && entropy.length === 32);
  this.network = Network.get(network);
  this.depth = 0;
  this.parentFingerPrint = new Buffer([0, 0, 0, 0]);
  this.childIndex = 0;
  this.chainCode = entropy;
  this.privateKey = key;
  this.publicKey = ec.publicKeyCreate(key, true);
  return this;
};

/**
 * Create an hd private key from a key and entropy bytes.
 * @param {Buffer} key
 * @param {Buffer} entropy
 * @param {(Network|NetworkType)?} network
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromKey = function fromKey(key, entropy, network) {
  return new HDPrivateKey().fromKey(key, entropy, network);
};

/**
 * Generate an hd private key.
 * @param {(Network|NetworkType)?} network
 * @returns {HDPrivateKey}
 */

HDPrivateKey.generate = function generate(network) {
  var key = ec.generatePrivateKey();
  var entropy = crypto.randomBytes(32);
  return HDPrivateKey.fromKey(key, entropy, network);
};

/**
 * Inject properties from base58 key.
 * @private
 * @param {Base58String} xkey
 * @param {Network?} network
 */

HDPrivateKey.prototype.fromBase58 = function fromBase58(xkey, network) {
  this.fromRaw(base58.decode(xkey));
  this._xprivkey = xkey;
  if (network && !this.verifyNetwork(network))
    throw new Error('Network mismatch for HD private key.');
  return this;
};

/**
 * Inject properties from serialized data.
 * @private
 * @param {Buffer} raw
 */

HDPrivateKey.prototype.fromReader = function fromReader(br) {
  var i, version, type, prefix;

  version = br.readU32BE();
  this.depth = br.readU8();
  this.parentFingerPrint = br.readBytes(4);
  this.childIndex = br.readU32BE();
  this.chainCode = br.readBytes(32);
  br.readU8();
  this.privateKey = br.readBytes(32);
  br.verifyChecksum();

  for (i = 0; i < networks.types.length; i++) {
    type = networks.types[i];
    prefix = networks[type].keyPrefix.xprivkey;
    if (version === prefix)
      break;
  }

  assert(i < networks.types.length, 'Network not found.');

  this.publicKey = ec.publicKeyCreate(this.privateKey, true);
  this.network = Network.get(type);

  return this;
};

/**
 * Inject properties from serialized data.
 * @private
 * @param {Buffer} raw
 */

HDPrivateKey.prototype.fromRaw = function fromRaw(raw) {
  return this.fromReader(new BufferReader(raw));
};

/**
 * Serialize key to a base58 string.
 * @param {(Network|NetworkType)?} network
 * @returns {Base58String}
 */

HDPrivateKey.prototype.toBase58 = function toBase58(network) {
  return base58.encode(this.toRaw(network));
};

/**
 * Calculate serialization size.
 * @returns {Number}
 */

HDPrivateKey.prototype.getSize = function getSize() {
  return 82;
};

/**
 * Write the key to a buffer writer.
 * @param {BufferWriter} bw
 * @param {(Network|NetworkType)?} network
 */

HDPrivateKey.prototype.toWriter = function toWriter(bw, network) {
  if (!network)
    network = this.network;

  network = Network.get(network);

  bw.writeU32BE(network.keyPrefix.xprivkey);
  bw.writeU8(this.depth);
  bw.writeBytes(this.parentFingerPrint);
  bw.writeU32BE(this.childIndex);
  bw.writeBytes(this.chainCode);
  bw.writeU8(0);
  bw.writeBytes(this.privateKey);
  bw.writeChecksum();

  return bw;
};

/**
 * Serialize the key.
 * @param {(Network|NetworkType)?} network
 * @returns {Buffer}
 */

HDPrivateKey.prototype.toRaw = function toRaw(network) {
  return this.toWriter(new StaticWriter(82), network).render();
};

/**
 * Calculate extended serialization size.
 * @returns {Number}
 */

HDPrivateKey.prototype.getExtendedSize = function getSize() {
  var size = this.getSize();

  size += 1;

  if (this.mnemonic)
    size += this.mnemonic.getSize();

  return size;
};

/**
 * Write the key in "extended"
 * format to a buffer writer.
 * @param {BufferWriter} bw
 * @param {(Network|NetworkType)?} network
 */

HDPrivateKey.prototype.toExtendedWriter = function toExtendedWriter(bw, network) {
  this.toWriter(bw, network);

  if (this.mnemonic) {
    bw.writeU8(1);
    this.mnemonic.toWriter(bw);
  } else {
    bw.writeU8(0);
  }

  return bw;
};

/**
 * Serialize the key in "extended"
 * format (includes the mnemonic).
 * @param {(Network|NetworkType)?} network
 * @returns {Buffer}
 */

HDPrivateKey.prototype.toExtended = function toExtended(network) {
  var size = this.getExtendedSize();
  return this.toExtendedWriter(new StaticWriter(size), network).render();
};

/**
 * Inject properties from buffer reader.
 * @private
 * @param {BufferReader} br
 */

HDPrivateKey.prototype.fromExtendedReader = function fromExtendedReader(br) {
  this.fromReader(br);
  if (br.readU8() === 1)
    this.mnemonic = Mnemonic.fromReader(br);
  return this;
};

/**
 * Inject properties from extended serialized data.
 * @private
 * @param {Buffer} data
 */

HDPrivateKey.prototype.fromExtended = function fromExtended(data) {
  return this.fromExtendedReader(new BufferReader(data));
};

/**
 * Instantiate key from "extended" buffer reader.
 * @param {BufferReader} br
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromExtendedReader = function fromExtendedReader(br) {
  return new HDPrivateKey().fromExtendedReader(br);
};

/**
 * Instantiate key from "extended" serialized data.
 * @param {Buffer} data
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromExtended = function fromExtended(data) {
  return new HDPrivateKey().fromExtended(data);
};

/**
 * Instantiate an HD private key from a base58 string.
 * @param {Base58String} xkey
 * @param {Network?} network
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromBase58 = function fromBase58(xkey, network) {
  return new HDPrivateKey().fromBase58(xkey, network);
};

/**
 * Instantiate key from buffer reader.
 * @param {BufferReader} br
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromReader = function fromReader(br) {
  return new HDPrivateKey().fromReader(br);
};

/**
 * Instantiate key from serialized data.
 * @param {Buffer} raw
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromRaw = function fromRaw(raw) {
  return new HDPrivateKey().fromRaw(raw);
};

/**
 * Convert key to a more json-friendly object.
 * @returns {Object}
 */

HDPrivateKey.prototype.toJSON = function toJSON() {
  return {
    xprivkey: this.xprivkey(),
    mnemonic: this.mnemonic ? this.mnemonic.toJSON() : null
  };
};

/**
 * Inject properties from json object.
 * @private
 * @param {Object} json
 * @param {Network?} network
 */

HDPrivateKey.prototype.fromJSON = function fromJSON(json, network) {
  assert(json.xprivkey, 'Could not handle key JSON.');

  this.fromBase58(json.xprivkey, network);

  if (json.mnemonic)
    this.mnemonic = Mnemonic.fromJSON(json.mnemonic);

  return this;
};

/**
 * Instantiate an HDPrivateKey from a jsonified key object.
 * @param {Object} json - The jsonified key object.
 * @param {Network?} network
 * @returns {HDPrivateKey}
 */

HDPrivateKey.fromJSON = function fromJSON(json, network) {
  return new HDPrivateKey().fromJSON(json, network);
};

/**
 * Test whether an object is an HDPrivateKey.
 * @param {Object} obj
 * @returns {Boolean}
 */

HDPrivateKey.isHDPrivateKey = function isHDPrivateKey(obj) {
  return obj
    && typeof obj.derive === 'function'
    && typeof obj.toExtended === 'function'
    && obj.chainCode !== undefined;
};

/*
 * Expose
 */

module.exports = HDPrivateKey;
