/*
 * 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 HashGenerator = require('./hash-generator');
var codec = require('scytl-codec');

module.exports = ZeroKnowledgeProofVerifier;

/**
 * @class ZeroKnowledgeProofVerifier
 * @classdesc Encapsulates a zero-knowledge proof of knowledge verifier that
 *            uses the PHI function defined in Maurer's unified framework.
 * @private
 * @param {MessageDigestService}
 *            messageDigestService The message digest service.
 * @param {MathematicalService}
 *            mathService The mathematical service.
 */
function ZeroKnowledgeProofVerifier(messageDigestService, mathService) {
  /**
   * Verifies a zero-knowledge proof of knowledge.
   *
   * @function verify
   * @memberof ZeroKnowledgeProofVerifier
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof verification are associated or
   *            belong, respectively.
   * @param {ZeroKnowledgeProof}
   *            proof The zero-knowledge proof of knowledge to verify.
   * @param {PhiFunction}
   *            phiFunction The PHI function used for the verification.
   * @param {ZpGroupElement[]}
   *            publicValues The public values used to verify the proof.
   * @param {Uint8Array|string}
   *            data Auxiliary data.
   * @param {ZpGroupElement[]|ProgressMeter}
   *            [elementsOrProgressMeter=Not used] The possible Zp group
   *            elements used to generate the ciphertext of an OR
   *            zero-knowledge proof <b>OR</b> a progress meter.
   * @param {number}
   *            [progressMeter=Not used] A parameter that can be used as a
   *            progress meter, for the case in which the previous parameter
   *            was of type <code>ZpGroupElements[]</code>.
   * @returns {boolean} <code>true</code> if the zero-knowledge proof of
   *          knowledge was verified, <code>false</code> otherwise.
   */
  this.verify = function(
      group, proof, phiFunction, publicValues, data, elementsOrProgressMeter,
      progressMeter) {
    var elements;
    if (!isArray(elementsOrProgressMeter)) {
      progressMeter = elementsOrProgressMeter;
    } else {
      elements = elementsOrProgressMeter;
    }

    var proofHash = proof.hash;

    var phiOutputs;
    var generatedValues;
    if (typeof elements === 'undefined') {
      phiOutputs = phiFunction.calculate(proof.values, progressMeter);
      generatedValues = generateValues(publicValues, phiOutputs, proofHash);
    } else {
      var numElements = elements.length;
      var proofValues = proof.values;
      var challenges = proofValues.slice(0, numElements);
      var phiInputs = proofValues.slice(numElements, numElements * 2);

      phiOutputs = phiFunction.calculate(phiInputs, progressMeter);
      generatedValues =
          generateValues(publicValues, phiOutputs, elements, challenges);
    }

    var calculatedHash =
        generateHash(group, publicValues, generatedValues, data);

    return proofHash.equals(calculatedHash);
  };

  function generateValues(
      publicValues, phiOutputs, proofHashOrElements, challenges) {
    var proofHash;
    var elements;
    if (!isArray(proofHashOrElements)) {
      proofHash = proofHashOrElements;
    } else {
      elements = proofHashOrElements;
    }

    var generatedValues = [];
    if (typeof proofHash !== 'undefined') {
      for (var i = 0; i < phiOutputs.length; i++) {
        generatedValues.push(phiOutputs[i].multiply(
            publicValues[i].exponentiate(proofHash.negate())));
      }
    } else {
      var gamma = publicValues[0];
      var phi = publicValues[1];

      for (var j = 0; j < challenges.length; j++) {
        var offset = 2 * j;
        var gammaFactor = gamma.invert().exponentiate(challenges[j]);
        var phiFactor =
            (phi.invert().multiply(elements[j])).exponentiate(challenges[j]);
        generatedValues.push(phiOutputs[offset].multiply(gammaFactor));
        generatedValues.push(phiOutputs[offset + 1].multiply(phiFactor));
      }
    }

    return generatedValues;
  }

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

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

    var value = codec.bytesToBigInteger(hashBytes);

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

  function isArray(a) {
    return (!!a) && (a.constructor === Array);
  }
}
