/*
 * 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
 */
cryptolib.modules.commons = cryptolib.modules.commons || {};

/** namespace commons/utils */
cryptolib.modules.commons.utils = function(box) {
  'use strict';

  box.commons = box.commons || {};
  /**
   * A module that holds utility functionalities.
   *
   * @exports commons/utils
   */
  box.commons.utils = {};

  var exceptions;
  cryptolib('commons.exceptions', function(box) {
    exceptions = box.commons.exceptions;
  });

  /**
   * Defines some useful functions for data manipulation and type conversion.
   *
   * @class
   */
  box.commons.utils.Converters = function() {};

  box.commons.utils.Converters.prototype = {
    /**
     * Base64 encodes some data.
     *
     * @function
     * @param {string}
     *            data data to Base64 encode.
     * @param {number}
     *            lineLength line length of encoding (No line breaks if not
     *            specified).
     * @returns {string} base64 encoded data.
     */
    base64Encode: function(data, lineLength) {
      if (lineLength === undefined || lineLength === null) {
        return forge.util.encode64(data);
      } else {
        return forge.util.encode64(data, lineLength);
      }
    },

    /**
     * Base64 decodes some data.
     *
     * @function
     * @param {string}
     *            dataB64 data to Base64 decode.
     * @returns {string} base64 decoded data.
     */
    base64Decode: function(dataB64) {
      return forge.util.decode64(dataB64);
    },

    /**
     * Converts a string to a collection of bytes.
     *
     * @function
     * @param {string}
     *            str string to convert to collection of bytes.
     * @returns collection of bytes from string.
     */
    bytesFromString: function(str) {
      var bytes = [];
      for (var i = 0; i < str.length; i++) {
        bytes.push(str.charCodeAt(i));
      }

      return bytes;
    },

    /**
     * Converts a collection of bytes to a string.
     *
     * @function
     * @param {string}
     *            bytes collection of bytes to convert to string.
     * @return {string} string from collection of bytes.
     */
    bytesToString: function(bytes) {
      var string = '';

      for (var i = 0; i < bytes.length; i++) {
        string += String.fromCharCode(bytes[i]);
      }

      return string;
    },

    /**
     * Converts a string to a hexadecimal string.
     *
     * @function
     * @param {string}
     *            str String to convert.
     * @returns string string in hexadecimal format.
     */
    hexFromString: function(str) {
      return forge.util.bytesToHex(str);
    },

    /**
     * Converts a hexadecimal string to a string.
     *
     * @function
     * @param {string}
     *            hexStr hexadecimal string to convert.
     * @returns {string} String from conversion.
     */
    hexToString: function(hexStr) {
      return forge.util.hexToBytes(hexStr);
    },


    /**
     * Converts a BigInteger object to a base64 string.
     *
     * @function
     * @param {object}
     *            bigInteger forge.jsbn.BigInteger to convert.
     * @returns {string} base64 string from conversion.
     */
    base64FromBigInteger: function(bigInteger) {
      var array = new Uint8Array(bigInteger.toByteArray());
      return box.forge.util.binary.base64.encode(array);
    },

    /**
     * Converts a base64 string to a BigInteger object.
     *
     * @function
     * @param {string}
     *            base64Str base64 string to convert.
     * @returns {object} forge.jsbn.BigInteger from conversion.
     */
    base64ToBigInteger: function(base64Str) {
      var buffer = this.base64Decode(base64Str);
      var hex = this.hexFromString(buffer);
      return new forge.jsbn.BigInteger(hex, 16);
    }
  };

  /**
   * Defines a set of bit operator utilities.
   *
   * @class
   */
  box.commons.utils.BitOperators = function() {};

  box.commons.utils.BitOperators.prototype = {

    /**
     * Bit-wise concatenates some data to some existing data.
     *
     * @function
     * @param {string}
     *            data1 existing data, as string.
     * @param {string}
     *            data2 data to concatenate, as string.
     * @returns {string} bit-wise concatenation of data, as string.
     */
    concatenate: function(data1, data2) {
      try {
        var converters = new box.commons.utils.Converters();
        var data1Bytes = converters.bytesFromString(data1);
        var data2Bytes = converters.bytesFromString(data2);
        var concatDataBytes = data1Bytes.concat(data2Bytes);

        return converters.bytesToString(concatDataBytes);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Data could not be bit-wise concatenated.', error);
      }
    },

    /**
     * Unpacks a data subset from a bitwise concatenation of data.
     *
     * @function
     * @param {string}
     *            concatData bit-wise concatenation of data, as string.
     * @param {number}
     *            offset offset of data subset in concatenation.
     * @param {number}
     *            size size of data subset.
     * @returns {string} data subset, as string.
     */
    extract: function(concatData, offset, size) {
      try {
        var converters = new box.commons.utils.Converters();

        var concatDataBytes = converters.bytesFromString(concatData);
        var dataSubsetBytes = concatDataBytes.slice(offset, size);

        return converters.bytesToString(dataSubsetBytes);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Data could not be bit-wise extracted.', error);
      }
    }
  };

  /**
   * Defines a set of time utilities.
   *
   * @class
   */
  box.commons.utils.TimeUtils = function() {};

  box.commons.utils.TimeUtils.prototype = {

    /**
     * Generates a time stamp in Unix time, i.e. the number of seconds that
     * have elapsed since 00:00:00 Coordinated Universal Time (UTC), 1
     * January 1970.
     *
     * @function
     * @returns {number} time stamp in seconds, as integer.
     */
    unixTimeStamp: function() {
      try {
        return Math.round(new Date().getTime() / 1000);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not generate Unix timestamp.', error);
      }
    },

    /**
     * Generates a time stamp in Unix time, i.e. the number of seconds that
     * have elapsed since 00:00:00 Coordinated Universal Time (UTC), 1
     * January 1970. The value is returned as a floating point number.
     *
     * @function
     * @returns {number} time stamp in seconds, as floating point number.
     */
    unixTimeStampFloat: function() {
      try {
        return new Date().getTime() / 1000;
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not generate Unix timestamp.', error);
      }
    }
  };

  /**
   * Defines a set of xpath utilities.
   *
   * @class
   * @param {string}
   *            path path in XPathForm notation Eg.
   * <form><name>test</name></form> to '/form/name'
   */
  box.commons.utils.XPath = function(xml, path) {
    this.path = path;
    this.node = xml;

    var pathTree = path ? path.split('/') : [];

    for (var i = 0; i < pathTree.length; i++) {
      if (pathTree[i] && this.node) {
        var found = false;

        for (var j = 0; j < this.node.childNodes.length; j++) {
          var child = this.node.childNodes[j];

          // we found the node
          if (child.nodeType === Node.ELEMENT_NODE &&
              child.tagName.toLowerCase() === pathTree[i].toLowerCase()) {
            this.node = child;
            found = true;
            break;
          }
        }

        // invalid xpath
        if (!found) {
          this.node = null;
          throw new exceptions.CryptoLibException('Invalid xpath for: ' + path);
        }
      }
    }
  };

  box.commons.utils.XPath.prototype = {
    /**
     * Gets the value of a node. For example <node>value</node> returns
     * value.
     *
     * @function
     * @returns {string} the value, as string.
     */
    getValue: function() {
      return this.node ? this.node.childNodes[0].nodeValue : '';
    },

    /**
     * Returns the value of an attribute of a node. For example <node
     * attr="attr value">node value</node> returns "attr value".
     *
     * @function
     * @param {object}
     *            attribute attribute to get the value.
     * @returns attribute value, as string.
     */
    getAttribute: function(attribute) {
      return this.node ? this.node.getAttribute(attribute) : '';
    },

    /**
     * Gets the node itself-
     *
     * @function
     * @returns {object} xml document, as a DOM object.
     */
    getXml: function() {
      return this.node;
    },

    /**
     * Gets an array of child nodes
     *
     * @function
     * @returns {object} xml document array with all children as DOM
     *          objects.
     */
    getChildren: function() {
      return this.node ? this.node.childNodes : '';
    }
  };

  /**
   * Defines a set of string utilities.
   *
   * @class
   */
  box.commons.utils.StringUtils = function() {

  };

  box.commons.utils.StringUtils.prototype = {
    /**
     * Check if a string is within another string
     *
     * @function
     * @param {string}
     *            main text
     * @param {string}
     *            substring substring to search in the main text
     * @param caseSensitive
     *            boolean, case sensitive the search
     *
     * @returns {boolean} boolean, true if found
     */
    containsSubString: function(string, substring, caseSensitive) {
      var main = string, sub = substring;

      if (!caseSensitive) {
        main = main.toLowerCase();
        sub = sub.toLowerCase();
      }

      return main.indexOf(sub) > -1;
    }
  };

  /**
   * Defines a set of parser utilities.
   *
   * @class
   */
  box.commons.utils.Parsers = function() {};

  box.commons.utils.Parsers.prototype = {

    /**
     * Converts a string to an XML document.
     *
     * @function
     * @param {string}
     *            str String to convert.
     * @returns {string} XML document, as string.
     */
    xmlFromString: function(str) {

      if (this.isIE()) {
        // Remove null terminating character if exists
        var strNoNull = this.removeNullTerminatingChar(str);

        var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM');
        xmlDoc.async = false;
        xmlDoc.loadXML(strNoNull);
        return xmlDoc;
      } else if (typeof window.DOMParser !== 'undefined') {
        return (new window.DOMParser()).parseFromString(str, 'text/xml');
      } else {
        throw new exceptions.CryptoLibException(
            'XML string could not be parsed.');
      }
    },

    /**
     * Converts an XML document to a string.
     *
     * @fuction
     * @param {string}
     *            xmlDoc xml document to convert, as string.
     * @param {boolean}
     *            removeSelfClosingTags removes self closing tags to
     *            explicit tags. Example: <tag/> to <tag></tag>
     * @returns {string} string from conversion.
     */
    xmlToString: function(xmlDoc, removeSelfClosingTags) {

      var result = this.xmlToStringIeCompatible(xmlDoc);

      if (removeSelfClosingTags) {
        result = this.xmlRemoveSelfClosingTags(result);
      }

      return result;
    },

    xmlRemoveSelfClosingTags: function(data) {

      var split = data.split('/>');
      var newXml = '';
      for (var i = 0; i < split.length - 1; i++) {
        var edsplit = split[i].split('<');
        newXml +=
            split[i] + '></' + edsplit[edsplit.length - 1].split(' ')[0] + '>';
      }

      return newXml + split[split.length - 1];
    },

    xmlToStringIeCompatible: function(xmlNode) {

      if (this.isIE()) {
        return xmlNode.xml;
      } else if (typeof window.XMLSerializer !== 'undefined') {
        return (new window.XMLSerializer()).serializeToString(xmlNode);
      } else {
        throw new exceptions.CryptoLibException(
            'Error while tring to construct XML from String.');
      }
    },

    /**
     * Removes all newline characters from some data.
     *
     * @function
     * @param {string}
     *            data data from which to remove all newline characters, as
     *            string.
     * @returns {string} data with all newline characters removed, as
     *          string.
     */
    removeNewLineChars: function(data) {
      return data.replace(/(\r\n|\n|\r)/gm, '');
    },

    /**
     * Removes all carriage return characters from some data.
     *
     * @param data
     *            Data from which to remove all carriage return characters,
     *            as string.
     * @return Data with all carriage return characters removed, as string.
     */
    removeCarriageReturnChars: function(data) {
      return data.replace(/(\r)/gm, '');
    },

    /**
     * Removes the null terminating character from some data.
     *
     * @function
     * @param {string}
     *            data Data from which to remove the null terminating
     *            character, as string.
     * @returns {string} data with null terminating character removed, as
     *          string.
     */
    removeNullTerminatingChar: function(data) {
      return data.replace(/\x00+$/, '');
    },

    /**
     * Removes the XML header from a string representing an XML.
     *
     * @param data
     *            The XML string from which to remove the XML header.
     * @return XML string with the XML header removed.
     */
    removeXmlHeaderFromString: function(data) {
      return data.replace('<?xml version="1.0" encoding="UTF-8"?>', '');
    },

    /**
     * Removes the signature node from a string representing an XML.
     *
     * @param data
     *            The XML string from which to remove the signature node.
     * @return XML string with the signature node removed.
     */
    removeXmlSignature: function(data) {
      return data.replace(/<Signature[\s\S]*?Signature>/g, '');
    },

    /**
     * Detects if the client agent is Microsoft Internet Explorer. This has
     * been tested to work with Internet Explorer 9, 10 and 11. If this
     * software is to be used on other versions, then it should be checked
     * if those versions are supported by this function and if they are not
     * then this function should be updated.
     */
    isIE: function() {
      var ua = window.navigator.userAgent;
      var msie = ua.indexOf('MSIE ');

      if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./)) {
        return true;
      } else {
        return false;
      }
    }
  };

  /**
   * Defines a set of connectors.
   *
   * @class
   */
  box.commons.utils.Connectors = function() {};

  box.commons.utils.Connectors.prototype = {

    /**
     * Posts a request message to a destination via HTTP.
     *
     * @function
     * @param {string}
     *            request request message to post, as string.
     * @param {string}
     *            url url of request destination, as string.
     * @param {boolean}
     *            isAsynch boolean indicating whether the post is
     *            asynchronous.
     * @return {string} response retrieved from request destination, as
     *         string.
     */
    postHttpRequestMessage: function(request, url, isAsynch) {
      // Check if a post destination was provided.
      if (url === '') {
        throw new exceptions.CryptoLibException(
            'No URL provided for posting HTTP request.');
      }

      // Create HTTP request object.
      var httpRequest;
      var done;
      if (window.XMLHttpRequest) {
        httpRequest = new XMLHttpRequest();
        done = XMLHttpRequest.DONE;
      } else {
        throw new exceptions.CryptoLibException(
            'Could not create HTTP request object for this browser.');
      }

      // Define post response message handling.
      var response;
      httpRequest.onreadystatechange = function() {
        if (httpRequest.readyState === done) {
          if (httpRequest.status === 200) {
            response = httpRequest.responseText;
          } else {
            var errorMsg =
                'HTTP request message POST was not successful.\n Status: ' +
                httpRequest.status + ', Message: ' + httpRequest.statusText;
            throw new exceptions.CryptoLibException(errorMsg);
          }
        }
      };

      // Open HTTP request object for synchronous post.
      httpRequest.open('POST', url, isAsynch);

      // Post HTTP request message.
      try {
        httpRequest.send(request);
      } catch (error) {
        var errorMsg =
            'Could not post HTTP request message; IP of web application might be incorrect.';
        throw new exceptions.CryptoLibException(errorMsg, error);
      }

      return response;
    }

  };

  /**
   * Defines a progress meter.
   *
   * @param {integer}
   *            progressMax Maximum amount of progress to be attained.
   * @param {integer}
   *            progressCallback Progress callback function.
   * @param {integer}
   *            progressPercentMinCheckInterval Progress percentage minimum
   *            check interval (optional) (default: 10%).
   * @class
   */
  box.commons.utils.ProgressMeter = function(
      progressMax, progressCallback, progressPercentMinCheckInterval) {
    this.progressMax = progressMax;
    this.progressCallback = progressCallback;
    this.progressPercentMinCheckInterval =
        progressPercentMinCheckInterval || 10;

    this.lastProgressPercent = 0;
  };

  box.commons.utils.ProgressMeter.prototype = {

    /**
     * Calculates the progress as a percentage of the total and provides it
     * as input to the provided callback function.
     *
     * @function
     * @param {integer}
     *            progress Present amount of progress.
     */
    update: function(progress) {
      if (typeof this.progressCallback === 'undefined') {
        return;
      }

      var progressPercent = Math.floor((progress / this.progressMax) * 100);
      progressPercent = Math.min(progressPercent, 100);

      var progressPercentChange = progressPercent - this.lastProgressPercent;

      if (progressPercentChange > 0) {
        var checkProgress =
            (progressPercentChange >= this.progressPercentMinCheckInterval) ||
            (progressPercent === 100);
        if (checkProgress) {
          this.lastProgressPercent = progressPercent;
          this.progressCallback(progressPercent);
        }
      }
    }
  };
};
