/*
 * 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';

var forge = require('node-forge');

var BigInteger = forge.jsbn.BigInteger;

/**
 * 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 a positive number.
   *
   * @function checkIsPositiveNumber
   * @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 a positive number.
   */
  checkIsPositiveNumber: function(value, label) {
    this.checkIsType(value, 'number', label);

    if (value === 0 || value < 0) {
      throw new TypeError(label + ' must be at least 1. Found: ' + value);
    }
  },

  /**
   * 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 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 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 BigInteger object.
   *
   * @function checkIsBigInteger
   * @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 a BigInteger object.
   */
  checkIsBigInteger: function(value, label) {
    this.checkIsDefinedAndNotNull(value, label);

    if (typeof value.abs === 'undefined') {
      throw new TypeError(label + ' is not a BigInteger object.');
    }
  },

  /**
   * Checks if a value is a positive BigInteger object.
   *
   * @function checkIsPositiveBigInteger
   * @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 a positive BigInteger object.
   */
  checkIsPositiveBigInteger: function(value, label) {
    this.checkIsBigInteger(value, label);

    if (value.compareTo(BigInteger.ONE) < 0) {
      throw new TypeError(label + ' must not be less than 1; Found: ' + value);
    }
  },

  /**
   * 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 Zp subgroup modulus.
   *
   * @function checkZpSubgroupModulus
   * @private
   * @param {BigInteger}
   *            p The modulus to check.
   * @param {string}
   *            label The error message label for the modulus.
   * @throws {TypeError}
   *             If the modulus is not valid.
   */
  checkZpSubgroupModulus: function(p, label) {
    this.checkIsPositiveBigInteger(p, label);

    if (p.compareTo(new BigInteger('3')) < 0) {
      throw new TypeError(label + ' must not be less than 3; Found: ' + p);
    }
  },

  /**
   * Checks the validity of a Zp subgroup order.
   *
   * @function checkZpSubgroupOrder
   * @private
   * @param {BigInteger}
   *            q The order to check.
   * @param {string}
   *            label The error message label for the order.
   * @param {BigInteger}
   *            [p] An optional parameter pertaining to the modulus of the Zp
   *            subgroup. If this option is provided, the order will be
   *            checked against this modulus and throw an error if the check
   *            fails.
   * @throws {TypeError}
   *             If the order is not valid.
   */
  checkZpSubgroupOrder: function(q, label, p) {
    this.checkIsPositiveBigInteger(q, label);

    if (typeof p !== 'undefined') {
      if (q.compareTo(p) >= 0) {
        throw new TypeError(
            label + ' must be less than modulus p: ' + p + ' ; Found: ' + q);
      }
    }
  },

  /**
   * Checks the validity of a Zp subgroup generator.
   *
   * @function checkZpSubgroupGenerator
   * @private
   * @param {BigInteger}
   *            g The generator to check.
   * @param {string}
   *            label The error message label for the generator.
   * @param {BigInteger}
   *            [p] An optional parameter pertaining to the modulus of the Zp
   *            subgroup. If this option is provided, the generator will be
   *            checked against this modulus and throw an error if the check
   *            fails.
   * @throws {TypeError}
   *             If the generator is not valid.
   */
  checkZpSubgroupGenerator: function(g, label, p) {
    this.checkIsPositiveBigInteger(g, label);

    if (g.compareTo(new BigInteger('2')) < 0) {
      throw new TypeError(label + ' must not be less than 2; Found: ' + g);
    }

    if (typeof p !== 'undefined' &&
        g.compareTo(p) >= 0) {
        throw new TypeError(
            label + ' must be less than modulus p: ' + p + ' ; Found: ' + g);
    }
  },

  /**
   * Checks the validity of a Zp group element value.
   *
   * @function checkZpGroupElementValue
   * @private
   * @param {BigInteger}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @param {BigInteger}
   *            [p] An optional parameter pertaining to the modulus of the Zp
   *            subgroup. If this option is provided, the generator will be
   *            checked against this modulus and throw an error if the check
   *            fails.
   * @throws {TypeError}
   *             If the Zp group element value is not valid.
   */
  checkZpGroupElementValue: function(value, label, p) {
    this.checkIsPositiveBigInteger(value, label);

    if (typeof p !== 'undefined' &&
        value.compareTo(p) >= 0) {
        throw new TypeError(
            label + ' must be less than modulus p: ' + p +
            ' ; Found: ' + value);
    }
  },

  /**
   * Checks the validity of an exponent value.
   *
   * @function checkExponentValue
   * @private
   * @param {BigInteger}
   *            value The value to check.
   * @param {string}
   *            label The error message label for the value.
   * @throws {TypeError}
   *             If the exponent value is not valid.
   */
  checkExponentValue: function(value, label) {
    this.checkIsBigInteger(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 exponent 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);
    }
  }
};
