/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
var cryptoPRNG = (function() {
  'use strict';
  return {
    _collectorsStarted: false,
    _collectors: [],
    _events: [],
    _entropy: '',
    _entropyCounter: 0,
    _maxEntropyCounter: 0,
    _collectorHashUpdater: null,
    _hashUpdaterInterval: 0,
    _fullEntropyCallback: null,
    _tools: null,
    _seedLength: 0,
    _prng: null,
    _privateKeyPem: '',
    _pinEntropy: 0,
    _usePrivateKeyEntropy: false,

    /**
     * Initializes all the round object fields.
     *
     * @function
     * @param hashUpdaterInterval
     *            the interval, in millis, that the function that replaces the
     *            current entropy content by an hash is called. By default it is
     *            initialized to 5000.
     * @param maxEntropyCounter
     *            number of maximum bits used to determine when the collectors
     *            should stop collecting entropy. By default it is initialized
     *            to 256.
     */
    _initAndStart: function(hashUpdaterInterval, fullEntropyCallback) {

      this._collectorsStarted = false;
      this._collectors = [];
      this._events = [];
      this._entropy = '';
      this._entropyCounter = 0;
      this._maxEntropyCounter = 256;

      this._collectorHashUpdater = null;
      this._hashUpdaterInterval =
          (hashUpdaterInterval ? hashUpdaterInterval : 1000);
      this._fullEntropyCallback = fullEntropyCallback;
      this._tools = cryptoPRNG.Tools.init();

      this._initCollectors();
      this._startCollectors();
    },

    /**
     * adds a new collector to the list of available connectors
     *
     * @param collector
     *            must return an object that implements the startCollector and
     *            stopCollector methods
     */
    _addCollector: function(collector) {
      this._collectors.push(collector);
    },

    /**
     * adds a new event to the list of available events
     *
     * @param type
     *            event type
     * @param listener
     *            function that will be executed
     */
    _addEvent: function(type, listener) {
      this._events.push({'type': type, 'listener': listener});
    },

    /**
     * Initializes the random generator with the web kit random collector if
     * available.
     *
     * If it is not, it considers the rest of collectors: ajax calls collector,
     * JS calls execution collector, navigator information collector, math
     * random collector, web kit random collector, mouse events collectors, key
     * events collectors, load event collector, scroll collector)
     */
    _initCollectors: function() {
      if (this._noWindow()) {
        this._addNonWindowEntropyCollectors();
        return;
      }

      var _crypto = this._getCrypto();

      if ((_crypto) && (_crypto.getRandomValues)) {
        this._addCollector(cryptoPRNG.Collectors.getWebKitRandomCollector(
            _crypto, this._maxEntropyCounter));
      } else {
        this._addNonWindowEntropyCollectors();

        // The API detects and delivers accelerometer data 50 times per
        // second
        if (window.DeviceOrientationEvent) {
          // Listen for the device orientation event and handle
          // DeviceOrientationEvent object
          this._addEvent(
              'deviceorientation',
              cryptoPRNG.Collectors.getDeviceOrientationCollector);
        } else if (window.OrientationEvent) {
          // Listen for the MozOrientation event and handle
          // OrientationData object
          this._addEvent(
              'MozOrientation',
              cryptoPRNG.Collectors.getDeviceOrientationCollector);
        }
        if (window.DeviceMotionEvent) {
          this._addEvent(
              'devicemotion', cryptoPRNG.Collectors.getDeviceMotionCollector);
        }
      }
    },

    _addNonWindowEntropyCollectors: function() {
      if (this._usePrivateKeyEntropy) {
        this._addCollector(cryptoPRNG.Collectors.getPrivateKeyCollector(
            this._privateKeyPem, this._pinEntropy));
      }
      this._addCollector(cryptoPRNG.Collectors.getAjaxCollector());
      this._addCollector(cryptoPRNG.Collectors.getJSExecutionCollector());
      this._addCollector(cryptoPRNG.Collectors.getNavigatorInfoCollector());
      this._addCollector(cryptoPRNG.Collectors.getMathRandomCollector());
      // mouse events collectors
      this._addEvent('mousemove', cryptoPRNG.Collectors.getMouseMoveCollector);
      this._addEvent(
          'mousewheel', cryptoPRNG.Collectors.getMouseWheelCollector);
      this._addEvent('mouseup', cryptoPRNG.Collectors.getMouseUpCollector);
      this._addEvent('mousedown', cryptoPRNG.Collectors.getMouseDownCollector);

      this._addEvent(
          'touchstart', cryptoPRNG.Collectors.getTouchStartCollector);
      this._addEvent('touchmove', cryptoPRNG.Collectors.getTouchMoveCollector);
      this._addEvent('touchend', cryptoPRNG.Collectors.getTouchEndCollector);

      this._addEvent(
          'gesturestart', cryptoPRNG.Collectors.getGestureStartCollector);
      this._addEvent(
          'gestureend', cryptoPRNG.Collectors.getGestureEndCollector);

      // keyboard events collectors
      this._addEvent('keyup', cryptoPRNG.Collectors.getKeyUpCollector);
      this._addEvent('keydown', cryptoPRNG.Collectors.getKeyDownCollector);
      // page events collectors
      this._addEvent('load', cryptoPRNG.Collectors.getLoadCollector);
      this._addEvent('scroll', cryptoPRNG.Collectors.getScrollCollector);
      // requests collector collectors
      this._addEvent('beforeload', cryptoPRNG.Collectors.getRequestsCollector);
    },

    _getCrypto: function() {
      return window.crypto || window.msCrypto;
    },

    _noWindow: function() {
      return (typeof window === 'undefined');
    },

    /**
     * Start the collectors
     */
    _startCollectors: function() {
      if (this._collectorsStarted) {
        return;
      }

      var i = 0;
      // start all the collectors
      while (i < this._collectors.length) {
        try {
          this._collectors[i].startCollector();
        } catch (e) {
          // do not do anything about any exception that is thrown
          // by the execution of the collectors
        }
        i++;
      }

      i = 0;
      // start all the events
      while (i < this._events.length) {
        try {
          if (window.addEventListener) {
            window.addEventListener(
                this._events[i].type, this._events[i].listener, false);
          } else if (document.attachEvent) {
            document.attachEvent(
                'on' + this._events[i].type, this._events[i].listener);
          }
        } catch (e) {
          // do not do anything about any exception that is thrown
          // by the execution of the events
        }
        i++;
      }

      this._tools._mdUpdate.start();
      this._collectorHashUpdater = setInterval(function() {
        cryptoPRNG._hashUpdater();
      }, this._hashUpdaterInterval);

      this._collectorsStarted = true;
    },

    /**
     * Stop the collectors
     */
    _stopCollectors: function() {
      if (!this._collectorsStarted) {
        return;
      }

      var i = 0;
      while (i < this._collectors.length) {
        try {
          this._collectors[i].stopCollector();
        } catch (e) {
          // do not do anything about any exception that is thrown
          // by the execution of the collectors
        }
        i++;
      }

      i = 0;
      while (i < this._events.length) {
        try {
          if (window.removeEventListener) {
            window.removeEventListener(
                this._events[i].type, this._events[i].listener, false);
          } else if (document.detachEvent) {
            document.detachEvent(
                'on' + this._events[i].type, this._events[i].listener);
          }
        } catch (e) {
          // do not do anything about any exception that is thrown
          // by the remove of the events
        }
        i++;
      }

      if (this._collectorHashUpdater) {
        clearInterval(this._collectorHashUpdater);
      }

      this._collectorsStarted = false;
    },

    /**
     * Usually this method is called from the collectors and events. It adds
     * entropy to the already collected entropy and increments the entropy
     * counter
     *
     * @param data
     *            the entropy data to be added
     * @param entropyCounter
     *            the entropy counter to be added
     */
    _collectEntropy: function(data, entropyCounter) {
      this._entropy += data;
      this._entropyCounter += (entropyCounter ? entropyCounter : 0);

      // if the entropy counter has reach the limit, update the hash and stop
      // the collectors
      if (this._entropyCounter >= this._maxEntropyCounter &&
          this._collectorsStarted) {
        this._hashUpdater();
      }
    },

    /**
     * Sets the entropy data with the hash that is created using the current
     * entropy data value. If the entropy counter has reached the max entropy
     * counter set, it stops the collectors
     *
     * @return the generated entropy hash
     */
    _hashUpdater: function() {
      var entropyHash = this._updateEntropyHash();

      if (this._entropyCounter >= this._maxEntropyCounter &&
          this._collectorsStarted) {
        entropyHash = this._stopCollectEntropy();

        var newPRNG = new cryptoPRNG.PRNG(entropyHash, this._tools);

        if (this._fullEntropyCallback) {
          this._fullEntropyCallback(newPRNG);
        } else {
          this._prng = newPRNG;
        }
      }
      return entropyHash;
    },

    _stopCollectEntropy: function() {
      // stop collecting entropy
      this._stopCollectors();
      var entropyHash = this._updateEntropyHash();
      this._entropy = '';
      this._entropyCounter = 0;

      return entropyHash;
    },

    _updateEntropyHash: function() {
      var digester = this._tools._mdUpdate;
      digester.update(this._entropy);
      var entropyHash = forge.util.bytesToHex(digester.digest().getBytes());
      this._entropy = entropyHash;
      return entropyHash;
    },

    /**
     * Set Private Key and an Entropy it adds to be used to collect Entropy. The
     * amount of randomness(entropy) depends on the pin that is used for Private
     * Key generating.
     *
     * @function
     * @param privateKey
     *            {string} private key, as string in PEM format.
     * @param pinEntropy
     *            the amount of randomness that can be extracted from the
     *            Private Key.
     */
    setDataForPrivateKeyEntropyCollector: function(privateKeyPem, pinEntropy) {
      if (privateKeyPem && (!isNaN(pinEntropy)) && (pinEntropy > 0)) {
        this._privateKeyPem = privateKeyPem;
        this._pinEntropy = pinEntropy;
        this._usePrivateKeyEntropy = true;
      }
    },

    /**
     * Initializes collectors and starts them. Set Private Key and Entropy
     * before if this information should be used to collect Entropy.
     */
    startEntropyCollection: function(entropyCollectorCallback) {
      var hashUpdaterInterval = 1000;

      this._initAndStart(hashUpdaterInterval, entropyCollectorCallback);
    },

    /**
     * In the case that the maximum amount of entropy had been reached before
     * calling this method, it does not do anything, and a false is returned.
     * Otherwise, this method creates a PRNG if the collectors have not gathered
     * the maximum amount of entropy. In addition, it stops the collectors.
     *
     * This method return a true if the maximum entropy was already reached and
     * false in other case.
     */
    stopEntropyCollectionAndCreatePRNG: function() {
      var generatedWithMaximumEntropy = true;

      if (!this._prng) {
        var entropyHash = this._stopCollectEntropy();
        this._prng = new cryptoPRNG.PRNG(entropyHash, this._tools);
        generatedWithMaximumEntropy = false;
      }

      return generatedWithMaximumEntropy;
    },

    /**
     * This method creates a PRNG from a given seed, in Hexadecimal format. This
     * method is to be used from a Worker, which has received the seed from the
     * main thread.
     *
     * @param seed
     *            A string of 64 characters. It represents a 32-byte array in
     *            Hexadecimal.
     *
     */
    createPRNGFromSeed: function(seed) {
      this._prng = new cryptoPRNG.PRNG(seed, this._tools);
    },

    /**
     * Generates a random array of bytes, which is then formatted to hexadecimal
     * This method is to be called from the main thread, and the given string is
     * to be passed to a Worker.
     */
    generateRandomSeedInHex: function(lengthSeed) {
      this._seedLength = (lengthSeed ? lengthSeed : 32);
      var seedInBytes = this._prng.generate(this._seedLength);

      return forge.util.bytesToHex(seedInBytes);
    },

    getPRNG: function() {
      if (!this._prng) {
        throw new Error('The PRNG has not been initialized yet');
      }

      return this._prng;
    },

    getEntropyCollectedAsPercentage: function() {
      if (this._entropyCounter >= this._maxEntropyCounter) {
        return 100;
      }
      return this._entropyCounter * 100 / this._maxEntropyCounter;
    }
  };
})();

cryptoPRNG.Tools = (function() {
  'use strict';
  return {
    _mdUpdate: null,
    _mdReseed: null,
    _formatKey: null,
    _formatSeed: null,
    _cipher: null,

    init: function() {
      this._mdUpdate = forge.md.sha256.create();
      this._mdReseed = forge.md.sha256.create();
      this._formatKey = function(key) {
        // convert the key into 32-bit integers
        var tmp = forge.util.createBuffer(key);
        key = [
        	tmp.getInt32(),
        	tmp.getInt32(),
        	tmp.getInt32(),
        	tmp.getInt32(),
        	tmp.getInt32(),
        	tmp.getInt32(),
        	tmp.getInt32(),
        	tmp.getInt32()        	
        ];
        // return the expanded key
        return forge.aes._expandKey(key, false);
      };
      this._formatSeed = function(seed) {
        // convert seed into 32-bit integers
        var tmp = forge.util.createBuffer(seed);
        seed = [
        	tmp.getInt32(),
        	tmp.getInt32(),
        	tmp.getInt32(),
        	tmp.getInt32()
        ];
        return seed;
      };
      this._cipher = function(key, counter) {
        var aes_output = [];
        forge.aes._updateBlock(
            this._formatKey(key), this._formatSeed('' + counter), aes_output,
            false);
        var aes_buffer = forge.util.createBuffer();
        aes_buffer.putInt32(aes_output[0]);
        aes_buffer.putInt32(aes_output[1]);
        aes_buffer.putInt32(aes_output[2]);
        aes_buffer.putInt32(aes_output[3]);
        return aes_buffer.getBytes();
      };

      return this;
    }
  };
})();

cryptoPRNG.PRNG = function(entropyHash, tools) {
  'use strict';
  this._key = '';
  this._counter = 0;
  this._tools = null;
  if (tools) {
    this._tools = tools;
  } else {
    this._tools = cryptoPRNG.Tools.init();
  }
  this._entropyHash = entropyHash;
};

cryptoPRNG.PRNG.prototype = (function() {
  'use strict';
  return {
    /**
     * Generates a random array of bytes using the gathered entropy data.
     *
     * @param count
     *            the total number of random bytes to generate
     */
    generate: function(count) {
      var keyAux;

      if (this._key === null || this._key === '') {
        this._reseed();
      }

      // buffer where to store the random bytes
      var b = forge.util.createBuffer();
      var limitReach = 1;
      while (b.length() < count) {
        b.putBytes(this._tools._cipher(this._key, this._counter));
        this._counter++;

        if (b.length >= (limitReach * Math.pow(2, 20))) {
          keyAux = this._key;
          this._key = '';
          for (var i = 0; i < 2; i++) {
            this._key += this._tools._cipher(keyAux, this._counter);
            this._counter++;
          }
          limitReach++;
        }
      }

      // do it two times to ensure a key with 256 bits
      keyAux = this._key;
      this._key = '';
      for (var j = 0; j < 2; j++) {
        this._key += this._tools._cipher(keyAux, this._counter);
        this._counter++;
      }

      return b.getBytes(count);
    },

    /**
     * Reseeds the generator
     */
    _reseed: function() {
      var digester = this._tools._mdReseed;
      digester.start();
      digester.update(this._entropyHash);
      digester.update(this._key);
      this._key = forge.util.bytesToHex(digester.digest().getBytes());
      this._counter++;
    }
  };
})();

cryptoPRNG.Collectors = (function() {
  'use strict';

  var MIN_MATH_ROUND_ENTROPY_FACTOR = 1000000;

  function _getMathRoundWithEntropy(data) {
    return Math.round(data * MIN_MATH_ROUND_ENTROPY_FACTOR);
  }

  return {
    getAjaxCollector: function() {
      return {
        startCollector: function() {
          // if jQuery is included
          if (window.jQuery !== undefined) {
            $(window)
                .ajaxStart(function() {
                  cryptoPRNG._collectEntropy((+new Date()), 1);
                })
                .ajaxComplete(function() {
                  cryptoPRNG._collectEntropy((+new Date()), 1);
                });
          }
        },

        stopCollector: function() {
          // if jQuery is included
          if (window.jQuery !== undefined) {
            $(window).unbind('ajaxStart');
            $(window).unbind('ajaxComplete');
          }
          return true;
        }
      };
    },

    getJSExecutionCollector: function() {
      return {
        _collectTimeout: null,

        startCollector: function() {
          var timer_start = (+new Date()), timer_end;

          this._collectTimeout = (function collect() {
            timer_end = (+new Date());
            var total_time = timer_end - timer_start;
            // because of browser baseline time checks we limit it
            if (total_time > 20) {
              cryptoPRNG._collectEntropy((+new Date()), 1);
            }
            timer_start = timer_end;
            return setTimeout(collect, 0);
          })();
        },

        stopCollector: function() {
          clearTimeout(this._collectTimeout);
        }
      };
    },

    getNavigatorInfoCollector: function() {
      return {
        startCollector: function() {
          if (typeof(navigator) !== 'undefined') {
            var _navString = '';
            for (var key in navigator) {
              if (typeof key !== 'undefined') {
                try {
                  if (typeof(navigator[key]) === 'string') {
                    _navString += navigator[key];
                  }
                } catch (e) {
                  // ignore any kind of exception
                }
              }
            }
            cryptoPRNG._collectEntropy(_navString, 1);
          }
        },

        stopCollector: function() {
          // just executed once, so no need to execute nothing more
          return true;
        }
      };
    },

    getMathRandomCollector: function() {
      return {
        startCollector: function() {
          cryptoPRNG._collectEntropy(Math.random(), 0);
        },

        stopCollector: function() {
          // just executed once, so no need to execute nothing more
          return true;
        }
      };
    },

    getPrivateKeyCollector: function(privateKeyPem, pinEntropy) {

      var privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
      return {
        pkCollector: true,
        startCollector: function() {
          cryptoPRNG._collectEntropy(privateKey.d, pinEntropy);
        },

        stopCollector: function() {
          // just executed once, so no need to execute nothing more
          return true;
        }
      };
    },

    getWebKitRandomCollector: function(globalCrypto, entropyQuantity) {
      return {
        startCollector: function() {
          var numPositions = entropyQuantity / 32;
          // get cryptographically strong entropy in Webkit
          var ab = new Uint32Array(numPositions);
          globalCrypto.getRandomValues(ab);
          var data = '';
          for (var i = 0; i < ab.length; i++) {
            data += '' + (ab[i]);
          }
          cryptoPRNG._collectEntropy(data, entropyQuantity);
        },

        stopCollector: function() {
          // just executed once, so no need to execute nothing more
          return true;
        }
      };
    },

    getMouseMoveCollector: function(ev) {
      // to ensure compatibility with IE 8
      ev = ev || window.event;
      cryptoPRNG._collectEntropy(
          (ev.x || ev.clientX || ev.offsetX || 0) +
              (ev.y || ev.clientY || ev.offsetY || 0) + (+new Date()),
          3);
    },

    getMouseWheelCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy((ev.offsetY || 0) + (+new Date()), 3);
    },

    getMouseDownCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy(
          (ev.x || ev.clientX || ev.offsetX || 0) +
              (ev.y || ev.clientY || ev.offsetY || 0) + (+new Date()),
          3);
    },

    getMouseUpCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy(
          (ev.x || ev.clientX || ev.offsetX || 0) +
              (ev.y || ev.clientY || ev.offsetY || 0) + (+new Date()),
          3);
    },

    getTouchStartCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy(
          (ev.touches[0].pageX || ev.clientX || 0) +
              (ev.touches[0].pageY || ev.clientY || 0) + (+new Date()),
          5);
    },

    getTouchMoveCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy(
          (ev.touches[0].pageX || ev.clientX || 0) +
              (ev.touches[0].pageY || ev.clientY || 0) + (+new Date()),
          3);
    },

    getTouchEndCollector: function() {
      cryptoPRNG._collectEntropy((+new Date()), 1);
    },

    getGestureStartCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy(
          (ev.touches[0].pageX || ev.clientX || 0) +
              (ev.touches[0].pageY || ev.clientY || 0) + (+new Date()),
          5);
    },

    getGestureEndCollector: function() {
      cryptoPRNG._collectEntropy((+new Date()), 1);
    },

    motionX: null,
    motionY: null,
    motionZ: null,

    getDeviceMotionCollector: function(ev) {
      ev = ev || window.event;
      var acceleration = ev.accelerationIncludingGravity;
      var currentX = (_getMathRoundWithEntropy(acceleration.x) || 0);
      var currentY = (_getMathRoundWithEntropy(acceleration.y) || 0);
      var currentZ = (_getMathRoundWithEntropy(acceleration.z) || 0);
      var rotation = ev.rotationRate;
      if (rotation !== null) {
        currentX += _getMathRoundWithEntropy(rotation.alpha);
        currentY += _getMathRoundWithEntropy(rotation.beta);
        currentZ += _getMathRoundWithEntropy(rotation.gamma);
      }
      // The API detects and delivers accelerometer data 50 times per
      // second
      // even if there is any event related, so this is
      // a way to control if it really changed or not
      if ((cryptoPRNG.Collectors.motionX === null) ||
          (cryptoPRNG.Collectors.motionY === null) ||
          (cryptoPRNG.Collectors.motionZ === null) ||
          (cryptoPRNG.Collectors.motionX !== currentX) ||
          (cryptoPRNG.Collectors.motionY !== currentY) ||
          (cryptoPRNG.Collectors.motionZ !== currentZ)) {
        cryptoPRNG.Collectors.motionX = currentX;
        cryptoPRNG.Collectors.motionY = currentY;
        cryptoPRNG.Collectors.motionZ = currentZ;
        cryptoPRNG._collectEntropy(
            currentX + currentY + currentZ + (+new Date()), 1);
      }
    },

    deviceOrientationX: null,
    deviceOrientationY: null,
    deviceOrientationZ: null,

    getDeviceOrientationCollector: function(ev) {
      ev = ev || window.event;
      var currentX =
          (_getMathRoundWithEntropy(ev.gamma) ||
           _getMathRoundWithEntropy(ev.x) || 0);
      var currentY =
          (_getMathRoundWithEntropy(ev.beta) ||
           _getMathRoundWithEntropy(ev.y) || 0);
      var currentZ =
          (_getMathRoundWithEntropy(ev.alpha) ||
           _getMathRoundWithEntropy(ev.z) || 0);
      // The API detects and delivers accelerometer data 50 times per
      // second
      // even if there is any event related, so this is
      // a way to control if it really changed or not
      if ((cryptoPRNG.Collectors.deviceOrientationX === null) ||
          (cryptoPRNG.Collectors.deviceOrientationY === null) ||
          (cryptoPRNG.Collectors.deviceOrientationZ === null) ||
          (cryptoPRNG.Collectors.deviceOrientationX !== currentX) ||
          (cryptoPRNG.Collectors.deviceOrientationY !== currentY) ||
          (cryptoPRNG.Collectors.deviceOrientationZ !== currentZ)) {
        cryptoPRNG.Collectors.deviceOrientationX = currentX;
        cryptoPRNG.Collectors.deviceOrientationY = currentY;
        cryptoPRNG.Collectors.deviceOrientationZ = currentZ;
        cryptoPRNG._collectEntropy(
            currentX + currentY + currentZ + (+new Date()), 1);
      }
    },

    getKeyDownCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3);
    },

    getKeyUpCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3);
    },

    getLoadCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3);
    },

    getScrollCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3);
    },

    getRequestsCollector: function(ev) {
      ev = ev || window.event;
      cryptoPRNG._collectEntropy((ev.keyCode) + (+new Date()), 3);
    }
  };
})();
