/*
 * 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.products.ov.mixnet.shuffler.reencryption;

import java.util.ArrayList;
import java.util.List;

import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.ov.mixnet.commons.ballots.ElGamalEncryptedBallots;
import com.scytl.products.ov.mixnet.commons.concurrent.LoopParallelizerTask;
import com.scytl.products.ov.mixnet.commons.concurrent.ParallelProcessTask;
import com.scytl.products.ov.mixnet.commons.concurrent.operations.EncryptedBallotsCollectionAdderOperation;
import com.scytl.products.ov.mixnet.commons.concurrent.operations.PreComputationsCollectionAdderOperation;
import com.scytl.products.ov.mixnet.commons.concurrent.processor.EncryptConcurrentCalculatorProcessor;
import com.scytl.products.ov.mixnet.commons.concurrent.processor.MultiplyConcurrentCalculatorProcessor;
import com.scytl.products.ov.mixnet.commons.concurrent.processor.PreComputationCalculatorProcessor;
import com.scytl.products.ov.mixnet.commons.homomorphic.Cryptosystem;
import com.scytl.products.ov.mixnet.commons.homomorphic.Randomness;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.GjosteenElGamal;
import com.scytl.products.ov.mixnet.commons.tools.LoopTools;
import com.scytl.products.ov.mixnet.commons.tools.LoopTools.Range;

/**
 * Provides functionality that is equivalent to recrypting the ballots contained within a
 * {@link ElGamalEncryptedBallots}.
 */
public class ElGamalReEncrypter implements ReEncrypter {

    private final int concurrencyLevel;

    private final Cryptosystem cryptoSystem;

    private final ZpSubgroup group;

    public ElGamalReEncrypter(final ZpSubgroup group, final ElGamalPublicKey publicKey, final int concurrencyLevel) {

        this.group = group;
        this.cryptoSystem = new GjosteenElGamal(group, publicKey);

        this.concurrencyLevel = concurrencyLevel;
    }

    /**
     * @see ReEncrypter#reEncrypt(ElGamalEncryptedBallots, Randomness[], List)
     */
    @Override
    public ElGamalEncryptedBallots reEncrypt(final ElGamalEncryptedBallots elGamalEncryptedBallots,
            final Randomness[] randomExponents, final List<Ciphertext> batchPreComputations) {

        validateInputs(elGamalEncryptedBallots, randomExponents);

        List<Ciphertext> inputEncryptedBallots = elGamalEncryptedBallots.getBallots();
        int lengthEncryptedBallots = inputEncryptedBallots.size();
        
        ParallelProcessTask<List<Ciphertext>> process;
        if (batchPreComputations == null) { // no preComputations were given for this batch
            process = new EncryptConcurrentCalculatorProcessor(randomExponents, inputEncryptedBallots, cryptoSystem, group);
        } else {
            int preComputationsSize = batchPreComputations.size();
            int numPreComputationsRequired = lengthEncryptedBallots - preComputationsSize;
            // firstly we add the already precomputed elements
            ZpGroupElement[] arrayOfIdentityElement =
                getArrayOfIdentityElement(elGamalEncryptedBallots.getBallots().get(0).getPhis().size());
            List<Ciphertext> completePreComputations =
                addRemainingPreComputations(randomExponents, batchPreComputations, preComputationsSize,
                    numPreComputationsRequired, arrayOfIdentityElement, concurrencyLevel);
            process = new MultiplyConcurrentCalculatorProcessor(completePreComputations, inputEncryptedBallots);
        }
        
        List<Range> ranges = LoopTools.asRangesList(lengthEncryptedBallots, concurrencyLevel);
        
        LoopParallelizerTask<List<Ciphertext>> task =
                new LoopParallelizerTask<>(process, ranges, new EncryptedBallotsCollectionAdderOperation());
        return new ElGamalEncryptedBallots(task.invoke());
    }

    private List<Ciphertext> addRemainingPreComputations(final Randomness[] randomExponents,
            final List<Ciphertext> preComputations, final int preComputationsSize, final int numPreComputationsRequired,
            final ZpGroupElement[] arrayOfIdentityElement, final int concurrencyLevel) {

        List<Ciphertext> completePreComputations = new ArrayList<>();
        completePreComputations.addAll(preComputations);

        if (numPreComputationsRequired > 0) {

            List<Range> ranges = LoopTools.asRangesList(numPreComputationsRequired, concurrencyLevel);

            Randomness[] requiredRandomExponents = new Randomness[numPreComputationsRequired];
            System.arraycopy(randomExponents, preComputationsSize, requiredRandomExponents, 0,
                numPreComputationsRequired);

            PreComputationCalculatorProcessor process =
                new PreComputationCalculatorProcessor(arrayOfIdentityElement, requiredRandomExponents, cryptoSystem);

            LoopParallelizerTask<List<Ciphertext>> task =
                new LoopParallelizerTask<>(process, ranges, new PreComputationsCollectionAdderOperation());

            completePreComputations.addAll(task.invoke());
        }

        return completePreComputations;
    }

    private static void validateInputs(final ElGamalEncryptedBallots elGamalEncryptedBallots,
            final Randomness[] randomExponents) {

        if (elGamalEncryptedBallots == null || randomExponents == null) {
            throw new IllegalArgumentException("The given parameters should be initialized.");
        } else if (elGamalEncryptedBallots.getBallots().size() != randomExponents.length) {
            throw new IllegalArgumentException(
                "The given list of ballots and the random exponents must be of the same length.");
        }

    }

    private ZpGroupElement[] getArrayOfIdentityElement(final int numRequiredElements) {

        final ZpGroupElement identityElement = group.getIdentity();
        final ZpGroupElement[] arrayOfIdentityElement = new ZpGroupElement[numRequiredElements];
        for (int i = 0; i < numRequiredElements; i++) {
            arrayOfIdentityElement[i] = identityElement;
        }
        return arrayOfIdentityElement;
    }

}
