/*
 * 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.math.BigInteger;
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.Exponent;
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.
 */
public class TestElGamalCipher extends Command {

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

    private static final String JS_ENCRYPTED_ZP_GROUP_ELEMENTS_EXCEPTION_DATA_KEY =
        "ElGamal encrypted Zp group elements (JSON format) [JS]";

    private static final String JAVA_DECRYPTED_ZP_GROUP_ELEMENTS_EXCEPTION_DATA_KEY =
        "ElGamal decrypted Zp 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 CryptoAPIElGamalEncrypter _encrypter;

    private CryptoAPIElGamalDecrypter _decrypter;

    private List<ZpGroupElement> _zpGroupElements;

    private String _zpGroupElementsJson;

    private ElGamalEncrypterValues _javaPreComputedValues;

    private String _javaPreComputedValuesJson;

    private ElGamalEncrypterValues _jsPreComputedValues;

    private String _jsPreComputedValuesJson;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the ElGamal cipher
     *             interoperation tests implemented in JavaScript.
     */
    public TestElGamalCipher() throws CommandException {
        super(ElGamalConstants.JS_ELGAMAL_CIPHER_TEST_PATH);

        _namespace =
            _engine.get(ElGamalConstants.JS_ELGAMAL_CIPHER_TEST_NAMESPACE);

        clearExceptionData();

        try {
            _elGamalService = new ElGamalService();

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

    /**
     * ElGamal encrypts a collection of Zp group elements using Java, decrypts
     * the result using JavaScript, encrypts the same element collection 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 was successful.
     *
     * @throws CommandException
     *             if the key pair cannot be retrieved, the key pair cannot be
     *             converted to JSON format, the Zp 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 encryptedZpGroupElementsJson = encryptUsingJava();

        decryptUsingJavaScript(encryptedZpGroupElementsJson);

        encryptedZpGroupElementsJson = encryptUsingJavaScript();

        decryptUsingJava(encryptedZpGroupElementsJson);

        encryptedZpGroupElementsJson =
            encryptViaPreComputedValuesUsingJava();

        decryptPreComputedEncryptionUsingJavaScript(
            encryptedZpGroupElementsJson);

        encryptedZpGroupElementsJson =
            encryptViaPreComputedValuesUsingJavaScript();

        decryptPreComputedEncryptionUsingJava(
            encryptedZpGroupElementsJson);

        encryptAndDecryptWithJavaPreComputedValuesUsingJavaScript();

        encryptAndDecryptWithJavaScriptPreComputedValuesUsingJava();

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "INTEROPERATION TEST FOR ELGAMAL CIPHER WAS SUCCESSFUL.");
    }

    private void generateInputData()
            throws GeneralCryptoLibException, CommandException {

        ZpSubgroup zpSubgroup =
            MathematicalTestDataGenerator.getZpSubgroup();
        ElGamalEncryptionParameters elGamalEncryptionParameters =
            ElGamalTestDataGenerator
                .getElGamalEncryptionParameters(zpSubgroup);

        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);

        _encrypter = _elGamalService.createEncrypter(_elGamalPublicKey);
        _decrypter = _elGamalService.createDecrypter(_elGamalPrivateKey);

        _zpGroupElements =
            MathematicalTestDataGenerator.getZpGroupElements(zpSubgroup,
                ElGamalConstants.NUM_ZP_GROUP_ELEMENTS);
        _zpGroupElementsJson = MathematicalCollectionSerializer
            .zpGroupElementsToJson(_zpGroupElements);
        putExceptionData(
            "Zp group elements to ElGamal encrypt (JSON format) [Java]",
            _zpGroupElementsJson);

        _javaPreComputedValues = _encrypter.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_TEST_NAMESPACE
                        + "."
                        + ElGamalConstants.JS_ELGAMAL_PRE_COMPUTED_VALUES_GENERATION_FUNCTION
                        + " in file "
                        + ElGamalConstants.JS_ELGAMAL_CIPHER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate pre-computed ElGamal encryption values, using JavaScript.",
                e);
        }
        ElGamalComputationsValues elGamalComputationsValues =
            ElGamalComputationsValues.fromJson(_jsPreComputedValuesJson);
        _jsPreComputedValues = new ElGamalEncrypterValues(
            new Exponent(BigInteger.ONE, BigInteger.ZERO),
            elGamalComputationsValues);
    }

    private String encryptUsingJava() throws CommandException {

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

        try {
            return _encrypter.encryptGroupElements(_zpGroupElements)
                .getComputationValues().toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not ElGamal encrypt data, using Java.", e);
        }
    }

    private void decryptUsingJavaScript(
            final String encryptedZpGroupElementsJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal decrypting data, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                ElGamalConstants.JS_ELGAMAL_DECRYPTION_FUNCTION,
                _elGamalPrivateKeyJson, encryptedZpGroupElementsJson,
                _zpGroupElementsJson);
        } 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 encrypted data, using JavaScript.",
                e);
        }
    }

    private String encryptUsingJavaScript() throws CommandException {

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

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ElGamalConstants.JS_ELGAMAL_ENCRYPTION_FUNCTION,
                _elGamalPublicKeyJson, _zpGroupElementsJson);
        } 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, using JavaScript.", e);
        }
    }

    private void decryptUsingJava(
            final String encryptedZpGroupElementsJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal decrypting data, using Java...");

        putExceptionData(JS_ENCRYPTED_ZP_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_ZP_GROUP_ELEMENTS_EXCEPTION_DATA_KEY,
                decryptedZpGroupElementsJson);

            if (!decryptedZpGroupElements.equals(_zpGroupElements)) {
                throw new GeneralCryptoLibException(
                    "Expected ElGamal decrypted Zp group elements (JSON format): "
                        + _zpGroupElementsJson + " ; Found: "
                        + decryptedZpGroupElementsJson);
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not ElGamal decrypt JavaScript encrypted data, using Java.",
                e);
        }
    }

    private String encryptViaPreComputedValuesUsingJava()
            throws CommandException {

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

        putExceptionData(
            JAVA_PRE_COMPUTED_ENCRYPTION_VALUES_EXCEPTION_DATA_KEY,
            _javaPreComputedValuesJson);

        try {
            return _encrypter
                .encryptGroupElements(_zpGroupElements,
                    _javaPreComputedValues)
                .getComputationValues().toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not ElGamal encrypt data, via pre-computed values, using Java.",
                e);
        }
    }

    private void decryptPreComputedEncryptionUsingJavaScript(
            final String encryptedZpGroupElementsJson)
            throws CommandException {

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

        try {
            getInvocable().invokeMethod(_namespace,
                ElGamalConstants.JS_ELGAMAL_DECRYPTION_FUNCTION,
                _elGamalPrivateKeyJson, encryptedZpGroupElementsJson,
                _zpGroupElementsJson);
        } 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 pre-computed values, using JavaScript.",
                e);
        }
    }

    private String encryptViaPreComputedValuesUsingJavaScript()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal encrypting data, via 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, _zpGroupElementsJson,
                _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 pre-computed values, using JavaScript.",
                e);
        }
    }

    private void decryptPreComputedEncryptionUsingJava(
            final String encryptedZpGroupElementsJson)
            throws CommandException {

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

        removeExceptionData(
            JAVA_PRE_COMPUTED_ENCRYPTION_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(JS_ENCRYPTED_ZP_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_ZP_GROUP_ELEMENTS_EXCEPTION_DATA_KEY,
                decryptedZpGroupElementsJson);

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

    private void encryptAndDecryptWithJavaPreComputedValuesUsingJavaScript()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal encrypting and decrypting data, via Java generated pre-computed values, using JavaScript...");

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

        String encryptedZpGroupElementsJson;
        try {
            encryptedZpGroupElementsJson =
                (String) getInvocable().invokeMethod(_namespace,
                    ElGamalConstants.JS_ELGAMAL_ENCRYPTION_FUNCTION,
                    _elGamalPublicKeyJson, _zpGroupElementsJson,
                    _javaPreComputedValuesJson);
        } 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 Java generated pre-computed values, using JavaScript.",
                e);
        }

        try {
            getInvocable().invokeMethod(_namespace,
                ElGamalConstants.JS_ELGAMAL_DECRYPTION_FUNCTION,
                _elGamalPrivateKeyJson, encryptedZpGroupElementsJson,
                _zpGroupElementsJson);
        } 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 encrypted data, encrypted via Java generated pre-computed values, using JavaScript.",
                e);
        }
    }

    private void encryptAndDecryptWithJavaScriptPreComputedValuesUsingJava()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "ElGamal encrypting and decrypting data, via JavaScript generated pre-computed values, using Java...");

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

        ElGamalComputationsValues encryptedZpGroupElements;
        try {
            encryptedZpGroupElements =
                _encrypter.encryptGroupElements(_zpGroupElements,
                    _jsPreComputedValues).getComputationValues();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not ElGamal encrypt data, via JavaScript generated pre-computed values, using Java.",
                e);
        }

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

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

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