/* eslint-disable new-cap */

/**
 * @fileoverview
 * BigInteger implementation of basic operations
 * that wraps the native BigInt library.
 * Operations are not constant time,
 * but we try and limit timing leakage where we can
 * @module biginteger/native
 * @private
 */

/**
 * @private
 */
export default class BigInteger {
  /**
   * Get a BigInteger (input must be big endian for strings and arrays)
   * @param {Number|String|Uint8Array} n - Value to convert
   * @throws {Error} on null or undefined input
   */
  constructor(n) {
    if (n === undefined) {
      throw new Error('Invalid BigInteger input');
    }

    if (n instanceof Uint8Array) {
      const bytes = n;
      const hex = new Array(bytes.length);
      for (let i = 0; i < bytes.length; i++) {
        const hexByte = bytes[i].toString(16);
        hex[i] = (bytes[i] <= 0xF) ? ('0' + hexByte) : hexByte;
      }
      this.value = BigInt('0x0' + hex.join(''));
    } else {
      this.value = BigInt(n);
    }
  }

  clone() {
    return new BigInteger(this.value);
  }

  /**
   * BigInteger increment in place
   */
  iinc() {
    this.value++;
    return this;
  }

  /**
   * BigInteger increment
   * @returns {BigInteger} this + 1.
   */
  inc() {
    return this.clone().iinc();
  }

  /**
   * BigInteger decrement in place
   */
  idec() {
    this.value--;
    return this;
  }

  /**
   * BigInteger decrement
   * @returns {BigInteger} this - 1.
   */
  dec() {
    return this.clone().idec();
  }

  /**
   * BigInteger addition in place
   * @param {BigInteger} x - Value to add
   */
  iadd(x) {
    this.value += x.value;
    return this;
  }

  /**
   * BigInteger addition
   * @param {BigInteger} x - Value to add
   * @returns {BigInteger} this + x.
   */
  add(x) {
    return this.clone().iadd(x);
  }

  /**
   * BigInteger subtraction in place
   * @param {BigInteger} x - Value to subtract
   */
  isub(x) {
    this.value -= x.value;
    return this;
  }

  /**
   * BigInteger subtraction
   * @param {BigInteger} x - Value to subtract
   * @returns {BigInteger} this - x.
   */
  sub(x) {
    return this.clone().isub(x);
  }

  /**
   * BigInteger multiplication in place
   * @param {BigInteger} x - Value to multiply
   */
  imul(x) {
    this.value *= x.value;
    return this;
  }

  /**
   * BigInteger multiplication
   * @param {BigInteger} x - Value to multiply
   * @returns {BigInteger} this * x.
   */
  mul(x) {
    return this.clone().imul(x);
  }

  /**
   * Compute value modulo m, in place
   * @param {BigInteger} m - Modulo
   */
  imod(m) {
    this.value %= m.value;
    if (this.isNegative()) {
      this.iadd(m);
    }
    return this;
  }

  /**
   * Compute value modulo m
   * @param {BigInteger} m - Modulo
   * @returns {BigInteger} this mod m.
   */
  mod(m) {
    return this.clone().imod(m);
  }

  /**
   * Compute modular exponentiation using square and multiply
   * @param {BigInteger} e - Exponent
   * @param {BigInteger} n - Modulo
   * @returns {BigInteger} this ** e mod n.
   */
  modExp(e, n) {
    if (n.isZero()) throw Error('Modulo cannot be zero');
    if (n.isOne()) return new BigInteger(0);
    if (e.isNegative()) throw Error('Unsopported negative exponent');

    let exp = e.value;
    let x = this.value;

    x %= n.value;
    let r = BigInt(1);
    while (exp > BigInt(0)) {
      const lsb = exp & BigInt(1);
      exp >>= BigInt(1); // e / 2
      // Always compute multiplication step, to reduce timing leakage
      const rx = (r * x) % n.value;
      // Update r only if lsb is 1 (odd exponent)
      r = lsb ? rx : r;
      x = (x * x) % n.value; // Square
    }
    return new BigInteger(r);
  }


  /**
   * Compute the inverse of this value modulo n
   * Note: this and and n must be relatively prime
   * @param {BigInteger} n - Modulo
   * @returns {BigInteger} x such that this*x = 1 mod n
   * @throws {Error} if the inverse does not exist
   */
  modInv(n) {
    const { gcd, x } = this._egcd(n);
    if (!gcd.isOne()) {
      throw new Error('Inverse does not exist');
    }
    return x.add(n).mod(n);
  }

  /**
   * Extended Eucleadian algorithm (http://anh.cs.luc.edu/331/notes/xgcd.pdf)
   * Given a = this and b, compute (x, y) such that ax + by = gdc(a, b)
   * @param {BigInteger} b - Second operand
   * @returns {{ gcd, x, y: BigInteger }}
   */
  _egcd(b) {
    let x = BigInt(0);
    let y = BigInt(1);
    let xPrev = BigInt(1);
    let yPrev = BigInt(0);

    let a = this.value;
    b = b.value;

    while (b !== BigInt(0)) {
      const q = a / b;
      let tmp = x;
      x = xPrev - q * x;
      xPrev = tmp;

      tmp = y;
      y = yPrev - q * y;
      yPrev = tmp;

      tmp = b;
      b = a % b;
      a = tmp;
    }

    return {
      x: new BigInteger(xPrev),
      y: new BigInteger(yPrev),
      gcd: new BigInteger(a)
    };
  }

  /**
   * Compute greatest common divisor between this and n
   * @param {BigInteger} b - Operand
   * @returns {BigInteger} gcd
   */
  gcd(b) {
    let a = this.value;
    b = b.value;
    while (b !== BigInt(0)) {
      const tmp = b;
      b = a % b;
      a = tmp;
    }
    return new BigInteger(a);
  }

  /**
   * Shift this to the left by x, in place
   * @param {BigInteger} x - Shift value
   */
  ileftShift(x) {
    this.value <<= x.value;
    return this;
  }

  /**
   * Shift this to the left by x
   * @param {BigInteger} x - Shift value
   * @returns {BigInteger} this << x.
   */
  leftShift(x) {
    return this.clone().ileftShift(x);
  }

  /**
   * Shift this to the right by x, in place
   * @param {BigInteger} x - Shift value
   */
  irightShift(x) {
    this.value >>= x.value;
    return this;
  }

  /**
   * Shift this to the right by x
   * @param {BigInteger} x - Shift value
   * @returns {BigInteger} this >> x.
   */
  rightShift(x) {
    return this.clone().irightShift(x);
  }

  /**
   * Whether this value is equal to x
   * @param {BigInteger} x
   * @returns {Boolean}
   */
  equal(x) {
    return this.value === x.value;
  }

  /**
   * Whether this value is less than x
   * @param {BigInteger} x
   * @returns {Boolean}
   */
  lt(x) {
    return this.value < x.value;
  }

  /**
   * Whether this value is less than or equal to x
   * @param {BigInteger} x
   * @returns {Boolean}
   */
  lte(x) {
    return this.value <= x.value;
  }

  /**
   * Whether this value is greater than x
   * @param {BigInteger} x
   * @returns {Boolean}
   */
  gt(x) {
    return this.value > x.value;
  }

  /**
   * Whether this value is greater than or equal to x
   * @param {BigInteger} x
   * @returns {Boolean}
   */
  gte(x) {
    return this.value >= x.value;
  }

  isZero() {
    return this.value === BigInt(0);
  }

  isOne() {
    return this.value === BigInt(1);
  }

  isNegative() {
    return this.value < BigInt(0);
  }

  isEven() {
    return !(this.value & BigInt(1));
  }

  abs() {
    const res = this.clone();
    if (this.isNegative()) {
      res.value = -res.value;
    }
    return res;
  }

  /**
   * Get this value as a string
   * @returns {String} this value.
   */
  toString() {
    return this.value.toString();
  }

  /**
   * Get this value as an exact Number (max 53 bits)
   * Fails if this value is too large
   * @returns {Number}
   */
  toNumber() {
    const number = Number(this.value);
    if (number > Number.MAX_SAFE_INTEGER) {
      // We throw and error to conform with the bn.js implementation
      throw new Error('Number can only safely store up to 53 bits');
    }
    return number;
  }

  /**
   * Get value of i-th bit
   * @param {Number} i - Bit index
   * @returns {Number} Bit value.
   */
  getBit(i) {
    const bit = (this.value >> BigInt(i)) & BigInt(1);
    return (bit === BigInt(0)) ? 0 : 1;
  }

  /**
   * Compute bit length
   * @returns {Number} Bit length.
   */
  bitLength() {
    const zero = new BigInteger(0);
    const one = new BigInteger(1);
    const negOne = new BigInteger(-1);

    // -1n >> -1n is -1n
    // 1n >> 1n is 0n
    const target = this.isNegative() ? negOne : zero;
    let bitlen = 1;
    const tmp = this.clone();
    while (!tmp.irightShift(one).equal(target)) {
      bitlen++;
    }
    return bitlen;
  }

  /**
   * Compute byte length
   * @returns {Number} Byte length.
   */
  byteLength() {
    const zero = new BigInteger(0);
    const negOne = new BigInteger(-1);

    const target = this.isNegative() ? negOne : zero;
    const eight = new BigInteger(8);
    let len = 1;
    const tmp = this.clone();
    while (!tmp.irightShift(eight).equal(target)) {
      len++;
    }
    return len;
  }

  /**
   * Get Uint8Array representation of this number
   * @param {String} endian - Endianess of output array (defaults to 'be')
   * @param {Number} length - Of output array
   * @returns {Uint8Array}
   */
  toUint8Array(endian = 'be', length) {
    // we get and parse the hex string (https://coolaj86.com/articles/convert-js-bigints-to-typedarrays/)
    // this is faster than shift+mod iterations
    let hex = this.value.toString(16);
    if (hex.length % 2 === 1) {
      hex = '0' + hex;
    }

    const rawLength = hex.length / 2;
    const bytes = new Uint8Array(length || rawLength);
    // parse hex
    const offset = length ? (length - rawLength) : 0;
    let i = 0;
    while (i < rawLength) {
      bytes[i + offset] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
      i++;
    }

    if (endian !== 'be') {
      bytes.reverse();
    }

    return bytes;
  }
}
