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

import static java.util.Objects.requireNonNull;

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

import javax.annotation.concurrent.NotThreadSafe;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.CiphertextImpl;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncrypterValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;

/**
 * Permits a ciphertext to be reencrypted.
 * <P>
 * Instances of this class should be used by a single thread. In other words, instances of this class should NOT be
 * shared amongst multiple threads.
 */
@NotThreadSafe
final class ElGamalReencrypter {

    private final ElGamalService elGamalService;

    private final CryptoAPIElGamalEncrypter encrypter;

    private final ElGamalPublicKey elGamalPublicKey;

    /**
     * Initialize a reencrypter using the received public key.
     * 
     * @param elGamalPublicKey
     *            the public key to be used by this reencrypter.
     * @throws GeneralCryptoLibException
     *             if there is any problem when creating an ElGamalService or the creation of an ElGamal encrypter.
     */
    public ElGamalReencrypter(ElGamalPublicKey elGamalPublicKey) throws GeneralCryptoLibException {

        validateInputs(elGamalPublicKey);

        this.elGamalPublicKey = elGamalPublicKey;

        elGamalService = new ElGamalService();

        encrypter = elGamalService.createEncrypter(elGamalPublicKey);
    }

    /**
     * Reencrypt a single ciphertext.
     * <P>
     * The approach that is used to perform the reencryption is the following:
     * <ul>
     * <li>Generate a list of the same size as the number of elements that were encrypted to produce the ciphertext (in
     * other words, the size of the ciphertext minus one). The value of each element in the created list is set to the
     * identity element.</li>
     * <li>Encrypt the list of identity elements using the public key.</li>
     * <li>Multiply both ciphertexts together (they should have the same number of elements). The result of the
     * multiplication is the reencryption.</li>
     * </ul>
     * 
     * @param ciphertext
     *            the ciphertext to be reencrypted.
     * @return the reencrypted ciphertext and the random exponents used during the encryption.
     * @throws IllegalArgumentException
     *             if the inputs are not valid.
     * @throws GeneralCryptoLibException
     *             if anything anything goes wrong during the encryption of the list of identity elements or during the
     *             multiplication of ciphertexts.
     */
    public ReencryptionOutput reencrypt(Ciphertext ciphertext) throws GeneralCryptoLibException {

        validateInputs(ciphertext);

        int numElementsPerCiphertext = ciphertext.size() - 1;
        ZpSubgroup group = elGamalPublicKey.getGroup();

        List<ZpGroupElement> listOfIdentityElements = new ArrayList<>();
        for (int i = 0; i < numElementsPerCiphertext; i++) {
            listOfIdentityElements.add(group.getIdentity());
        }

        ElGamalEncrypterValues identityElementEncrypted = encrypter.encryptGroupElements(listOfIdentityElements);
        Ciphertext ciphertextsMultiplied = identityElementEncrypted.multiply(ciphertext);

        return new ReencryptionOutput(ciphertextsMultiplied, identityElementEncrypted.getExponent());
    }

    /**
     * WORK IN PROGRESS - The following is an alternative method for doing the enencryption. If this method is used then
     * a "packed" ciphertext can be mixed, and the data can be recovered, but the proofs dont verify.
     */
    public ReencryptionOutput reencrypt_alternative(Ciphertext ciphertext) throws GeneralCryptoLibException {

        validateInputs(ciphertext);

        ZpSubgroup group = elGamalPublicKey.getGroup();

        List<ZpGroupElement> listOfIdentityElements = new ArrayList<>();

        for (int i = 0; i < 9; i++) {
            listOfIdentityElements.add(group.getIdentity());
        }

        ElGamalEncrypterValues identityElementEncrypted = encrypter.encryptGroupElements(listOfIdentityElements);

        Ciphertext extractCiphertext1 = new CiphertextImpl(ciphertext.getGamma(), ciphertext.getPhis().get(0));
        Ciphertext extractCiphertext2 = new CiphertextImpl(ciphertext.getPhis().get(1), ciphertext.getPhis().get(2));
        Ciphertext extractCiphertext3 = new CiphertextImpl(ciphertext.getPhis().get(3), ciphertext.getPhis().get(4));
        Ciphertext extractCiphertext4 = new CiphertextImpl(ciphertext.getPhis().get(5), ciphertext.getPhis().get(6));
        Ciphertext extractCiphertext5 = new CiphertextImpl(ciphertext.getPhis().get(7), ciphertext.getPhis().get(8));

        Ciphertext cc1 =
            new CiphertextImpl(identityElementEncrypted.getGamma(), identityElementEncrypted.getPhis().get(0));
        Ciphertext ciphertextsMultiplied1 = extractCiphertext1.multiply(cc1);

        Ciphertext cc2 =
            new CiphertextImpl(identityElementEncrypted.getGamma(), identityElementEncrypted.getPhis().get(2));
        Ciphertext ciphertextsMultiplied2 = extractCiphertext2.multiply(cc2);

        Ciphertext cc3 =
            new CiphertextImpl(identityElementEncrypted.getGamma(), identityElementEncrypted.getPhis().get(4));
        Ciphertext ciphertextsMultiplied3 = extractCiphertext3.multiply(cc3);

        Ciphertext cc4 =
            new CiphertextImpl(identityElementEncrypted.getGamma(), identityElementEncrypted.getPhis().get(6));
        Ciphertext ciphertextsMultiplied4 = extractCiphertext4.multiply(cc4);

        Ciphertext cc5 =
            new CiphertextImpl(identityElementEncrypted.getGamma(), identityElementEncrypted.getPhis().get(8));
        Ciphertext ciphertextsMultiplied5 = extractCiphertext5.multiply(cc5);

        ZpGroupElement gamma = ciphertextsMultiplied1.getGamma();
        List<ZpGroupElement> phis = new ArrayList<>();
        phis.add(ciphertextsMultiplied1.getPhis().get(0));
        phis.add(ciphertextsMultiplied2.getGamma());
        phis.add(ciphertextsMultiplied2.getPhis().get(0));
        phis.add(ciphertextsMultiplied3.getGamma());
        phis.add(ciphertextsMultiplied3.getPhis().get(0));
        phis.add(ciphertextsMultiplied4.getGamma());
        phis.add(ciphertextsMultiplied4.getPhis().get(0));
        phis.add(ciphertextsMultiplied5.getGamma());
        phis.add(ciphertextsMultiplied5.getPhis().get(0));

        Ciphertext ciphertextsMultiplied = new CiphertextImpl(gamma, phis);

        return new ReencryptionOutput(ciphertextsMultiplied, identityElementEncrypted.getExponent());
    }

    /**
     * Reencrypt a list of ciphertexts.
     * <P>
     * The approach that is used to perform the reencryption is the same as in the case of a single ciphertext, but
     * iterating over the list.
     * 
     * @param ciphertexts
     *            the list of ciphertexts to be reencrypted.
     * @return a list of the reencrypted ciphertext and the random exponents used during the encryption.
     * @throws IllegalArgumentException
     *             if the inputs are not valid.
     * @throws GeneralCryptoLibException
     *             if anything anything goes wrong during the encryption of the list of identity elements or during the
     *             multiplication of ciphertexts.
     */
    public List<ReencryptionOutput> reencrypt(List<Ciphertext> ciphertexts) throws GeneralCryptoLibException {

        validateInputs(ciphertexts);

        List<ReencryptionOutput> result = new ArrayList<>();

        for (Ciphertext singleCiphertext : ciphertexts) {
            result.add(reencrypt(singleCiphertext));
        }

        return result;
    }

    private static void validateInputs(ElGamalPublicKey elGamalPublicKey) {

        requireNonNull(elGamalPublicKey, "The public key was null");
    }

    private void validateInputs(Ciphertext ciphertext) {

        requireNonNull(ciphertext, "The ciphertext was null");

        int expectedNumCiphertextElements = elGamalPublicKey.getKeys().size() + 1;

        if (ciphertext.size() != expectedNumCiphertextElements) {
            String errorMsg = String.format(
                "A ciphertext does not contain the expected number of elements (based on the size of the key). Expected: %s, Actual: %s",
                expectedNumCiphertextElements, ciphertext.size());
            throw new IllegalArgumentException(errorMsg);
        }
    }

    private void validateInputs(List<Ciphertext> ciphertexts) {

        requireNonNull(ciphertexts, "The list of ciphertexts was null");
        if (ciphertexts.isEmpty()) {
            throw new IllegalArgumentException("The list of ciphertexts was empty");
        }

        if (elGamalPublicKey == null) {
            throw new IllegalArgumentException("The public key was null");
        }

        int expectedNumCiphertextElements = elGamalPublicKey.getKeys().size() + 1;
        for (Ciphertext ciphertext : ciphertexts) {
            if (ciphertext.size() != expectedNumCiphertextElements) {
                String errorMsg = String.format(
                    "A ciphertext does not contain the expected number of elements (based on the size of the key). Expected: %s, Actual: %s",
                    expectedNumCiphertextElements, ciphertext.size());
                throw new IllegalArgumentException(errorMsg);
            }
        }
    }
}
