/** @file      verifier.hpp
 *****************************************************************************
 * @author     Janno Siim, Stefanos Chaliasos
 * @copyright  MIT license (see LICENSE file)
 *****************************************************************************/
#ifndef VERIFIER_H_
#define VERIFIER_H_

#include <vector>
#include <future>
#include "omp.h" //to get max threads
#include <libff/algebra/curves/public_params.hpp>
#include <libff/algebra/scalar_multiplication/multiexp.hpp>
#include "crs.hpp"
#include "types.hpp"
#include "shuffle_util.hpp"
#include "prover.hpp"
#include "utils.hpp"

using namespace std;

/*! Verifies a zero-knowledge proof of a shuffle.
 * It can separately verify offline and online phases,
 * but offline phase must be verified before the online phase can be verified.
 *
 * \brief Verifies zero-knowledge proof of a shuffle.
 *
 * \tparam ppT elliptic curve group
 */
template<typename ppT>
class Verifier {

public:

	/*! Constructor for the verifier.
	 * \param crs common reference string generated by a trusted third party
	 */
	Verifier(CRS<ppT> crs, long k): crs {crs}, k {k} {

#ifdef PARALLEL
		chunks = omp_get_max_threads(); //otherwise == 1
#endif

	}

	/*! Verifies offline part of the proof.
     * This includes permutation matrix argument and the same-message argument.
     *
	 * \param offline_proof offline part of the proof
	 * \returns true if offline proof is correct and false otherwise
	 */
	bool verify_offline(Offline_proof<ppT> offline_proof) {
		libff::enter_block("Offline verifier", true);
		off_proof = offline_proof;

        bool correct = true;

		//Add last commitment
		off_proof.a_coms.push_back(crs.g1_sum_Pi -
                vector_sum<libff::G1<ppT>>(offline_proof.a_coms));
		off_proof.a_hat_coms.push_back(crs.g1_sum_Pi_hat -
                vector_sum<libff::G1<ppT>>(offline_proof.a_hat_coms));
		off_proof.b_coms.push_back(crs.g2_sum_Pi -
                vector_sum<libff::G2<ppT>>(offline_proof.b_coms));

		libff::enter_block("Permutation proof verification", false);
		libff::inhibit_profiling_info = true;
		correct = correct && check_permutation_proof();
		libff::inhibit_profiling_info = false;
		libff::leave_block("Permutation proof verification", false);

        if (correct){
			libff::enter_block("Same-message proof verification", false);
			libff::inhibit_profiling_info = true;
			correct = correct && check_same_message_proof();
			libff::inhibit_profiling_info = false;
			libff::leave_block("Same-message proof verification", false);

			batch_elem3 = generate_randomizer<ppT>(k);

			libff::inhibit_profiling_info = true;
			precomputation = ppT::reduced_pairing(off_proof.t_com,
                                                 batch_elem3 *
                                                    libff::G2<ppT>::one() +
                                                        crs.g2_sk);
			libff::inhibit_profiling_info = false;
        }
		libff::leave_block("Offline verifier", true);
		return correct;
	}


	/*! Verifies online part of the proof (consistency argument).
     * This could be potentially more efficient if it would use fixed base exponentiation.
     *
	 * \param online_proof online part of the proof
	 * \returns Online part of the proof.
	 */
	bool verify_online(ElGamal_vector<ppT> ciphertexts,
                       Online_proof<ppT> online_proof) {
		libff::enter_block("Online verifier = consistency argument", true);
		libff::inhibit_profiling_info = true;

		long amount_per_thread = crs.n / chunks;

		long begin = 0;
		long end = amount_per_thread;
		vector<future<libff::GT<ppT>>> thread_comps;

		for (size_t i = 0;  i < chunks; i++) {
			if (i == chunks - 1) end = crs.n - 1;
			thread_comps.push_back(async(launch::async,
                                         &Verifier::online_proof_left_chunk,
                                         this, begin, end,
                                         online_proof.output));
			begin = end + 1;
			end += amount_per_thread;
		}

		libff::GT<ppT> lhs = libff::GT<ppT>::one();
		for (unsigned long i = 0; i < thread_comps.size(); i++) {
			lhs = lhs * thread_comps.at(i).get();
		}
		lhs = lhs * ppT::reduced_pairing(crs.g1_rho_hat,
                                         batch_elem3 *
                                            online_proof.consist.first +
                                                online_proof.consist.second);


		begin = 0;
		end = amount_per_thread;
		vector<future<libff::GT<ppT>>> thread_comps2;

		for (size_t i = 0;  i < chunks; i++) {
			if (i == chunks - 1) end = crs.n - 1;
			thread_comps2.push_back(async(launch::async,
                                          &Verifier::online_proof_right_chunk,
                                          this, begin, end, ciphertexts));
			begin = end + 1;
			end += amount_per_thread;
		}

		libff::GT<ppT> rhs = precomputation;
		for (unsigned long i = 0; i < thread_comps.size(); i++) {
			rhs = rhs * thread_comps2.at(i).get();
		}
		libff::inhibit_profiling_info = false;
		libff::leave_block("Online verifier = consistency argument", true);
		return lhs == rhs;
	}


	/*! Verifies both offline and online part of the proof.
     *
	 * \param proof shuffle proof
	 * \returns true if proof is correct and false otherwise.
	 */
	bool verify(ElGamal_vector<ppT> ciphertexts, Proof<ppT> proof) {
		bool correct = true;
		libff::enter_block("Verifier", true);
		correct = correct && verify_offline(proof.first);
		if(correct){
			correct = correct && verify_online(ciphertexts, proof.second);
		}
		libff::leave_block("Verifier", true);
		return correct;
	}


private:
	//! Number of threads that can be used
	size_t chunks = 1;

	//! Common reference string
	CRS<ppT> crs;

	//! Security parameter in number of bits.
    //  Batching randomizers are taken to be k bits long.
	long k;

	//! Offline proof
	Offline_proof<ppT> off_proof;

	//! Batching element p_{3_1}
	libff::Fr<ppT> batch_elem3;

	//! Element e(t, (p3  + sk) * g2)
	libff::GT<ppT> precomputation;


	/*! Checks if permutation matrix proof is correct
     *
	 * \returns true if argument holds and false otherwise
	 */
	bool check_permutation_proof() {
		libff::Fr_vector<ppT> batch_randomizers = generate_randomizers<ppT>(
            crs.n - 1, k);
		batch_randomizers.push_back(libff::Fr<ppT>::random_element());

		long amount_per_thread = crs.n / chunks;

		long begin = 0;
		long end = amount_per_thread;
		vector<future<libff::GT<ppT>>> thread_comps;

        libff::Fr<ppT> alpha = libff::Fr<ppT>::random_element();
        libff::G1<ppT> g1_alpha_P0 = (alpha * libff::G1<ppT>::one()) +
                                       crs.g1_P0;
        libff::G2<ppT> g2_neg_alpha_P0 = (-alpha * libff::G2<ppT>::one()) +
                                             crs.g2_P0;

        libff::GT<ppT> gt_alpha2 = ppT::reduced_pairing(libff::G1<ppT>::one(),
                                                        libff::G2<ppT>::one())
                                                        ^ (libff::Fr<ppT>::one()
                                                        - (alpha ^ 2));

		for (size_t i = 0;  i < chunks; i++) {
			if (i == chunks - 1) end = crs.n - 1;
			thread_comps.push_back(async(launch::async,
                                         &Verifier::permutation_proof_product,
                                         this, batch_randomizers, g1_alpha_P0,
                                         g2_neg_alpha_P0, begin, end));
			begin = end + 1;
			end += amount_per_thread;
		}

		libff::GT<ppT> lhs = libff::GT<ppT>::one();
		for (unsigned long i = 0; i < thread_comps.size(); i++) {
			lhs = lhs * thread_comps.at(i).get();
		}

		libff::G1<ppT> prod_rhs =
            libff::multi_exp<libff::G1<ppT>, libff::Fr<ppT>, libff::multi_exp_method_BDLO12>
                (off_proof.uvs.begin(), off_proof.uvs.end(),
                 batch_randomizers.begin(), batch_randomizers.end(),
                 chunks);

		libff::GT<ppT> rhs = ppT::reduced_pairing(prod_rhs, crs.g2_rho) *
                               (gt_alpha2 ^
                                   (vector_sum<libff::Fr<ppT>>(
                                       batch_randomizers)));

		return lhs == rhs;
	}


	/*! Checks if same-message proof is correct
     *
	 * \returns true if argument holds and false otherwise
	 */
	bool check_same_message_proof() {
		libff::Fr_vector<ppT> batch_randomizers = generate_randomizers<ppT>(
                                                      crs.n - 1, k);
		batch_randomizers.push_back(libff::Fr<ppT>::one());
		libff::G1<ppT> prod_lhs =
            libff::multi_exp<libff::G1<ppT>, libff::Fr<ppT>, libff::multi_exp_method_BDLO12>(
                off_proof.same_msgs.begin(), off_proof.same_msgs.end(),
		        batch_randomizers.begin(), batch_randomizers.end(), chunks);

		libff::G1<ppT> prod_rhs1 =
            libff::multi_exp<libff::G1<ppT>, libff::Fr<ppT>, libff::multi_exp_method_BDLO12>(
                off_proof.a_coms.begin(), off_proof.a_coms.end(),
		        batch_randomizers.begin(), batch_randomizers.end(), chunks);
		libff::G1<ppT> prod_rhs2 =
            libff::multi_exp<libff::G1<ppT>, libff::Fr<ppT>, libff::multi_exp_method_BDLO12>(
                off_proof.a_hat_coms.begin(), off_proof.a_hat_coms.end(),
		        batch_randomizers.begin(), batch_randomizers.end(), chunks);

		libff::GT<ppT> lhs = ppT::reduced_pairing(prod_lhs,
                                                  libff::G2<ppT>::one());
		libff::GT<ppT> rhs = ppT::reduced_pairing(prod_rhs1, crs.g2_beta) *
                                 ppT::reduced_pairing(prod_rhs2,
                                                      crs.g2_beta_hat);

		return lhs == rhs;
	}


	/*! Computes part of left hand side of
     * permutation matrix proof verification.
     * Part is fixed by begin and end index.
	 * This is used to paralellize online computation.
     *
	 * \param batch_randomizers randomizers used for batching
	 * \param begin begin index
	 * \end end index
	 * \returns pairing product of values between begin and end index
	 */
	libff::GT<ppT> permutation_proof_product(
        libff::Fr_vector<ppT> batch_randomizers, libff::G1<ppT> g1_alpha_P0,
        libff::G2<ppT> g2_neg_alpha_P0, long begin, long end) {

      vector<libff::G1_precomp<ppT>> precomps1;
	  for (long i = begin; i <= end; i++) {
          precomps1.push_back(ppT::precompute_G1(batch_randomizers.at(i) *
              (off_proof.a_coms.at(i) + g1_alpha_P0)));
      }

      vector<libff::G2_precomp<ppT>> precomps2;
	  for (long i = begin; i <= end; i++) {
          precomps2.push_back(ppT::precompute_G2(off_proof.b_coms.at(i) +
              g2_neg_alpha_P0));
      }

	  libff::Fqk<ppT> result = libff::Fqk<ppT>::one();
	  for (unsigned long i = 0; i < precomps1.size(); i++) {
          result = result * ppT::miller_loop(precomps1.at(i), precomps2.at(i));
      }

	  return ppT::final_exponentiation(result);
	}


	/*! Computes part of the left hand side pairings of online verification.
     * Part is fixed by begin and end index.
	 *  This is used to paralellize online computation.
     *
	 * \param begin begin index
	 * \end end index
	 * \returns pairing product of values between begin and end index
	 */
	libff::GT<ppT> online_proof_left_chunk(long begin, long end,
                                           ElGamal_vector<ppT> shuffled_ciphers)
    {
		vector<libff::G1_precomp<ppT>> precomps1;
		vector<libff::G2_precomp<ppT>> precomps2;
		for (long i = begin; i <= end; i++) {
			precomps1.push_back(ppT::precompute_G1(crs.g1_Pi_hats.at(i)));
			precomps2.push_back(ppT::precompute_G2((batch_elem3 *
                shuffled_ciphers.at(i).first) + shuffled_ciphers.at(i).second));
		}

		libff::Fqk<ppT> result = libff::Fqk<ppT>::one();
		for (unsigned long i = 0; i < precomps1.size(); i++) {
            result =  result * ppT::miller_loop(precomps1.at(i),
                                                precomps2.at(i));
        }
		return ppT::final_exponentiation(result);

	}


	/*! Computes part of the right hand side pairings of online verification.
     * Part is fixed by begin and end index.
	 * This is used to paralellize online computation.
     *
	 * \param begin begin index
	 * \end end index
	 * \returns pairing product of values between begin and end index
	 */
	libff::GT<ppT> online_proof_right_chunk(long begin, long end,
                                            ElGamal_vector<ppT> ciphertexts) {
		vector<libff::G1_precomp<ppT>> precomps1;
		vector<libff::G2_precomp<ppT>> precomps2;
		for (long i = begin; i <= end; i++) {
			precomps1.push_back(ppT::precompute_G1(off_proof.a_hat_coms.at(i)));
			precomps2.push_back(ppT::precompute_G2((batch_elem3 *
                ciphertexts.at(i).first) + ciphertexts.at(i).second));
		}

		libff::Fqk<ppT> result = libff::Fqk<ppT>::one();

		for (unsigned long i = 0; i < precomps1.size(); i++) {
            result = result * ppT::miller_loop(precomps1.at(i),
                                               precomps2.at(i));
        }

		return ppT::final_exponentiation(result);
	}
};

#endif /* VERIFIER_H_ */
