/*
 * 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.proofs;

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.api.proofs.ProofsServiceAPI;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncrypterValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.elgamal.cryptoapi.Witness;
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.ProofConstants;
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;
import com.scytl.cryptolib.proofs.bean.ProofPreComputedValues;
import com.scytl.cryptolib.proofs.cryptoapi.ProofPreComputerAPI;
import com.scytl.cryptolib.proofs.cryptoapi.ProofProverAPI;
import com.scytl.cryptolib.proofs.cryptoapi.ProofVerifierAPI;
import com.scytl.cryptolib.proofs.proof.Proof;
import com.scytl.cryptolib.proofs.service.ProofsService;

/**
 * Tests the Java-JavaScript interoperation for plaintext zero knowledge proof
 * of knowledge applications.
 */
public class TestPlaintextProof extends Command {

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

    private static final String JS_GENERATED_PLAINTEXT_PROOF_EXCEPTION_DATA_KEY =
        "Plaintext zero knowledge proof (JSON format) [JS]";

    private static final String JAVA_PRE_COMPUTED_PLAINTEXT_PROOF_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed plaintext proof values (JSON format) [Java]";

    private static final String JS_PRE_COMPUTED_PLAINTEXT_PROOF_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed plaintext proof values (JSON format) [JS]";

    private final Object _namespace;

    private final ZpSubgroup _zpSubgroup;

    private final String _zpSubgroupJson;

    private final ProofProverAPI _prover;

    private final ProofVerifierAPI _verifier;

    private final ProofPreComputerAPI _preComputer;

    private ElGamalPublicKey _publicKey;

    private String _publicKeyJson;

    private List<ZpGroupElement> _plaintext;

    private String _plaintextJson;

    private Witness _witness;

    private String _witnessJson;

    private Ciphertext _ciphertext;

    private String _ciphertextJson;

    private ProofPreComputedValues _javaPreComputedValues;

    private String _javaPreComputedValuesJson;

    private String _jsPreComputedValuesJson;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the plaintext zero
     *             knowledge proof of knowledge interoperation tests implemented
     *             in JavaScript.
     */
    public TestPlaintextProof() throws CommandException {
        super(ProofConstants.JS_PLAINTEXT_PROOF_TEST_PATH);

        _namespace =
            _engine.get(ProofConstants.JS_PLAINTEXT_PROOF_TEST_NAMESPACE);

        clearExceptionData();

        try {
            _zpSubgroup = MathematicalTestDataGenerator.getZpSubgroup();
            _zpSubgroupJson = _zpSubgroup.toJson();
            putExceptionData("Zp subgroup (JSON format) [Java]",
                _zpSubgroupJson);

            ProofsServiceAPI proofsService = new ProofsService();
            _prover = proofsService.createProofProverAPI(_zpSubgroup);
            _verifier = proofsService.createProofVerifierAPI(_zpSubgroup);
            _preComputer =
                proofsService.createProofPreComputerAPI(_zpSubgroup);

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

    /**
     * Generates a plaintext zero knowledge proof, verifies the result using
     * JavaScript, generates a plaintext zero knowledge proof with the same data
     * using JavaScript and verifies the result using Java. Repeats the same
     * steps described above, but using pre-computed values. If no exceptions
     * are thrown and all verifications are true, then the interoperation test
     * for plaintext zero knowledge proof of knowledge applications was
     * successful.
     *
     * @throws CommandException
     *             if a JavaScript method cannot be found, a cryptographic
     *             operation fails or a proof verification fails.
     */
    @Override
    public void execute() throws CommandException {

        String proofJson = generateProofUsingJava();

        verifyProofUsingJavaScript(proofJson);

        proofJson = generateProofUsingJavaScript();

        verifyProofUsingJava(proofJson);

        proofJson = generatePreComputedProofUsingJava();

        verifyPreComputedProofUsingJavaScript(proofJson);

        proofJson = generatePreComputedProofUsingJavaScript();

        verifyPreComputedProofUsingJava(proofJson);

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "INTEROPERATION TEST FOR PLAINTEXT ZERO KNOWLEDGE PROOF WAS SUCCESSFUL.");
    }

    private void generateInputData()
            throws GeneralCryptoLibException, CommandException {

        _plaintext = MathematicalTestDataGenerator.getZpGroupElements(
            _zpSubgroup, ProofConstants.PLAINTEXT_PROOF_PLAINTEXT_LENGTH);
        _plaintextJson = MathematicalCollectionSerializer
            .zpGroupElementsToJson(_plaintext);
        putExceptionData(
            "Plaintext used to generate ciphertext (JSON format) [Java]",
            _plaintextJson);

        _publicKey = ElGamalTestDataGenerator
            .getKeyPair(_zpSubgroup,
                ProofConstants.PLAINTEXT_PROOF_PLAINTEXT_LENGTH)
            .getPublicKeys();
        _publicKeyJson = _publicKey.toJson();
        putExceptionData("ElGamal public key (JSON format) [Java]",
            _publicKeyJson);

        ElGamalEncrypterValues encrypterValues =
            (ElGamalEncrypterValues) ElGamalTestDataGenerator
                .encryptGroupElements(_publicKey, _plaintext);
        _witness = encrypterValues;
        _witnessJson = _witness.getExponent().toJson();
        putExceptionData("Witness (JSON format) [Java]", _witnessJson);
        _ciphertext = encrypterValues;
        _ciphertextJson = encrypterValues.getComputationValues().toJson();
        putExceptionData("Ciphertext (JSON format) [Java]",
            _ciphertextJson);

        _javaPreComputedValues =
            _preComputer.preComputePlaintextProof(_publicKey);
        _javaPreComputedValuesJson = MathematicalCollectionSerializer
            .proofPreComputedValuesToJson(_javaPreComputedValues);

        try {
            _jsPreComputedValuesJson =
                (String) getInvocable().invokeMethod(_namespace,
                    ProofConstants.JS_PROOF_PRE_COMPUTED_VALUES_GENERATION_FUNCTION,
                    _zpSubgroupJson, _publicKeyJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_PLAINTEXT_PROOF_TEST_NAMESPACE
                        + "."
                        + ProofConstants.JS_PROOF_PRE_COMPUTED_VALUES_GENERATION_FUNCTION
                        + " in file "
                        + ProofConstants.JS_PLAINTEXT_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate pre-computed plaintext proof values, using JavaScript.",
                e);
        }
    }

    private String generateProofUsingJava() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating plaintext proof, using Java...");

        try {
            return _prover.createPlaintextProof(_publicKey, _ciphertext,
                _plaintext, _witness).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate plaintext proof, using Java.", e);
        }
    }

    private void verifyProofUsingJavaScript(final String proofJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying plaintext proof, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_VERIFICATION_FUNCTION,
                _zpSubgroupJson, _publicKeyJson, _plaintextJson,
                _ciphertextJson, proofJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ProofConstants.JS_PLAINTEXT_PROOF_TEST_NAMESPACE
                    + "." + ProofConstants.JS_PROOF_VERIFICATION_FUNCTION
                    + " in resources file "
                    + ProofConstants.JS_PLAINTEXT_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify Java generated plaintext proof, using JavaScript.",
                e);
        }
    }

    private String generateProofUsingJavaScript() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating plaintext proof, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_GENERATION_FUNCTION,
                _zpSubgroupJson, _publicKeyJson, _plaintextJson,
                _witnessJson, _ciphertextJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_PLAINTEXT_PROOF_TEST_NAMESPACE
                        + "." + ProofConstants.JS_PROOF_GENERATION_FUNCTION
                        + " in file "
                        + ProofConstants.JS_PLAINTEXT_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate plaintext proof, using JavaScript.",
                e);
        }
    }

    private void verifyProofUsingJava(final String proofJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying plaintext proof, using Java...");

        putExceptionData(JS_GENERATED_PLAINTEXT_PROOF_EXCEPTION_DATA_KEY,
            proofJson);

        try {
            boolean verified = _verifier.verifyPlaintextProof(_publicKey,
                _ciphertext, _plaintext, Proof.fromJson(proofJson));

            if (!verified) {
                throw new GeneralCryptoLibException(
                    "Verification of plaintext proof (JSON format) "
                        + proofJson + " was false.");
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not verify JavaScript generated plaintext proof, using Java.",
                e);
        }
    }

    private String generatePreComputedProofUsingJava()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating plaintext proof, via pre-computed values, using Java...");

        removeExceptionData(
            JS_PRE_COMPUTED_PLAINTEXT_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JAVA_PRE_COMPUTED_PLAINTEXT_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _javaPreComputedValuesJson);

        try {
            return _prover.createPlaintextProof(_publicKey, _ciphertext,
                _plaintext, _witness, _javaPreComputedValues).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate plaintext proof, via pre-computed values, using Java.",
                e);
        }
    }

    private void verifyPreComputedProofUsingJavaScript(
            final String proofJson) throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying pre-computed plaintext proof, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_VERIFICATION_FUNCTION,
                _zpSubgroupJson, _publicKeyJson, _plaintextJson,
                _ciphertextJson, proofJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ProofConstants.JS_PLAINTEXT_PROOF_TEST_NAMESPACE
                    + "." + ProofConstants.JS_PROOF_VERIFICATION_FUNCTION
                    + " in resources file "
                    + ProofConstants.JS_PLAINTEXT_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify Java generated pre-computed plaintext proof, using JavaScript.",
                e);
        }
    }

    private String generatePreComputedProofUsingJavaScript()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating plaintext proof, via pre-computed values, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_GENERATION_FUNCTION,
                _zpSubgroupJson, _publicKeyJson, _plaintextJson,
                _witnessJson, _ciphertextJson, _jsPreComputedValuesJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_PLAINTEXT_PROOF_TEST_NAMESPACE
                        + "." + ProofConstants.JS_PROOF_GENERATION_FUNCTION
                        + " in file "
                        + ProofConstants.JS_PLAINTEXT_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate plaintext proof, via pre-computed values, using JavaScript.",
                e);
        }
    }

    private void verifyPreComputedProofUsingJava(final String proofJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying pre-computed plaintext proof, using Java...");

        putExceptionData(JS_GENERATED_PLAINTEXT_PROOF_EXCEPTION_DATA_KEY,
            proofJson);
        removeExceptionData(
            JAVA_PRE_COMPUTED_PLAINTEXT_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JS_PRE_COMPUTED_PLAINTEXT_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _jsPreComputedValuesJson);

        try {
            boolean verified = _verifier.verifyPlaintextProof(_publicKey,
                _ciphertext, _plaintext, Proof.fromJson(proofJson));

            if (!verified) {
                throw new GeneralCryptoLibException(
                    "Verification of plaintext proof (JSON format) "
                        + proofJson + " was false.");
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not verify JavaScript generated pre-computed plaintext proof, using Java.",
                e);
        }
    }
}
