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

import java.io.IOException;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
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.beans.WarmUpOutput;
import com.scytl.products.ov.mixnet.commons.concurrent.LoopParallelizerTask;
import com.scytl.products.ov.mixnet.commons.concurrent.operations.PreComputationsCollectionAdderOperation;
import com.scytl.products.ov.mixnet.commons.concurrent.processor.PreComputationCalculatorProcessor;
import com.scytl.products.ov.mixnet.commons.configuration.ApplicationConfig;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationsProvider;
import com.scytl.products.ov.mixnet.commons.exceptions.MixingException;
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.homomorphic.impl.GjosteenElGamalPlaintext;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.GjosteenElGamalRandomness;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.RandomnessGenerator;
import com.scytl.products.ov.mixnet.commons.io.ElgamalPublicKeyReader;
import com.scytl.products.ov.mixnet.commons.io.ZpGroupReader;
import com.scytl.products.ov.mixnet.commons.tools.LoopTools;

public final class WarmUpService {

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

    public WarmUpOutput warmUp(final ApplicationConfig applicationConfig) {

        validateNotNull(applicationConfig);

        final Integer warmUpSize = applicationConfig.getMixer().getWarmUpSize();
        if (warmUpSize == null) {
            throw new MixingException("The warm up size property should be given to perform the warm up.");
        }

        final LocationsProvider locationsProvider = LocationsProvider.build(applicationConfig);
        final ZpSubgroup zpGroup;
        final ElGamalPublicKey publicKey;

        try {
            zpGroup = loadGroup(locationsProvider);
            publicKey = loadPublicKey(locationsProvider);
        } catch (final IOException | GeneralCryptoLibException e) {
            throw new MixingException("An error occurred while loading the input files.", e);
        }

        LOG.info("Warming up the mixing...");

        final GjosteenElGamal elGamalEncrypter = new GjosteenElGamal(zpGroup, publicKey);

        LOG.info("Generating " + warmUpSize + " random exponents...");

        Randomness[] randomExponents = getRandomness(zpGroup, warmUpSize);

        final ZpGroupElement[] arrayOfIdentityElement =
            getArrayOfIdentityElement(zpGroup, applicationConfig.getNumOptions());

        LOG.info("Generating preComputations...");

        List<Ciphertext> preComputations;
        if (applicationConfig.getConcurrencyLevel() > 1) {
            final List<LoopTools.Range> ranges =
                    LoopTools.asRangesList(warmUpSize, applicationConfig.getConcurrencyLevel());

            final PreComputationCalculatorProcessor process =
                    new PreComputationCalculatorProcessor(arrayOfIdentityElement, randomExponents, elGamalEncrypter);
            
            final LoopParallelizerTask<List<Ciphertext>> loopParallelizerTask =
                    new LoopParallelizerTask<>(process, ranges, new PreComputationsCollectionAdderOperation());
            
            final ForkJoinPool pool = new ForkJoinPool(applicationConfig.getConcurrencyLevel());
            try {
                pool.execute(loopParallelizerTask);
                preComputations = loopParallelizerTask.join();
            } finally {
                pool.shutdown();
            }
        } else {
            preComputations = Stream.of(randomExponents)
                .map(exponent -> elGamalEncrypter.encrypt(new GjosteenElGamalPlaintext(arrayOfIdentityElement),
                    new GjosteenElGamalRandomness(exponent.getExponent())))
                .collect(Collectors.toList());
        }

        LOG.info("Warm up process finished correctly...");

        return new WarmUpOutput(randomExponents, preComputations);
    }

    public ZpGroupElement[] getArrayOfIdentityElement(final ZpSubgroup group, 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;
    }

    public ElGamalPublicKey loadPublicKey(final LocationsProvider locationsProvider)
            throws IOException, GeneralCryptoLibException {
        return ElgamalPublicKeyReader
            .readPublicKeyFromStream(locationsProvider.getMixerLocationsProvider().getPublicKeyInput());
    }

    private static void validateNotNull(final ApplicationConfig applicationConfig) {
        if (applicationConfig == null) {
            throw new MixingException("The received application config was null");
        }
    }

    private static Randomness[] getRandomness(final ZpSubgroup zpGroup, final Integer warmUpSize) {
        final RandomnessGenerator randomnessGenerator = new RandomnessGenerator(zpGroup.getQ());
        return randomnessGenerator.generate(warmUpSize);
    }

    private ZpSubgroup loadGroup(final LocationsProvider locationsProvider) throws IOException {
        return createZpGroup(locationsProvider);
    }

    private static ZpSubgroup createZpGroup(final LocationsProvider locationsProvider) throws IOException {
        return ZpGroupReader.build(locationsProvider.getMixerLocationsProvider().getEncryptionParametersInput());
    }
}
