/*
 * 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 elGamal = require('scytl-elgamal');
var mathematical = require('scytl-mathematical');
var forge = require('node-forge');

module.exports = CommonTestData;

var BigInteger = forge.jsbn.BigInteger;

/**
 * Provides the common data and functions needed by the zero-knowledge proof of
 * knowledge unit tests.
 */
function CommonTestData() {
  var mathService = mathematical.newService();
  var mathRandomGenerator_ = mathService.newRandomGenerator();
  var elGamalService_ = elGamal.newService();

  var p_ = new BigInteger(
      '25701322016688441576103032148674110774925156427397637132202577741938936247112125492211659378990027134829873026348759004510642939209382852797923423233606398455768339503774439196473285206499419017249519687083918862292745452369770073017370862703937184066962152692474974647384317528679859283948201449898123774491462507974828236623365121967170401847974506359775053259024902177376974036791676038887180065668218851769610708183278395407393103098286493672450097566567641277262751905925272593655979627326843104589717100845609213331825322091528671293329804211235906640910489075537455976735341285644494144220105240375300711488393');
  var q_ = new BigInteger(
      '81259987775362040571620137377019110900790585487969528507141757273696249070213');
  var g_ = new BigInteger(
      '21303916314604998551484796420422871007468543247831789236879215369694842734099405650535148287249791144057375987697205687518340016316179892851284240278138045703633698036256611527988640313051528625117090689542380890153876064768602704006774164341226969138952495075758675629665109761968589385514561688161321002676900768077022957299407868881468752268501397255241070908793893954188678747644443826465068701078202311876709764302561277744548084228639372458598925393551439452959527949768006498598646518365505226629313181567048632516237514971091380204424481498506499338185144271546651990709723565928420753129878206710329253950578');
  var group_ = mathService.newZpSubgroup(p_, q_, g_);

  var anotherP_ = new BigInteger(
      '22115396014581083143553195792384915983375949993753365299824374182778648887213105675373952839695834312125954513956465041609718392131788741228259266178434294275575978809365653903991346366383637500069010795276889266879661859362340655085050484891149389971190945080643692669920059928787982790808824970597361825060408436004650739931286605415552480363242364319544423959620330010413847743583301091182870021747196927889763205955078275353307312408480155002017730305672359814713050632922694161739281353676187308501682728826507718002850698488786266115311952718697872338756199429262811547458969260835283688979875294699655014592833');
  var anotherQ_ = new BigInteger(
      '79783631514788897688995869254536231266441581267248557197866166084121363399063');
  var anotherG_ = new BigInteger(
      '9484337033848536557358945372517536146444081505783459082983213315915020922388822299017798461933010391712259501201285084235930755858547311934865298396110947362913733900057881561201738951725697128822802249693116611294569213895789823297341865759919045291915705783128173481781221176839434866976235031358969583527093743179257827355453820268832913547364509208164670680381449515691300440305333919098105147193087681183020489724214850151272955879869448467832934822639783688344322183540621808345771717172404141548242866493539424279296365911355186761897533654055060787119955689115570505208857288933136456026541073605147828202749');
  var anotherGroup_ =
      mathService.newZpSubgroup(anotherP_, anotherQ_, anotherG_);

  /**
   * @function getP
   * @returns {forge.jsbn.BigInteger} The modulus of a Zp subgroup.
   */
  this.getP = function() {
    return p_;
  };

  /**
   * @function getQ
   * @returns {forge.jsbn.BigInteger} The order of a Zp subgroup.
   */
  this.getQ = function() {
    return q_;
  };

  /**
   * @function getG
   * @returns {forge.jsbn.BigInteger} The generator of a Zp subgroup.
   */
  this.getG = function() {
    return g_;
  };

  /**
   * @function getGroup
   * @returns {ZpSubgroup} A Zp subgroup.
   */
  this.getGroup = function() {
    return group_;
  };

  /**
   * @function getAnotherP
   * @returns {forge.jsbn.BigInteger} The modulus of another Zp subgroup.
   */
  this.getAnotherP = function() {
    return anotherP_;
  };

  /**
   * @function getAnotherQ
   * @returns {forge.jsbn.BigInteger} The order of another Zp subgroup.
   */
  this.getAnotherQ = function() {
    return anotherQ_;
  };

  /**
   * @function getAnotherG
   * @returns {forge.jsbn.BigInteger} The generator of another Zp subgroup.
   */
  this.getAnotherG = function() {
    return anotherG_;
  };

  /**
   * @function getAnotherGroup
   * @returns {ZpSubgroup} Another Zp subgroup.
   */
  this.getAnotherGroup = function() {
    return anotherGroup_;
  };

  /**
   * Generates a random string.
   *
   * @function generateRandomString
   * @param {number}
   *            length The number of characters that are to constitute the
   *            string.
   * @returns {string} The random string.
   */
  this.generateRandomString = function(length) {
    var charset = 'abcdefghijklmnopqrstuvwxyz0123456789';

    var string = '';
    for (var i = 0; i < length; i++) {
      string += charset.charAt(Math.floor(Math.random() * charset.length));
    }

    return string;
  };

  /**
   * Generates a random integer whose value is within a specified range.
   *
   * @function generateRandomInteger
   * @param {number}
   *            min The minimum value of the integer.
   * @param {number}
   *            max The maximum value of the integer.
   * @returns {number} The random integer.
   */
  this.generateRandomInteger = function(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  };

  /**
   * Generates an ElGamal key pair.
   *
   * @function generateKeyPair
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which the public key elements belong
   *            and to which the private key exponents are associated.
   * @param {number}
   *            numElements The number of Zp group elements in the public key.
   * @returns {ElGamalKeyPair} The ElGamal key pair.
   */
  this.generateKeyPair = function(group, numElements) {
    var elements = [];
    var exponents = [];

    var exponent;
    var element;
    for (var i = 0; i < numElements; i++) {
      exponent = mathRandomGenerator_.nextExponent(group);
      exponents.push(exponent);
      element = group.generator.exponentiate(exponent);
      elements.push(element);
    }

    var publicKey = elGamalService_.newPublicKey(group, elements);
    var privateKey = elGamalService_.newPrivateKey(group, exponents);

    return elGamalService_.newKeyPair(publicKey, privateKey);
  };

  /**
   * Generates a random array of Zp group elements.
   *
   * @function generateRandomGroupElements
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which the Zp group elements belong.
   * @param {number}
   *            numElements The number of Zp group elements to generate.
   * @returns {ZpGroupElement[]} The array of Zp group elements.
   */
  this.generateRandomGroupElements = function(group, numElements) {
    var elements = [];
    for (var i = 0; i < numElements; i++) {
      elements.push(mathRandomGenerator_.nextZpGroupElement(group));
    }

    return elements;
  };

  /**
   * Exponentiates an array of Zp group base elements with an exponent.
   *
   * @function exponentiateElements
   * @param {ZpGroupElement[]}
   *            baseElements The Zp group base elements to exponentiate.
   * @param {Exponent}
   *            exponent The exponent to use for the exponentiation.
   * @return {ZpGroupElement[]} The array of exponentiated Zp group base
   *         elements.
   */
  this.exponentiateElements = function(baseElements, exponent) {
    var exponentiatedElements = [];
    for (var i = 0; i < baseElements.length; i++) {
      exponentiatedElements.push(baseElements[i].exponentiate(exponent));
    }

    return exponentiatedElements;
  };

  /**
   *
   * Generates an array of simple plaintext Zp group elements. By definition,
   * all of these elements will have the same value. The number of elements
   * requested must be an even number
   *
   * @function generateSimplePlaintext
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which the simple plaintext Zp group
   *            elements belong.
   * @param {number}
   *            numElements The number of simple plaintext Zp group elements
   *            to generate.
   * @returns {ZpGroupElement[]} The array of simple plaintext Zp group
   *          elements.
   * @throws {Error}
   *             If the number of simple plaintext Zp group elements requested
   *             was an odd number.
   */
  this.generateSimplePlaintext = function(group, numElements) {
    var simplePlaintext = [];
    if ((numElements % 2) !== 0) {
      throw new Error(
          'Simple plaintext must be generated using an even number of elements; Found ' +
          numElements);
    }

    var element = mathRandomGenerator_.nextZpGroupElement(group);
    for (var i = 0; i < numElements; i++) {
      simplePlaintext.push(element);
    }

    return simplePlaintext;
  };

  /**
   * Generates an array of non-simple plaintext Zp group elements. By
   * definition, the array is non-simple because if it contains at least 2
   * elements whose values are different.
   *
   * @function generateNonSimplePlaintext
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all of the generated Zp group
   *            elements belong.
   * @param {number}
   *            numElements The number of non-simple Zp group elements to
   *            generate.
   * @returns {ZpGroupElement[]} The array of non-simple Zp group elements.
   */
  this.generateNonSimplePlaintext = function(group, numElements) {
    var nonSimplePlaintext = [];
    var element = mathRandomGenerator_.nextZpGroupElement(group);
    nonSimplePlaintext.push(element);
    for (var i = 1; i < numElements; i++) {
      do {
        element = mathRandomGenerator_.nextZpGroupElement(group);
      } while (element.value.equals(nonSimplePlaintext[i - 1].value));
      nonSimplePlaintext.push(element);
    }

    return nonSimplePlaintext;
  };

  /**
   * Retrieves an ElGamal public subkey from an existing ElGamal public key.
   *
   * @function getSubPublicKey
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all Zp group elements of the
   *            subkey belong.
   * @param {ElGamalPublicKey}
   *            publicKey The ElGamal public from which to extract the subkey.
   * @param {number}
   *            fromIndex The index (inclusive) of the array of Zp group
   *            elements of the existing key that marks the beginning of the
   *            subkey.
   * @param {number}
   *            toIndex The index (exlusive) of the array of Zp group elements
   *            of the existing key that marks the end of the subkey.
   * @returns {ElGamalPublic} The ElGamal public subkey.
   */
  this.getSubPublicKey = function(group, publicKey, fromIndex, toIndex) {
    var subElements = [];
    var elements = publicKey.elements;
    for (var i = fromIndex; i < toIndex; i++) {
      subElements.push(elements[i]);
    }

    return elGamalService_.newPublicKey(group, subElements);
  };

  /**
   * Retrieves an ElGamalEncryptedElements object from an existing
   * ElGamalEncryptedElements object that contains a subset of the phi
   * elements of the existing object.
   *
   * @function getSubEncryptedElements
   * @param {ElGamalEncryptedElements}
   *            encryptedElements The ElGamalEncryptedElements object from
   *            which to extract the new object.
   * @param {number}
   *            fromIndex The index (inclusive) of the phi array of the
   *            existing ElGamalEncryptedElements object that marks the
   *            beginning of the new phi array.
   * @param {number}
   *            toIndex The index (exclusive) of the phi array of the
   *            ElGamalEncryptedElements object that marks the end of the
   *            new phi array.
   * @returns {ElGamalEncryptedElements} The new ElGamalEncryptedElements
   *          object that contains a subset of the phis.
   */
  this.getSubEncryptedElements = function(
      encryptedElements, fromIndex, toIndex) {
    var gamma = encryptedElements.gamma;
    var phis = encryptedElements.phis;

    var subPhis = [];
    for (var i = fromIndex; i < toIndex; i++) {
      subPhis.push(phis[i]);
    }

    return elGamalService_.newEncryptedElements(
        gamma, subPhis, encryptedElements.secret);
  };

  /**
   * Generates the ciphertext needed to generate a plaintext exponent equality
   * zero-knowledge proof of knowledge. The array of base elements provided as
   * input must have an odd number of elements.
   *
   * The procedure is the following:
   * <ol>
   * <li>Generate the plaintext to encrypt by exponentiating a subset of the
   * base elements with the ephermeral key</li>
   * <li>Construct the ElGamal public key from an equal number of remaining
   * base elements</li>
   * <li>Generate the gamma element of the ciphertext by exponentiating the
   * remaining base element with the ephemeral key</li>
   * <li>Generate the phi array of the ciphertext by multiplying each
   * plaintext element with its corresponding public key element, the latter
   * being exponentiated with the message key</li>
   * <li>The resulting ciphertext can be used in a zero knowledge proof of
   * knowledge to show that each plaintext element was generated from the same
   * ephemeral key and that each phi element was generated from the same
   * message key.</li>
   * </ol>
   *
   * @param {ZpGroupElement[]}
   *            baseElements The array of base elements.
   * @param {Exponent}
   *            ephemeralKey The ephemeral key.
   * @param {Exponent}
   *            messageKey The message key.
   * @returns {ElGamalEncryptedElements} The ciphertext.
   * @throws {Error}
   *             If the number base elements provided as input is not an odd
   *             number.
   */
  this.generatePlaintextExponentEqualityProofCiphertext = function(
      baseElements, ephemeralKey, messageKey) {
    var numBaseElements = baseElements.length;
    if ((numBaseElements % 2) === 0) {
      throw new Error(
          'Number of base elements must be an odd number for plaintext exponent equality proof generation; Found ' +
          numBaseElements);
    }

    var numPlaintextElements = ((numBaseElements - 1) / 2);

    var plaintext = [];
    for (var i = 0; i < numPlaintextElements; i++) {
      plaintext.push(baseElements[1 + i].exponentiate(ephemeralKey));
    }

    var publicKeyElements = [];
    for (var j = 0; j < numPlaintextElements; j++) {
      publicKeyElements.push(baseElements[1 + numPlaintextElements + j]);
    }

    var gamma = baseElements[0].exponentiate(ephemeralKey);

    var phis = [];
    for (var k = 0; k < numPlaintextElements; k++) {
      phis.push(
          plaintext[k].multiply(publicKeyElements[k].exponentiate(messageKey)));
    }

    return elGamalService_.newEncryptedElements(gamma, phis);
  };
}
