/*!
 * amount.js - amount object for bcoin
 * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var assert = require('assert');
var util = require('../utils/util');

/**
 * Represents a bitcoin amount (satoshis internally).
 * @alias module:btc.Amount
 * @constructor
 * @param {(String|Number)?} value
 * @param {String?} unit
 * @param {Boolean?} num
 * @property {Amount} value
 */

function Amount(value, unit, num) {
  if (!(this instanceof Amount))
    return new Amount(value, unit, num);

  this.value = 0;

  if (value != null)
    this.fromOptions(value, unit, num);
}

/**
 * Inject properties from options.
 * @private
 * @param {(String|Number)?} value
 * @param {String?} unit
 * @param {Boolean?} num
 * @returns {Amount}
 */

Amount.prototype.fromOptions = function fromOptions(value, unit, num) {
  if (typeof unit === 'string')
    return this.from(unit, value, num);

  if (typeof value === 'number')
    return this.fromValue(value);

  return this.fromBTC(value);
};

/**
 * Get satoshi value.
 * @returns {Amount}
 */

Amount.prototype.toValue = function toValue() {
  return this.value;
};

/**
 * Get satoshi string or value.
 * @param {Boolean?} num
 * @returns {String|Amount}
 */

Amount.prototype.toSatoshis = function toSatoshis(num) {
  if (num)
    return this.value;

  return this.value.toString(10);
};

/**
 * Get bits string or value.
 * @param {Boolean?} num
 * @returns {String|Amount}
 */

Amount.prototype.toBits = function toBits(num) {
  return Amount.serialize(this.value, 2, num);
};

/**
 * Get mbtc string or value.
 * @param {Boolean?} num
 * @returns {String|Amount}
 */

Amount.prototype.toMBTC = function toMBTC(num) {
  return Amount.serialize(this.value, 5, num);
};

/**
 * Get btc string or value.
 * @param {Boolean?} num
 * @returns {String|Amount}
 */

Amount.prototype.toBTC = function toBTC(num) {
  return Amount.serialize(this.value, 8, num);
};

/**
 * Get unit string or value.
 * @param {String} unit - Can be `sat`,
 * `ubtc`, `bits`, `mbtc`, or `btc`.
 * @param {Boolean?} num
 * @returns {String|Amount}
 */

Amount.prototype.to = function to(unit, num) {
  switch (unit) {
    case 'sat':
      return this.toSatoshis(num);
    case 'ubtc':
    case 'bits':
      return this.toBits(num);
    case 'mbtc':
      return this.toMBTC(num);
    case 'btc':
      return this.toBTC(num);
  }
  throw new Error('Unknown unit "' + unit + '".');
};

/**
 * Convert amount to bitcoin string.
 * @returns {String}
 */

Amount.prototype.toString = function toString() {
  return this.toBTC();
};

/**
 * Inject properties from value.
 * @private
 * @param {Amount} value
 * @returns {Amount}
 */

Amount.prototype.fromValue = function fromValue(value) {
  assert(util.isInt53(value), 'Value must be an int64.');
  this.value = value;
  return this;
};

/**
 * Inject properties from satoshis.
 * @private
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.prototype.fromSatoshis = function fromSatoshis(value, num) {
  this.value = Amount.parse(value, 0, num);
  return this;
};

/**
 * Inject properties from bits.
 * @private
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.prototype.fromBits = function fromBits(value, num) {
  this.value = Amount.parse(value, 2, num);
  return this;
};

/**
 * Inject properties from mbtc.
 * @private
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.prototype.fromMBTC = function fromMBTC(value, num) {
  this.value = Amount.parse(value, 5, num);
  return this;
};

/**
 * Inject properties from btc.
 * @private
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.prototype.fromBTC = function fromBTC(value, num) {
  this.value = Amount.parse(value, 8, num);
  return this;
};

/**
 * Inject properties from unit.
 * @private
 * @param {String} unit
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.prototype.from = function from(unit, value, num) {
  switch (unit) {
    case 'sat':
      return this.fromSatoshis(value, num);
    case 'ubtc':
    case 'bits':
      return this.fromBits(value, num);
    case 'mbtc':
      return this.fromMBTC(value, num);
    case 'btc':
      return this.fromBTC(value, num);
  }
  throw new Error('Unknown unit "' + unit + '".');
};

/**
 * Instantiate amount from options.
 * @param {(String|Number)?} value
 * @param {String?} unit
 * @param {Boolean?} num
 * @returns {Amount}
 */

Amount.fromOptions = function fromOptions(value, unit, num) {
  return new Amount().fromOptions(value);
};

/**
 * Instantiate amount from value.
 * @private
 * @param {Amount} value
 * @returns {Amount}
 */

Amount.fromValue = function fromValue(value) {
  return new Amount().fromValue(value);
};

/**
 * Instantiate amount from satoshis.
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.fromSatoshis = function fromSatoshis(value, num) {
  return new Amount().fromSatoshis(value, num);
};

/**
 * Instantiate amount from bits.
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.fromBits = function fromBits(value, num) {
  return new Amount().fromBits(value, num);
};

/**
 * Instantiate amount from mbtc.
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.fromMBTC = function fromMBTC(value, num) {
  return new Amount().fromMBTC(value, num);
};

/**
 * Instantiate amount from btc.
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.fromBTC = function fromBTC(value, num) {
  return new Amount().fromBTC(value, num);
};

/**
 * Instantiate amount from unit.
 * @param {String} unit
 * @param {Number|String} value
 * @param {Bolean?} num
 * @returns {Amount}
 */

Amount.from = function from(unit, value, num) {
  return new Amount().from(unit, value, num);
};

/**
 * Inspect amount.
 * @returns {String}
 */

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

/**
 * Safely convert satoshis to a BTC string.
 * This function explicitly avoids any
 * floating point arithmetic.
 * @param {Amount} value - Satoshis.
 * @returns {String} BTC string.
 */

Amount.btc = function btc(value, num) {
  if (util.isFloat(value))
    return value;

  return Amount.serialize(value, 8, num);
};

/**
 * Safely convert satoshis to a BTC string.
 * This function explicitly avoids any
 * floating point arithmetic.
 * @param {Amount} value
 * @param {Number} exp - Exponent.
 * @param {Boolean} num - Return a number.
 * @returns {String}
 */

Amount.serialize = function serialize(value, exp, num) {
  var negative = false;
  var hi, lo, result;

  assert(util.isInt(value), 'Non-satoshi value for conversion.');

  if (value < 0) {
    value = -value;
    negative = true;
  }

  value = value.toString(10);

  assert(value.length <= 16, 'Number exceeds 2^53-1.');

  while (value.length < exp + 1)
    value = '0' + value;

  hi = value.slice(0, -exp);
  lo = value.slice(-exp);

  lo = lo.replace(/0+$/, '');

  if (lo.length === 0)
    lo += '0';

  result = hi + '.' + lo;

  if (negative)
    result = '-' + result;

  if (num)
    return +result;

  return result;
};

/**
 * Unsafely convert satoshis to a BTC string.
 * @param {Amount} value
 * @param {Number} exp - Exponent.
 * @param {Boolean} num - Return a number.
 * @returns {String}
 */

Amount.serializeUnsafe = function serializeUnsafe(value, exp, num) {
  assert(util.isInt(value), 'Non-satoshi value for conversion.');

  value /= pow10(exp);
  value = value.toFixed(exp);

  if (num)
    return +value;

  if (exp !== 0) {
    value = value.replace(/0+$/, '');
    if (value[value.length - 1] === '.')
      value += '0';
  }

  return value;
};

/**
 * Safely convert a BTC string to satoshis.
 * @param {String} value - BTC
 * @returns {Amount} Satoshis.
 * @throws on parse error
 */

Amount.value = function value(value, num) {
  if (util.isInt(value))
    return value;

  return Amount.parse(value, 8, num);
};

/**
 * Safely convert a BTC string to satoshis.
 * This function explicitly avoids any
 * floating point arithmetic. It also does
 * extra validation to ensure the resulting
 * Number will be 53 bits or less.
 * @param {String} value - BTC
 * @param {Number} exp - Exponent.
 * @param {Boolean} num - Allow numbers.
 * @returns {Amount} Satoshis.
 * @throws on parse error
 */

Amount.parse = function parse(value, exp, num) {
  var negative = false;
  var mult = pow10(exp);
  var maxLo = modSafe(mult);
  var maxHi = divSafe(mult);
  var parts, hi, lo, result;

  if (num && typeof value === 'number') {
    assert(util.isNumber(value), 'Non-BTC value for conversion.');
    value = value.toString(10);
  }

  assert(util.isFloat(value), 'Non-BTC value for conversion.');

  if (value[0] === '-') {
    negative = true;
    value = value.substring(1);
  }

  parts = value.split('.');

  assert(parts.length <= 2, 'Bad decimal point.');

  hi = parts[0] || '0';
  lo = parts[1] || '0';

  hi = hi.replace(/^0+/, '');
  lo = lo.replace(/0+$/, '');

  assert(hi.length <= 16 - exp, 'Number exceeds 2^53-1.');
  assert(lo.length <= exp, 'Too many decimal places.');

  if (hi.length === 0)
    hi = '0';

  while (lo.length < exp)
    lo += '0';

  hi = parseInt(hi, 10);
  lo = parseInt(lo, 10);

  assert(hi < maxHi || (hi === maxHi && lo <= maxLo),
    'Number exceeds 2^53-1.');

  result = hi * mult + lo;

  if (negative)
    result = -result;

  return result;
};

/**
 * Unsafely convert a BTC string to satoshis.
 * @param {String} value - BTC
 * @param {Number} exp - Exponent.
 * @param {Boolean} num - Allow numbers.
 * @returns {Amount} Satoshis.
 * @throws on parse error
 */

Amount.parseUnsafe = function parseUnsafe(value, exp, num) {
  if (typeof value === 'string') {
    assert(util.isFloat(value), 'Non-BTC value for conversion.');
    value = parseFloat(value, 10);
  } else {
    assert(util.isNumber(value), 'Non-BTC value for conversion.');
    assert(num, 'Cannot parse number.');
  }

  value *= pow10(exp);

  assert(value % 1 === 0, 'Too many decimal places.');

  return value;
};

/*
 * Helpers
 */

function pow10(exp) {
  switch (exp) {
    case 0:
      return 1;
    case 1:
      return 10;
    case 2:
      return 100;
    case 3:
      return 1000;
    case 4:
      return 10000;
    case 5:
      return 100000;
    case 6:
      return 1000000;
    case 7:
      return 10000000;
    case 8:
      return 100000000;
    default:
      assert(false, 'Exponent is too large.');
      break;
  }
}

function modSafe(mod) {
  switch (mod) {
    case 1:
      return 0;
    case 10:
      return 1;
    case 100:
      return 91;
    case 1000:
      return 991;
    case 10000:
      return 991;
    case 100000:
      return 40991;
    case 1000000:
      return 740991;
    case 10000000:
      return 4740991;
    case 100000000:
      return 54740991;
    default:
      assert(false, 'Exponent is too large.');
      break;
  }
}

function divSafe(div) {
  switch (div) {
    case 1:
      return 9007199254740991;
    case 10:
      return 900719925474099;
    case 100:
      return 90071992547409;
    case 1000:
      return 9007199254740;
    case 10000:
      return 900719925474;
    case 100000:
      return 90071992547;
    case 1000000:
      return 9007199254;
    case 10000000:
      return 900719925;
    case 100000000:
      return 90071992;
    default:
      assert(false, 'Exponent is too large.');
      break;
  }
}

/*
 * Expose
 */

module.exports = Amount;
