/*
 * 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
 */
package com.scytl.cryptolib.interoperation.js.commands.elgamal;

import java.util.List;

import javax.script.ScriptException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncrypterValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.elgamal.bean.ElGamalKeyPair;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalDecrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.elgamal.utils.ElGamalTestDataGenerator;
import com.scytl.cryptolib.interoperation.js.commands.Command;
import com.scytl.cryptolib.interoperation.js.commands.CommandException;
import com.scytl.cryptolib.interoperation.js.constants.CommonConstants;
import com.scytl.cryptolib.interoperation.js.constants.ElGamalConstants;
import com.scytl.cryptolib.interoperation.js.utils.MathematicalCollectionSerializer;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.mathematical.groups.utils.MathematicalTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for ElGamal cipher applications,
 * using a short exponent for encryption.
 */
public class TestElGamalCipherShortExponent extends Command {

    private static final Logger LOGGER =
        LoggerFactory.getLogger(TestElGamalCipherShortExponent.class);

    private static final String JS_ENCRYPTED_QR_GROUP_ELEMENTS_EXCEPTION_DATA_KEY =
        "ElGamal encrypted quadratic residue group elements (JSON format) [JS]";

    private static final String JAVA_DECRYPTED_QR_GROUP_ELEMENTS_EXCEPTION_DATA_KEY =
        "ElGamal decrypted quadratic residue group elements (JSON format) [Java]";

    private static final String JAVA_PRE_COMPUTED_ENCRYPTION_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed ElGamal encryption values (JSON format) [Java]";

    private static final String JS_PRE_COMPUTED_ENCRYPTION_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed ElGamal encryption values (JSON format) [JS]";

    private final Object _namespace;

    private final ElGamalService _elGamalService;

    private ElGamalPublicKey _elGamalPublicKey;

    private String _elGamalPublicKeyJson;

    private ElGamalPrivateKey _elGamalPrivateKey;

    private String _elGamalPrivateKeyJson;

    private List<ZpGroupElement> _qrGroupElements;

    private String _qrGroupElementsJson;

    private static CryptoAPIElGamalEncrypter _encrypterForShortExponents;

    private static CryptoAPIElGamalDecrypter _decrypter;

    private ElGamalEncrypterValues _javaPreComputedValues;

    private String _javaPreComputedValuesJson;

    private String _jsPreComputedValuesJson;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the ElGamal cipher
     *             interoperation tests, using a short exponent for encryption,
     *             implemented in JavaScript.
     */
    public TestElGamalCipherShortExponent() throws CommandException {
        super(ElGamalConstants.JS_ELGAMAL_CIPHER_SHORT_EXPONENT_TEST_PATH);

        _namespace = _engine.get(
            ElGamalConstants.JS_ELGAMAL_CIPHER_SHORT_EXPONENT_TEST_NAMESPACE);

        clearExceptionData();

        try {
            _elGamalService = new ElGamalService();

            generateInputData();
        } catch (GeneralCryptoLibException e) {
            throw new CommandException.Builder().addCause(e).build();
        }
    }

    /**
     * ElGamal encrypts a collection of quadratic residue group elements, via a
     * short exponent, using Java, decrypts the result using JavaScript,
     * encrypts the same element collection, via a short exponent, using
     * JavaScript and decrypts the result using Java. If no exceptions are
     * thrown and the original element collection can be retrieved, then the
     * interoperation test for ElGamal cipher applications, using a short
     * exponent for encryption, was successful.
     *
     * @throws CommandException
     *             if the key pair cannot be retrieved, the key pair cannot be
     *             converted to JSON format, the quadratic residue group element
     *             collection cannot be retrieved, a cryptographic operation
     *             fails or a decryption operation yields data that does not
     *             correspond to the original data.
     */
    @Override
    public void execute() throws CommandException {

        String encryptedQrGroupElementsJson = encryptUsingJava();

        decryptUsingJavaScript(encryptedQrGroupElementsJson);

        encryptedQrGroupElementsJson = encryptUsingJavaScript();

        decryptUsingJava(encryptedQrGroupElementsJson);

        encryptedQrGroupElementsJson =
            encryptViaPreComputedValuesUsingJava();

        decryptPreComputedEncryptionUsingJavaScript(
            encryptedQrGroupElementsJson);

        encryptedQrGroupElementsJson =
            encryptViaPreComputedValuesUsingJavaScript();

        decryptPreComputedEncryptionUsingJava(
            encryptedQrGroupElementsJson);

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "INTEROPERATION TEST FOR ELGAMAL CIPHER, USING A SHORT EXPONENT, WAS SUCCESSFUL.");
    }

    private void generateInputData()
            throws GeneralCryptoLibException, CommandException {

        ZpSubgroup qrSubgroup =
            MathematicalTestDataGenerator.getQrSubgroup();
        ElGamalEncryptionParameters elGamalEncryptionParameters =
            ElGamalTestDataGenerator
                .getElGamalEncryptionParameters(qrSubgroup);

        ElGamalKeyPair elGamalKeyPair =
            _elGamalService.getElGamalKeyPairGenerator().generateKeys(
                elGamalEncryptionParameters,
                ElGamalConstants.NUM_ZP_GROUP_ELEMENTS);
        _elGamalPublicKey = elGamalKeyPair.getPublicKeys();
        _elGamalPublicKeyJson = _elGamalPublicKey.toJson();
        putExceptionData("ElGamal public key (JSON format) [Java]",
            _elGamalPublicKeyJson);
        _elGamalPrivateKey = elGamalKeyPair.getPrivateKeys();
        _elGamalPrivateKeyJson = _elGamalPrivateKey.toJson();
        putExceptionData("ElGamal private key (JSON format) [Java]",
            _elGamalPrivateKeyJson);

        _encrypterForShortExponents = ElGamalTestDataGenerator
            .getEncrypterForShortExponents(_elGamalPublicKey);
        _decrypter = _elGamalService.createDecrypter(_elGamalPrivateKey);

        _qrGroupElements =
            MathematicalTestDataGenerator.getZpGroupElements(qrSubgroup,
                ElGamalConstants.NUM_ZP_GROUP_ELEMENTS);
        _qrGroupElementsJson = MathematicalCollectionSerializer
            .zpGroupElementsToJson(_qrGroupElements);
        putExceptionData(
            "Quadratic residue group elements to ElGamal encrypt (JSON format) [Java]",
            _qrGroupElementsJson);

        _javaPreComputedValues = _encrypterForShortExponents.preCompute();
        _javaPreComputedValuesJson =
            _javaPreComputedValues.getComputationValues().toJson();

        try {
            _jsPreComputedValuesJson =
                (String) getInvocable().invokeMethod(_namespace,
                    ElGamalConstants.JS_ELGAMAL_PRE_COMPUTED_VALUES_GENERATION_FUNCTION,
                    _elGamalPublicKeyJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ElGamalConstants.JS_ELGAMAL_CIPHER_SHORT_EXPONENT_TEST_NAMESPACE
                        + "."
                        + ElGamalConstants.JS_ELGAMAL_PRE_COMPUTED_VALUES_GENERATION_FUNCTION
                        + " in file "
                        + ElGamalConstants.JS_ELGAMAL_CIPHER_SHORT_EXPONENT_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate pre-computed ElGamal encryption values, via a short exponent, using JavaScript.",
                e);
        }
    }

    private String encryptUsingJava() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal encrypting data, via a short exponent, using Java...");

        try {
            return _encrypterForShortExponents
                .encryptGroupElementsWithShortExponent(_qrGroupElements)
                .getComputationValues().toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not ElGamal encrypt data, via a short exponent, using Java.",
                e);
        }
    }

    private void decryptUsingJavaScript(
            final String encryptedQrGroupElementsJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal decrypting data, encrypted via a short exponent, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                ElGamalConstants.JS_ELGAMAL_DECRYPTION_FUNCTION,
                _elGamalPrivateKeyJson, encryptedQrGroupElementsJson,
                _qrGroupElementsJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ElGamalConstants.JS_ELGAMAL_CIPHER_SHORT_EXPONENT_TEST_NAMESPACE
                    + "." + ElGamalConstants.JS_ELGAMAL_DECRYPTION_FUNCTION
                    + " in resources file "
                    + ElGamalConstants.JS_ELGAMAL_CIPHER_SHORT_EXPONENT_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not ElGamal decrypt Java encrypted data, encrypted via a short exponent, using JavaScript.",
                e);
        }
    }

    private String encryptUsingJavaScript() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal encrypting data, via a short exponent, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ElGamalConstants.JS_ELGAMAL_ENCRYPTION_FUNCTION,
                _elGamalPublicKeyJson, _qrGroupElementsJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ElGamalConstants.JS_ELGAMAL_CIPHER_SHORT_EXPONENT_TEST_NAMESPACE
                    + "." + ElGamalConstants.JS_ELGAMAL_ENCRYPTION_FUNCTION
                    + " in file "
                    + ElGamalConstants.JS_ELGAMAL_CIPHER_SHORT_EXPONENT_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not ElGamal encrypt data, via a short exponent, using JavaScript.",
                e);
        }
    }

    private void decryptUsingJava(
            final String encryptedQrGroupElementsJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal decrypting data, encrypted via a short exponent, using Java...");

        putExceptionData(JS_ENCRYPTED_QR_GROUP_ELEMENTS_EXCEPTION_DATA_KEY,
            encryptedQrGroupElementsJson);

        ElGamalComputationsValues encryptedZpGroupElements;
        try {
            encryptedZpGroupElements = ElGamalComputationsValues
                .fromJson(encryptedQrGroupElementsJson);
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not deserialize JavaScript ElGamal encrypted quadratic residue group elements, encrypted via a short exponent.",
                e);
        }

        try {
            List<ZpGroupElement> decryptedQrGroupElements =
                _decrypter.decrypt(encryptedZpGroupElements, true);

            String decryptedQrGroupElementsJson =
                MathematicalCollectionSerializer
                    .zpGroupElementsToJson(decryptedQrGroupElements);
            putExceptionData(
                JAVA_DECRYPTED_QR_GROUP_ELEMENTS_EXCEPTION_DATA_KEY,
                decryptedQrGroupElementsJson);

            if (!decryptedQrGroupElements.equals(_qrGroupElements)) {
                throw new GeneralCryptoLibException(
                    "Expected ElGamal decrypted quadratic residue group elements (JSON format): "
                        + _qrGroupElements + " ; Found: "
                        + decryptedQrGroupElementsJson);
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not ElGamal decrypt JavaScript encrypted data, encrypted via a short exponent, using Java.",
                e);
        }
    }

    private String encryptViaPreComputedValuesUsingJava()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal encrypting data, via a short exponent and pre-computed values, using Java...");

        putExceptionData(
            JAVA_PRE_COMPUTED_ENCRYPTION_VALUES_EXCEPTION_DATA_KEY,
            _javaPreComputedValuesJson);

        try {
            return _encrypterForShortExponents
                .encryptGroupElements(_qrGroupElements,
                    _javaPreComputedValues)
                .getComputationValues().toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not ElGamal encrypt data, via a short exponent and pre-computed values, using Java.",
                e);
        }
    }

    private void decryptPreComputedEncryptionUsingJavaScript(
            final String encryptedZpGroupElementsJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal decrypting data, encrypted via a short exponent and pre-computed values, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                ElGamalConstants.JS_ELGAMAL_DECRYPTION_FUNCTION,
                _elGamalPrivateKeyJson, encryptedZpGroupElementsJson,
                _qrGroupElementsJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ElGamalConstants.JS_ELGAMAL_CIPHER_TEST_NAMESPACE
                    + "." + ElGamalConstants.JS_ELGAMAL_DECRYPTION_FUNCTION
                    + " in resources file "
                    + ElGamalConstants.JS_ELGAMAL_CIPHER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not ElGamal decrypt Java encypted data, encrypted via a short exponent and pre-computed values, using JavaScript.",
                e);
        }
    }

    private String encryptViaPreComputedValuesUsingJavaScript()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal encrypting data, via a short exponent and pre-computed values, using JavaScript...");

        removeExceptionData(
            JAVA_PRE_COMPUTED_ENCRYPTION_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JS_PRE_COMPUTED_ENCRYPTION_VALUES_EXCEPTION_DATA_KEY,
            _jsPreComputedValuesJson);

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ElGamalConstants.JS_ELGAMAL_ENCRYPTION_FUNCTION,
                _elGamalPublicKeyJson, _qrGroupElementsJson,
                _jsPreComputedValuesJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ElGamalConstants.JS_ELGAMAL_CIPHER_TEST_NAMESPACE
                    + "." + ElGamalConstants.JS_ELGAMAL_ENCRYPTION_FUNCTION
                    + " in file "
                    + ElGamalConstants.JS_ELGAMAL_CIPHER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not ElGamal encrypt data, via a short exponent and pre-computed values, using JavaScript.",
                e);
        }
    }

    private void decryptPreComputedEncryptionUsingJava(
            final String encryptedZpGroupElementsJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal decrypting data, encrypted via a short exponent and pre-computed values, using Java...");

        putExceptionData(JS_ENCRYPTED_QR_GROUP_ELEMENTS_EXCEPTION_DATA_KEY,
            encryptedZpGroupElementsJson);

        ElGamalComputationsValues encryptedZpGroupElements;
        try {
            encryptedZpGroupElements = ElGamalComputationsValues
                .fromJson(encryptedZpGroupElementsJson);
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not deserialize JavaScript ElGamal encrypted Zp group elements.",
                e);
        }

        try {
            List<ZpGroupElement> decryptedZpGroupElements =
                _decrypter.decrypt(encryptedZpGroupElements, true);

            String decryptedZpGroupElementsJson =
                MathematicalCollectionSerializer
                    .zpGroupElementsToJson(decryptedZpGroupElements);
            putExceptionData(
                JAVA_DECRYPTED_QR_GROUP_ELEMENTS_EXCEPTION_DATA_KEY,
                decryptedZpGroupElementsJson);

            if (!decryptedZpGroupElements.equals(_qrGroupElements)) {
                throw new GeneralCryptoLibException(
                    "Expected ElGamal decrypted Zp group elements (JSON format): "
                        + _qrGroupElementsJson + " ; Found: "
                        + decryptedZpGroupElementsJson);
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not ElGamal decrypt JavaScript encrypted data, encrypted via a short exponent and pre-computed values, using Java.",
                e);
        }
    }
}
