/*
 * 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 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.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.primitives.primes.utils.PrimitivesTestDataGenerator;
import com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants;
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;
import com.scytl.cryptolib.test.tools.utils.CommonTestDataGenerator;

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

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

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

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

    private static final String JS_PRE_COMPUTED_SCHNORR_PROOF_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed Schnorr 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 String _voterId;

    private String _electionEventId;

    private Witness _witness;

    private String _witnessJson;

    private static ZpGroupElement _exponentiatedGenerator;

    private static String _exponentiatedGeneratorJson;

    private ProofPreComputedValues _javaPreComputedValues;

    private String _javaPreComputedValuesJson;

    private String _jsPreComputedValuesJson;

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

        _namespace =
            _engine.get(ProofConstants.JS_SCHNORR_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 Schnorr zero knowledge proof, verifies the result using
     * JavaScript, generates a Schnorr 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 the Schnorr 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 SCHNORR ZERO KNOWLEDGE PROOF WAS SUCCESSFUL.");
    }

    private void generateInputData()
            throws GeneralCryptoLibException, CommandException {

        int numChars = CommonTestDataGenerator.getInt(1,
            SecureRandomConstants.MAXIMUM_GENERATED_STRING_LENGTH);
        _voterId = PrimitivesTestDataGenerator.getString64(numChars);
        putExceptionData("Voter ID [Java]", _voterId);

        _electionEventId =
            PrimitivesTestDataGenerator.getString64(numChars);
        putExceptionData("Election event ID [Java]", _electionEventId);

        _witness = ElGamalTestDataGenerator.getWitness(_zpSubgroup);
        _witnessJson = _witness.getExponent().toJson();
        putExceptionData("Witness (JSON format) [Java]", _witnessJson);

        ZpGroupElement generatorElement =
            new ZpGroupElement(_zpSubgroup.getG(), _zpSubgroup);
        _exponentiatedGenerator =
            generatorElement.exponentiate(_witness.getExponent());
        _exponentiatedGeneratorJson = _exponentiatedGenerator.toJson();
        putExceptionData("Exponentiated generator (JSON format) [Java]",
            _exponentiatedGeneratorJson);

        _javaPreComputedValues = _preComputer.preComputeSchnorrProof();
        _javaPreComputedValuesJson = MathematicalCollectionSerializer
            .proofPreComputedValuesToJson(_javaPreComputedValues);

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

    private String generateProofUsingJava() throws CommandException {

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

        try {
            return _prover.createSchnorrProof(_voterId, _electionEventId,
                _exponentiatedGenerator, _witness).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate Schnorr proof, using Java.", e);
        }
    }

    private void verifyProofUsingJavaScript(final String proofJson)
            throws CommandException {

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

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

    private String generateProofUsingJavaScript() throws CommandException {

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

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

    private void verifyProofUsingJava(final String proofJson)
            throws CommandException {

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

        putExceptionData(JS_GENERATED_SCHNORR_PROOF_EXCEPTION_DATA_KEY,
            proofJson);

        try {
            boolean verified =
                _verifier.verifySchnorrProof(_exponentiatedGenerator,
                    _voterId, _electionEventId, Proof.fromJson(proofJson));

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

    private String generatePreComputedProofUsingJava()
            throws CommandException {

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

        removeExceptionData(
            JS_PRE_COMPUTED_SCHNORR_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JAVA_PRE_COMPUTED_SCHNORR_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _javaPreComputedValuesJson);

        try {
            return _prover.createSchnorrProof(_voterId, _electionEventId,
                _exponentiatedGenerator, _witness, _javaPreComputedValues)
                .toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate Schnorr proof, via pre-computed values, using Java.",
                e);
        }
    }

    private void verifyPreComputedProofUsingJavaScript(
            final String proofJson) throws CommandException {

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

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

    private String generatePreComputedProofUsingJavaScript()
            throws CommandException {

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

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

    private void verifyPreComputedProofUsingJava(final String proofJson)
            throws CommandException {

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

        putExceptionData(JS_GENERATED_SCHNORR_PROOF_EXCEPTION_DATA_KEY,
            proofJson);
        removeExceptionData(
            JAVA_PRE_COMPUTED_SCHNORR_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JS_PRE_COMPUTED_SCHNORR_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _jsPreComputedValuesJson);

        try {
            boolean verified =
                _verifier.verifySchnorrProof(_exponentiatedGenerator,
                    _voterId, _electionEventId, Proof.fromJson(proofJson));

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