/*
 * 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.proofs.maurer.factory;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

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.elgamal.cryptoapi.Witness;
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.messagedigest.configuration.ConfigMessageDigestAlgorithmAndProvider;
import com.scytl.cryptolib.proofs.bean.ProofPreComputedValues;
import com.scytl.cryptolib.proofs.maurer.configuration.ConfigProofHashCharset;
import com.scytl.cryptolib.proofs.proof.Proof;
import com.scytl.cryptolib.proofs.utils.HashBuilder;
import com.scytl.cryptolib.securerandom.cryptoapi.CryptoAPIRandomInteger;

/**
 * OR proof generator.
 * <p>
 * Instances of this class are not reusable;
 */
public final class ORProofGenerator {
    private final Ciphertext ciphertext;

    private final ElGamalPublicKey publicKey;

    private final Witness witness;

    private final int index;

    private final List<ZpGroupElement> elements;

    private final String data;

    private final CryptoAPIRandomInteger random;

    private final ConfigMessageDigestAlgorithmAndProvider algorithmAndProvider;

    private final ConfigProofHashCharset charset;

    private final ProofPreComputedValues preComputedValues;

    /**
     * Constructor.
     * 
     * @param ciphertext
     *            the ciphertext
     * @param publicKey
     *            the ElGamal public key used for the encryption
     * @param witness
     *            the secret exponent used for the encryption
     * @param index
     *            the index of the encrypted group element
     * @param elements
     *            the group elements
     * @param data
     *            the string data to be included into hash
     * @param random
     *            the BigInteger random generator
     * @param algorithmAndProvider
     *            the hash algorithm and provider
     * @param charset
     *            the charset of the data
     * @param preComputedValues
     *            the proof pre-computed values
     */
    public ORProofGenerator(Ciphertext ciphertext,
            ElGamalPublicKey publicKey, Witness witness, int index,
            List<ZpGroupElement> elements, String data,
            CryptoAPIRandomInteger random,
            ConfigMessageDigestAlgorithmAndProvider algorithmAndProvider,
            ConfigProofHashCharset charset,
            ProofPreComputedValues preComputedValues) {
        this.ciphertext = ciphertext;
        this.publicKey = publicKey;
        this.witness = witness;
        this.index = index;
        this.elements = elements;
        this.data = data;
        this.random = random;
        this.algorithmAndProvider = algorithmAndProvider;
        this.charset = charset;
        this.preComputedValues = preComputedValues;
    }

    /**
     * Generates an OR proof.
     * 
     * @return the generated proof.
     * @throws GeneralCryptoLibException
     *             failed to generate the proof.
     */
    public Proof generate() throws GeneralCryptoLibException {
        List<Exponent> exponents = generateExponents();
        List<ZpGroupElement> values = generateValues(exponents);
        Exponent hash = calculateHash(values);
        correctExponents(hash, exponents);
        return new Proof(hash, exponents);
    }

    /**
     * Generates an OR proof.
     * 
     * @return the generated proof.
     * @throws GeneralCryptoLibException
     *             failed to generate the proof.
     */
    public Proof generate2() throws GeneralCryptoLibException {
        List<Exponent> exponents = generateExponents();
        List<ZpGroupElement> values = generateValues(exponents);
        Exponent hash = calculateHash(values);
        correctExponents(hash, exponents);
        return new Proof(hash, exponents);
    }

    private Exponent calculateHash(List<ZpGroupElement> values)
            throws GeneralCryptoLibException {
        HashBuilder builder =
            new HashBuilder(algorithmAndProvider, charset);
        byte[] hash = builder.buildHashForProofs(ciphertext.getElements(),
            values, data);
        return new Exponent(publicKey.getGroup().getQ(),
            new BigInteger(1, hash));
    }

    private void correctExponents(Exponent hash, List<Exponent> exponents)
            throws GeneralCryptoLibException {
        Exponent c = hash;
        for (int i = 0; i < elements.size(); i++) {
            if (i != index) {
                c = c.subtract(exponents.get(i));
            }
        }
        exponents.set(index, c);
        Exponent z = exponents.get(index + elements.size());
        z = z.add(witness.getExponent().multiply(c));
        exponents.set(index + elements.size(), z);
    }

    private List<Exponent> generateExponents()
            throws GeneralCryptoLibException {
        List<Exponent> exponents = new ArrayList<>(elements.size() * 2);
        ZpSubgroup group = publicKey.getGroup();
        GroupUtils utils = new GroupUtils();
        for (int i = 0; i < elements.size(); i++) {
            if (i == index) {
                exponents.add(preComputedValues.getExponents().get(i));
            } else {
                exponents.add(utils.getRandomExponent(group, random));
            }
        }
        exponents.addAll(preComputedValues.getExponents());
        return exponents;
    }

    private List<ZpGroupElement> generateValues(List<Exponent> exponents)
            throws GeneralCryptoLibException {
        List<ZpGroupElement> values = new ArrayList<>(elements.size() * 2);
        ZpGroupElement invertedX = ciphertext.getGamma().invert();
        ZpGroupElement invertedY = ciphertext.getPhis().get(0).invert();
        List<ZpGroupElement> outputs = preComputedValues.getPhiOutputs();
        for (int i = 0; i < elements.size(); i++) {
            ZpGroupElement a = outputs.get(i * 2);
            ZpGroupElement b = outputs.get(i * 2 + 1);
            if (i != index) {
                Exponent c = exponents.get(i);
                a = a.multiply(invertedX.exponentiate(c));
                ZpGroupElement e = elements.get(i).multiply(invertedY);
                b = b.multiply(e.exponentiate(c));
            }
            values.add(a);
            values.add(b);
        }
        return values;
    }
}
