/*
 * 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.ov.mixing.proofs.bg.multiexp.basic;

import java.math.BigInteger;

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

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.ov.mixing.commons.beans.proofs.MultiExponentiationBasicProofAnswer;
import com.scytl.ov.mixing.commons.beans.proofs.MultiExponentiationBasicProofInitialMessage;
import com.scytl.ov.mixing.commons.homomorphic.Cryptosystem;
import com.scytl.ov.mixing.commons.homomorphic.Randomness;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.CommitmentParams;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.PublicCommitment;
import com.scytl.ov.mixing.commons.tools.CiphertextTools;
import com.scytl.ov.mixing.commons.tools.ExponentTools;
import com.scytl.ov.mixing.commons.tools.GroupTools;
import com.scytl.ov.mixing.commons.tools.MultiExponentiation;
import com.scytl.ov.mixing.commons.tools.RandomOracleHash;

/**
 *
 */
public class MultiExponentiationBasicProofVerifier {

    private static final Logger LOG = LoggerFactory.getLogger("std");

    private final Cryptosystem cryptosystem;

    private final Ciphertext[][] vecC;

    private final Ciphertext chipertext;

    private final PublicCommitment[] cA;

    private final int m;

    private final int n;

    private final RandomOracleHash rO;

    private final BigInteger groupOrder;

    private final CommitmentParams comParams;

    private final CiphertextTools ciphertextTools;

    private final MultiExponentiation multiExponentiation;

    public MultiExponentiationBasicProofVerifier(final int m, final int n, final Cryptosystem cryptosystem,
            final CommitmentParams compars, final Ciphertext[][] vecC, final Ciphertext c, final PublicCommitment[] cA,
            final BigInteger groupOrder, final CiphertextTools ciphertextTools,
            final MultiExponentiation multiExponentiation) {
        this.cryptosystem = cryptosystem;
        this.comParams = compars;
        this.vecC = vecC;
        this.chipertext = c;
        this.cA = cA;
        this.m = m;
        this.n = n;
        this.groupOrder = groupOrder;
        this.rO = new RandomOracleHash(this.groupOrder);

        this.ciphertextTools = ciphertextTools;
        this.multiExponentiation = multiExponentiation;
    }

    public boolean verify(final MultiExponentiationBasicProofInitialMessage initial,
            final MultiExponentiationBasicProofAnswer answer) {

        final PublicCommitment cA0 = initial.getCommitmentPublicA0();

        final PublicCommitment[] cB = initial.getCommitmentPublicB();

        final Ciphertext[] cE = initial.getCiphertextsE();
        
        boolean correct = checkingAsGroupElements(cA0, cB, cE);
  
        final Exponent[] a = answer.getExponentsA();
        final Exponent b = answer.getExponentsB();
        final Exponent r = answer.getExponentR();
        final Exponent s = answer.getExponentS();
        final Randomness tau = answer.getRandomnessTau();

        correct = checkingAsValidExponents(a, r, s, tau, correct);
  
        try {
            // Checking that cB[m] is a commitment to 0
            if (!cB[m].verifyOpening(new Exponent[] {new Exponent(groupOrder, BigInteger.ZERO) }, //
                new Exponent(groupOrder, BigInteger.ZERO), comParams, multiExponentiation)) {

                correct = false;
                LOG.error("ERROR(multiExpoBasicArg): cB[m] is not" + " a commitment to 0 with randomness 0");
            }

            // checking that C is equal to E[_m]
            if (!chipertext.equals(cE[m])) {
                LOG.error("ERROR(multiExpoBasicArg): C is not" + " equal to E[_m]");
                correct = false;
            }

            // Done with the membership checks. Now checking commitments match

            // We first compute the commitments from the challenge

            PublicCommitment comCA = cA0;
            PublicCommitment comCB = cB[0];
            Ciphertext acumE = cE[0];
            Ciphertext acumC = cryptosystem.encryptRaisingToRandom(b, tau);

            Exponent oldaccumulator = new Exponent(groupOrder, BigInteger.ONE);

            rO.addDataToRO(vecC);
            rO.addDataToRO(chipertext);
            rO.addDataToRO(cA);
            rO.addDataToRO(initial);
            final Exponent challengeX = rO.getHash();

            Exponent accumulator = challengeX;

            for (int i = 1; i <= m; i++) {
                comCA = comCA.multiply(cA[i - 1].exponentiate(accumulator));
                comCB = comCB.multiply(cB[i].exponentiate(accumulator));
                acumE = acumE.multiply(cE[i].exponentiate(accumulator));
                final Exponent[] aux = ExponentTools.multiplyByScalar(a, oldaccumulator);

                acumC = acumC.multiply(ciphertextTools.compVecCiphVecExp(vecC[m - i], aux));

                oldaccumulator = accumulator;
                accumulator = accumulator.multiply(challengeX);
            }

            for (int i = m + 1; i < 2 * m; i++) {
                comCB = comCB.multiply(cB[i].exponentiate(accumulator));
                acumE = acumE.multiply(cE[i].exponentiate(accumulator));
                accumulator = accumulator.multiply(challengeX);
            }

            // Now we check the openings

            if (!comCA.verifyOpening(a, r, comParams, multiExponentiation)) {
                LOG.error("ERROR(multiExpoBasicArg): the commitment to a is incorrect");
                correct = false;
            }

            if (!comCB.verifyOpening(b, s, comParams, multiExponentiation)) {
                LOG.error("ERROR(multiExpoBasicArg): the commitment to B is incorrect");
                correct = false;
            }
            if (!acumE.equals(acumC)) {
                LOG.error("ERROR(multiExpoBasicArg): the encryptions don't match");
                correct = false;
            }

            if (correct) {
                LOG.info("The Basic Multi Expo Argument was verified successfully!");
            }
            return correct;

        } catch (GeneralCryptoLibException ex) {
            throw new CryptoLibException(ex);
        }
    }

    
	/**
	 * Checking that things are group elements
	 * 
	 * @param cA0 PublicCommitment
	 * @param cB  PublicCommitment[]	
	 * @param cE   Ciphertext[]
	 * @return boolean checking result
	 */
    private boolean checkingAsGroupElements(PublicCommitment cA0, PublicCommitment[] cB, Ciphertext[] cE) {
    	
    	boolean correct = true;
    	
    	if (!GroupTools.isGroupElement(cA0.getElement())) {
            LOG.error("ERROR(multiExpoBasicArg): cA0 is not a group element");
            correct = false;
        }

        if (cB.length != 2 * m) {
            LOG.error("ERROR(multiExpoBasicArg): cB does not have" + " the expected length");
            correct = false;
        }

        for (int k = 0; k < cB.length; k++) {
            if (!GroupTools.isGroupElement(cB[k].getElement())) {
                LOG.error("ERROR(multiExpoBasicArg): cB[" + k + "] is not a group element");
                correct = false;
            }
        }

        if (cE.length != 2 * m) {
            LOG.error("ERROR(multiExpoBasicArg): E does not have" + " the expected length");
            correct = false;
        }

        for (int k = 0; k < cE.length; k++) {
            if (!CiphertextTools.isValidCiphertext(cE[k])) {
                LOG.error("ERROR(multiExpoBasicArg): E[" + k + "] is not a valid ciphertext");
                correct = false;
            }
        }
        
        return correct;
    }
    
	/**
	 * Checking that things are valid exponents
	 * 
	 * @param a Exponent[]
	 * @param b Exponent
	 * @param r Exponent
	 * @param s Exponent
	 * @param tau Randomness
	 * @param checkingAsGroupElements
	 * @return boolean checking result
	 */
    private boolean checkingAsValidExponents(Exponent[] a, Exponent r, Exponent s, Randomness tau,
			boolean checkingAsGroupElements) {

		boolean correct = checkingAsGroupElements;

		if (a.length != n) {
            correct = false;
            LOG.error("ERROR(multiExpoBasicArg): a doesn't have the" + " expected length");
        }
		
        for (int i = 0; (i < a.length) && correct; i++) {
            if (!ExponentTools.isExponent(a[i])) {
                correct = false;
                LOG.error("ERROR(multiExpoBasicArg): a[" + i + "] is not a valid exponent");
            }
            
            if (!hasSameOrder(a[i])) {
                correct = false;
                LOG.error("ERROR(multiExpoBasicArg): a[" + i + "] is not a group element");
            }
        }

        if (!ExponentTools.isExponent(r)) {
            correct = false;
            LOG.error("ERROR(multiExpoBasicArg): r is not a valid exponent");
        }
        
        if (!hasSameOrder(r)) {
            correct = false;
            LOG.error("ERROR(multiExpoBasicArg): r is not a group element");
        }

        if (!ExponentTools.isExponent(s)) {
            correct = false;
            LOG.error("ERROR(multiExpoBasicArg): s is not a valid exponent");
        }
        
        if (!hasSameOrder(s)) {
            correct = false;
            LOG.error("ERROR(multiExpoBasicArg): s is not a group element");
        }

        if (!tau.isRandomness()) {
            correct = false;
            LOG.error("ERROR(multiExpoBasicArg): tau is not a valid randomness");
        }
        
        if (!hasSameOrder(tau.getExponent())) {
            correct = false;
            LOG.error("ERROR(multiExpoBasicArg): tau exponent is not a group element");
        }
        
		return correct;
    }
    
    private boolean hasSameOrder(Exponent exponent) {
        return groupOrder.equals(exponent.getQ());
    }
}
