/*-------------------------------------------------------------------------------------------------
 - #%L                                                                                            -
 - chvote-protocol-poc                                                                            -
 - %%                                                                                             -
 - Copyright (C) 2016 - 2017 République et Canton de Genève                                       -
 - %%                                                                                             -
 - This program is free software: you can redistribute it and/or modify                           -
 - it under the terms of the GNU Affero General Public License as published by                    -
 - the Free Software Foundation, either version 3 of the License, or                              -
 - (at your option) any later version.                                                            -
 -                                                                                                -
 - This program is distributed in the hope that it will be useful,                                -
 - but WITHOUT ANY WARRANTY; without even the implied warranty of                                 -
 - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                                   -
 - GNU General Public License for more details.                                                   -
 -                                                                                                -
 - You should have received a copy of the GNU Affero General Public License                       -
 - along with this program. If not, see <http://www.gnu.org/licenses/>.                           -
 - #L%                                                                                            -
 -------------------------------------------------------------------------------------------------*/

package ch.ge.ve.protopoc.service.algorithm;

import ch.ge.ve.protopoc.service.exception.BallotNotFoundRuntimeException;
import ch.ge.ve.protopoc.service.model.*;
import ch.ge.ve.protopoc.service.model.polynomial.Point;
import ch.ge.ve.protopoc.service.support.ByteArrayUtils;
import ch.ge.ve.protopoc.service.support.Hash;
import com.google.common.base.Preconditions;

import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

import static ch.ge.ve.protopoc.arithmetic.BigIntegerArithmetic.modExp;

/**
 * Algorithms for the vote confirmation phase, on the authorities side
 */
public class VoteConfirmationAuthorityAlgorithms {
    private final PublicParameters publicParameters;
    private final GeneralAlgorithms generalAlgorithms;
    private final VoteCastingAuthorityAlgorithms voteCastingAuthorityAlgorithms;
    private final Hash hash;

    public VoteConfirmationAuthorityAlgorithms(PublicParameters publicParameters, GeneralAlgorithms generalAlgorithms,
                                               VoteCastingAuthorityAlgorithms voteCastingAuthorityAlgorithms, Hash hash) {
        this.publicParameters = publicParameters;
        this.generalAlgorithms = generalAlgorithms;
        this.voteCastingAuthorityAlgorithms = voteCastingAuthorityAlgorithms;
        this.hash = hash;
    }

    /**
     * Algorithm 7.34: CheckConfirmation
     *
     * @param i          the voter index
     * @param gamma      the voter's confirmation, including public confirmation credential and proof of knowledge of
     *                   the private confirmation credential
     * @param bold_y_hat the list of public confirmation credentials, as generated during the preparation phase
     * @param upper_b    the current list of ballots
     * @param upper_c    the current list of confirmations
     * @return true if the confirmation is allowed (ballot present, confirmation not present, credentials match) and the
     * proof is valid
     */
    public boolean checkConfirmation(Integer i, Confirmation gamma, List<BigInteger> bold_y_hat,
                                     Collection<BallotEntry> upper_b, Collection<ConfirmationEntry> upper_c) {
        return voteCastingAuthorityAlgorithms.hasBallot(i, upper_b) &&
                !hasConfirmation(i, upper_c) &&
                bold_y_hat.get(i).compareTo(gamma.getY_hat()) == 0 &&
                checkConfirmationProof(gamma.getPi(), gamma.getY_hat());
    }

    /**
     * Algorithm 7.35: HasConfirmation
     *
     * @param i       the voter index
     * @param upper_c the list of confirmations
     * @return true if the list of confirmation contains a confirmation for the given voter index, false otherwise
     */
    public boolean hasConfirmation(Integer i, Collection<ConfirmationEntry> upper_c) {
        return upper_c.stream().anyMatch(c -> c.getI().equals(i));
    }

    /**
     * Algorithm 7.36: CheckConfirmationProof
     *
     * @param pi    the proof of knowledge of private confirmation credential y, provided by the voting client
     * @param y_hat the public confirmation credential corresponding to the private credential y
     * @return true if the proof of knowledge is valid, false otherwise
     */
    public boolean checkConfirmationProof(NonInteractiveZKP pi, BigInteger y_hat) {
        Preconditions.checkNotNull(pi);
        Preconditions.checkNotNull(pi.getT());
        Preconditions.checkNotNull(pi.getS());
        Preconditions.checkNotNull(y_hat);
        Preconditions.checkArgument(pi.getT().size() == 1);
        Preconditions.checkArgument(pi.getS().size() == 1);

        BigInteger p_hat = publicParameters.getIdentificationGroup().getP_hat();
        BigInteger g_hat = publicParameters.getIdentificationGroup().getG_hat();
        int tau = publicParameters.getSecurityParameters().getTau();

        BigInteger t = pi.getT().get(0);
        BigInteger s = pi.getS().get(0);

        Preconditions.checkArgument(generalAlgorithms.isMember_G_q_hat(t),
                "t must be in G_q_hat");
        Preconditions.checkArgument(generalAlgorithms.isInZ_q_hat(s),
                "s must be in Z_q_hat");
        //noinspection SuspiciousNameCombination
        Preconditions.checkArgument(generalAlgorithms.isMember_G_q_hat(y_hat),
                "y_hat must be in G_q_hat");

        BigInteger c = generalAlgorithms.getNIZKPChallenge(new BigInteger[]{y_hat}, new BigInteger[]{t}, tau);
        BigInteger t_prime = modExp(g_hat, s, p_hat).multiply(modExp(y_hat, c.negate(), p_hat)).mod(p_hat);

        return t.compareTo(t_prime) == 0;
    }

    /**
     * Algorithm 7.37: GetFinalization
     *
     * @param i            the voter index
     * @param upper_bold_p the point matrix, one point per voter per candidate
     * @param upper_b      the current ballot list
     * @return this authority's part of the finalization code
     */
    public FinalizationCodePart getFinalization(Integer i, List<List<Point>> upper_bold_p, Collection<BallotEntry> upper_b) {
        BigInteger p_prime = publicParameters.getPrimeField().getP_prime();
        Preconditions.checkArgument(upper_bold_p.stream().flatMap(Collection::stream)
                        .allMatch(point -> BigInteger.ZERO.compareTo(point.x) <= 0 &&
                                point.x.compareTo(p_prime) < 0 &&
                                BigInteger.ZERO.compareTo(point.y) <= 0 &&
                                point.y.compareTo(p_prime) < 0),
                "All points' coordinates must be in Z_p_prime");
        Preconditions.checkElementIndex(i, upper_bold_p.size());

        Object[] bold_p_i = upper_bold_p.get(i).toArray();
        byte[] upper_f_i = ByteArrayUtils.truncate(hash.recHash_L(bold_p_i), publicParameters.getUpper_l_f());

        BallotEntry ballotEntry = upper_b.stream().filter(b -> Objects.equals(b.getI(), i)).findFirst().orElseThrow(
                () -> new BallotNotFoundRuntimeException(String.format("Couldn't find any ballot for voter %d", i))
        );

        return new FinalizationCodePart(upper_f_i, ballotEntry.getBold_r());
    }
}