/*
 * 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
 */
/* jshint node:true */
'use strict';

/**
 * Input data validation utility for this module. Only intended for internal
 * use.
 */
module.exports = {
  /**
   * Checks if a value is defined.
   *
   * @function checkIsDefined
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is undefined.
   */
  checkIsDefined: function(value, label) {
    if (typeof value === 'undefined') {
      throw new TypeError(label + ' is undefined.');
    }
  },

  /**
   * Checks if a value is not null.
   *
   * @function checkIsNotNull
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is null.
   */
  checkIsNotNull: function(value, label) {
    if (value === null) {
      throw new TypeError(label + ' is null.');
    }
  },

  /**
   * Checks if a value is defined and not null.
   *
   * @function checkIsDefinedAndNotNull
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is not defined or it is null.
   */
  checkIsDefinedAndNotNull: function(value, label) {
    this.checkIsDefined(value, label);
    this.checkIsNotNull(value, label);
  },

  /**
   * Checks if a value is of an expected type.
   *
   * @function checkIsType
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {string}
   *            type The expected type of the value.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is not of the expected type.
   */
  checkIsType: function(value, type, label) {
    var typeFound = typeof value;
    if (typeFound !== type) {
      throw new TypeError(
          'Expected ' + label + ' to have type \'' + type + '\' ; Found: \'' +
          typeFound + '\'');
    }
  },

  /**
   * Checks if a value is an instance of an Object.
   *
   * @function checkIsInstanceOf
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {Object}
   *            obj The Object to check against.
   * @param {string}
   *            objName The Object name, for error handling purposes.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the object is undefined, null, or it is not an instance of
   * the Object.
   */
  checkIsInstanceOf: function(value, obj, objName, label) {
    this.checkIsDefinedAndNotNull(value, label);

    if (!(value instanceof obj)) {
      throw new TypeError(label + ' is not an instance of Object ' + objName);
    }
  },

  /**
   * Checks if a value is an object.
   *
   * @function checkIsObject
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is not an object.
   */
  checkIsObject: function(value, label) {
    if (typeof value !== 'object') {
      throw new TypeError(label + ' is not an object.');
    }
  },

  /**
   * Checks if a value is an object and has properties.
   *
   * @function checkIsObjectWithProperties
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is not an object or it has no properties.
   */
  checkIsObjectWithProperties: function(value, label) {
    this.checkIsDefinedAndNotNull(value, label);

    this.checkIsObject(value, label);

    if (!Object.getOwnPropertyNames(value).length) {
      throw new TypeError(label + ' does not have any properties.');
    }
  },

  /**
   * Checks if a value is an Array object.
   *
   * @function checkIsArray
   * @private
   * @param {Array}
   *            array The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is not an Array object.
   */
  checkIsArray: function(value, label) {
    this.checkIsDefinedAndNotNull(value, 'Array of ' + label);

    if (value.constructor !== Array) {
      throw new TypeError('Array of ' + label + ' is not of type Array.');
    }
  },

  /**
   * Checks if a value is a non-empty string.
   *
   * @function checkIsNonEmptyString
   * @private
   * @param {string}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is not a non-empty string.
   */
  checkIsNonEmptyString: function(value, label) {
    this.checkIsType(value, 'string', label);

    if (value.length === 0) {
      throw new TypeError(label + ' is empty.');
    }
  },

  /**
   * Checks if a value is a non-empty Array object.
   *
   * @function checkIsNonEmptyArray
   * @private
   * @param {Array}
   *            array The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is not a non-empty Array object.
   */
  checkIsNonEmptyArray: function(value, label) {
    this.checkIsArray(value, label);

    if (value.length < 1) {
      throw new TypeError('Array of ' + label + ' is empty.');
    }
  },

  /**
   * Checks if a value is an array of strings.
   *
   * @function checkIsStringArray
   * @private
   * @param {string[]}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If value is not an array of strings.
   */
  checkIsStringArray: function(value, label) {
    this.checkIsNonEmptyArray(value, label);

    for (var i = 0; i < value.length; i++) {
      this.checkIsType(value[i], 'string', 'Element ' + i + ' of ' + label);
    }
  },

  /**
   * Checks if a value is a valid JSON object.
   *
   * @function checkIsJsonObject
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is a valid JSON object.
   */
  checkIsJsonObject: function(value, label) {
    try {
      JSON.parse(value);
    } catch (error) {
      throw new TypeError(label + ' is not a valid JSON object.');
    }
  },

  /**
   * Checks if a value is a valid JSON string.
   *
   * @function checkIsJsonString
   * @private
   * @param {Object}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the value is a valid JSON string.
   */
  checkIsJsonString: function(value, label) {
    this.checkIsNonEmptyString(value, label);

    this.checkIsJsonObject(value, label);
  },

  /**
   * Checks the validity of a ZpGroupElement object.
   *
   * @function checkZpGroupElement
   * @private
   * @param {ZpGroupElement}
   *            element The object to check.
   * @param {string}
   *            label The error message label for the object.
   * @param {Zpsubgroup}
   *            [group] Optional parameter pertaining to a Zp subgroup. If
   *            this option is provided, the modulus and order of the element
   *            will be checked against those of the group.
   * @throws {TypeError}
   *             If the value is not a valid ZpGroupElement object.
   */
  checkZpGroupElement: function(element, label, group) {
    this.checkIsObjectWithProperties(element, label);

    if (typeof group !== 'undefined') {
      var groupP = group.p;
      var groupQ = group.q;

      var elementP = element.p;
      if (!elementP.equals(groupP)) {
        throw new TypeError(
            'Expected modulus p of ' + label +
            ' to equal modulus of Zp subgroup provided as input: ' + groupP +
            ' ; Found: ' + elementP);
      }

      var elementQ = element.q;
      if (!elementQ.equals(groupQ)) {
        throw new TypeError(
            'Expected order q of ' + label +
            ' to equal order of Zp subgroup provided as input: ' + groupQ +
            ' ; Found: ' + elementQ);
      }
    }
  },

  /**
   * Checks the validity of an Exponent object.
   *
   * @function checkExponent
   * @private
   * @param {Exponent}
   *            exponent The object to check.
   * @param {string}
   *            label The error message label for the object.
   * @param {Zpsubgroup}
   *            [q] Optional parameter pertaining to a Zp subgroup order. If
   *            this option is provided, the order of the exponent will be
   *            checked against that order.
   * @throws {TypeError}
   *             If the value is not a valid Exponent value.
   */
  checkExponent: function(exponent, label, q) {
    this.checkIsObjectWithProperties(exponent, label);

    if (typeof q !== 'undefined') {
      var exponentQ = exponent.q;
      if (!exponentQ.equals(q)) {
        throw new TypeError(
            'Expected order q of ' + label +
            ' to equal order of Zp subgroup provided as input: ' + q +
            ' ; Found: ' + exponentQ);
      }
    }
  },

  /**
   * Checks the validity of an array of ZpGroupElement objects.
   *
   * @function checkZpGroupElements
   * @private
   * @param {ZpGroupElement[]}
   *            elements The array to check.
   * @param {string}
   *            label The error message label for the array.
   * @param {Zpsubgroup}
   *            [group] Optional parameter pertaining to a Zp subgroup. If
   *            this option is provided, the modulus and order of each element
   *            will be checked against those of the group.
   * @throws {TypeError}
   *             If the array of ZpGroupElement objects is not valid.
   */
  checkZpGroupElements: function(elements, label, group) {
    this.checkIsNonEmptyArray(elements, label);

    for (var i = 0; i < elements.length; i++) {
      this.checkZpGroupElement(
          elements[i], 'element ' + i + ' of ' + label, group);
    }
  },

  /**
   * Checks the validity of an array of Exponent objects.
   *
   * @function checkExponents
   * @private
   * @param {Exponent[]}
   *            exponents The array to check.
   * @param {string}
   *            label The error message label for the array.
   * @param {Zpsubgroup}
   *            [q] Optional parameter pertaining to a Zp subgroup order. If
   *            this option is provided, the order of the exponent will be
   *            checked against that order.
   * @throws {TypeError}
   *             If the array of Exponent objects is not valid.
   */
  checkExponents: function(exponents, label, q) {
    this.checkIsNonEmptyArray(exponents, label);

    for (var i = 0; i < exponents.length; i++) {
      this.checkExponent(exponents[i], 'exponent ' + i + ' of ' + label, q);
    }
  },

  /**
   * Checks the validity of an ElGamalPublicKey object.
   *
   * @function checkElGamalPublicKey
   * @private
   * @param {ElGamalPublicKey}
   *            publicKey The object to check.
   * @param {string}
   *            label The error message label for the object.
   * @param {Zpsubgroup}
   *            [group] Optional parameter pertaining to a Zp subgroup. If
   *            this option is provided, the modulus and order of the public
   *            key will be checked against those of the group.
   * @throws {TypeError}
   *             If the ElGamalPublicKey object is not valid.
   */
  checkElGamalPublicKey: function(publicKey, label, group) {
    this.checkIsObjectWithProperties(publicKey, label);

    if (typeof group !== 'undefined') {
      var groupP = group.p;
      var groupQ = group.q;

      var publicKeyP = publicKey.group.p;
      if (!publicKeyP.equals(groupP)) {
        throw new TypeError(
            'Expected modulus p of ' + label +
            ' to equal modulus of Zp subgroup provided as input: ' + groupP +
            ' ; Found: ' + publicKeyP);
      }

      var publicKeyQ = publicKey.group.q;
      if (!publicKeyQ.equals(groupQ)) {
        throw new TypeError(
            'Expected order q of ' + label +
            ' to equal order of Zp subgroup provided as input: ' + groupQ +
            ' ; Found: ' + publicKeyQ);
      }
    }
  },

  /**
   * Checks the validity of an ElGamalPrivateKey object.
   *
   * @function checkElGamalPrivateKey
   * @private
   * @param {ElGamalPrivateKey}
   *            privateKey The object to check.
   * @param {string}
   *            label The error message label for the object.
   * @param {Zpsubgroup}
   *            [group] Optional parameter pertaining to a Zp subgroup. If
   *            this option is provided, the order of the private key will be
   *            checked against that of the group.
   * @throws {TypeError}
   *             If the ElGamalPrivateKey object is not valid.
   */
  checkElGamalPrivateKey: function(privateKey, label, group) {
    this.checkIsObjectWithProperties(privateKey, label);

    if (typeof group !== 'undefined') {
      var groupQ = group.q;

      var privateKeyQ = privateKey.group.q;
      if (!privateKeyQ.equals(groupQ)) {
        throw new TypeError(
            'Expected order q of ' + label +
            ' to equal order of Zp subgroup provided as input: ' + groupQ +
            ' ; Found: ' + privateKeyQ);
      }
    }
  },

  /**
   * Checks the validity of an ElGamalEncryptedElements object.
   *
   * @function checkElGamalEncryptedElements
   * @private
   * @param {ElGamalEncryptedElements}
   *            encryptedElements The object to check.
   * @param {string}
   *            label The error message label for the object.
   * @param {Zpsubgroup}
   *            [group] Optional parameter pertaining to a Zp subgroup. If
   *            this option is provided, the modulus and order of the value's
   *            secret and ElGamal encrypted elements will be checked against
   *            those of the group.
   * @throws {TypeError}
   *             If the ElGamalEncryptedElements object is not valid.
   */
  checkElGamalEncryptedElements: function(encryptedElements, label, group) {
    this.checkIsObjectWithProperties(encryptedElements, label);

    if (typeof group !== 'undefined') {
      var groupP = group.p;
      var groupQ = group.q;

      var gamma = encryptedElements.gamma;

      var gammaP = gamma.p;
      if (!gammaP.equals(groupP)) {
        throw new TypeError(
            'Expected modulus p of gamma element of ' + label +
            ' to equal modulus of Zp subgroup provided as input: ' + groupP +
            ' ; Found: ' + gammaP);
      }

      var gammaQ = gamma.q;
      if (!gammaQ.equals(groupQ)) {
        throw new TypeError(
            'Expected order q of gamma element of ' + label +
            ' to equal order of Zp subgroup provided as input: ' + groupQ +
            ' ; Found: ' + gammaQ);
      }
    }

    var secret = encryptedElements.secret;
    if (typeof secret !== 'undefined') {
      this.checkExponent(secret, group);
    }
  }
};
