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

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.bean.CiphertextImpl;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.ov.mixing.commons.beans.proofs.MultiExponentiationReductionAnswer;
import com.scytl.ov.mixing.commons.beans.proofs.MultiExponentiationReductionInitialMessage;
import com.scytl.ov.mixing.commons.homomorphic.Cryptosystem;
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;
import com.scytl.ov.mixing.proofs.bg.multiexp.basic.MultiExponentiationBasicProofVerifier;

/**
 *
 */
public class MultiExponentiationReductionProofVerifier {

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

    private final Cryptosystem cryptosystem;

    private final ZpSubgroup group;

    private final CommitmentParams compars;

    private final Ciphertext[][] vecC;

    private final Ciphertext c;

    private final PublicCommitment[] cA;

    private final int mu;

    private final int n;

    private final int m;

    private final int mprime;

    private final int numiterationsleft;

    private final int lengthB;

    private final RandomOracleHash rO;

    private final CiphertextTools ciphertextTools;

    private final MultiExponentiation multiExponentiation;

    public MultiExponentiationReductionProofVerifier(final int m, final int n, final Cryptosystem cryptosystem,
            final ZpSubgroup group, final CommitmentParams compars, final Ciphertext[][] vecC, final Ciphertext c,
            final PublicCommitment[] cA, final int mu, final int numiterations, final CiphertextTools ciphertextTools,
            final MultiExponentiation multiExponentiation) {
        this.cryptosystem = cryptosystem;
        this.lengthB = this.cryptosystem.getNumberOfMessages();
        this.group = group;
        this.compars = compars;
        this.vecC = vecC;
        this.c = c;
        this.cA = cA;
        this.n = n;
        this.m = m;
        this.mu = mu;
        this.numiterationsleft = numiterations - 1;
        this.mprime = this.cA.length / this.mu; // assuming it is really a multiple
        this.rO = new RandomOracleHash(this.group.getQ());
        this.ciphertextTools = ciphertextTools;
        this.multiExponentiation = multiExponentiation;
    }

    public boolean verify(final MultiExponentiationReductionInitialMessage initial,
            final MultiExponentiationReductionAnswer answer) {

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

        boolean correct = true;

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

        final Ciphertext[] cE = initial.getCiphertextsE();

        // Checking that things are group elements

        if (cb.length != 2 * mu - 1) {
            LOG.error("ERROR(multiExpoReductionArg): 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(multiExpoReductionArg): cB[" + k + "] is not a group element");
                correct = false;
            }
        }

        if (cE.length != 2 * mu - 1) {
            LOG.error("ERROR(multiExpoReductionArg): 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(multiExpoReductionArg): E[" + k + "] is not a valid ciphertext");

                correct = false;
            }
        }

        final Exponent[] b = answer.getExponentsB();
        final Exponent s = answer.getExponentS();

        if (b.length != lengthB) {
            correct = false;
            LOG.error("ERROR(multiExpoReductionArg): b length is not  valid ");
        }
        for (int i = 0; i < b.length; i++) {
            if (!ExponentTools.isExponent(b[i])) {
                correct = false;
                LOG.error("ERROR(multiExpoReductionArg): b[" + i + "] is not a valid exponent");
            }
            if (!hasSameOrder(b[i])) {
                correct = false;
                LOG.error("ERROR(multiExpoReductionArg): b[" + i + "] is not a group element");
            }
        }

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

        try {
            // Checking that cb[mu-1] is a commitment to 0

            if (!cb[mu - 1].verifyOpening(
                //
                new Exponent[] {new Exponent(group.getQ(), BigInteger.ZERO) }, //
                new Exponent(group.getQ(), BigInteger.ZERO), compars, multiExponentiation)) {

                correct = false;
                LOG.error("ERROR(multiExpoReductionArg): cb[mu-1] is not" + " a commitment to 0 with randomness 0");
            }

            // checking that C is equal to E[_mu-1]
            if (!c.equals(cE[mu - 1])) {
                LOG.error("ERROR(multiExpoReductionArg): C is not" + " equal to E[_mu-1]");
                correct = false;
            }

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

            // We first compute the commitments from the challenge
            PublicCommitment comCb = cb[0];

            Exponent accumulator = challengeX;

            for (int i = 1; i < cb.length; i++) {
                comCb = comCb.multiply(cb[i].exponentiate(accumulator));

                accumulator = accumulator.multiply(challengeX);
            }

            // Now we check the openings

            if (!comCb.verifyOpening(b, s, compars, multiExponentiation)) {
                LOG.error("ERROR(multiExpoReductionArg): the commitment to B is incorrect");
                correct = false;
            }

            // Prepare the basic argument

            final Ciphertext[][] vecCprime = new CiphertextImpl[mprime][n];
            final PublicCommitment[] cAprime = new PublicCommitment[mprime];
            Exponent one = new Exponent(group.getQ(), BigInteger.ONE);
            for (int l = 0; l < mprime; l++) {
                Exponent xacum = one;
                for (int i = 0; i < mu; i++) {
                    // compute Cl' component by component
                    for (int k = 0; k < n; k++) {
                        final Ciphertext aux = vecC[mu * l + mu - 1 - i][k].exponentiate(xacum);
                        vecCprime[l][k] = i == 0 ? aux : vecCprime[l][k].multiply(aux);
                    }
                    // compute ca'l
                    final PublicCommitment aux = cA[mu * l + i].exponentiate(xacum);
                    cAprime[l] = i == 0 ? aux : cAprime[l].multiply(aux);
                    xacum = xacum.multiply(challengeX);
                }
            }

            Exponent xacum = one;
            final Exponent[] vecX = new Exponent[2 * mu - 1];
            for (int i = 0; i < vecX.length; i++) {
                vecX[i] = xacum;
                xacum = xacum.multiply(challengeX);
            }

            CiphertextImpl cPrime = ciphertextTools.compVecCiphVecExp(cE, vecX);

            cryptosystem.getEncryptionOf1();

            // [VM]: THIS SHOULD NOT HAVE BEEN COMMENTED!!
            // final Ciphertext aux = _cryptosystem.encryptRaisingToRandom(auxb, _cryptosystem.get0Randomness());
            // Cprime = Cprime.multiply(aux);

            if (numiterationsleft == 0) {
                final MultiExponentiationBasicProofVerifier verifier =
                    new MultiExponentiationBasicProofVerifier(m, n, cryptosystem, compars, vecCprime, cPrime,
                        cAprime, group.getQ(), ciphertextTools, multiExponentiation);
                if (!verifier.verify(answer.getIniBasic(), answer.getAnsBasic())) {
                    LOG.error("ERROR(multiExpoReductionArg):  MultiExpoBasic argument didn't verify");
                    correct = false;
                }
            } else {
                final MultiExponentiationReductionProofVerifier verifier =
                    new MultiExponentiationReductionProofVerifier(m, n, cryptosystem, group, compars, vecCprime,
                        cPrime, cAprime, mu, numiterationsleft, ciphertextTools, multiExponentiation);
                if (!verifier.verify(answer.getIniReduct(), answer.getAnsReduct())) {
                    LOG.error("ERROR(multiExpoReductionArg):  MultiExpoBasic argument didn't verify");
                    correct = false;
                }
            }
            if (correct) {
                LOG.info("The Multi Expo Reduction Argument was verified successfully!");
            }
            return correct;

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

    private boolean hasSameOrder(Exponent exponent) {
        return compars.getGroup().getQ().equals(exponent.getQ());
    }
}
