/*!
 * mnemonic.js - hd mnemonics 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 StaticWriter = require('../utils/staticwriter');
var BufferReader = require('../utils/reader');
var encoding = require('../utils/encoding');
var wordlist = require('./wordlist');
var common = require('./common');
var nfkd = require('../utils/nfkd');

/**
 * HD Mnemonic
 * @alias module:hd.Mnemonic
 * @constructor
 * @param {Object} options
 * @param {Number?} options.bit - Bits of entropy (Must
 * be a multiple of 8) (default=128).
 * @param {Buffer?} options.entropy - Entropy bytes. Will
 * be generated with `options.bits` bits of entropy
 * if not present.
 * @param {String?} options.phrase - Mnemonic phrase (will
 * be generated if not present).
 * @param {String?} options.passphrase - Optional salt for
 * key stretching (empty string if not present).
 * @param {String?} options.language - Language.
 */

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

  this.bits = common.MIN_ENTROPY;
  this.language = 'english';
  this.entropy = null;
  this.phrase = null;
  this.passphrase = '';

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

/**
 * List of languages.
 * @const {String[]}
 * @default
 */

Mnemonic.languages = [
  'simplified chinese',
  'traditional chinese',
  'english',
  'french',
  'italian',
  'japanese',
  'spanish'
];

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

Mnemonic.prototype.fromOptions = function fromOptions(options) {
  if (typeof options === 'string')
    options = { phrase: options };

  if (options.bits != null) {
    assert(util.isNumber(options.bits));
    assert(options.bits >= common.MIN_ENTROPY);
    assert(options.bits <= common.MAX_ENTROPY);
    assert(options.bits % 32 === 0);
    this.bits = options.bits;
  }

  if (options.language) {
    assert(typeof options.language === 'string');
    assert(Mnemonic.languages.indexOf(options.language) !== -1);
    this.language = options.language;
  }

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

  if (options.phrase) {
    this.fromPhrase(options.phrase);
    return this;
  }

  if (options.entropy) {
    this.fromEntropy(options.entropy);
    return this;
  }

  return this;
};

/**
 * Instantiate mnemonic from options.
 * @param {Object} options
 * @returns {Mnemonic}
 */

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

/**
 * Destroy the mnemonic (zeroes entropy).
 */

Mnemonic.prototype.destroy = function destroy() {
  this.bits = common.MIN_ENTROPY;
  this.language = 'english';
  if (this.entropy) {
    crypto.cleanse(this.entropy);
    this.entropy = null;
  }
  this.phrase = null;
  this.passphrase = '';
};

/**
 * Generate the seed.
 * @param {String?} passphrase
 * @returns {Buffer} pbkdf2 seed.
 */

Mnemonic.prototype.toSeed = function toSeed(passphrase) {
  var phrase, passwd;

  if (!passphrase)
    passphrase = this.passphrase;

  this.passphrase = passphrase;

  phrase = nfkd(this.getPhrase());
  passwd = nfkd('mnemonic' + passphrase);

  return crypto.pbkdf2(
    new Buffer(phrase, 'utf8'),
    new Buffer(passwd, 'utf8'),
    2048, 64, 'sha512');
};

/**
 * Get or generate entropy.
 * @returns {Buffer}
 */

Mnemonic.prototype.getEntropy = function getEntropy() {
  if (!this.entropy)
    this.entropy = crypto.randomBytes(this.bits / 8);

  assert(this.bits / 8 === this.entropy.length);

  return this.entropy;
};

/**
 * Generate a mnemonic phrase from chosen language.
 * @returns {String}
 */

Mnemonic.prototype.getPhrase = function getPhrase() {
  var i, j, phrase, wordlist, bits, ent, entropy;
  var index, pos, oct, bit;

  if (this.phrase)
    return this.phrase;

  phrase = [];
  wordlist = Mnemonic.getWordlist(this.language);

  ent = this.getEntropy();
  bits = this.bits;

  // Include the first `ENT / 32` bits
  // of the hash (the checksum).
  bits += bits / 32;

  // Append the hash to the entropy to
  // make things easy when grabbing
  // the checksum bits.
  entropy = new Buffer(Math.ceil(bits / 8));
  ent.copy(entropy, 0);
  crypto.sha256(ent).copy(entropy, ent.length);

  // Build the mnemonic by reading
  // 11 bit indexes from the entropy.
  for (i = 0; i < bits / 11; i++) {
    index = 0;
    for (j = 0; j < 11; j++) {
      pos = i * 11 + j;
      bit = pos % 8;
      oct = (pos - bit) / 8;
      index <<= 1;
      index |= (entropy[oct] >>> (7 - bit)) & 1;
    }
    phrase.push(wordlist[index]);
  }

  // Japanese likes double-width spaces.
  if (this.language === 'japanese')
    phrase = phrase.join('\u3000');
  else
    phrase = phrase.join(' ');

  this.phrase = phrase;

  return phrase;
};

/**
 * Inject properties from phrase.
 * @private
 * @param {String} phrase
 */

Mnemonic.prototype.fromPhrase = function fromPhrase(phrase) {
  var i, j, bits, pos, oct, bit, b, ent, entropy, lang;
  var chk, word, wordlist, index, cbits, cbytes, words;

  assert(typeof phrase === 'string');

  words = phrase.split(/[ \u3000]+/);
  bits = words.length * 11;
  cbits = bits % 32;
  cbytes = Math.ceil(cbits / 8);
  bits -= cbits;

  assert(bits >= common.MIN_ENTROPY);
  assert(bits <= common.MAX_ENTROPY);
  assert(bits % 32 === 0);
  assert(cbits !== 0, 'Invalid checksum.');

  ent = new Buffer(Math.ceil((bits + cbits) / 8));
  ent.fill(0);

  lang = Mnemonic.getLanguage(words[0]);
  wordlist = Mnemonic.getWordlist(lang);

  for (i = 0; i < words.length; i++) {
    word = words[i];
    index = util.binarySearch(wordlist, word, util.strcmp);

    if (index === -1)
      throw new Error('Could not find word.');

    for (j = 0; j < 11; j++) {
      pos = i * 11 + j;
      bit = pos % 8;
      oct = (pos - bit) / 8;
      b = (index >>> (10 - j)) & 1;
      ent[oct] |= b << (7 - bit);
    }
  }

  entropy = ent.slice(0, ent.length - cbytes);
  ent = ent.slice(ent.length - cbytes);
  chk = crypto.sha256(entropy);

  for (i = 0; i < cbits; i++) {
    bit = i % 8;
    oct = (i - bit) / 8;
    b = (ent[oct] >>> (7 - bit)) & 1;
    j = (chk[oct] >>> (7 - bit)) & 1;
    if (b !== j)
      throw new Error('Invalid checksum.');
  }

  assert(bits / 8 === entropy.length);

  this.bits = bits;
  this.language = lang;
  this.entropy = entropy;
  this.phrase = phrase;

  return this;
};

/**
 * Instantiate mnemonic from a phrase (validates checksum).
 * @param {String} phrase
 * @returns {Mnemonic}
 * @throws on bad checksum
 */

Mnemonic.fromPhrase = function fromPhrase(phrase) {
  return new Mnemonic().fromPhrase(phrase);
};

/**
 * Inject properties from entropy.
 * @private
 * @param {Buffer} entropy
 * @param {String?} lang
 */

Mnemonic.prototype.fromEntropy = function fromEntropy(entropy, lang) {
  assert(Buffer.isBuffer(entropy));
  assert(entropy.length * 8 >= common.MIN_ENTROPY);
  assert(entropy.length * 8 <= common.MAX_ENTROPY);
  assert((entropy.length * 8) % 32 === 0);
  assert(!lang || Mnemonic.languages.indexOf(lang) !== -1);

  this.entropy = entropy;
  this.bits = entropy.length * 8;

  if (lang)
    this.language = lang;

  return this;
};

/**
 * Instantiate mnemonic from entropy.
 * @param {Buffer} entropy
 * @param {String?} lang
 * @returns {Mnemonic}
 */

Mnemonic.fromEntropy = function fromEntropy(entropy, lang) {
  return new Mnemonic().fromEntropy(entropy, lang);
};

/**
 * Determine a single word's language.
 * @param {String} word
 * @returns {String} Language.
 * @throws on not found.
 */

Mnemonic.getLanguage = function getLanguage(word) {
  var i, lang, wordlist;

  for (i = 0; i < Mnemonic.languages.length; i++) {
    lang = Mnemonic.languages[i];
    wordlist = Mnemonic.getWordlist(lang);
    if (util.binarySearch(wordlist, word, util.strcmp) !== -1)
      return lang;
  }

  throw new Error('Could not determine language.');
};

/**
 * Retrieve the wordlist for a language.
 * @param {String} language
 * @returns {String[]}
 */

Mnemonic.getWordlist = function getWordlist(language) {
  return wordlist.get(language);
};

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

Mnemonic.prototype.toJSON = function toJSON() {
  return {
    bits: this.bits,
    language: this.language,
    entropy: this.getEntropy().toString('hex'),
    phrase: this.getPhrase(),
    passphrase: this.passphrase
  };
};

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

Mnemonic.prototype.fromJSON = function fromJSON(json) {
  assert(util.isNumber(json.bits));
  assert(typeof json.language === 'string');
  assert(typeof json.entropy === 'string');
  assert(typeof json.phrase === 'string');
  assert(typeof json.passphrase === 'string');
  assert(json.bits >= common.MIN_ENTROPY);
  assert(json.bits <= common.MAX_ENTROPY);
  assert(json.bits % 32 === 0);
  assert(json.bits / 8 === json.entropy.length / 2);

  this.bits = json.bits;
  this.language = json.language;
  this.entropy = new Buffer(json.entropy, 'hex');
  this.phrase = json.phrase;
  this.passphrase = json.passphrase;

  return this;
};

/**
 * Instantiate mnemonic from json object.
 * @param {Object} json
 * @returns {Mnemonic}
 */

Mnemonic.fromJSON = function fromJSON(json) {
  return new Mnemonic().fromJSON(json);
};

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

Mnemonic.prototype.getSize = function getSize() {
  var size = 0;
  size += 3;
  size += this.getEntropy().length;
  size += encoding.sizeVarString(this.getPhrase(), 'utf8');
  size += encoding.sizeVarString(this.passphrase, 'utf8');
  return size;
};

/**
 * Write the mnemonic to a buffer writer.
 * @params {BufferWriter} bw
 */

Mnemonic.prototype.toWriter = function toWriter(bw) {
  var lang = Mnemonic.languages.indexOf(this.language);

  assert(lang !== -1);

  bw.writeU16(this.bits);
  bw.writeU8(lang);
  bw.writeBytes(this.getEntropy());
  bw.writeVarString(this.getPhrase(), 'utf8');
  bw.writeVarString(this.passphrase, 'utf8');

  return bw;
};

/**
 * Serialize mnemonic.
 * @returns {Buffer}
 */

Mnemonic.prototype.toRaw = function toRaw(writer) {
  var size = this.getSize();
  return this.toWriter(new StaticWriter(size)).render();
};

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

Mnemonic.prototype.fromReader = function fromReader(br) {
  this.bits = br.readU16();
  this.language = Mnemonic.languages[br.readU8()];
  this.entropy = br.readBytes(this.bits / 8);
  this.phrase = br.readVarString('utf8');
  this.passphrase = br.readVarString('utf8');

  assert(this.language);
  assert(this.bits >= common.MIN_ENTROPY);
  assert(this.bits <= common.MAX_ENTROPY);
  assert(this.bits % 32 === 0);

  return this;
};

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

Mnemonic.prototype.fromRaw = function fromRaw(data) {
  return this.fromReader(new BufferReader(data));
};

/**
 * Instantiate mnemonic from buffer reader.
 * @param {BufferReader} br
 * @returns {Mnemonic}
 */

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

/**
 * Instantiate mnemonic from serialized data.
 * @param {Buffer} data
 * @returns {Mnemonic}
 */

Mnemonic.fromRaw = function fromRaw(data) {
  return new Mnemonic().fromRaw(data);
};

/**
 * Convert the mnemonic to a string.
 * @returns {String}
 */

Mnemonic.prototype.toString = function toString() {
  return this.getPhrase();
};

/**
 * Inspect the mnemonic.
 * @returns {String}
 */

Mnemonic.prototype.inspect = function inspect() {
  return '<Mnemonic: ' + this.getPhrase() + '>';
};

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

Mnemonic.isMnemonic = function isMnemonic(obj) {
  return obj
    && typeof obj.bits === 'number'
    && typeof obj.toSeed === 'function';
};

/*
 * Expose
 */

module.exports = Mnemonic;
