/*
 * 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 ElGamalKeyPair = require('./key-pair');
var ElGamalPublicKey = require('./public-key');
var ElGamalPrivateKey = require('./private-key');
var ElGamalEncrypter = require('./encrypter');
var ElGamalDecrypter = require('./decrypter');
var ElGamalPrngDecrypter = require('./prng-decrypter');
var ElGamalEncryptedElements = require('./encrypted-elements');
var mathematical = require('scytl-mathematical');
var validator = require('./input-validator');
var codec = require('scytl-codec');

module.exports = ElGamalCryptographyService;

/**
 * @class ElGamalCryptographyService
 * @classdesc The ElGamal cryptography service API. To instantiate this object,
 *            use the method {@link newService}.
 * @hideconstructor
 * @param {Object}
 *            [options] An object containing optional arguments.
 * @param {SecureRandomService}
 *            [options.secureRandomService=Created internally] The secure random
 *            service to use.
 * @param {MathematicalService}
 *            [options.mathematicalService=Created internally] The mathematical
 *            service to use.
 */
function ElGamalCryptographyService(options) {
  options = options || {};

  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 ElGamalPublicKey object, which encapsulates an ElGamal
   * public key.
   *
   * @function newPublicKey
   * @memberof ElGamalCryptographyService
   * @param {ZpSubgroup|string}
   *            groupOrJson The Zp subgroup to which the elements of the
   *            public key belong <b>OR</b> a JSON string representation of
   *            an ElGamalPublicKey object, compatible with its
   *            <code>toJson</code> method. For the latter case, any
   *            additional input arguments will be ignored.
   * @param {ZpGroupElement[]}
   *            elements The Zp group elements that comprise the public key.
   * @returns {ElGamalPublicKey} The ElGamalPublicKey object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newPublicKey = function(groupOrJson, elements) {
    if (typeof groupOrJson !== 'string') {
      validator.checkIsObjectWithProperties(
          groupOrJson, 'Zp subgroup for new ElGamalPublicKey object');
      validator.checkZpGroupElements(
          elements, 'Zp group elements for new ElGamalPublicKey object',
          groupOrJson);

      return new ElGamalPublicKey(groupOrJson, elements);
    } else {
      return jsonToPublicKey(groupOrJson);
    }
  };

  /**
   * Creates a new ElGamalPrivateKey object, which encapsulates an ElGamal
   * private key.
   *
   * @function newPrivateKey
   * @memberof ElGamalCryptographyService
   * @param {ZpSubgroup|string}
   *            groupOrJson The Zp subgroup to which the exponents of the
   *            private key are associated <b>OR</b> a JSON string
   *            representation of an ElGamalPrivateKey object, compatible with
   *            its <code>toJson</code> method. For the latter case, any
   *            additional input arguments will be ignored.
   * @param {Exponent[]}
   *            exponents The exponents that comprise the private key.
   * @returns {ElGamalPrivateKey} The new ElGamalPrivateKey object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newPrivateKey = function(groupOrJson, exponents) {
    if (typeof groupOrJson !== 'string') {
      validator.checkIsObjectWithProperties(
          groupOrJson, 'Zp subgroup for new ElGamalPrivateKey object');
      validator.checkExponents(
          exponents, 'Exponents for new ElGamalPrivateKey object',
          groupOrJson.q);

      return new ElGamalPrivateKey(groupOrJson, exponents);
    } else {
      return jsonToPrivateKey(groupOrJson);
    }
  };

  /**
   * Creates a new ElGamalKeyPair object, which encapsulates an ElGamal key
   * pair.
   *
   * @function newKeyPair
   * @memberof ElGamalCryptographyService
   * @param {ElGamalPublicKey}
   *            publicKey The ElGamal public key comprising the key pair.
   * @param {ElGamalPrivateKey}
   *            privateKey The ElGamal private key comprising the key pair.
   * @returns {ElGamalKeyPair} The new ElGamalKeyPair object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newKeyPair = function(publicKey, privateKey) {
    checkElGamalPublicKey(
        publicKey, 'ElGamal public key for new ElGamalKeyPair object');
    checkElGamalPrivateKey(
        privateKey, 'ElGamal private key for new ElGamalKeyPair object');

    return new ElGamalKeyPair(publicKey, privateKey);
  };

  /**
   * Creates a new ElGamalEncryptedElements object, which encapsulates the
   * encryption or encryption pre-computation of some Zp group elements.
   *
   * @function newEncryptedElements
   * @memberof ElGamalCryptographyService
   * @param {ZpGroupElement|string}
   *            gammaOrJson The gamma Zp group element comprising the
   *            encryption or pre-computation <b>OR</b> a JSON string
   *            representation of an ElGamalEncryptedElements object,
   *            compatible with its <code>toJson</code> method. For the
   *            latter case, any additional input arguments will be ignored.
   * @param {ZpGroupElement[]}
   *            phis The phi Zp group elements comprising the encryption or
   *            pre-computation.
   * @param {Exponent}
   *            [secret] The secret exponent comprising the encryption or
   *            pre-computation. <b>CAUTION:</b> Provide only when the secret
   *            is really needed later (e.g. for zero-knowledge proof
   *            generation).
   * @returns {ElGamalEncryptedElements} The new ElGamalEncryptedElements
   *          object.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.newEncryptedElements = function(gammaOrJson, phis, secret) {
    if (typeof gammaOrJson !== 'string') {
      validator.checkZpGroupElement(
          gammaOrJson,
          'Gamma Zp group element for new ElGamalEncryptedElements object');
      validator.checkZpGroupElements(
          phis,
          'Phi Zp group elements for new ElGamalEncryptedElements object');
      if (typeof secret !== 'undefined') {
        validator.checkExponent(secret, 'ElGamal encryption secret exponent');
      }

      return new ElGamalEncryptedElements(gammaOrJson, phis, secret);
    } else {
      return jsonToEncryptedElements(gammaOrJson);
    }
  };

  /**
   * Creates a new ElGamalEncrypter object for ElGamal encrypting data. It
   * must be initialized with an ElGamal public key.
   *
   * @function newEncrypter
   * @memberof ElGamalCryptographyService
   * @returns {ElGamalEncrypter} The new ElGamalEncrypter object.
   */
  this.newEncrypter = function() {
    return new ElGamalEncrypter(mathService_);
  };

  /**
   * Creates a new ElGamalDecrypter object for ElGamal decrypting data. It
   * must be initialized with an ElGamal private key.
   *
   * @function newDecrypter
   * @memberof ElGamalCryptographyService
   * @returns {ElGamalDecrypter} The new ElGamalDecrypter object.
   */
  this.newDecrypter = function() {
    return new ElGamalDecrypter(mathService_);
  };

  /**
   * Creates a new ElGamalPrngDecrypter object for ElGamal decrypting data,
   * via the PRNG seed used for encrypting the data. It must be initialized
   * with an ElGamal public key and a secure random generator, the latter
   * having been initialized with the seed.
   *
   * @function newPrngDecrypter
   * @memberof ElGamalCryptographyService
   * @returns {ElGamalPrngDecrypter} The new ElGamalPrngDecrypter object.
   */
  this.newPrngDecrypter = function() {
    return new ElGamalPrngDecrypter(mathService_);
  };

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

  function checkElGamalPrivateKey(privateKey, label) {
    validator.checkElGamalPrivateKey(privateKey, label);
    validator.checkIsInstanceOf(
        privateKey, ElGamalPrivateKey, 'ElGamalPrivateKey', label);
  }

  function jsonToPublicKey(json) {
    validator.checkIsJsonString(
        json, 'JSON string to deserialize to ElGamalPublicKey object');

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

    var p = codec.bytesToBigInteger(
        codec.base64Decode(parsed.zpSubgroup.p.toString()));
    var q = codec.bytesToBigInteger(
        codec.base64Decode(parsed.zpSubgroup.q.toString()));
    var g = codec.bytesToBigInteger(
        codec.base64Decode(parsed.zpSubgroup.g.toString()));
    var group = mathService_.newZpSubgroup(p, q, g);

    var elements = [];
    for (var i = 0; i < parsed.elements.length; i++) {
      var value =
          codec.bytesToBigInteger(codec.base64Decode(parsed.elements[i]));
      var element = mathService_.newZpGroupElement(p, q, value);
      elements.push(element);
    }

    return new ElGamalPublicKey(group, elements);
  }

  function jsonToPrivateKey(json) {
    validator.checkIsJsonString(
        json, 'JSON string representation of ElGamalPrivateKey object');

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

    var g = codec.bytesToBigInteger(
        codec.base64Decode(parsed.zpSubgroup.g.toString()));
    var p = codec.bytesToBigInteger(
        codec.base64Decode(parsed.zpSubgroup.p.toString()));
    var q = codec.bytesToBigInteger(
        codec.base64Decode(parsed.zpSubgroup.q.toString()));
    var group = mathService_.newZpSubgroup(p, q, g);

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

    return new ElGamalPrivateKey(group, exponents);
  }

  function jsonToEncryptedElements(json) {
    validator.checkIsJsonString(
        json, 'JSON string representation of ElGamalEncryptedElements object');

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

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

    var gammaFromJson = mathService_.newZpGroupElement(
        p, q, codec.bytesToBigInteger(codec.base64Decode(parsed.gamma)));

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

    return new ElGamalEncryptedElements(gammaFromJson, phisFromJson);
  }
}
