 'use strict';

// Represent Ripple amounts and currencies.
// - Numbers in hex are big-endian.

var assert = require('assert');
var extend = require('extend');
var utils = require('./utils');
var UInt160 = require('./uint160').UInt160;
var Seed = require('./seed').Seed;
var Currency = require('./currency').Currency;
var GlobalBigNumber = require('bignumber.js');

var BigNumber = GlobalBigNumber.another({
  ROUNDING_MODE: GlobalBigNumber.ROUND_HALF_UP,
  DECIMAL_PLACES: 40
});


function inverse(number) {
  return (new BigNumber(number)).toPower(-1);
}

function Amount() {
  // Json format:
  //  integer : XRP
  //  { 'value' : ..., 'currency' : ..., 'issuer' : ...}

  this._value = new BigNumber(NaN);
  this._is_native = true; // Default to XRP. Only valid if value is not NaN.
  this._currency = new Currency();
  this._issuer = new UInt160();
}

/**
 * Set strict_mode = false to disable amount range checking
 */

Amount.strict_mode = true;

var consts = {
  currency_xns: 0,
  currency_one: 1,
  xns_precision: 6,

  // bi_ prefix refers to "big integer"
  // TODO: we shouldn't expose our BigNumber library publicly
  bi_5: new BigNumber(5),
  bi_7: new BigNumber(7),
  bi_10: new BigNumber(10),
  bi_1e14: new BigNumber(1e14),
  bi_1e16: new BigNumber(1e16),
  bi_1e17: new BigNumber(1e17),
  bi_1e32: new BigNumber(1e32),
  bi_man_max_value: new BigNumber('9999999999999999'),
  bi_man_min_value: new BigNumber(1e15),
  bi_xns_max: new BigNumber(1e17),
  bi_xns_min: new BigNumber(-1e17),
  bi_xns_unit: new BigNumber(1e6),

  cMinOffset: -96,
  cMaxOffset: 80,

  // Maximum possible amount for non-XRP currencies using the maximum mantissa
  // with maximum exponent. Corresponds to hex 0xEC6386F26FC0FFFF.
  max_value: '9999999999999999e80',
  // Minimum possible amount for non-XRP currencies.
  min_value: '-1000000000000000e-96'
};

var MAX_XRP_VALUE = new BigNumber(1e11);
var MAX_IOU_VALUE = new BigNumber(consts.max_value);
var MIN_IOU_VALUE = (new BigNumber(consts.min_value)).abs();

// Add constants to Amount class
extend(Amount, consts);

// DEPRECATED: Use Amount instead, e.g. Amount.currency_xns
exports.consts = consts;

// Given '100/USD/ISSUER' return the a string with ISSUER remapped.
Amount.text_full_rewrite = function(j) {
  return Amount.from_json(j).to_text_full();
};

// Given '100/USD/ISSUER' return the json.
Amount.json_rewrite = function(j) {
  return Amount.from_json(j).to_json();
};

Amount.from_number = function(n) {
  return (new Amount()).parse_number(n);
};

Amount.from_json = function(j) {
  return (new Amount()).parse_json(j);
};

Amount.from_quality = function(quality, currency, issuer, opts) {
  return (new Amount()).parse_quality(quality, currency, issuer, opts);
};

Amount.from_human = function(j, opts) {
  return (new Amount()).parse_human(j, opts);
};

Amount.is_valid = function(j) {
  return Amount.from_json(j).is_valid();
};

Amount.is_valid_full = function(j) {
  return Amount.from_json(j).is_valid_full();
};

Amount.NaN = function() {
  var result = new Amount();
  result._value = new BigNumber(NaN); // should have no effect
  return result;                      // but let's be careful
};

// be sure that _is_native is set properly BEFORE calling _set_value
Amount.prototype._set_value = function(value, roundingMode) {
  assert(value instanceof BigNumber);
  this._value = value.isZero() && value.isNegative() ? value.negated() : value;
  this.canonicalize(roundingMode);
  this._check_limits();
};

// Returns a new value which is the absolute value of this.
Amount.prototype.abs = function() {
  return this.clone(this.is_negative());
};

Amount.prototype.add = function(addend) {
  var addendAmount = Amount.from_json(addend);

  if (!this.is_comparable(addendAmount)) {
    return new Amount(NaN);
  }

  return this._copy(this._value.plus(addendAmount._value));
};

Amount.prototype.subtract = function(subtrahend) {
  // Correctness over speed, less code has less bugs, reuse add code.
  return this.add(Amount.from_json(subtrahend).negate());
};

// XXX Diverges from cpp.
Amount.prototype.multiply = function(multiplicand) {
  var multiplicandAmount = Amount.from_json(multiplicand);
  // TODO: probably should just multiply by multiplicandAmount._value
  var multiplyBy = multiplicandAmount.is_native() ?
    multiplicandAmount._value.times(Amount.bi_xns_unit)
    : multiplicandAmount._value;
  return this._copy(this._value.times(multiplyBy));
};

Amount.prototype.divide = function(divisor) {
  var divisorAmount = Amount.from_json(divisor);
  if (!this.is_valid()) {
    throw new Error('Invalid dividend');
  }
  if (!divisorAmount.is_valid()) {
    throw new Error('Invalid divisor');
  }
  if (divisorAmount.is_zero()) {
    throw new Error('divide by zero');
  }
  // TODO: probably should just divide by divisorAmount._value
  var divideBy = divisorAmount.is_native() ?
    divisorAmount._value.times(Amount.bi_xns_unit)
    : divisorAmount._value;
  return this._copy(this._value.dividedBy(divideBy));
};

/**
 * This function calculates a ratio - such as a price - between two Amount
 * objects.
 *
 * The return value will have the same type (currency) as the numerator. This is
 * a simplification, which should be sane in most cases. For example, a USD/XRP
 * price would be rendered as USD.
 *
 * @example
 *   var price = buy_amount.ratio_human(sell_amount);
 *
 * @this {Amount} The numerator (top half) of the fraction.
 * @param {Amount} denominator The denominator (bottom half) of the fraction.
 * @param opts Options for the calculation.
 * @param opts.reference_date {Date|Number} Date based on which
 * demurrage/interest should be applied. Can be given as JavaScript Date or int
 * for Ripple epoch.
 * @return {Amount} The resulting ratio. Unit will be the same as numerator.
 */

Amount.prototype.ratio_human = function(denominator, opts) {
  opts = extend({ }, opts);

  /*eslint-disable consistent-this */
  var numerator = this;
  /*eslint-enable consistent-this */

  denominator = Amount.from_json(denominator);

  // If either operand is NaN, the result is NaN.
  if (!numerator.is_valid() || !denominator.is_valid()) {
    return new Amount(NaN);
  }

  if (denominator.is_zero()) {
    return new Amount(NaN);
  }

  // Apply interest/demurrage
  //
  // We only need to apply it to the second factor, because the currency unit of
  // the first factor will carry over into the result.
  if (opts.reference_date) {
    denominator = denominator.applyInterest(opts.reference_date);
  }

  // Special case: The denominator is a native (XRP) amount.
  //
  // In that case, it's going to be expressed as base units (1 XRP =
  // 10^xns_precision base units).
  //
  // However, the unit of the denominator is lost, so when the resulting ratio
  // is printed, the ratio is going to be too small by a factor of
  // 10^xns_precision.
  //
  // To compensate, we multiply the numerator by 10^xns_precision.
  if (denominator._is_native) {
    numerator._set_value(numerator._value.times(Amount.bi_xns_unit));
  }

  return numerator.divide(denominator);
};

/**
 * Calculate a product of two amounts.
 *
 * This function allows you to calculate a product between two amounts which
 * retains XRPs human/external interpretation (i.e. 1 XRP = 1,000,000 base
 * units).
 *
 * Intended use is to calculate something like: 10 USD * 10 XRP/USD = 100 XRP
 *
 * @example
 *   var sell_amount = buy_amount.product_human(price);
 *
 * @see Amount#ratio_human
 *
 * @param {Amount} factor The second factor of the product.
 * @param {Object} opts Options for the calculation.
 * @param {Date|Number} opts.reference_date Date based on which
 * demurrage/interest should be applied. Can be given as JavaScript Date or int
 * for Ripple epoch.
 * @return {Amount} The product. Unit will be the same as the first factor.
 */
Amount.prototype.product_human = function(factor, opts) {
  opts = opts || {};

  factor = Amount.from_json(factor);

  // If either operand is NaN, the result is NaN.
  if (!this.is_valid() || !factor.is_valid()) {
    return new Amount(NaN);
  }

  // Apply interest/demurrage
  //
  // We only need to apply it to the second factor, because the currency unit of
  // the first factor will carry over into the result.
  if (opts.reference_date) {
    factor = factor.applyInterest(opts.reference_date);
  }

  var product = this.multiply(factor);

  // Special case: The second factor is a native (XRP) amount expressed as base
  // units (1 XRP = 10^xns_precision base units).
  //
  // See also Amount#ratio_human.
  if (factor._is_native) {
    product._set_value(product._value.dividedBy(Amount.bi_xns_unit));
  }

  return product;
};

/**
 * Turn this amount into its inverse.
 *
 * @return {Amount} self
 * @private
 */
Amount.prototype._invert = function() {
  this._set_value(inverse(this._value));
  return this;
};

/**
 * Return the inverse of this amount.
 *
 * @return {Amount} New Amount object with same currency and issuer, but the
 *   inverse of the value.
 */
Amount.prototype.invert = function() {
  return this.clone()._invert();
};

/**
 * Canonicalize amount value
 *
 * Mirrors rippled's internal Amount representation
 * From https://github.com/ripple/rippled/blob/develop/src/ripple/data
 * /protocol/STAmount.h#L31-L40
 *
 * Internal form:
 * 1: If amount is zero, then value is zero and offset is -100
 * 2: Otherwise:
 *    legal offset range is -96 to +80 inclusive
 *    value range is 10^15 to (10^16 - 1) inclusive
 *    amount = value * [10 ^ offset]
 *
 * -------------------
 *
 * The amount can be epxresses as A x 10^B
 * Where:
 * - A must be an integer between 10^15 and (10^16)-1 inclusive
 * - B must be between -96 and 80 inclusive
 *
 * This results
 * - minumum: 10^15 x 10^-96 -> 10^-81 -> -1e-81
 * - maximum: (10^16)-1 x 10^80 -> 9999999999999999e80
 *
 * @returns {Amount}
 * @throws {Error} if offset exceeds legal ranges, meaning the amount value is
 * bigger than supported
 */

Amount.prototype.canonicalize = function(roundingMode) {
  if (this._is_native) {
    this._value = this._value.round(6, BigNumber.ROUND_DOWN);
  } else if (roundingMode) {
    this._value = new BigNumber(this._value.toPrecision(16, roundingMode));
  } else {
    this._value = new BigNumber(this._value.toPrecision(16));
  }
};

Amount.prototype._check_limits = function() {
  if (!Amount.strict_mode) {
    return this;
  }
  if (this._value.isNaN() || this._value.isZero()) {
    return this;
  }
  var absval = this._value.absoluteValue();
  if (this._is_native) {
    if (absval.greaterThan(MAX_XRP_VALUE)) {
      throw new Error('Exceeding max value of ' + MAX_XRP_VALUE.toString());
    }
  } else {
    if (absval.lessThan(MIN_IOU_VALUE)) {
      throw new Error('Exceeding min value of ' + MIN_IOU_VALUE.toString());
    }
    if (absval.greaterThan(MAX_IOU_VALUE)) {
      throw new Error('Exceeding max value of ' + MAX_IOU_VALUE.toString());
    }
  }
  return this;
};

Amount.prototype.clone = function(negate) {
  return this.copyTo(new Amount(), negate);
};

Amount.prototype._copy = function(value) {
  var copy = this.clone();
  copy._set_value(value);
  return copy;
};

Amount.prototype.compareTo = function(to) {
  var toAmount = Amount.from_json(to);
  if (!this.is_comparable(toAmount)) {
    return new Amount(NaN);
  }
  return this._value.comparedTo(toAmount._value);
};

// Make d a copy of this. Returns d.
// Modification of objects internally refered to is not allowed.
Amount.prototype.copyTo = function(d, negate) {
  d._value = negate ? this._value.negated() : this._value;
  d._is_native = this._is_native;
  d._currency = this._currency;
  d._issuer = this._issuer;
  return d;
};

Amount.prototype.currency = function() {
  return this._currency;
};

Amount.prototype.equals = function(d, ignore_issuer) {
  if (!(d instanceof Amount)) {
    return this.equals(Amount.from_json(d));
  }

  return this.is_valid() && d.is_valid()
         && this._is_native === d._is_native
         && this._value.equals(d._value)
         && (this._is_native || (this._currency.equals(d._currency)
              && (ignore_issuer || this._issuer.equals(d._issuer))));
};

// True if Amounts are valid and both native or non-native.
Amount.prototype.is_comparable = function(v) {
  return this.is_valid() && v.is_valid() && this._is_native === v._is_native;
};

Amount.prototype.is_native = function() {
  return this._is_native;
};

Amount.prototype.is_negative = function() {
  return this._value.isNegative();
};

Amount.prototype.is_positive = function() {
  return !this.is_zero() && !this.is_negative();
};

// Only checks the value. Not the currency and issuer.
Amount.prototype.is_valid = function() {
  return !this._value.isNaN();
};

Amount.prototype.is_valid_full = function() {
  return this.is_valid()
  && this._currency.is_valid()
  && this._issuer.is_valid();
};

Amount.prototype.is_zero = function() {
  return this._value.isZero();
};

Amount.prototype.issuer = function() {
  return this._issuer;
};

// Return a new value.
Amount.prototype.negate = function() {
  return this.clone('NEGATE');
};

/**
 * Tries to correctly interpret an amount as entered by a user.
 *
 * Examples:
 *
 *   XRP 250     => 250000000/XRP
 *   25.2 XRP    => 25200000/XRP
 *   USD 100.40  => 100.4/USD/?
 *   100         => 100000000/XRP
 *
 *
 * The regular expression below matches above cases, broken down for better
 * understanding:
 *
 * // either 3 letter alphabetic currency-code or 3 digit numeric currency-code.
 * // See ISO 4217
 * ([A-z]{3}|[0-9]{3})
 *
 * // end of string
 * $
 */

Amount.prototype.parse_human = function(j, opts) {
  opts = opts || {};

  var hex_RE = /^[a-fA-F0-9]{40}$/;
  var currency_RE = /^([a-zA-Z]{3}|[0-9]{3})$/;

  var value;
  var currency;

  var words = j.split(' ').filter(function(word) {
    return word !== '';
  });

  function isNumber(s) {
    return isFinite(s) && s !== '' && s !== null;
  }

  if (words.length === 1) {
    if (isNumber(words[0])) {
      value = words[0];
      currency = 'XRP';
    } else {
      value = words[0].slice(0, -3);
      currency = words[0].slice(-3);
      if (!(isNumber(value) && currency.match(currency_RE))) {
        return new Amount(NaN);
      }
    }
  } else if (words.length === 2) {
    if (isNumber(words[0]) && words[1].match(hex_RE)) {
      value = words[0];
      currency = words[1];
    } else if (words[0].match(currency_RE) && isNumber(words[1])) {
      value = words[1];
      currency = words[0];
    } else if (isNumber(words[0]) && words[1].match(currency_RE)) {
      value = words[0];
      currency = words[1];
    } else {
      return new Amount(NaN);
    }
  } else {
    return new Amount(NaN);
  }

  currency = currency.toUpperCase();
  this.set_currency(currency);
  this._is_native = (currency === 'XRP');
  this._set_value(new BigNumber(value));

  // Apply interest/demurrage
  if (opts.reference_date && this._currency.has_interest()) {
    var interest = this._currency.get_interest_at(opts.reference_date);
    this._set_value(this._value.dividedBy(interest.toString()));
  }

  return this;
};

Amount.prototype.parse_issuer = function(issuer) {
  this._issuer = UInt160.from_json(issuer);
  return this;
};

/**
 * Decode a price from a BookDirectory index.
 *
 * BookDirectory ledger entries each encode the offer price in their index. This
 * method can decode that information and populate an Amount object with it.
 *
 * It is possible not to provide a currency or issuer, but be aware that Amount
 * objects behave differently based on the currency, so you may get incorrect
 * results.
 *
 * Prices involving demurraging currencies are tricky, since they depend on the
 * base and counter currencies.
 *
 * @param {String} quality 8 hex bytes quality or 32 hex bytes BookDirectory
 *   index.
 * @param {Currency|String} counterCurrency currency of the resulting Amount
 *   object.
 * @param {Issuer|String} counterIssuer Issuer of the resulting Amount object.
 * @param {Object} opts Additional options
 * @param {Boolean} opts.inverse If true, return the inverse of the price
 *   encoded in the quality.
 * @param {Currency|String} opts.base_currency The other currency. This plays a
 *   role with interest-bearing or demurrage currencies. In that case the
 *   demurrage has to be applied when the quality is decoded, otherwise the
 *   price will be false.
 * @param {Date|Number} opts.reference_date Date based on which
 * demurrage/interest should be applied. Can be given as JavaScript Date or int
 * for Ripple epoch.
 * @param {Boolean} opts.xrp_as_drops Whether XRP amount should be treated as
 *   drops. When the base currency is XRP, the quality is calculated in drops.
 *   For human use however, we want to think of 1000000 drops as 1 XRP and
 *   prices as per-XRP instead of per-drop.
 * @return {Amount} self
 */
Amount.prototype.parse_quality =
function(quality, counterCurrency, counterIssuer, opts) {
  opts = opts || {};

  var baseCurrency = Currency.from_json(opts.base_currency);

  var mantissa_hex = quality.substring(quality.length - 14);
  var offset_hex = quality.substring(quality.length - 16, quality.length - 14);
  var mantissa = new BigNumber(mantissa_hex, 16);
  var offset = parseInt(offset_hex, 16) - 100;

  var value = new BigNumber(mantissa.toString() + 'e' + offset.toString());

  this._currency = Currency.from_json(counterCurrency);
  this._issuer = UInt160.from_json(counterIssuer);
  this._is_native = this._currency.is_native();

  if (this._is_native && baseCurrency.is_native()) {
    throw new Error('XRP/XRP quality is not allowed');
  }

  /*
  The quality, as stored in the last 64 bits of a directory index, is stored as
  the quotient of TakerPays/TakerGets.

  When `opts.inverse` is true we are looking at a quality used for determining a
  `bid` price and it must first be inverted, before our declared base/counter
  currencies are in line with the price.

  For example:

    quality as stored :  5 USD          /  3000000 drops
    inverted          :  3000000 drops  /          5 USD
  */
  var adjusted = opts.inverse ? inverse(value) : value;
  var nativeAdjusted = adjusted;

  if (!opts.xrp_as_drops) {
    // `In a currency exchange, the exchange rate is quoted as the units of the
    //  counter currency in terms of a single unit of a base currency`. A
    //  quality is how much taker must `pay` to get ONE `gets` unit thus:
    //    pays ~= counterCurrency
    //    gets ~= baseCurrency.
    if (this._is_native) {
      // pay:$price              drops  get:1 X
      // pay:($price / 1,000,000)  XRP  get:1 X
      nativeAdjusted = adjusted.div(Amount.bi_xns_unit);
    } else if (baseCurrency.is_valid() && baseCurrency.is_native()) {
      // pay:$price X                   get:1 drop
      // pay:($price * 1,000,000) X     get:1 XRP
      nativeAdjusted = adjusted.times(Amount.bi_xns_unit);
    }
  }

  this._set_value(nativeAdjusted);

  if (opts.reference_date && baseCurrency.is_valid()
    && baseCurrency.has_interest()) {
    var interest = baseCurrency.get_interest_at(opts.reference_date);
    this._set_value(this._value.dividedBy(interest.toString()));
  }

  return this;
};

Amount.prototype.parse_number = function(n) {
  this._is_native = false;
  this._currency = Currency.from_json(1);
  this._issuer = UInt160.from_json(1);
  this._set_value(new BigNumber(n));
  return this;
};

// <-> j
Amount.prototype.parse_json = function(j) {
  switch (typeof j) {
    case 'string':
      // .../.../... notation is not a wire format.  But allowed for easier
      // testing.
      var m = j.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);

      if (m) {
        this._currency = Currency.from_json(m[2]);
        if (m[3]) {
          this._issuer = UInt160.from_json(m[3]);
        } else {
          this._issuer = UInt160.from_json('1');
        }
        this.parse_value(m[1]);
      } else {
        this.parse_native(j);
        this._currency = Currency.from_json('0');
        this._issuer = UInt160.from_json('0');
      }
      break;

    case 'number':
      this.parse_json(String(j));
      break;

    case 'object':
      if (j === null) {
        break;
      }

      if (j instanceof Amount) {
        j.copyTo(this);
      } else if (j.hasOwnProperty('value')) {
        // Parse the passed value to sanitize and copy it.
        this._currency.parse_json(j.currency, true); // Never XRP.

        if (typeof j.issuer === 'string') {
          this._issuer.parse_json(j.issuer);
        }

        this.parse_value(j.value);
      }
      break;

    default:
      this._set_value(new BigNumber(NaN));
  }

  return this;
};

// Parse a XRP value from untrusted input.
// - integer = raw units
// - float = with precision 6
// XXX Improvements: disallow leading zeros.
Amount.prototype.parse_native = function(j) {
  if (j && typeof j === 'string' && !isNaN(j)) {
    if (j.indexOf('.') >= 0) {
      throw new Error('Native amounts must be specified in integer drops');
    }
    var value = new BigNumber(j);
    this._is_native = true;
    this._set_value(value.dividedBy(Amount.bi_xns_unit));
  } else {
    this._set_value(new BigNumber(NaN));
  }

  return this;
};

// Parse a non-native value for the json wire format.
// Requires _currency to be set!
Amount.prototype.parse_value = function(j) {
  this._is_native = false;
  this._set_value(new BigNumber(j), BigNumber.ROUND_DOWN);
  return this;
};

Amount.prototype.set_currency = function(c) {
  this._currency = Currency.from_json(c);
  this._is_native = this._currency.is_native();
  return this;
};

Amount.prototype.set_issuer = function(issuer) {
  if (issuer instanceof UInt160) {
    this._issuer = issuer;
  } else {
    this._issuer = UInt160.from_json(issuer);
  }

  return this;
};

Amount.prototype.to_number = function() {
  return Number(this.to_text());
};

// Convert only value to JSON wire format.
Amount.prototype.to_text = function() {
  if (!this.is_valid()) {
    return 'NaN';
  }

  if (this._is_native) {
    return this._value.times(Amount.bi_xns_unit).toString();
  }

  // not native
  var offset = this._value.e - 15;
  var sign = this._value.isNegative() ? '-' : '';
  var mantissa = utils.getMantissaDecimalString(this._value.absoluteValue());
  if (offset !== 0 && (offset < -25 || offset > -4)) {
    // Use e notation.
    // XXX Clamp output.
    return sign + mantissa.toString() + 'e' + offset.toString();
  }

  var val = '000000000000000000000000000'
  + mantissa.toString()
  + '00000000000000000000000';
  var pre = val.substring(0, offset + 43);
  var post = val.substring(offset + 43);
  var s_pre = pre.match(/[1-9].*$/);  // Everything but leading zeros.
  var s_post = post.match(/[1-9]0*$/); // Last non-zero plus trailing zeros.

  return sign + (s_pre ? s_pre[0] : '0')
  + (s_post ? '.' + post.substring(0, 1 + post.length - s_post[0].length) : '');
};

/**
 * Calculate present value based on currency and a reference date.
 *
 * This only affects demurraging and interest-bearing currencies.
 *
 * User should not store amount objects after the interest is applied. This is
 * intended by display functions such as toHuman().
 *
 * @param {Date|Number} referenceDate Date based on which demurrage/interest
 *   should be applied. Can be given as JavaScript Date or int for Ripple epoch.
 * @return {Amount} The amount with interest applied.
 */
Amount.prototype.applyInterest = function(referenceDate) {
  if (!this._currency.has_interest()) {
    return this;
  }
  var interest = this._currency.get_interest_at(referenceDate);
  return this._copy(this._value.times(interest.toString()));
};

/**
 * Format only value in a human-readable format.
 *
 * @example
 *   var pretty = amount.to_human({precision: 2});
 *
 * @param {Object} opts Options for formatter.
 * @param {Number} opts.precision Max. number of digits after decimal point.
 * @param {Number} opts.min_precision Min. number of digits after dec. point.
 * @param {Boolean} opts.skip_empty_fraction Don't show fraction if it is zero,
 *   even if min_precision is set.
 * @param {Number} opts.max_sig_digits Maximum number of significant digits.
 *   Will cut fractional part, but never integer part.
 * @param {Boolean|String} opts.group_sep Whether to show a separator every n
 *   digits, if a string, that value will be used as the separator. Default: ','
 * @param {Number} opts.group_width How many numbers will be grouped together,
 *   default: 3.
 * @param {Boolean|String} opts.signed Whether negative numbers will have a
 *   prefix. If String, that string will be used as the prefix. Default: '-'
 * @param {Date|Number} opts.reference_date Date based on which
 * demurrage/interest should be applied. Can be given as JavaScript Date or int
 * for Ripple epoch.
 * @return {String} amount string
 */
Amount.prototype.to_human = function(opts) {
  opts = opts || {};

  if (!this.is_valid()) {
    return 'NaN';
  }

  /* eslint-disable consistent-this */
  // Apply demurrage/interest
  var ref = this;
  /* eslint-enable consistent-this */

  if (opts.reference_date) {
    ref = this.applyInterest(opts.reference_date);
  }

  var isNegative = ref._value.isNegative();
  var valueString = ref._value.abs().toString();
  var parts = valueString.split('.');
  var int_part = parts[0];
  var fraction_part = parts.length === 2 ? parts[1] : '';

  int_part = int_part.replace(/^0*/, '');
  fraction_part = fraction_part.replace(/0*$/, '');

  if (fraction_part.length || !opts.skip_empty_fraction) {
    // Enforce the maximum number of decimal digits (precision)
    if (typeof opts.precision === 'number') {
      var precision = Math.max(0, opts.precision);
      precision = Math.min(precision, fraction_part.length);
      var rounded = Number('0.' + fraction_part).toFixed(precision);

      if (rounded < 1) {
        fraction_part = rounded.substring(2);
      } else {
        int_part = (Number(int_part) + 1).toString();
        fraction_part = '';
      }

      while (fraction_part.length < precision) {
        fraction_part = '0' + fraction_part;
      }
    }

    // Limit the number of significant digits (max_sig_digits)
    if (typeof opts.max_sig_digits === 'number') {
      // First, we count the significant digits we have.
      // A zero in the integer part does not count.
      var int_is_zero = Number(int_part) === 0;
      var digits = int_is_zero ? 0 : int_part.length;

      // Don't count leading zeros in the fractional part if the integer part is
      // zero.
      var sig_frac = int_is_zero
      ? fraction_part.replace(/^0*/, '')
      : fraction_part;
      digits += sig_frac.length;

      // Now we calculate where we are compared to the maximum
      var rounding = digits - opts.max_sig_digits;

      // If we're under the maximum we want to cut no (=0) digits
      rounding = Math.max(rounding, 0);

      // If we're over the maximum we still only want to cut digits from the
      // fractional part, not from the integer part.
      rounding = Math.min(rounding, fraction_part.length);

      // Now we cut `rounding` digits off the right.
      if (rounding > 0) {
        fraction_part = fraction_part.slice(0, -rounding);
      }
    }

    // Enforce the minimum number of decimal digits (min_precision)
    if (typeof opts.min_precision === 'number') {
      opts.min_precision = Math.max(0, opts.min_precision);
      while (fraction_part.length < opts.min_precision) {
        fraction_part += '0';
      }
    }
  }

  if (opts.group_sep !== false) {
    var sep = (typeof opts.group_sep === 'string') ? opts.group_sep : ',';
    var groups = utils.chunkString(int_part, opts.group_width || 3, true);
    int_part = groups.join(sep);
  }

  var formatted = '';
  if (isNegative && opts.signed !== false) {
    formatted += '-';
  }

  formatted += int_part.length ? int_part : '0';
  formatted += fraction_part.length ? '.' + fraction_part : '';

  return formatted;
};

Amount.prototype.to_human_full = function(opts) {
  opts = opts || {};
  var value = this.to_human(opts);
  var currency = this._currency.to_human();
  var issuer = this._issuer.to_json(opts);
  var base = value + '/' + currency;
  return this.is_native() ? base : (base + '/' + issuer);
};

Amount.prototype.to_json = function() {
  if (this._is_native) {
    return this.to_text();
  }

  var amount_json = {
    value: this.to_text(),
    currency: this._currency.has_interest() ?
    this._currency.to_hex() : this._currency.to_json()
  };

  if (this._issuer.is_valid()) {
    amount_json.issuer = this._issuer.to_json();
  }

  return amount_json;
};

Amount.prototype.to_text_full = function(opts) {
  if (!this.is_valid()) {
    return 'NaN';
  }
  return this._is_native
      ? this.to_human() + '/XRP'
      : this.to_text() + '/' + this._currency.to_json()
        + '/' + this._issuer.to_json(opts);
};

// For debugging.
Amount.prototype.not_equals_why = function(d, ignore_issuer) {
  if (typeof d === 'string') {
    return this.not_equals_why(Amount.from_json(d));
  }
  if (!(d instanceof Amount)) {
    return 'Not an Amount';
  }
  if (!this.is_valid() || !d.is_valid()) {
    return 'Invalid amount.';
  }
  if (this._is_native !== d._is_native) {
    return 'Native mismatch.';
  }

  var type = this._is_native ? 'XRP' : 'Non-XRP';
  if (!this._value.isZero() && this._value.negated().equals(d._value)) {
    return type + ' sign differs.';
  }
  if (!this._value.equals(d._value)) {
    return type + ' value differs.';
  }
  if (!this._is_native) {
    if (!this._currency.equals(d._currency)) {
      return 'Non-XRP currency differs.';
    }
    if (!ignore_issuer && !this._issuer.equals(d._issuer)) {
      return 'Non-XRP issuer differs: '
      + d._issuer.to_json()
      + '/'
      + this._issuer.to_json();
    }
  }
};

exports.Amount = Amount;

// DEPRECATED: Include the corresponding files instead.
exports.Currency = Currency;
exports.Seed = Seed;
exports.UInt160 = UInt160;

// vim:sw=2:sts=2:ts=8:et
