/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';
const Integer = require('./integer');
const utils = require('../utils');

/** @module types */
/**
 * Constructs an immutable arbitrary-precision signed decimal number.
 * A <code>BigDecimal</code> consists of an [arbitrary precision integer]{@link module:types~Integer}
 * <i>unscaled value</i> and a 32-bit integer <i>scale</i>.  If zero
 * or positive, the scale is the number of digits to the right of the
 * decimal point.  If negative, the unscaled value of the number is
 * multiplied by ten to the power of the negation of the scale.  The
 * value of the number represented by the <code>BigDecimal</code> is
 * therefore <tt>(unscaledValue &times; 10<sup>-scale</sup>)</tt>.
 * @class
 * @classdesc The <code>BigDecimal</code> class provides operations for
 * arithmetic, scale manipulation, rounding, comparison and
 * format conversion.  The {@link #toString} method provides a
 * canonical representation of a <code>BigDecimal</code>.
 * @param {Integer|Number} unscaledValue The integer part of the decimal.
 * @param {Number} scale The scale of the decimal.
 * @constructor
 */
function BigDecimal(unscaledValue, scale) {
  if (typeof unscaledValue === 'number') {
    unscaledValue = Integer.fromNumber(unscaledValue);
  }
  /**
   * @type {Integer}
   * @private
   */
  this._intVal = unscaledValue;
  /**
   * @type {Number}
   * @private
   */
  this._scale = scale;
}

/**
 * Returns the BigDecimal representation of a buffer composed of the scale (int32BE) and the unsigned value (varint BE)
 * @param {Buffer} buf
 * @returns {BigDecimal}
 */
BigDecimal.fromBuffer = function (buf) {
  const scale = buf.readInt32BE(0);
  const unscaledValue = Integer.fromBuffer(buf.slice(4));
  return new BigDecimal(unscaledValue, scale);
};

/**
 * Returns a buffer representation composed of the scale as a BE int 32 and the unsigned value as a BE varint
 * @param {BigDecimal} value
 * @returns {Buffer}
 */
BigDecimal.toBuffer = function (value) {
  const unscaledValueBuffer = Integer.toBuffer(value._intVal);
  const scaleBuffer = utils.allocBufferUnsafe(4);
  scaleBuffer.writeInt32BE(value._scale, 0);
  return Buffer.concat([scaleBuffer, unscaledValueBuffer], scaleBuffer.length + unscaledValueBuffer.length);
};

/**
 * Returns a BigDecimal representation of the string
 * @param {String} value
 * @returns {BigDecimal}
 */
BigDecimal.fromString = function (value) {
  if (!value) {
    throw new TypeError('Invalid null or undefined value');
  }
  value = value.trim();
  const scaleIndex = value.indexOf('.');
  let scale = 0;
  if (scaleIndex >= 0) {
    scale = value.length - 1 - scaleIndex;
    value = value.substr(0, scaleIndex) + value.substr(scaleIndex + 1);
  }
  return new BigDecimal(Integer.fromString(value), scale);
};

/**
 * Returns a BigDecimal representation of the Number
 * @param {Number} value
 * @returns {BigDecimal}
 */
BigDecimal.fromNumber = function (value) {
  if (isNaN(value)) {
    return new BigDecimal(Integer.ZERO, 0);
  }
  let textValue = value.toString();
  if (textValue.indexOf('e') >= 0) {
    //get until scale 20
    textValue = value.toFixed(20);
  }
  return BigDecimal.fromString(textValue);
};

/**
 * Returns true if the value of the BigDecimal instance and other are the same
 * @param {BigDecimal} other
 * @returns {Boolean}
 */
BigDecimal.prototype.equals = function (other) {
  return ((other instanceof BigDecimal) && this.compare(other) === 0);
};

BigDecimal.prototype.inspect = function () {
  return this.constructor.name + ': ' + this.toString();
};

/**
 * @param {BigDecimal} other
 * @returns {boolean}
 */
BigDecimal.prototype.notEquals = function (other) {
  return !this.equals(other);
};

/**
 * Compares this BigDecimal with the given one.
 * @param {BigDecimal} other Integer to compare against.
 * @return {number} 0 if they are the same, 1 if the this is greater, and -1
 *     if the given one is greater.
 */
BigDecimal.prototype.compare = function (other) {
  const diff = this.subtract(other);
  if (diff.isNegative()) {
    return -1;
  }
  if (diff.isZero()) {
    return 0;
  }
  return +1;
};

/**
 * Returns the difference of this and the given BigDecimal.
 * @param {BigDecimal} other The BigDecimal to subtract from this.
 * @return {!BigDecimal} The BigDecimal result.
 */
BigDecimal.prototype.subtract = function (other) {
  const first = this;
  if (first._scale === other._scale) {
    return new BigDecimal(first._intVal.subtract(other._intVal), first._scale);
  }
  let diffScale;
  let unscaledValue;
  if (first._scale < other._scale) {
    //The scale of this is lower
    diffScale = other._scale - first._scale;
    //multiple this unScaledValue to compare in the same scale
    unscaledValue = first._intVal
      .multiply(Integer.fromNumber(Math.pow(10, diffScale)))
      .subtract(other._intVal);
    return new BigDecimal(unscaledValue, other._scale);
  }
  //The scale of this is higher
  diffScale = first._scale - other._scale;
  //multiple this unScaledValue to compare in the same scale
  unscaledValue = first._intVal
    .subtract(
      other._intVal.multiply(Integer.fromNumber(Math.pow(10, diffScale))));
  return new BigDecimal(unscaledValue, first._scale);
};

/**
 * Returns the sum of this and the given <code>BigDecimal</code>.
 * @param {BigDecimal} other The BigDecimal to sum to this.
 * @return {!BigDecimal} The BigDecimal result.
 */
BigDecimal.prototype.add = function (other) {
  const first = this;
  if (first._scale === other._scale) {
    return new BigDecimal(first._intVal.add(other._intVal), first._scale);
  }
  let diffScale;
  let unscaledValue;
  if (first._scale < other._scale) {
    //The scale of this is lower
    diffScale = other._scale - first._scale;
    //multiple this unScaledValue to compare in the same scale
    unscaledValue = first._intVal
      .multiply(Integer.fromNumber(Math.pow(10, diffScale)))
      .add(other._intVal);
    return new BigDecimal(unscaledValue, other._scale);
  }
  //The scale of this is higher
  diffScale = first._scale - other._scale;
  //multiple this unScaledValue to compare in the same scale
  unscaledValue = first._intVal
    .add(
      other._intVal.multiply(Integer.fromNumber(Math.pow(10, diffScale))));
  return new BigDecimal(unscaledValue, first._scale);
};

/**
 * Returns true if the current instance is greater than the other
 * @param {BigDecimal} other
 * @returns {boolean}
 */
BigDecimal.prototype.greaterThan = function (other) {
  return this.compare(other) === 1;
};

/** @return {boolean} Whether this value is negative. */
BigDecimal.prototype.isNegative = function () {
  return this._intVal.isNegative();
};

/** @return {boolean} Whether this value is zero. */
BigDecimal.prototype.isZero = function () {
  return this._intVal.isZero();
};

/**
 * Returns the string representation of this <code>BigDecimal</code>
 * @returns {string}
 */
BigDecimal.prototype.toString = function () {
  let intString = this._intVal.toString();
  if (this._scale === 0) {
    return intString;
  }
  let signSymbol = '';
  if (intString.charAt(0) === '-') {
    signSymbol = '-';
    intString = intString.substr(1);
  }
  let separatorIndex = intString.length - this._scale;
  if (separatorIndex <= 0) {
    //add zeros at the beginning, plus an additional zero
    intString = utils.stringRepeat('0', (-separatorIndex) + 1) + intString;
    separatorIndex = intString.length - this._scale;
  }
  return signSymbol + intString.substr(0, separatorIndex) + '.' + intString.substr(separatorIndex);
};

/**
 * Returns a Number representation of this <code>BigDecimal</code>.
 * @returns {Number}
 */
BigDecimal.prototype.toNumber = function () {
  return parseFloat(this.toString());
};

/**
 * Returns the string representation.
 * Method used by the native JSON.stringify() to serialize this instance.
 */
BigDecimal.prototype.toJSON = function () {
  return this.toString();
};


module.exports = BigDecimal;