/*
 * 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 ZeroKnowledgeProof = require('./proof.js');
var ZeroKnowledgeProofPreComputation = require('./pre-computation.js');
var HashGenerator = require('./hash-generator');
var codec = require('scytl-codec');

module.exports = ZeroKnowledgeProofProver;

/**
 * @class ZeroKnowledgeProofProver
 * @classdesc Encapsulates a zero-knowledge proof of knowledge prover that uses
 *            the PHI function defined in Maurer's unified framework.
 * @private
 * @param {MessageDigestService}
 *            messageDigestService The message digest service to use.
 * @param {MathematicalService}
 *            mathService The mathematical service to use.
 */
function ZeroKnowledgeProofProver(messageDigestService, mathService) {
  var mathRandomGenerator_ = mathService.newRandomGenerator();

  /**
   * Generates a zero-knowledge proof of knowledge.
   *
   * @function prove
   * @memberof ZeroKnowledgeProofProver
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof generation are associated or
   *            belong, respectively.
   * @param {Exponent[]}
   *            privateValues The private values used to generate the proof.
   * @param {ZpGroupElement[]}
   *            publicValues The public values used to generate the proof.
   * @param {Uint8Array|string}
   *            data Auxiliary data.
   * @param {ZeroKnowledgeProofPreComputation}
   *            preComputation The zero-knowledge proof of knowledge
   *            pre-computation.
   * @param {ZpGroupElement[]}
   *            [elements] A parameter to be used only for the generation of
   *            OR zero-knowledge proofs of knowledge. It consists of the
   *            possible Zp group elements used to generate the ciphertext.
   * @param {number}
   *            [index] A parameter that is only to be used if the previous
   *            parameter was used. It is the secret index of the Zp group
   *            element that was used to generate the ciphertext of an OR
   *            zero-knowledge proof of knowledge.
   * @returns {ZeroKnowledgeProof} The generated zero-knowledge proof of
   *          knowledge.
   */
  this.prove = function(
      group, privateValues, publicValues, data, preComputation, elements,
      index) {
    if (typeof elements !== 'undefined') {
      preComputation = updatePreComputation(
          group, preComputation, publicValues, elements, index);
    }

    var hash =
        generateHash(group, publicValues, preComputation.phiOutputs, data);

    var proofValues = generateProofValues(
        group, privateValues, hash, preComputation.exponents, index);

    return new ZeroKnowledgeProof(hash, proofValues);
  };

  /**
   * Pre-computes a Schnorr zero-knowledge proof of knowledge. IMPORTANT: The
   * same pre-computed values must not be used twice.
   *
   * @function preComputeSchnorrProof
   * @memberof ZeroKnowledgeProofProver
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the pre-computation are associated or
   *            belong, respectively.
   * @param {PhiFunction}
   *            phiFunction The PHI function used for the pre-computation.
   * @param {ProgressMeter}
   *            [progressMeter=Not used] A progress meter.
   * @returns {ZeroKnowledgeProofPreComputation} The Schnorr zero-knowledge
   *          proof of knowledge pre-computation.
   */
  this.preCompute = function(group, phiFunction, progressMeter) {
    var exponents = generateExponents(group, phiFunction.numInputs);

    var phiOutputs = phiFunction.calculate(exponents, progressMeter);

    return new ZeroKnowledgeProofPreComputation(exponents, phiOutputs);
  };

  function generateHash(group, publicValues, phiOutputs, data) {
    var hashGenerator = new HashGenerator(messageDigestService);

    var hashBytes = hashGenerator.generate(publicValues, phiOutputs, data);
    var hashByteArray = Array.apply([], hashBytes);
    hashByteArray.unshift(0);

    var value = codec.bytesToBigInteger(hashBytes);

    return mathService.newExponent(group.q, value);
  }

  function generateProofValues(group, privateValues, hash, exponents, index) {
    var proofValues = [];
    var proofValue;
    if (typeof index === 'undefined') {
      for (var i = 0; i < privateValues.length; i++) {
        proofValue =
            exponents[i].value.add(privateValues[i].value.multiply(hash.value));

        proofValues.push(mathService.newExponent(group.q, proofValue));
      }
    } else {
      var numElements = exponents.length / 2;

      var updatedHash = hash;
      for (var j = 0; j < numElements; j++) {
        if (j === index) {
          for (var k = 0; k < numElements; k++) {
            if (k !== index) {
              updatedHash = updatedHash.subtract(exponents[k]);
            }
          }
          proofValues.push(updatedHash);
        } else {
          proofValues.push(exponents[j]);
        }
      }

      for (var m = numElements; m < (numElements * 2); m++) {
        if (m === (numElements + index)) {
          proofValue = exponents[m].value.add(
              privateValues[0].value.multiply(updatedHash.value));

          proofValues.push(mathService.newExponent(group.q, proofValue));
        } else {
          proofValues.push(exponents[m]);
        }
      }
    }

    return proofValues;
  }

  function updatePreComputation(
      group, preComputation, publicValues, elements, index) {
    var gamma = publicValues[0];
    var phi = publicValues[1];
    var numElements = elements.length;

    var challenges = generateExponents(group, numElements);

    var updatedExponents = challenges.concat(preComputation.exponents);

    var updatedPhiOutputs = [];
    var phiOutputs = preComputation.phiOutputs;
    for (var j = 0; j < numElements; j++) {
      var offset = 2 * j;
      if (j === index) {
        updatedPhiOutputs.push(phiOutputs[offset]);
        updatedPhiOutputs.push(phiOutputs[offset + 1]);
      } else {
        var gammaFactor = gamma.invert().exponentiate(challenges[j]);
        var phiFactor =
            (phi.invert().multiply(elements[j])).exponentiate(challenges[j]);
        updatedPhiOutputs.push(phiOutputs[offset].multiply(gammaFactor));
        updatedPhiOutputs.push(phiOutputs[offset + 1].multiply(phiFactor));
      }
    }

    return new ZeroKnowledgeProofPreComputation(
        updatedExponents, updatedPhiOutputs);
  }

  function generateExponents(group, numExponents) {
    var exponents = [];
    for (var i = 0; i < numExponents; i++) {
      exponents.push(mathRandomGenerator_.nextExponent(group));
    }

    return exponents;
  }
}
