/*
 * 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');
var ZeroKnowledgeProofPreComputation = require('./pre-computation');
var SchnorrProofHandler = require('./handlers/schnorr-proof-handler');
var ExponentiationProofHandler =
    require('./handlers/exponentiation-proof-handler');
var PlaintextProofHandler = require('./handlers/plaintext-proof-handler');
var SimplePlaintextEqualityProofHandler =
    require('./handlers/simple-plaintext-equality-proof-handler');
var PlaintextEqualityProofHandler =
    require('./handlers/plaintext-equality-proof-handler');
var PlaintextExponentEqualityProofHandler =
    require('./handlers/plaintext-exponent-equality-proof-handler');
var OrProofHandler = require('./handlers/or-proof-handler');
var cryptoPolicy = require('scytl-cryptopolicy');
var messageDigest = require('scytl-messagedigest');
var mathematical = require('scytl-mathematical');
var validator = require('./input-validator');
var codec = require('scytl-codec');
require('./array');

module.exports = ZeroKnowledgeProofService;

/**
 * @class ZeroKnowledgeProofService
 * @classdesc The zero-knowledge proof service API. To instantiate this object,
 *            use the method {@link newService}.
 * @hideconstructor
 * @param {Object}
 *            [options] An object containing optional arguments.
 * @param {Policy}
 *            [options.policy=Default policy] The cryptographic policy to use.
 * @param {MessageDigestService}
 *            [options.messageDigestService=Created internally] The message
 *            digest service to use.
 * @param {SecureRandomService}
 *            [options.secureRandomService=Created internally] The secure random
 *            service to use.
 * @param {MathematicalService}
 *            [options.mathematicalService=Created internally] The mathematical
 *            service to use.
 */
function ZeroKnowledgeProofService(options) {
  options = options || {};

  var policy;
  if (options.policy) {
    policy = options.policy;
  } else {
    policy = cryptoPolicy.newInstance();
  }

  var messageDigestService_;
  if (options.messageDigestService) {
    messageDigestService_ = options.messageDigestService;
  } else {
    messageDigestService_ = messageDigest.newService({policy: policy});
  }

  var secureRandomService;
  if (options.secureRandomService) {
    secureRandomService = options.secureRandomService;
  }

  var mathService_;
  if (options.mathematicalService) {
    mathService_ = options.mathematicalService;
  } else if (secureRandomService) {
    mathService_ =
        mathematical.newService({secureRandomService: secureRandomService});
  } else {
    mathService_ = mathematical.newService();
  }

  /**
   * Creates a new ZeroKnowledgeProof object from a provided zero-knowledge
   * proof of knowledge or its components.
   *
   * @function newProof
   * @memberof ZeroKnowledgeProofService
   * @param {Exponent}
   *            hashOrJson The hash of the zero-knowledge proof of knowledge
   *            <b>OR</b> a JSON string representation of a
   *            ZeroKnowledgeProof object, compatible with its
   *            <code>toJson</code> method. For the latter case, any
   *            additional input arguments will be ignored.
   * @param {Exponent[]}
   *            values The values of the zero-knowledge proof of knowledge.
   * @returns {ZeroKnowledgeProof} The new ZeroKnowledgeProof object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newProof = function(hashOrJson, exponents) {
    if (typeof hashOrJson !== 'string') {
      validator.checkExponent(
          hashOrJson, 'Hash to create new ZeroKnowledgeProof object');
      validator.checkExponents(
          exponents, 'Exponents to create new ZeroKnowledgeProof object');

      return new ZeroKnowledgeProof(hashOrJson, exponents);
    } else {
      return jsonToProof(hashOrJson);
    }
  };

  /**
   * Creates a new ZeroKnowledgeProofPreComputation object from a provided
   * zero-knowledge proof of knowledge pre-computation or its components.
   *
   * @function newPreComputation
   * @memberof ZeroKnowledgeProofService
   * @param {Exponent[]}
   *            exponentsOrJson The array of randomly generated exponents that
   *            comprise the pre-computation <b>OR</b> a JSON string
   *            representation of a ZeroKnowledgeProofPreComputation object,
   *            compatible with its <code>toJson</code> method. For the
   *            latter case, any additional input arguments will be ignored.
   * @param {ZpGroupElements[]}
   *            phiOutputs The array of PHI function output elements that
   *            comprise the pre-computation.
   * @returns {ZeroKnowledgeProofPreComputation} The new
   *          ZeroKnowledgeProofPreComputation object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newPreComputation = function(exponentsOrJson, phiOutputs) {
    if (typeof exponentsOrJson !== 'string') {
      validator.checkExponents(
          exponentsOrJson,
          'Exponents to create new ZeroKnowledgeProofPreComputation object');
      validator.checkZpGroupElements(
          phiOutputs,
          'PHI function output elements to create new ZeroKnowledgeProofPreComputation object');

      return new ZeroKnowledgeProofPreComputation(exponentsOrJson, phiOutputs);
    } else {
      return jsonToPreComputation(exponentsOrJson);
    }
  };

  /**
   * Creates new SchnorrProofHandler object, for generating, pre-computing and
   * verifying Schnorr zero-knowledge proofs of knowledge.
   *
   * @function newSchnorrProofHandler
   * @memberof ZeroKnowledgeProofService
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof generation are associated or
   *            belong, respectively.
   * @returns {SchnorrProofHandler} The SchnorrProofHandler object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newSchnorrProofHandler = function(group) {
    validator.checkIsObjectWithProperties(
        group, 'Zp subgroup for Schnorr proof handler');

    return new SchnorrProofHandler(group, messageDigestService_, mathService_);
  };

  /**
   * Creates new ExponentiationProofHandler object, for generating,
   * pre-computing and verifying exponentation zero-knowledge proofs of
   * knowledge. It must be initialized with base elements before it is used.
   *
   * @function newExponentiationProofHandler
   * @memberof ZeroKnowledgeProofService
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof generation are associated or
   *            belong, respectively.
   * @returns {ExponentiationProofHandler} The ExponentiationProofHandler
   *          object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newExponentiationProofHandler = function(group) {
    validator.checkIsObjectWithProperties(
        group, 'Zp subgroup for exponentiation proof handler');

    return new ExponentiationProofHandler(
        group, messageDigestService_, mathService_);
  };

  /**
   * Creates new PlaintextProofHandler object, for generating, pre-computing
   * and verifying plaintext zero-knowledge proofs of knowledge. It must be
   * initialized with an ElGamal public key before it is used.
   *
   * @function newPlaintextProofHandler
   * @memberof ZeroKnowledgeProofService
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof generation are associated or
   *            belong, respectively.
   * @returns {PlaintextProofHandler} The PlaintextProofHandler object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newPlaintextProofHandler = function(group) {
    validator.checkIsObjectWithProperties(
        group, 'Zp subgroup for plaintext proof handler');

    return new PlaintextProofHandler(
        group, messageDigestService_, mathService_);
  };

  /**
   * Creates new SimplePlaintextEqualityProofHandler object, for generating,
   * pre-computing and verifying simple plaintext equality zero-knowledge
   * proofs of knowledge. It must be initialized with primary and secondary
   * ElGamal public keys before it is used.
   *
   * @function newSimplePlaintextEqualityProofHandler
   * @memberof ZeroKnowledgeProofService
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof generation are associated or
   *            belong, respectively.
   * @returns {SimplePlaintextEqualityProofHandler} The
   *          SimplePlaintextEqualityProofHandler object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newSimplePlaintextEqualityProofHandler = function(group) {
    validator.checkIsObjectWithProperties(
        group, 'Zp subgroup for simple plaintext equality proof handler');

    return new SimplePlaintextEqualityProofHandler(
        group, messageDigestService_, mathService_);
  };

  /**
   * Creates new PlaintextEqualityProofHandler object, for generating,
   * pre-computing and verifying plaintext equality zero-knowledge proofs of
   * knowledge. It must be initialized with primary and secondary ElGamal
   * public keys before it is used.
   *
   * @function newPlaintextEqualityProofHandler
   * @memberof ZeroKnowledgeProofService
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof generation are associated or
   *            belong, respectively.
   * @returns {PlaintextEqualityProofHandler} The
   *          PlaintextEqualityProofHandler object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newPlaintextEqualityProofHandler = function(group) {
    validator.checkIsObjectWithProperties(
        group, 'Zp subgroup for plaintext equality proof handler');

    return new PlaintextEqualityProofHandler(
        group, messageDigestService_, mathService_);
  };

  /**
   * Creates new PlaintextExponentEqualityProofHandler object, for generating,
   * pre-computing and verifying plaintext exponent equality zero-knowledge
   * proofs of knowledge. It must be initialized with base elements before it
   * is used.
   *
   * @function newPlaintextExponentEqualityProofHandler
   * @memberof ZeroKnowledgeProofService
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof generation are associated or
   *            belong, respectively.
   * @returns {PlaintextExponentEqualityProofHandler} The
   *          PlaintextExponentEqualityProofHandler object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newPlaintextExponentEqualityProofHandler = function(group) {
    validator.checkIsObjectWithProperties(
        group, 'Zp subgroup for plaintext exponent equality proof handler');

    return new PlaintextExponentEqualityProofHandler(
        group, messageDigestService_, mathService_);
  };

  /**
   * Creates new OrProofHandler object, for generating, pre-computing and
   * verifying OR zero-knowledge proofs of knowledge. It must be initialized
   * with an ElGamal public key before it is used.
   *
   * @function newOrProofHandler
   * @memberof ZeroKnowledgeProofService
   * @param {ZpSubgroup}
   *            group The Zp subgroup to which all exponents and Zp group
   *            elements required for the proof generation are associated or
   *            belong, respectively.
   * @returns {OrProofHandler} The OrProofHandler object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newOrProofHandler = function(group) {
    validator.checkIsObjectWithProperties(
        group, 'Zp subgroup for OR proof handler');

    return new OrProofHandler(group, messageDigestService_, mathService_);
  };

  function jsonToProof(json) {
    validator.checkIsJsonString(
        json, 'JSON to deserialize to ZeroKnowledgeProof object');

    var parsed = JSON.parse(json).zkProof;

    var q = codec.bytesToBigInteger(codec.base64Decode(parsed.q));
    var hash = mathService_.newExponent(
        q, codec.bytesToBigInteger(codec.base64Decode(parsed.hash)));

    var values = [];
    var valuesB64 = parsed.values;
    for (var i = 0; i < valuesB64.length; i++) {
      values.push(mathService_.newExponent(
          q, codec.bytesToBigInteger(codec.base64Decode(valuesB64[i]))));
    }

    return new ZeroKnowledgeProof(hash, values);
  }

  function jsonToPreComputation(json) {
    validator.checkIsJsonString(
        json, 'JSON to deserialize to ZeroKnowledgeProofPreComputation object');

    var parsed = JSON.parse(json).preComputed;

    var p = codec.bytesToBigInteger(codec.base64Decode(parsed.p));
    var q = codec.bytesToBigInteger(codec.base64Decode(parsed.q));

    var exponentValues = parsed.exponents;
    var exponents = [];
    for (var i = 0; i < exponentValues.length; i++) {
      exponents.push(mathService_.newExponent(
          q, codec.bytesToBigInteger(codec.base64Decode(exponentValues[i]))));
    }

    var phiOutputValues = parsed.phiOutputs;
    var phiOutputs = [];
    for (var j = 0; j < phiOutputValues.length; j++) {
      phiOutputs.push(mathService_.newZpGroupElement(
          p, q,
          codec.bytesToBigInteger(codec.base64Decode(phiOutputValues[j]))));
    }

    return new ZeroKnowledgeProofPreComputation(exponents, phiOutputs);
  }
}
