/*
 * 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 ElGamalPublicKey = require('./public-key');
var ElGamalEncryptedElements = require('./encrypted-elements');
var validator = require('./input-validator');

module.exports = ElGamalPrngDecrypter;

/**
 * @class ElGamalPrngDecrypter
 * @classdesc The ElGamal PRNG decrypter API. To instantiate this object, use
 *            the method {@link ElGamalCryptographyService.newPrngDecrypter}.
 * @hideconstructor
 * @param {MathematicalService}
 *            mathService The mathematical service to use.
 */
function ElGamalPrngDecrypter(mathService) {
  var mathArrayCompressor_ = mathService.newArrayCompressor();
  var mathRandomGenerator_ = mathService.newRandomGenerator();

  var publicKey_;
  var secureRandomGenerator_;

  /**
   * Initializes the ElGamal PRNG decrypter with the provided ElGamal public
   * key and secure random generator.
   *
   * @function init
   * @memberof ElGamalPrngDecrypter
   * @param {ElGamalPublicKey}
   *            publicKey The ElGamal public key with which to initialize the
   *            ElGamal PRNG decrypter.
   * @param {SecureRandomGenerator}
   *            secureRandomGenerator The secure random generator which which
   *            to initialize the ElGamal PRNG decrypter.
   * @returns {ElGamalPrngDecrypter} A reference to this object, to facilitate
   *          method chaining.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.init = function(publicKey, secureRandomGenerator) {
    checkElGamalPublicKey(
        publicKey,
        'ElGamal public key with which to initialize ElGamal PRNG decrypter');
    validator.checkIsObjectWithProperties(
        secureRandomGenerator,
        'Secure random generator with which to initialize ElGamal PRNG decrypter');

    publicKey_ = publicKey;
    secureRandomGenerator_ = secureRandomGenerator;

    return this;
  };

  /**
   * ElGamal decrypts some ElGamal encrypted Zp group elements. Before using
   * this method, the decrypter must have been initialized with an ElGamal
   * public key and a secure random generator, via the method
   * {@link ElGamalPrngDecrypter.init}.
   * <p>
   * The number of phi elements that comprise the encryption must be less than
   * or equal to the number of elements in the ElGamal public key that was
   * used to initialize the ElGamal PRNG decrypter.
   *
   * @function decrypt
   * @memberof ElGamalPrngDecrypter
   * @param {ElGamalEncryptedElements}
   *            encryptedElements The ElGamal encrypted Zp group elements.
   * @param {Object}
   *            [options] An object containing optional arguments.
   * @param {boolean}
   *            [options.confirmMembership=false] If <true>true</false>, then
   *            each of the encryption elements will be checked for membership
   *            in the Zp subgroup associated with the public key.
   * <b>WARNING:</b> This operation is computationally costly and increases
   *            linearly with the number of encrypted elements.
   * @param {boolean}
   *            [options.useShortExponent=false] If <code>true</code>, a
   *            <code>short</code> secret exponent was used for the
   *            encryption and must be regenerated for the decryption.
   * @returns {ZpGroupElement[]} The decrypted Zp group elements.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.decrypt = function(encryptedElements, options) {
    if (typeof publicKey_ === 'undefined') {
      throw new Error(
          'Could not ElGamal decrypt; PRNG decrypter has not been initialized with any ElGamal public key');
    }

    options = options || {};

    checkDecryptionData(publicKey_, encryptedElements, options);

    var phis = encryptedElements.phis;
    var group = publicKey_.group;

    var checkMembership = options.checkMembership || false;
    if (checkMembership) {
      for (var i = 0; i < phis.length; i++) {
        if (!(group.isGroupMember(phis[i]))) {
          throw new Error(
              'Found phi element with value: ' + phis[i].value +
              ' that is not member of Zp subgroup associated with public key.');
        }
      }
    }

    var compressedPublicKey = compressPublicKey(publicKey_, phis.length);
    var publicKeyElements = compressedPublicKey.elements;

    var useShortExponent = options.useShortExponent || false;
    var secret = mathRandomGenerator_.nextExponent(group, {
      secureRandomGenerator: secureRandomGenerator_,
      useShortExponent: useShortExponent
    });
    var negatedSecret = secret.negate();

    var decryptedElements = [];
    for (var j = 0; j < encryptedElements.phis.length; j++) {
      decryptedElements.push(
          publicKeyElements[j].exponentiate(negatedSecret).multiply(phis[j]));
    }

    return decryptedElements;
  };

  function checkElGamalPublicKey(publicKey, label) {
    validator.checkElGamalPublicKey(publicKey, label);
    validator.checkIsInstanceOf(
        publicKey, ElGamalPublicKey, 'ElGamalPublicKey', label);
  }

  function checkElGamalEncryptedElements(elements, label) {
    validator.checkElGamalEncryptedElements(elements, label);
    validator.checkIsInstanceOf(
        elements, ElGamalEncryptedElements, 'ElGamalEncryptedElements', label);
  }

  function checkDecryptionData(publicKey, encryptedElements, options) {
    checkElGamalEncryptedElements(
        encryptedElements, 'ElGamal encrypted elements to decrypt, using PRNG');

    var numPublicKeyElements = publicKey.elements.length;
    var numPhis = encryptedElements.phis.length;
    if (numPublicKeyElements < numPhis) {
      throw new Error(
          'Expected number of phi elements to decrypt, using PRNG, to be less than or equal to number of public key elements: ' +
          numPublicKeyElements + ' ; Found: ' + numPhis);
    }

    var confirmMembership = options.confirmMembership;
    if (typeof confirmMembership !== 'undefined') {
      validator.checkIsType(
          confirmMembership, 'boolean',
          '\"Confirm group membership\" flag for decryption, using PRNG');
    }

    var useShortExponent = options.useShortExponent;
    if (typeof useShortExponent !== 'undefined') {
      validator.checkIsType(
          useShortExponent, 'boolean',
          '\"Use shoft exponent\" flag for decryption, using PRNG');
    }
  }

  function compressPublicKey(publicKey, numPhis) {
    var elements = publicKey.elements;
    var group = publicKey.group;

    if (elements.length === numPhis) {
      return publicKey;
    } else {
      var compressedElements =
          mathArrayCompressor_.compressTrailingZpGroupElements(
              elements, numPhis);

      return new ElGamalPublicKey(group, compressedElements);
    }
  }
}
