/*
 This file is part of GNUnet.
 (C) 2014 Christian Grothoff (and other contributing authors)

 GNUnet is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published
 by the Free Software Foundation; either version 3, or (at your
 option) any later version.

 GNUnet 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 General Public License
 along with GNUnet; see the file COPYING.  If not, write to the
 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.
 */

package org.gnunet.voting;


import com.google.common.base.Optional;
import org.gnunet.construct.Construct;
import org.gnunet.construct.Message;
import org.gnunet.construct.NestedMessage;
import org.gnunet.secretsharing.Ciphertext;
import org.gnunet.secretsharing.Parameters;
import org.gnunet.secretsharing.ThresholdPublicKey;
import org.gnunet.util.BigIntegers;
import org.gnunet.util.Configuration;
import org.gnunet.util.Strings;
import org.gnunet.util.crypto.EcdsaPublicKey;
import org.gnunet.util.crypto.SignedContentMessage;

import java.math.BigInteger;
import java.util.Arrays;

public class EncryptedVote implements Message, SignedContentMessage {
    @NestedMessage
    public Ciphertext v;

    @NestedMessage
    public DisjunctionZkp disjunctionZkp;

    @NestedMessage
    public EcdsaPublicKey voterPublicKey;

    /**
     * Construct an EncryptedVote by parsing it from the configuration.
     * The voter public key must be passed separately.
     *
     * @param cfg configuration
     * @param voterPublicKey public key of the voter
     * @return the encrypted vote
     */
    public static EncryptedVote parseFromConfiguration(Configuration cfg, EcdsaPublicKey voterPublicKey) {
        Optional<String> optVal = cfg.getValueString("vote", "ENCRYPTED_VOTE_VAL");
        if (!optVal.isPresent()) {
            return null;
        }
        Optional<String> optZkp = cfg.getValueString("vote", "ZKP");
        if (!optZkp.isPresent()) {
            return null;
        }
        EncryptedVote encryptedVote = new EncryptedVote();
        encryptedVote.voterPublicKey = voterPublicKey;
        encryptedVote.v = Ciphertext.fromString(optVal.get());
        System.out.println("reading, string size " + optZkp.get().length());
        byte[] zkpData = Strings.stringToData(optZkp.get());
        if (null == zkpData) {
            throw new InvalidBallotException("could not read ZKP from ballot");
        }
        encryptedVote.disjunctionZkp = Construct.parseAs(zkpData, DisjunctionZkp.class);
        return encryptedVote;
    }

    public static EncryptedVote fromChoice(int choiceId, ThresholdPublicKey thresholdPublicKey, EcdsaPublicKey voterPublicKey,
                                           BigInteger[] generators) {

        EncryptedVote encryptedVote = new EncryptedVote();
        encryptedVote.v = new Ciphertext();
        encryptedVote.voterPublicKey = voterPublicKey;
        encryptedVote.disjunctionZkp = new DisjunctionZkp();

        encryptedVote.disjunctionZkp.chaumPedersenZkps = new ChaumPedersenZkp[generators.length];
        encryptedVote.disjunctionZkp.numProofs = generators.length;

        // the discrete logarithm
        BigInteger alpha = Parameters.randomQ();
        // the secret for the ZKP
        BigInteger w = Parameters.randomQ();

        BigInteger h = new BigInteger(1, thresholdPublicKey.bits);
        BigInteger g = Parameters.elgamalG;
        BigInteger p = Parameters.elgamalP;
        BigInteger q = Parameters.elgamalQ;

        BigInteger x = Parameters.modPowG(alpha);
        BigInteger y = h.modPow(alpha, p).multiply(generators[choiceId]).mod(p);

        encryptedVote.v.c_1 = BigIntegers.serializeUnsigned(x, Parameters.elgamalBits);
        encryptedVote.v.c_2 = BigIntegers.serializeUnsigned(y, Parameters.elgamalBits);

        // sum of all the simulation challenges
        BigInteger d_sim_sum = BigInteger.ZERO;

        // generate simulated proofs
        for (int i = 0; i < generators.length; i++) {
            if (i == choiceId) {
                continue;
            }
            BigInteger r = Parameters.randomQ();
            BigInteger d = Parameters.randomQ();
            BigInteger a = g.modPow(r, p).multiply(x.modPow(d, p)).mod(p);
            BigInteger b = h.modPow(r, p).multiply(y.multiply(generators[i].modInverse(p)).modPow(d, p)).mod(p);

            d_sim_sum = d_sim_sum.add(d);

            ChaumPedersenZkp zkp = new ChaumPedersenZkp();
            zkp.challenge_d = BigIntegers.serializeUnsigned(d, Parameters.elgamalBits);
            zkp.response_r = BigIntegers.serializeUnsigned(r, Parameters.elgamalBits);
            zkp.commit_a = BigIntegers.serializeUnsigned(a, Parameters.elgamalBits);
            zkp.commit_b = BigIntegers.serializeUnsigned(b, Parameters.elgamalBits);

            if (!zkp.verifySim(x, y, generators[i], h)) {
                throw new AssertionError("crypto not working");
            }

            encryptedVote.disjunctionZkp.chaumPedersenZkps[i] = zkp;
        }

        ChaumPedersenZkp zkp = new ChaumPedersenZkp();
        encryptedVote.disjunctionZkp.chaumPedersenZkps[choiceId] = zkp;
        BigInteger a = g.modPow(w, p);
        BigInteger b = h.modPow(w, p);
        zkp.commit_a = BigIntegers.serializeUnsigned(a, Parameters.elgamalBits);
        zkp.commit_b = BigIntegers.serializeUnsigned(b, Parameters.elgamalBits);


        BigInteger c = encryptedVote.disjunctionZkp.computeChallengeFromCommits();

        BigInteger d = c.subtract(d_sim_sum).mod(q);
        BigInteger r = w.subtract(alpha.multiply(d)).mod(q);


        zkp.challenge_d = BigIntegers.serializeUnsigned(d, Parameters.elgamalBits);
        zkp.response_r = BigIntegers.serializeUnsigned(r, Parameters.elgamalBits);


        if (!zkp.verifySim(x,y,generators[choiceId], h)) {
            throw new AssertionError("crypto (2) not working");
        }

        encryptedVote.disjunctionZkp.challenge_c = BigIntegers.serializeUnsigned(c, Parameters.elgamalBits);

        if (!encryptedVote.disjunctionZkp.verifyChallenge()) {
            throw new AssertionError("crypto not working (3)");
        }

        return encryptedVote;
    }

    public void writeToConfiguration(Configuration cfg) {
        cfg.setValueString("vote", "ENCRYPTED_VOTE_VAL", v.toString());
        byte[] zkpData = Construct.toBinary(disjunctionZkp);
        String zkpString = Strings.dataToString(zkpData);
        if (zkpString.length() != Strings.getEncodedStringLength(zkpData.length))
            throw new AssertionError("fail");
        if (zkpData.length != Strings.getDecodedDataLength(zkpString.length()))
            throw new AssertionError("fail, got " + zkpData.length + " expected " +
                    Strings.getDecodedDataLength(zkpString.length()) + "for string length" + zkpString.length());
        System.out.println("everyting ok, size binary size" + zkpData.length +" str size " + zkpString.length());
        byte[] zkpData2 = Strings.stringToData(zkpString);
        if (!Arrays.equals(zkpData, zkpData2)) {
            throw new AssertionError("something wrong");
        }
        System.out.println("everyting ok, size binary size" + zkpData.length +" str size " + zkpString.length());
        cfg.setValueString("vote", "ZKP", zkpString);
    }

    public boolean verify() {
        return false;
    }
}
