/*
 * 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.cryptolib.elgamal.factory;

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

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncrypterValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalReEncrypter;
import com.scytl.cryptolib.mathematical.groups.activity.GroupElementsCompressor;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.mathematical.groups.utils.GroupUtils;
import com.scytl.cryptolib.primitives.securerandom.factory.CryptoRandomInteger;

/**
 * Implementation of an ElGamal re-encrypter.
 */
public final class CryptoElGamalReEncrypter implements
        CryptoAPIElGamalReEncrypter {

    private final ElGamalPublicKey _elGamalPublicKey;

    private final int _elGamalPublicKeyLength;

    private final CryptoRandomInteger _cryptoRandomInteger;

    /**
     * Creates an ElGamal re-encrypter.
     *
     * @param elGamalPublicKey
     *            the ElGamal public key.
     * @param cryptoRandomInteger
     *            a generator of random integers.
     * @throws GeneralCryptoLibException
     *             if not all of the public keys belong to the one group.
     */
    CryptoElGamalReEncrypter(final ElGamalPublicKey elGamalPublicKey,
            final CryptoRandomInteger cryptoRandomInteger)
            throws GeneralCryptoLibException {

        _elGamalPublicKey = elGamalPublicKey;
        _elGamalPublicKeyLength = _elGamalPublicKey.getKeys().size();
        _cryptoRandomInteger = cryptoRandomInteger;
    }

    @Override
    public ElGamalEncrypterValues reEncryptGroupElements(
            final ElGamalComputationsValues ciphertext)
            throws GeneralCryptoLibException {

        Validate.notNull(ciphertext, "ElGamal ciphertext");
        Validate.notGreaterThan(ciphertext.getPhis().size(),
            _elGamalPublicKeyLength, "ElGamal ciphertext length",
            "re-encrypter public key length");

        ElGamalEncrypterValues preComputeValues = preCompute();

        ElGamalComputationsValues computationsValues =
            compute(ciphertext, preComputeValues.getComputationValues());

        return new ElGamalEncrypterValues(preComputeValues.getR(),
            computationsValues);
    }

    @Override
    public ElGamalEncrypterValues reEncryptGroupElements(
            final ElGamalComputationsValues ciphertext,
            final ElGamalEncrypterValues elGamalEncrypterValues)
            throws GeneralCryptoLibException {

        Validate.notNull(ciphertext, "ElGamal ciphertext");
        Validate.notGreaterThan(ciphertext.getPhis().size(),
            _elGamalPublicKeyLength, "ElGamal ciphertext length",
            "re-encrypter public key length");
        Validate.notNull(elGamalEncrypterValues,
            "ElGamal pre-computed values object");

        ElGamalComputationsValues preComputedValues =
            elGamalEncrypterValues.getComputationValues();

        Validate.notGreaterThan(preComputedValues.getPhis().size(),
            _elGamalPublicKeyLength,
            "Number of ElGamal pre-computed phi elements",
            "re-encrypter public key length");

        ElGamalComputationsValues computationsValues =
            compute(ciphertext, preComputedValues);

        return new ElGamalEncrypterValues(elGamalEncrypterValues.getR(),
            computationsValues);
    }

    @Override
    public ElGamalEncrypterValues preCompute() {

        List<ZpGroupElement> publicKeys = _elGamalPublicKey.getKeys();
        ZpSubgroup group = _elGamalPublicKey.getGroup();

        Exponent randomExponent;
        ZpGroupElement gamma;
        List<ZpGroupElement> prePhis;

        try {
            // Generate a random exponent
            randomExponent =
                new GroupUtils().getRandomExponent(group,
                    _cryptoRandomInteger);

            // Generate the first element of the ciphertext, gamma=g^random
            gamma = group.getGenerator().exponentiate(randomExponent);

            // For each element in the array of public keys, compute the
            // following:
            // element:prephi[i]=pubKey[i]^(random)
            ZpGroupElement pubKeyRaised;
            prePhis = new ArrayList<ZpGroupElement>(publicKeys.size());

            for (int i = 0; i < publicKeys.size(); i++) {
                pubKeyRaised =
                    publicKeys.get(i).exponentiate(randomExponent);
                prePhis.add(pubKeyRaised);
            }

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

        try {
            return new ElGamalEncrypterValues(randomExponent,
                new ElGamalComputationsValues(gamma, prePhis));
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(
                "Could not perform pre-computation of ElGamal re-encryption values.",
                e);
        }
    }

    private ElGamalComputationsValues compute(
            final ElGamalComputationsValues ciphertext,
            final ElGamalComputationsValues computationValues)
            throws GeneralCryptoLibException {

        List<ZpGroupElement> ciphertextPhis = ciphertext.getPhis();
        int numCiphertextPhis = ciphertextPhis.size();

        ElGamalComputationsValues compressedComputationValues =
            getCompressedList(numCiphertextPhis, computationValues);

        ZpGroupElement gamma =
            ciphertext.getGamma().multiply(
                compressedComputationValues.getGamma());

        List<ZpGroupElement> compressedComputationValuesPhis =
            compressedComputationValues.getPhis();
        List<ZpGroupElement> phis =
            new ArrayList<ZpGroupElement>(numCiphertextPhis);
        for (int i = 0; i < numCiphertextPhis; i++) {
            phis.add(ciphertextPhis.get(i).multiply(
                compressedComputationValuesPhis.get(i)));
        }

        return new ElGamalComputationsValues(gamma, phis);
    }

    private ElGamalComputationsValues getCompressedList(
            final int numCiphertextPhis,
            final ElGamalComputationsValues preComputationValues)
            throws GeneralCryptoLibException {

        List<ZpGroupElement> phis = preComputationValues.getPhis();

        if (phis.size() <= numCiphertextPhis) {
            return preComputationValues;
        }

        GroupElementsCompressor<ZpGroupElement> compressor =
            new GroupElementsCompressor<ZpGroupElement>();

        List<ZpGroupElement> compressedList =
            compressor.buildListWithCompressedFinalElement(
                numCiphertextPhis, phis);

        return new ElGamalComputationsValues(
            preComputationValues.getGamma(), compressedList);
    }
}
