/*
 * 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
 */
// The global namespace pollution and other glaring violations of anything
// resembling best practices is due to the intended short-livedness of this
// implementation.

var MAX_EXPONENT_BYTES = 2;
var PROGRESS_PERCENT_MIN_CHECK_INTERVAL = 10;
var GENERATOR =
    '21303916314604998551484796420422871007468543247831789236879215369694842734099405650535148287249791144057375987697205687518340016316179892851284240278138045703633698036256611527988640313051528625117090689542380890153876064768602704006774164341226969138952495075758675629665109761968589385514561688161321002676900768077022957299407868881468752268501397255241070908793893954188678747644443826465068701078202311876709764302561277744548084228639372458598925393551439452959527949768006498598646518365505226629313181567048632516237514971091380204424481498506499338185144271546651990709723565928420753129878206710329253950578';
var P =
    '25701322016688441576103032148674110774925156427397637132202577741938936247112125492211659378990027134829873026348759004510642939209382852797923423233606398455768339503774439196473285206499419017249519687083918862292745452369770073017370862703937184066962152692474974647384317528679859283948201449898123774491462507974828236623365121967170401847974506359775053259024902177376974036791676038887180065668218851769610708183278395407393103098286493672450097566567641277262751905925272593655979627326843104589717100845609213331825322091528671293329804211235906640910489075537455976735341285644494144220105240375300711488393';
var Q =
    '81259987775362040571620137377019110900790585487969528507141757273696249070213';
var ANOTHER_GENERATOR =
    '9484337033848536557358945372517536146444081505783459082983213315915020922388822299017798461933010391712259501201285084235930755858547311934865298396110947362913733900057881561201738951725697128822802249693116611294569213895789823297341865759919045291915705783128173481781221176839434866976235031358969583527093743179257827355453820268832913547364509208164670680381449515691300440305333919098105147193087681183020489724214850151272955879869448467832934822639783688344322183540621808345771717172404141548242866493539424279296365911355186761897533654055060787119955689115570505208857288933136456026541073605147828202749';
var ANOTHER_P =
    '22115396014581083143553195792384915983375949993753365299824374182778648887213105675373952839695834312125954513956465041609718392131788741228259266178434294275575978809365653903991346366383637500069010795276889266879661859362340655085050484891149389971190945080643692669920059928787982790808824970597361825060408436004650739931286605415552480363242364319544423959620330010413847743583301091182870021747196927889763205955078275353307312408480155002017730305672359814713050632922694161739281353676187308501682728826507718002850698488786266115311952718697872338756199429262811547458969260835283688979875294699655014592833';
var ANOTHER_Q =
    '79783631514788897688995869254536231266441581267248557197866166084121363399063';

var _converters;
var _keyFactory;
var _elGamalCipherFactory;
var _g;
var _p;
var _q;
var _zpSubgroup;
var _anotherZpSubgroup;
var _progressPercentSum;
var _progressPercentLatest;
var isInitialized = false;

function initializeTestVars() {
  if (!isInitialized) {
    cryptolib(
        'commons.mathematical', 'commons.utils', 'homomorphic.keypair',
        'homomorphic.cipher', function(box) {
          _converters = new box.commons.utils.Converters();
          _keyFactory = new box.homomorphic.keypair.factory.KeyFactory();
          _elGamalCipherFactory =
              new box.homomorphic.cipher.factory.ElGamalCipherFactory();

          _g = new box.forge.jsbn.BigInteger(GENERATOR);
          _p = new box.forge.jsbn.BigInteger(P);
          _q = new box.forge.jsbn.BigInteger(Q);
          _zpSubgroup = new box.commons.mathematical.ZpSubgroup(_g, _p, _q);

          var g = new box.forge.jsbn.BigInteger(ANOTHER_GENERATOR);
          var p = new box.forge.jsbn.BigInteger(ANOTHER_P);
          var q = new box.forge.jsbn.BigInteger(ANOTHER_Q);
          _anotherZpSubgroup = new box.commons.mathematical.ZpSubgroup(g, p, q);

          initializeProgressMeter();
        });

    isInitialized = true;
  }
}

function generateRandomInteger(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function generateRandomString(length) {
  var charset = 'abcdefghijklmnopqrstuvwxyz0123456789';

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

  return string;
}

function generateRandomExponent(zpSubgroup) {
  var exponent;
  cryptolib('commons.mathematical', function(box) {
    do {
      var randomInt = new box.forge.jsbn.BigInteger(
          box.forge.random.getBytes(MAX_EXPONENT_BYTES));
      exponent =
          new box.commons.mathematical.Exponent(zpSubgroup.getQ(), randomInt);
    } while (exponent.getValue().equals(box.forge.jsbn.BigInteger.ZERO));
  });

  return exponent;
}

function generateRandomGroupElement(zpSubgroup) {
  var element = zpSubgroup.getGenerator().exponentiate(
      generateRandomExponent(zpSubgroup));
  cryptolib('commons.exceptions', function(box) {
    if (!zpSubgroup.isGroupMember(element)) {
      throw new box.commons.exceptions.CryptoLibException(
          'Randomly generated element is not a member of the required mathematical group');
    }
  });

  return element;
}

function generateExponentiatedElements(baseElements, exponent) {
  var exponentiatedElements = [];
  for (var i = 0; i < baseElements.length; i++) {
    var element = baseElements[i].exponentiate(exponent);
    exponentiatedElements.push(element);
  }

  return exponentiatedElements;
}

function generatePlaintext(numElements) {
  var plaintext = [];
  var element = generateRandomGroupElement(_zpSubgroup);
  plaintext.push(element);
  for (var i = 1; i < numElements; i++) {
    element = generateRandomGroupElement(_zpSubgroup);
    plaintext.push(element);
  }

  return plaintext;
}

function generateSimplePlaintext(numElements) {
  var plaintext = [];
  cryptolib('commons.exceptions', function(box) {
    if ((numElements % 2) !== 0) {
      throw new box.commons.exceptions.CryptoLibException(
          'Plaintext must have even number of elements for simple plaintext equality proof generation.');
    }

    // Note that all elements of a simple plaintext must have the same value.
    var element = generateRandomGroupElement(_zpSubgroup);
    for (var i = 0; i < numElements; i++) {
      plaintext.push(element);
    }
  });

  return plaintext;
}

function generateNonSimplePlaintext(numElements) {
  var plaintext = [];
  var element = generateRandomGroupElement(_zpSubgroup);
  plaintext.push(element);
  for (var i = 1; i < numElements; i++) {
    do {
      element = generateRandomGroupElement(_zpSubgroup);
    } while (
        element.getElementValue().equals(plaintext[i - 1].getElementValue()));
    plaintext.push(element);
  }

  return plaintext;
}

function generateElGamalKeyPair(numElements) {
  var elements = [];
  var exponents = [];

  var exponent = generateRandomExponent(_zpSubgroup);
  var element = _zpSubgroup.getGenerator().exponentiate(exponent);
  exponents.push(exponent);
  elements.push(element);
  for (var i = 1; i < numElements; i++) {
    exponent = generateRandomExponent(_zpSubgroup);
    element = _zpSubgroup.getGenerator().exponentiate(exponent);
    exponents.push(exponent);
    elements.push(element);
  }

  var elGamalPublicKey = _keyFactory.createPublicKey(elements, _zpSubgroup);
  var elGamalPrivateKey = _keyFactory.createPrivateKey(exponents, _zpSubgroup);

  return _keyFactory.createKeyPair(elGamalPublicKey, elGamalPrivateKey);
}

function generateCiphertext(publicKey, plaintext) {
  var encrypter = _elGamalCipherFactory.createEncrypter(publicKey);

  return encrypter.encryptGroupElements(plaintext);
}

function generateExponentCiphertext(
    plaintext, primaryWitness, secondaryWitness) {
  var ciphertext;
  cryptolib('commons.exceptions', 'homomorphic.cipher', function(box) {
    var plaintextLength = plaintext.length;
    if ((plaintextLength % 2) === 0) {
      throw new box.commons.exceptions.CryptoLibException(
          'Plaintext must have odd number of elements for plaintext exponent equality proof generation.');
    }

    var numCiphertextElements = ((plaintextLength - 1) / 2) + 1;

    var exponents = [];
    exponents.push(primaryWitness);
    exponents.push(secondaryWitness);

    var gamma = plaintext[0].exponentiate(exponents[0]);

    var phis = [];
    for (var i = 0; i < (numCiphertextElements - 1); i++) {
      var phi = plaintext[i + 1].exponentiate(exponents[0]);
      phi = phi.multiply(
          plaintext[i + numCiphertextElements].exponentiate(exponents[1]));
      phis.push(phi);
    }

    ciphertext =
        new box.homomorphic.cipher.ElGamalEncrypterValues(null, gamma, phis);
  });

  return ciphertext;
}

function getSubElGamalPublicKey(publicKey, fromIndex, toIndex) {
  var elements = publicKey.getGroupElementsArray();
  var subElements = [];

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

  return _keyFactory.createPublicKey(subElements, _zpSubgroup);
}

function getSubCiphertext(encrypterValues, fromIndex, toIndex) {
  var subCiphertext;
  cryptolib('homomorphic.cipher', function(box) {
    var exponent = encrypterValues.getR();
    var gamma = encrypterValues.getGamma();
    var phis = encrypterValues.getPhis();
    var subPhis = [];
    for (var i = fromIndex; i < toIndex; i++) {
      subPhis.push(phis[i]);
    }

    subCiphertext = new box.homomorphic.cipher.ElGamalEncrypterValues(
        exponent, gamma, subPhis);
  });

  return subCiphertext;
}

function initializeProgressMeter() {
  _progressPercentSum = 0;
  _progressPercentLatest = 0;
}

function progressCallback(progressPercent) {
  _progressPercentSum += progressPercent;
  _progressPercentLatest = progressPercent;
};

function getProgressPercentSum(numChecks, minCheckInterval) {
  var percent = 0;
  var lastPercent = 0;
  var percentChange = 0;
  var incrementPercentSum = false;
  var percentSum = 0;

  for (var i = 0; i < numChecks; i++) {
    percent = Math.floor(((i + 1) / numChecks) * 100);
    percent = Math.min(percent, 100);

    percentChange = percent - lastPercent;
    if (percentChange > 0) {
      incrementPercentSum =
          (percentChange >= minCheckInterval) || (percent === 100);
      if (incrementPercentSum) {
        lastPercent = percent;
        percentSum += percent;
      }
    }
  }

  return percentSum;
}
