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

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.mathematical.groups.impl.Exponent;
import com.scytl.ov.mixing.commons.beans.proofs.SingleValueProductProofAnswer;
import com.scytl.ov.mixing.commons.beans.proofs.SingleValueProductProofInitialMessage;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.CommitmentParams;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.PrivateCommitment;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.PublicCommitment;
import com.scytl.ov.mixing.commons.tools.ExponentTools;
import com.scytl.ov.mixing.commons.tools.MultiExponentiation;
import com.scytl.ov.mixing.commons.tools.RandomOracleHash;
import com.scytl.ov.mixing.proofs.bg.product.ProductProofGenerator;
import com.scytl.ov.mixing.proofs.bg.state.StateHarvester;
import com.scytl.ov.mixing.proofs.bg.state.StateLabel;

/**
 * Implements the parallel mode.
 */
public class ParallelSingleValueProductProofGenerator implements SingleValueProductProofGenerator {

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

    private final int n;

    private final BigInteger groupOrder;

    private final CommitmentParams comParams;

    private final MultiExponentiation multiExponentiation;

    public ParallelSingleValueProductProofGenerator(final int n, final CommitmentParams comParams,
            final BigInteger order, final MultiExponentiation multiExponentiation) {
        this.n = n;
        this.comParams = comParams;
        this.groupOrder = order;
        this.multiExponentiation = multiExponentiation;
    }

    @Override
    public void generate(final StateHarvester harvester) {

        harvester.collect(Produced.RANDOM_ORACLE, new RandomOracleHash(groupOrder));

        prepare(harvester);

        LOG.info("\t PP SVP Generating initial message...");
        generateInitialMessage(harvester);
        LOG.info("\t PP SVP Generating answer...");
        generateAnswer(harvester);
    }

    private void prepare(final StateHarvester harvester) {

        try {
            final PrivateCommitment privateCommitments =
                harvester.offer(ProductProofGenerator.Produced.PRIVATE_COMMITS_Cb, PrivateCommitment.class);

            final Exponent[] exponentsA = privateCommitments.getM();

            final Exponent[] exponentsB = calculateExponentsB(n, exponentsA);

            final Exponent statementB = exponentsB[n - 1];

            final Exponent[] exponentsD = ExponentTools.getVectorRandomExponent(n, groupOrder);

            final Exponent[] delta = ExponentTools.getVectorRandomExponent(n, groupOrder);

            delta[0] = exponentsD[0];
            delta[n - 1] = new Exponent(groupOrder, BigInteger.ZERO);

            final Exponent[] cLowerDeltaComponents = new Exponent[n - 1];
            final Exponent[] cCapitalDeltaComponents = new Exponent[n - 1];

            for (int i = 0; i < n - 1; i++) {
                cLowerDeltaComponents[i] = delta[i].multiply(exponentsD[i + 1]).negate();

                cCapitalDeltaComponents[i] = delta[i + 1].add(exponentsA[i + 1].multiply(delta[i]).negate())
                    .add(exponentsB[i].multiply(exponentsD[i + 1]).negate());
            }

            harvester.collect(Produced.STATEMENT_B, statementB);
            harvester.collect(Produced.EXPONENTS_A, exponentsA);
            harvester.collect(Produced.EXPONENTS_B, exponentsB);
            harvester.collect(Produced.EXPONENTS_D, exponentsD);
            harvester.collect(Produced.EXPONENTS_DELTA, delta);
            harvester.collect(Produced.EXPONENTS_LOW_DELTA, cLowerDeltaComponents);
            harvester.collect(Produced.EXPONENTS_CAPITAL_DELTA, cCapitalDeltaComponents);

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

    private static Exponent[] calculateExponentsB(final int length, final Exponent[] exponents) {

        try {
            final Exponent[] productExponents = new Exponent[length];
            productExponents[0] = exponents[0];
            for (int i = 1; i < productExponents.length; i++) {
                productExponents[i] = productExponents[i - 1].multiply(exponents[i]);
            }
            return productExponents;

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

    private void generateInitialMessage(final StateHarvester harvester) {

        PrivateCommitment cD = generateCommitmentFromExponents(Produced.EXPONENTS_D, harvester, Produced.EXPONENT_RD);

        PrivateCommitment cLowerDelta =
            generateCommitmentFromExponents(Produced.EXPONENTS_LOW_DELTA, harvester, Produced.EXPONENT_S1);

        PrivateCommitment cCapitalDeltaComponents =
            generateCommitmentFromExponents(Produced.EXPONENTS_CAPITAL_DELTA, harvester, Produced.EXPONENT_SX);

        final SingleValueProductProofInitialMessage ini =
            new SingleValueProductProofInitialMessage(cD, cLowerDelta, cCapitalDeltaComponents);

        final RandomOracleHash randomOracle = harvester.offer(Produced.RANDOM_ORACLE, RandomOracleHash.class);
        final PublicCommitment statementCa =
            harvester.offer(ProductProofGenerator.Produced.PUBLIC_COMMITMENT_Cb, PublicCommitment.class);
        final Exponent statementB = harvester.offer(Produced.STATEMENT_B, Exponent.class);

        // c_{a}
        randomOracle.addDataToRO(statementCa);
        // b
        randomOracle.addDataToRO(statementB);
        randomOracle.addDataToRO(ini);

        harvester.collect(Produced.INITIAL_MSG, ini);
    }

    private PrivateCommitment generateCommitmentFromExponents(final StateLabel requiredExponentsArrayLabel,
            final StateHarvester harvester, final StateLabel producedExponentLabel) {

        final Exponent[] exponentsArrays = harvester.offer(requiredExponentsArrayLabel, Exponent[].class);

        final Exponent exponent = ExponentTools.getRandomExponent(groupOrder);

        harvester.collect(producedExponentLabel, exponent);

        return new PrivateCommitment(exponentsArrays, exponent, comParams, multiExponentiation);

    }

    private void generateAnswer(final StateHarvester harvester) {

        try {
            final RandomOracleHash randomOracle = harvester.offer(Produced.RANDOM_ORACLE, RandomOracleHash.class);

            final Exponent challengeX = randomOracle.getHash();
            randomOracle.reset();

            Exponent[] tildeA =
                calculateTildeExponentsArray(challengeX, harvester, Produced.EXPONENTS_A, Produced.EXPONENTS_D);

            Exponent[] tildeB =
                calculateTildeExponentsArray(challengeX, harvester, Produced.EXPONENTS_B, Produced.EXPONENTS_DELTA);

            final PrivateCommitment privateCommitments =
                harvester.offer(ProductProofGenerator.Produced.PRIVATE_COMMITS_Cb, PrivateCommitment.class);

            final Exponent r = privateCommitments.getR();
            final Exponent rd = harvester.offer(Produced.EXPONENT_RD, Exponent.class);
            Exponent tildeR = challengeX.multiply(r).add(rd);

            final Exponent sx = harvester.offer(Produced.EXPONENT_SX, Exponent.class);
            final Exponent s1 = harvester.offer(Produced.EXPONENT_S1, Exponent.class);
            final Exponent tildeS = challengeX.multiply(sx).add(s1);

            harvester.collect(Produced.ANSWER, new SingleValueProductProofAnswer(tildeA, tildeB, tildeR, tildeS));

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

    private static Exponent[] calculateTildeExponentsArray(final Exponent challengeX, final StateHarvester harvester,
            final StateLabel firstRequiredExponentsArrayLabel, final StateLabel secondRequiredExponentsArrayLabel) {

        final Exponent[] firstRequiredExponentsArray =
            harvester.offer(firstRequiredExponentsArrayLabel, Exponent[].class);
        final Exponent[] secondRequiredExponentsArray =
            harvester.offer(secondRequiredExponentsArrayLabel, Exponent[].class);

        return ExponentTools.addTwoVectors(ExponentTools.multiplyByScalar(firstRequiredExponentsArray, challengeX),
            secondRequiredExponentsArray);
    }

}
