#include <ctime>
#include <cmath>

#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>

#include <boost/unordered_map.hpp>
#include <boost/functional.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/random/mersenne_twister.hpp>
#include <boost/algorithm/string/join.hpp>
# include <boost/interprocess/managed_shared_memory.hpp>
# include <boost/interprocess/allocators/allocator.hpp>
# include <boost/interprocess/managed_mapped_file.hpp>
#include <boost/interprocess/containers/vector.hpp>
#include <boost/math/distributions/binomial.hpp>

#include <Eigen/Dense>
#include <Eigen/Sparse>
#include "maybe_omp.h"
#include <tclap/CmdLine.h>

#include "model.h"
#include "propagator.h"
#include "param.h"
#include "neuralClasses.h"
#include "graphClasses.h"
#include "util.h"
#include "multinomial.h"
//#include "gradientCheck.h"

//#define EIGEN_DONT_PARALLELIZE

using namespace std;
using namespace TCLAP;
using namespace Eigen;
using namespace boost;
using namespace boost::random;

using namespace nplm;

namespace ip = boost::interprocess;
typedef unordered_map<Matrix<int,Dynamic,1>, double> vector_map;

typedef ip::allocator<int, ip::managed_mapped_file::segment_manager> intAllocator;
typedef ip::vector<int, intAllocator> vec;
typedef ip::allocator<vec, ip::managed_mapped_file::segment_manager> vecAllocator;

typedef long long int data_size_t; // training data can easily exceed 2G instances

int main(int argc, char** argv)
{
    ios::sync_with_stdio(false);
    bool use_mmap_file, randomize;
    param myParam;
    try {
      // program options //
      CmdLine cmd("Trains a two-layer neural probabilistic language model.", ' ' , "0.2\n",
          "Changes since V0.1: Addition of biases");

      // The options are printed in reverse order

      ValueArg<string> unigram_probs_file("", "unigram_probs_file", "Unigram model (deprecated and ignored)." , false, "", "string", cmd);

      ValueArg<int> num_threads("", "num_threads", "Number of threads. Default: maximum.", false, 0, "int", cmd);

      ValueArg<double> final_momentum("", "final_momentum", "Final value of momentum. Default: 0.9.", false, 0.9, "double", cmd);
      ValueArg<double> initial_momentum("", "initial_momentum", "Initial value of momentum. Default: 0.9.", false, 0.9, "double", cmd);
      ValueArg<bool> use_momentum("", "use_momentum", "Use momentum (hidden layer weights only). 1 = yes, 0 = no. Default: 0.", false, 0, "bool", cmd);

      ValueArg<double> normalization_init("", "normalization_init", "Initial normalization parameter. Default: 0.", false, 0.0, "double", cmd);
      ValueArg<bool> normalization("", "normalization", "Learn individual normalization factors during training. 1 = yes, 0 = no. Default: 0.", false, 0, "bool", cmd);

      ValueArg<bool> mmap_file("", "mmap_file", "Use memory mapped files. This is useful if the entire data cannot fit in memory. prepareNeuralLM can generate memory mapped files", false, 0, "bool", cmd);

      ValueArg<bool> arg_randomize("", "randomize", "Randomize training instances for better training. 1 = yes, 0 = no. Default: 1.", false, true, "bool", cmd);

      ValueArg<int> num_noise_samples("", "num_noise_samples", "Number of noise samples for noise-contrastive estimation. Default: 100.", false, 100, "int", cmd);

      ValueArg<double> L2_reg("", "L2_reg", "L2 regularization strength (hidden layer weights only). Default: 0.", false, 0.0, "double", cmd);

      ValueArg<double> learning_rate("", "learning_rate", "Learning rate for stochastic gradient ascent. Default: 1.", false, 1., "double", cmd);

      ValueArg<double> conditioning_constant("", "conditioning_constant", "Constant to condition the RMS of the expected square of the gradient in ADADELTA. Default: 10E-3.", false, 10E-3, "double", cmd);

      ValueArg<double> decay("", "decay", "Decay for ADADELTA. Default: 0.95", false, 0.95, "double", cmd);
      ValueArg<double> adagrad_epsilon("", "adagrad_epsilon", "Constant to initialize the L2 squared norm of the gradients with.\
          Default: 10E-3", false, 10E-3, "double", cmd);
      ValueArg<int> validation_minibatch_size("", "validation_minibatch_size", "Minibatch size for validation. Default: 64.", false, 64, "int", cmd);
      ValueArg<int> minibatch_size("", "minibatch_size", "Minibatch size (for training). Default: 1000.", false, 1000, "int", cmd);

      ValueArg<int> num_epochs("", "num_epochs", "Number of epochs. Default: 10.", false, 10, "int", cmd);

      ValueArg<double> init_range("", "init_range", "Maximum (of uniform) or standard deviation (of normal) for initialization. Default: 0.01", false, 0.01, "double", cmd);
      ValueArg<bool> init_normal("", "init_normal", "Initialize parameters from a normal distribution. 1 = normal, 0 = uniform. Default: 0.", false, 0, "bool", cmd);

      ValueArg<string> loss_function("", "loss_function", "Loss function (log, nce). Default: nce.", false, "nce", "string", cmd);
      ValueArg<string> activation_function("", "activation_function", "Activation function (identity, rectifier, tanh, hardtanh). Default: rectifier.", false, "rectifier", "string", cmd);
      ValueArg<int> num_hidden("", "num_hidden", "Number of hidden nodes. Default: 100.", false, 100, "int", cmd);
	  ValueArg<int> num_second_hidden("", "num_second_hidden", "Number of hidden nodes in the second hidden layer. Default: 100.", false, 100, "int", cmd);

      ValueArg<bool> share_embeddings("", "share_embeddings", "Share input and output embeddings. 1 = yes, 0 = no. Default: 0.", false, 0, "bool", cmd);
      ValueArg<int> output_embedding_dimension("", "output_embedding_dimension", "Number of output embedding dimensions. Default: 50.", false, 50, "int", cmd);
      ValueArg<int> input_embedding_dimension("", "input_embedding_dimension", "Number of input embedding dimensions. Default: 50.", false, 50, "int", cmd);
      ValueArg<int> embedding_dimension("", "embedding_dimension", "Number of input and output embedding dimensions. Default: none.", false, -1, "int", cmd);
	  ValueArg<string> input_embeddings_file("","input_embeddings_file", "Read the input embeddings from the specified file. Default: none", false,"","string",cmd);
	  ValueArg<int> context_vector_size("", "context_vector_size", "Size of the context vector. Default: 128.", false, 128, "int", cmd);
      ValueArg<int> vocab_size("", "vocab_size", "Vocabulary size. Default: auto.", false, 0, "int", cmd);
      ValueArg<int> input_vocab_size("", "input_vocab_size", "Vocabulary size. Default: auto.", false, 0, "int", cmd);
      ValueArg<int> output_vocab_size("", "output_vocab_size", "Vocabulary size. Default: auto.", false, 0, "int", cmd);
      ValueArg<int> ngram_size("", "ngram_size", "Size of n-grams. Default: auto.", false, 0, "int", cmd);

      ValueArg<string> model_prefix("", "model_prefix", "Prefix for output model files." , false, "", "string", cmd);
      ValueArg<string> words_file("", "words_file", "Vocabulary." , false, "", "string", cmd);
      ValueArg<string> parameter_update("", "parameter_update", "parameter update type.\n Stochastic Gradient Descent(SGD)\n \
          ADAGRAD(ADA)\n \
          ADADELTA(ADAD)" , false, "SGD", "string", cmd);
      ValueArg<string> input_words_file("", "input_words_file", "Vocabulary." , false, "", "string", cmd);
      ValueArg<string> output_words_file("", "output_words_file", "Vocabulary." , false, "", "string", cmd);
      ValueArg<string> validation_file("", "validation_file", "Validation data (one numberized example per line)." , false, "", "string", cmd);
      ValueArg<string> train_file("", "train_file", "Training data (one numberized example per line)." , true, "", "string", cmd);

      ValueArg<string> model_file("", "model_file", "Model file.", false, "", "string", cmd);
      ValueArg<double> hidden1_dropout_value("", "hidden1_dropout_value", "The probability of drop out for hidden layer 1.\
          Default: 0.9", false, 0.9, "double", cmd);
      ValueArg<double> hidden2_dropout_value("", "hidden2_dropout_value", "The probability of drop out for hidden layer 2.\
          Default: 0.9", false, 0.9, "double", cmd);
      ValueArg<double> hidden3_dropout_value("", "hidden3_dropout_value", "The probability of drop out for hidden layer 3.\
          Default: 0.9", false, 0.9, "double", cmd);
	  ValueArg<string> train_context_vectors_file("", "train_context_vectors_file", "Training context vectors file."
		  , false, "", "string", cmd);
	  ValueArg<string> validation_context_vectors_file("", "validation_context_vectors_file", "Validation context vectors file."
		  , false, "", "string", cmd);

      cmd.parse(argc, argv);

      // define program parameters //
      use_mmap_file = mmap_file.getValue();
      randomize = arg_randomize.getValue();
      myParam.model_file = model_file.getValue();
      myParam.train_file = train_file.getValue();
      myParam.validation_file = validation_file.getValue();
      myParam.input_words_file = input_words_file.getValue();
      myParam.output_words_file = output_words_file.getValue();
      if (words_file.getValue() != "")
	      myParam.input_words_file = myParam.output_words_file = words_file.getValue();

      myParam.model_prefix = model_prefix.getValue();

      myParam.ngram_size = ngram_size.getValue();
      myParam.vocab_size = vocab_size.getValue();
      myParam.input_vocab_size = input_vocab_size.getValue();
      myParam.output_vocab_size = output_vocab_size.getValue();
      if (vocab_size.getValue() >= 0) {
	      myParam.input_vocab_size = myParam.output_vocab_size = vocab_size.getValue();
      }
      myParam.num_hidden = num_hidden.getValue();
	  myParam.num_second_hidden = num_second_hidden.getValue();
      myParam.activation_function = activation_function.getValue();
      myParam.loss_function = loss_function.getValue();

      myParam.num_threads = num_threads.getValue();

      myParam.num_noise_samples = num_noise_samples.getValue();

      myParam.input_embedding_dimension = input_embedding_dimension.getValue();
      myParam.output_embedding_dimension = output_embedding_dimension.getValue();
      if (embedding_dimension.getValue() >= 0) {
	      myParam.input_embedding_dimension = myParam.output_embedding_dimension = embedding_dimension.getValue();
      }
	  myParam.input_embeddings_file = input_embeddings_file.getValue();

	  myParam.context_vector_size = context_vector_size.getValue();

      myParam.minibatch_size = minibatch_size.getValue();
      myParam.validation_minibatch_size = validation_minibatch_size.getValue();
      myParam.num_epochs= num_epochs.getValue();
      myParam.learning_rate = learning_rate.getValue();
      myParam.conditioning_constant = conditioning_constant.getValue();
      myParam.decay = decay.getValue();
      myParam.adagrad_epsilon = adagrad_epsilon.getValue();
      myParam.use_momentum = use_momentum.getValue();
      myParam.share_embeddings = share_embeddings.getValue();
      myParam.normalization = normalization.getValue();
      myParam.initial_momentum = initial_momentum.getValue();
      myParam.final_momentum = final_momentum.getValue();
      myParam.L2_reg = L2_reg.getValue();
      myParam.init_normal= init_normal.getValue();
      myParam.init_range = init_range.getValue();
      myParam.normalization_init = normalization_init.getValue();
      myParam.parameter_update = parameter_update.getValue();
	  myParam.hidden1_dropout_value = hidden1_dropout_value.getValue();
	  myParam.hidden2_dropout_value = hidden2_dropout_value.getValue();
	  myParam.hidden3_dropout_value = hidden3_dropout_value.getValue();

	  myParam.train_context_vectors_file = train_context_vectors_file.getValue();
	  myParam.validation_context_vectors_file = validation_context_vectors_file.getValue();

      cerr << "Command line: " << endl;
      cerr << boost::algorithm::join(vector<string>(argv, argv+argc), " ") << endl;

      const string sep(" Value: ");
      cerr << train_file.getDescription() << sep << train_file.getValue() << endl;
      cerr << validation_file.getDescription() << sep << validation_file.getValue() << endl;
      cerr << input_words_file.getDescription() << sep << input_words_file.getValue() << endl;
      cerr << output_words_file.getDescription() << sep << output_words_file.getValue() << endl;
      cerr << model_prefix.getDescription() << sep << model_prefix.getValue() << endl;

      cerr << ngram_size.getDescription() << sep << ngram_size.getValue() << endl;
      cerr << input_vocab_size.getDescription() << sep << input_vocab_size.getValue() << endl;
      cerr << output_vocab_size.getDescription() << sep << output_vocab_size.getValue() << endl;
      cerr << mmap_file.getDescription() << sep << mmap_file.getValue() << endl;

      if (embedding_dimension.getValue() >= 0)
      {
	      cerr << embedding_dimension.getDescription() << sep << embedding_dimension.getValue() << endl;
      }
      else
      {
	      cerr << input_embedding_dimension.getDescription() << sep << input_embedding_dimension.getValue() << endl;
	      cerr << output_embedding_dimension.getDescription() << sep << output_embedding_dimension.getValue() << endl;
      }
      cerr << share_embeddings.getDescription() << sep << share_embeddings.getValue() << endl;
      if (share_embeddings.getValue() && input_embedding_dimension.getValue() != output_embedding_dimension.getValue())
      {
	      cerr << "error: sharing input and output embeddings requires that input and output embeddings have same dimension" << endl;
	      exit(1);
      }

      cerr << num_hidden.getDescription() << sep << num_hidden.getValue() << endl;

      if (string_to_activation_function(activation_function.getValue()) == InvalidFunction)
      {
	      cerr << "error: invalid activation function: " << activation_function.getValue() << endl;
	      exit(1);
      }
      cerr << activation_function.getDescription() << sep << activation_function.getValue() << endl;

      if (string_to_loss_function(loss_function.getValue()) == InvalidLoss)
      {
	      cerr << "error: invalid loss function: " << loss_function.getValue() << endl;
	      exit(1);
      }
      cerr << loss_function.getDescription() << sep << loss_function.getValue() << endl;

      cerr << init_normal.getDescription() << sep << init_normal.getValue() << endl;
      cerr << init_range.getDescription() << sep << init_range.getValue() << endl;

      cerr << num_epochs.getDescription() << sep << num_epochs.getValue() << endl;
      cerr << minibatch_size.getDescription() << sep << minibatch_size.getValue() << endl;
      if (myParam.validation_file != "") {
	     cerr << validation_minibatch_size.getDescription() << sep << validation_minibatch_size.getValue() << endl;
      }
      cerr << learning_rate.getDescription() << sep << learning_rate.getValue() << endl;
      cerr << L2_reg.getDescription() << sep << L2_reg.getValue() << endl;

      cerr << num_noise_samples.getDescription() << sep << num_noise_samples.getValue() << endl;

      cerr << normalization.getDescription() << sep << normalization.getValue() << endl;
      if (myParam.normalization){
	      cerr << normalization_init.getDescription() << sep << normalization_init.getValue() << endl;
      }

      cerr << use_momentum.getDescription() << sep << use_momentum.getValue() << endl;
      if (myParam.use_momentum)
      {
        cerr << initial_momentum.getDescription() << sep << initial_momentum.getValue() << endl;
        cerr << final_momentum.getDescription() << sep << final_momentum.getValue() << endl;
      }

      cerr << num_threads.getDescription() << sep << num_threads.getValue() << endl;

      if (unigram_probs_file.getValue() != "")
      {
	      cerr << "Note: --unigram_probs_file is deprecated and ignored." << endl;
      }
    }
    catch (TCLAP::ArgException &e)
    {
      cerr << "error: " << e.error() <<  " for arg " << e.argId() << endl;
      exit(1);
    }

    myParam.num_threads = setup_threads(myParam.num_threads);
    int save_threads;

    //unsigned seed = std::time(0);
	//cerr<<"Seed is "<<seed<<endl;
    unsigned seed = 1234; //for testing only
    mt19937 rng(seed);

    /////////////////////////READING IN THE TRAINING AND VALIDATION DATA///////////////////
    /////////////////////////////////////////////////////////////////////////////////////

    // Read training data

    vector<int> training_data_flat;
    vec * training_data_flat_mmap;
    data_size_t training_data_size; //num_tokens;
    ip::managed_mapped_file mmap_file;
    if (use_mmap_file == false) {
      cerr<<"Reading data from regular text file "<<endl;
      readDataFile(myParam.train_file, myParam.ngram_size, training_data_flat, myParam.minibatch_size);
      training_data_size = training_data_flat.size()/myParam.ngram_size;
    } else {
      cerr<<"Using mmaped file"<<endl;
      mmap_file = ip::managed_mapped_file(ip::open_only,myParam.train_file.c_str());
      training_data_flat_mmap = mmap_file.find<vec>("vector").first;
      cerr<<"Size of mmaped vector is "<<training_data_flat_mmap->size()<<endl;
      training_data_size = training_data_flat_mmap->size()/myParam.ngram_size;
      //randomly shuffle the data for better learning. The shuffling will
      //be different for a standard stl vector
      // Randomly shuffle training data to improve learning
      if (randomize == true) {
        cerr<<"Randomly shuffling data...";
        data_size_t counter =0;
        while (counter < training_data_size) {
          data_size_t upper_limit = counter+5000000;
          long int vector_size = 5000000;
          if (counter + 10000000 >= training_data_size) {
            upper_limit = training_data_size;
            vector_size = training_data_size - counter;
          }
          vector<int> temp(vector_size*myParam.ngram_size,0);
          for (int i=0;i<vector_size;i++){
           for (int k=0;k<myParam.ngram_size;k++) {
             temp[i*myParam.ngram_size+k] = training_data_flat_mmap->at((i+counter)*myParam.ngram_size+k);
           }
          }
          /*
          for (data_size_t i=upper_limit; i>counter; i--)
          {
            if (i %500000 == 0) {
              cerr<<"Shuffled "<<training_data_size-1<<" instances...";
            }
            data_size_t j = uniform_int_distribution<data_size_t>(0, i-1)(rng);
            for (int k=0;k<myParam.ngram_size;k++) {
              int temp_val = training_data_flat_mmap->at(i*myParam.ngram_size+k);
              training_data_flat_mmap->at(i*myParam.ngram_size+k) =
                training_data_flat_mmap->at(j*myParam.ngram_size+k);
              training_data_flat_mmap->at(j*myParam.ngram_size+k) = temp_val;
            }
          }
          */
          for (data_size_t i=vector_size-1; i>0; i--)
          {
            if (i %500000 == 0) {
              cerr<<"Shuffled "<<training_data_size-1<<" instances...";
            }
            data_size_t j = uniform_int_distribution<data_size_t>(0, i-1)(rng);
            for (int k=0;k<myParam.ngram_size;k++) {
              int temp_val = temp.at(i*myParam.ngram_size+k);
              temp.at(i*myParam.ngram_size+k) =
                temp.at(j*myParam.ngram_size+k);
              temp.at(j*myParam.ngram_size+k) = temp_val;
            }
          }
          //Putting it back
          for (int i=0;i<vector_size;i++){
           for (int k=0;k<myParam.ngram_size;k++) {
             training_data_flat_mmap->at((i+counter)*myParam.ngram_size+k) = temp[i*myParam.ngram_size+k];
           }
          }
          counter = upper_limit;
        }
        /*
        for (data_size_t i=training_data_size-1; i>0; i--)
        {
          if (i %500000 == 0) {
            cerr<<"Shuffled "<<training_data_size-1<<" instances...";
          }
          data_size_t j = uniform_int_distribution<data_size_t>(0, i-1)(rng);
          for (int k=0;k<myParam.ngram_size;k++) {
            int temp_val = training_data_flat_mmap->at(i*myParam.ngram_size+k);
            training_data_flat_mmap->at(i*myParam.ngram_size+k) =
              training_data_flat_mmap->at(j*myParam.ngram_size+k);
            training_data_flat_mmap->at(j*myParam.ngram_size+k) = temp_val;
          }
        }
        */
      cerr<<endl;
      }
    }
    //cerr<<"Num tokens "<<num_tokens<<endl;
    //data_size_t training_data_size = num_tokens / myParam.ngram_size;
    cerr << "Number of training instances: "<< training_data_size << endl;

    Matrix<int,Dynamic,Dynamic> training_data;
    //(training_data_flat.data(), myParam.ngram_size, training_data_size);

    #ifdef MAP
    cerr<<"Setting up eigen map"<<endl;
    if (use_mmap_file == false) {
      training_data = Map< Matrix<int,Dynamic,Dynamic> >(training_data_flat.data(), myParam.ngram_size, training_data_size);
    } else {
      training_data = Map< Matrix<int,Dynamic,Dynamic> >(training_data_flat_mmap->data().get(), myParam.ngram_size, training_data_size);
    }
    cerr<<"Created eigen map"<<endl;
    #else
    if (use_mmap_file == false) {
      training_data = Map< Matrix<int,Dynamic,Dynamic> >(training_data_flat.data(), myParam.ngram_size, training_data_size);
    }
    #endif
    // If neither --input_vocab_size nor --input_words_file is given, set input_vocab_size to the maximum word index
    if (myParam.input_vocab_size == 0 and myParam.input_words_file == "")
    {
        myParam.input_vocab_size = training_data.topRows(myParam.ngram_size-1).maxCoeff()+1;
    }

    // If neither --output_vocab_size nor --output_words_file is given, set output_vocab_size to the maximum word index
    if (myParam.output_vocab_size == 0 and myParam.output_words_file == "")
    {
        myParam.output_vocab_size = training_data.row(myParam.ngram_size-1).maxCoeff()+1;
    }

    // Read validation data
    vector<int> validation_data_flat;
    int validation_data_size = 0;

    if (myParam.validation_file != "")
    {
      readDataFile(myParam.validation_file, myParam.ngram_size, validation_data_flat);
      validation_data_size = validation_data_flat.size() / myParam.ngram_size;
      cerr << "Number of validation instances: " << validation_data_size << endl;
    }

	// Read train context vectors
	vector<double> training_context_vectors_flat;
	//int context_size = 128;
    if (myParam.train_context_vectors_file != "")
    {
      readContextVectorsFile(myParam.train_context_vectors_file, myParam.context_vector_size, training_context_vectors_flat);
      int train_context_vectors_flat_size  = training_context_vectors_flat.size() / myParam.context_vector_size;
	  if (train_context_vectors_flat_size != training_data_size){
		  cerr<<"The nubmer of training context vectors was not equal to the training data!"<<endl;
		  exit(1);
	  }
    }

	// Read validation context vectors
	vector<double> validation_context_vectors_flat;
    if (myParam.validation_context_vectors_file != "")
    {
      readContextVectorsFile(myParam.validation_context_vectors_file, myParam.context_vector_size, validation_context_vectors_flat);
      int validation_context_vectors_flat_size  = validation_context_vectors_flat.size() / myParam.context_vector_size;
	  if (validation_context_vectors_flat_size != validation_data_size){
		  cerr<<"The nubmer of validation context vectors was not equal to the validationing data!"<<endl;
		  exit(1);
	  }
    }

	Map< Matrix<double,Dynamic,Dynamic> > validation_context_vectors(validation_context_vectors_flat.data(),
																	myParam.context_vector_size, validation_data_size);

	Map< Matrix<double,Dynamic,Dynamic> > training_context_vectors(training_context_vectors_flat.data(),
																	myParam.context_vector_size, training_data_size);

    Map< Matrix<int,Dynamic,Dynamic> > validation_data(validation_data_flat.data(), myParam.ngram_size, validation_data_size);

    if (use_mmap_file == false && randomize == true) {
      cerr<<"Randomly shuffling data..."<<endl;
      // Randomly shuffle training data to improve learning. Shuffling the context vectors as well
      for (data_size_t i=training_data_size-1; i>0; i--)
      {
        data_size_t j = uniform_int_distribution<data_size_t>(0, i-1)(rng);
        training_data.col(i).swap(training_data.col(j));
		//training_context_vectors.col(i).swap(training_context_vectors.col(j));
      }
    }

    ///// Read in vocabulary file. We don't actually use it; it just gets reproduced in the output file

    vector<string> input_words;
    if (myParam.input_words_file != "")
    {
        readWordsFile(myParam.input_words_file, input_words);
	if (myParam.input_vocab_size == 0)
	    myParam.input_vocab_size = input_words.size();
    }

    vector<string> output_words;
    if (myParam.output_words_file != "")
    {
        readWordsFile(myParam.output_words_file, output_words);
	if (myParam.output_vocab_size == 0)
	    myParam.output_vocab_size = output_words.size();
    }

    ///// Construct unigram model and sampler that will be used for NCE

    vector<data_size_t> unigram_counts(myParam.output_vocab_size);
    for (data_size_t train_id=0; train_id < training_data_size; train_id++)
    {
        int output_word;
        if (use_mmap_file == false) {
          output_word = training_data(myParam.ngram_size-1, train_id);
        } else {
	      //cerr<<"mmap word is "<<training_data_flat_mmap->at((train_id+1)*myParam.ngram_size - 1)<<endl;
          output_word = training_data_flat_mmap->at((train_id+1)*myParam.ngram_size - 1);
        }
		//cerr<<"output word is "<<output_word<<endl;
	    unigram_counts[output_word] += 1;
    }
    multinomial<data_size_t> unigram (unigram_counts);

    ///// Create and initialize the neural network and associated propagators.
    model nn;
    // IF THE MODEL FILE HAS BEEN DEFINED, THEN
    // LOAD THE NEURAL NETWORK MODEL
    //if (myParam.model_file != ""){
    //nn.read(myParam.model_file);
    // cerr<<"reading the model"<<endl;

	//} else {
      nn.resize(myParam.ngram_size,
          myParam.input_vocab_size,
          myParam.output_vocab_size,
          myParam.input_embedding_dimension,
          myParam.num_hidden,
		  myParam.num_second_hidden,
          myParam.output_embedding_dimension,
		  myParam.context_vector_size);

      nn.initialize(rng,
          myParam.init_normal,
          myParam.init_range,
          -log(myParam.output_vocab_size),
          myParam.parameter_update,
          myParam.adagrad_epsilon);
      nn.set_activation_function(string_to_activation_function(myParam.activation_function));
	  // If the input embeddings file has been specified then read from the input embeddings files
	  if (myParam.input_embeddings_file != ""){
		  cerr<<" Reading the input embeddings from file"<<myParam.input_embeddings_file<<endl;
		  nn.input_layer.read(myParam.input_embeddings_file);
	  }
      if (myParam.model_file != ""){
        nn.read(myParam.model_file);
        cerr<<"reading the model"<<endl;
      }
    //}
    loss_function_type loss_function = string_to_loss_function(myParam.loss_function);

    propagator prop(nn, myParam.minibatch_size);
    propagator prop_validation(nn, myParam.validation_minibatch_size);
    SoftmaxNCELoss<multinomial<data_size_t> > softmax_loss(unigram);
    // normalization parameters
    vector_map c_h, c_h_running_gradient;

    ///////////////////////TRAINING THE NEURAL NETWORK////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////////////

    data_size_t num_batches = (training_data_size-1)/myParam.minibatch_size + 1;
    cerr<<"Number of training minibatches: "<<num_batches<<endl;

    int num_validation_batches = 0;
    if (validation_data_size > 0)
    {
        num_validation_batches = (validation_data_size-1)/myParam.validation_minibatch_size+1;
	cerr<<"Number of validation minibatches: "<<num_validation_batches<<endl;
    }

    double current_momentum = myParam.initial_momentum;
    double momentum_delta = (myParam.final_momentum - myParam.initial_momentum)/(myParam.num_epochs-1);
    double current_learning_rate = myParam.learning_rate;
    double current_validation_ll = 0.0;
	double current_validation_accuracy = 0.0;

    int ngram_size = myParam.ngram_size;
    int input_vocab_size = myParam.input_vocab_size;
    int output_vocab_size = myParam.output_vocab_size;
    int minibatch_size = myParam.minibatch_size;
    int validation_minibatch_size = myParam.validation_minibatch_size;
    int num_noise_samples = myParam.num_noise_samples;

    if (myParam.normalization)
    {
      for (data_size_t i=0;i<training_data_size;i++)
      {
          Matrix<int,Dynamic,1> context = training_data.block(0,i,ngram_size-1,1);
          if (c_h.find(context) == c_h.end())
          {
              c_h[context] = -myParam.normalization_init;
          }
      }
    }

	//Setting up the dropout sampler
	boost::random::uniform_real_distribution<> real_01(0, 1);

    for (int epoch=0; epoch<myParam.num_epochs; epoch++)
    {
		double train_correct_labels = 0.;
        cerr << "Epoch " << epoch+1 << endl;
        cerr << "Current learning rate: " << current_learning_rate << endl;

        if (myParam.use_momentum)
	    cerr << "Current momentum: " << current_momentum << endl;
	else
            current_momentum = -1;

	cerr << "Training minibatches: ";

	double log_likelihood = 0.0;

	int num_samples = 0;
	if (loss_function == LogLoss)
	    num_samples = output_vocab_size;
	else if (loss_function == NCELoss)
	    num_samples = 1+num_noise_samples;

	Matrix<double,Dynamic,Dynamic> minibatch_weights(num_samples, minibatch_size);
	Matrix<int,Dynamic,Dynamic> minibatch_samples(num_samples, minibatch_size);
	Matrix<double,Dynamic,Dynamic> scores(num_samples, minibatch_size);
	Matrix<double,Dynamic,Dynamic> probs(num_samples, minibatch_size);
	Matrix<double,Dynamic,Dynamic> first_hidden_dropout_mask(myParam.num_hidden,minibatch_size);
	Matrix<double,Dynamic,Dynamic> second_hidden_dropout_mask(myParam.output_embedding_dimension,minibatch_size);

  for(data_size_t batch=0;batch<num_batches;batch++)
        {
            if (batch > 0 && batch % 10000 == 0)
            {
	        cerr << batch <<"...";
            }

            data_size_t minibatch_start_index = minibatch_size * batch;

      int current_minibatch_size = min(static_cast<data_size_t>(minibatch_size), training_data_size - minibatch_start_index);
      #ifdef MAP
	    Matrix<int,Dynamic,Dynamic> minibatch = training_data.middleCols(minibatch_start_index, current_minibatch_size);
      #else
      //ALTERNATIVE OPTION IF YOU'RE NOT USING eigen map interface on the mmapped file
	    Matrix<int,Dynamic,Dynamic> minibatch;// = training_data.middleCols(minibatch_start_index, current_minibatch_size);
		//cerr<<"Minibatch start index "<<minibatch_start_index<<endl;
		//cerr<<"Minibatch size "<<current_minibatch_size<<endl;
            if (use_mmap_file == true) {
            minibatch.setZero(ngram_size,current_minibatch_size);
            //now reading the ngrams from the mmaped file
              for (int k=0; k<ngram_size; k++){
                for (data_size_t index = 0 ; index<current_minibatch_size; index++) {
				  data_size_t current_index = index + minibatch_start_index;
				  //cerr<<"the value in the mmap file "<<index<<" "<<k<<" is "<<training_data_flat_mmap->at(current_index*ngram_size+k)<<endl;
                  minibatch(k,index) = training_data_flat_mmap->at(current_index*ngram_size+k);
                }
              }
            } else {
              minibatch = training_data.middleCols(minibatch_start_index, current_minibatch_size);
            }
      #endif
            double adjusted_learning_rate = current_learning_rate/current_minibatch_size;
            //cerr<<"Adjusted learning rate: "<<adjusted_learning_rate<<endl;

            /*
            if (batch == rand() % num_batches)
            {
                cerr<<"we are checking the gradient in batch "<<batch<<endl;
                /////////////////////////CHECKING GRADIENTS////////////////////////////////////////
                gradientChecking(myParam,minibatch_start_index,current_minibatch_size,word_nodes,context_nodes,hidden_layer_node,hidden_layer_to_output_node,
                              shuffled_training_data,c_h,unif_real_vector,eng_real_vector,unif_int_vector,eng_int_vector,unigram_probs_vector,
                              q_vector,J_vector,D_prime);
            }
            */

		// Generating the dropout mask
		for(int q=0; q<current_minibatch_size; q++){
			for (int p=0; p<myParam.num_hidden; p++){
				first_hidden_dropout_mask(p,q) = ( real_01(rng) <1.0 - myParam.hidden1_dropout_value ) ? 0: 1;
			}
			for (int p=0; p<myParam.output_embedding_dimension; p++){
				second_hidden_dropout_mask(p,q) = ( real_01(rng) < 1.0 - myParam.hidden2_dropout_value) ? 0: 1;
			}

		}
		//cerr<<"Current minibatch size is "<<current_minibatch_size<<endl;
		//cerr<<"Shape of dropout mask is "<<second_hidden_dropout_mask.rows()<<" "<<second_hidden_dropout_mask.cols()<<endl;
		//cerr<<"Shape of dropout mask is "<<first_hidden_dropout_mask.rows()<<" "<<first_hidden_dropout_mask.cols()<<endl;
		//cerr<<"First hidden dropout mask is "<<first_hidden_dropout_mask<<endl;
		//cerr<<"dropout mas is "<<second_hidden_dropout_mask<<endl;
		//getchar();

        ///// Forward propagation
        prop.fPropDropout(minibatch.topRows(ngram_size-1),
					first_hidden_dropout_mask,
					second_hidden_dropout_mask);

	    if (loss_function == NCELoss)
	    {
	      ///// Noise-contrastive estimation

	      // Generate noise samples. Gather positive and negative samples into matrix.

	      start_timer(3);

        minibatch_samples.block(0, 0, 1, current_minibatch_size) = minibatch.bottomRows(1);

        for (int sample_id = 1; sample_id < num_noise_samples+1; sample_id++)
            for (int train_id = 0; train_id < current_minibatch_size; train_id++)
                minibatch_samples(sample_id, train_id) = unigram.sample(rng);

        stop_timer(3);

        // Final forward propagation step (sparse)
        start_timer(4);
        prop.output_layer_node.param->fProp(prop.second_hidden_activation_node.fProp_matrix,
                    minibatch_samples, scores);
        stop_timer(4);

        // Apply normalization parameters
        if (myParam.normalization)
        {
            for (int train_id = 0;train_id < current_minibatch_size;train_id++)
            {
          Matrix<int,Dynamic,1> context = minibatch.block(0, train_id, ngram_size-1, 1);
          scores.col(train_id).array() += c_h[context];
            }
        }

        double minibatch_log_likelihood;
        start_timer(5);
        softmax_loss.fProp(scores.leftCols(current_minibatch_size),
               minibatch_samples,
               probs,
			   minibatch_log_likelihood);
        stop_timer(5);
        log_likelihood += minibatch_log_likelihood;

        ///// Backward propagation

        start_timer(6);
        softmax_loss.bProp(probs, minibatch_weights);
        stop_timer(6);

        // Update the normalization parameters

        if (myParam.normalization)
        {
          for (int train_id = 0;train_id < current_minibatch_size;train_id++)
          {
            Matrix<int,Dynamic,1> context = minibatch.block(0, train_id, ngram_size-1, 1);
            c_h[context] += adjusted_learning_rate * minibatch_weights.col(train_id).sum();
          }
        }

        // Be careful of short minibatch
        prop.bProp(minibatch.topRows(ngram_size-1),
             minibatch_samples.leftCols(current_minibatch_size),
             minibatch_weights.leftCols(current_minibatch_size),
			 first_hidden_dropout_mask.leftCols(current_minibatch_size),
			 second_hidden_dropout_mask.leftCols(current_minibatch_size),
             adjusted_learning_rate,
             current_momentum,
             myParam.L2_reg,
             myParam.parameter_update,
             myParam.conditioning_constant,
             myParam.decay);
	    }
	    else if (loss_function == LogLoss)
	    {
	      ///// Standard log-likelihood
	      start_timer(4);
        prop.output_layer_node.param->fProp(prop.second_hidden_activation_node.fProp_matrix, scores);
        stop_timer(4);

        double minibatch_log_likelihood;
        start_timer(5);
        SoftmaxLogLoss().fProp(scores.leftCols(current_minibatch_size),
                   minibatch.row(ngram_size-1),
                   probs,
                   minibatch_log_likelihood);
        stop_timer(5);
        log_likelihood += minibatch_log_likelihood;

        ///// Backward propagation

        start_timer(6);
        SoftmaxLogLoss().bProp(minibatch.row(ngram_size-1).leftCols(current_minibatch_size),
                   probs.leftCols(current_minibatch_size),
                   minibatch_weights);
        stop_timer(6);
        //Computing the training accuracy
   		for (int minibatch_instance=0; minibatch_instance < current_minibatch_size; minibatch_instance++){
   			Matrix<double,1,Dynamic>::Index max_index;
   			probs.col(minibatch_instance).maxCoeff(&max_index);
   			if (max_index ==
   				minibatch.row(ngram_size-1).leftCols(current_minibatch_size)(minibatch_instance)){
   					train_correct_labels += 1;
   				}
   		}

        prop.bProp(minibatch.topRows(ngram_size-1).leftCols(current_minibatch_size),
             minibatch_weights,
			 first_hidden_dropout_mask,
			 second_hidden_dropout_mask,
             adjusted_learning_rate,
             current_momentum,
             myParam.L2_reg,
             myParam.parameter_update,
             myParam.conditioning_constant,
             myParam.decay);
          }
      }

	cerr << "done." << endl;

	if (loss_function == LogLoss)
	{
	    cerr << "Training log-likelihood: " << log_likelihood << endl;
        cerr << "         perplexity:     "<< exp(-log_likelihood/training_data_size) << endl;
		cerr << "         accuracy:       "<< train_correct_labels/training_data_size <<endl;
	}
	else if (loss_function == NCELoss)
	    cerr << "Training NCE log-likelihood: " << log_likelihood << endl;

        current_momentum += momentum_delta;

	#ifdef USE_CHRONO
	cerr << "Propagation times:";
	for (int i=0; i<timer.size(); i++)
	  cerr << " " << timer.get(i);
	cerr << endl;
	#endif

	//Scaled the model before writing
	nn.scale(myParam.hidden1_dropout_value,
			myParam.hidden2_dropout_value,
			myParam.hidden3_dropout_value);
	if (myParam.model_prefix != "")
	{
	    cerr << "Writing model" << endl;
	    if (myParam.input_words_file != "")
	        nn.write(myParam.model_prefix + "." + lexical_cast<string>(epoch+1), input_words, output_words);
	    else
	        nn.write(myParam.model_prefix + "." + lexical_cast<string>(epoch+1));
	}

        if (epoch % 1 == 0 && validation_data_size > 0)
        {
		    //Matrix<int,Dynamic,1> validation_argmaxes;
			//validation_argmaxes.setZero(validation_data_size);
			vector<int> validation_argmaxes;
			string argmax_file = "argmax."+lexical_cast<string>(epoch+1);
            //////COMPUTING VALIDATION SET PERPLEXITY///////////////////////
            ////////////////////////////////////////////////////////////////

            double log_likelihood = 0.0;

	    Matrix<double,Dynamic,Dynamic> scores(output_vocab_size, validation_minibatch_size);
	    Matrix<double,Dynamic,Dynamic> output_probs(output_vocab_size, validation_minibatch_size);
	    Matrix<int,Dynamic,Dynamic> minibatch(ngram_size, validation_minibatch_size);
		double validation_correct_labels  =0;
            for (int validation_batch =0;validation_batch < num_validation_batches;validation_batch++)
            {
                int validation_minibatch_start_index = validation_minibatch_size * validation_batch;
		int current_minibatch_size = min(validation_minibatch_size,
						 validation_data_size - validation_minibatch_start_index);
		minibatch.leftCols(current_minibatch_size) = validation_data.middleCols(validation_minibatch_start_index,
											current_minibatch_size);
		prop_validation.fPropContext(minibatch.topRows(ngram_size-1));

		// Do full forward prop through output word embedding layer
		start_timer(4);
		prop_validation.output_layer_node.param->fProp(prop_validation.second_hidden_activation_node.fProp_matrix, scores);
		stop_timer(4);

		// And softmax and loss. Be careful of short minibatch
		double minibatch_log_likelihood;
		start_timer(5);
		SoftmaxLogLoss().fProp(scores.leftCols(current_minibatch_size),
				       minibatch.row(ngram_size-1),
				       output_probs,
				       minibatch_log_likelihood);
		stop_timer(5);
		log_likelihood += minibatch_log_likelihood;
	   		for (int minibatch_instance=0; minibatch_instance < current_minibatch_size; minibatch_instance++){
	   			Matrix<double,1,Dynamic>::Index max_index;
	   			output_probs.col(minibatch_instance).maxCoeff(&max_index);
				validation_argmaxes.push_back(max_index);
	   			if (max_index ==
	   				minibatch.row(ngram_size-1)(minibatch_instance)){
	   					validation_correct_labels += 1;
	   				}
	   		}
	    }
		   /*
           //CREATING AN EIGEN MATRIX OUT OF THE ARGMAXES
		   Matrix<int,Dynamic,1> argmaxes_vector(validation_data_size);
		   for (data_size_t index=0; index<=validation_data_size; validation_data_size++){
		     argmaxes_vector(index) = validation_argmaxes[index];
		   }
		   */
		   //argmaxes_vector = Map< Matrix<int,Dynamic,1> >(validation_argmaxes.data(),validation_data_size,1);
            cerr << "Validation log-likelihood: "<< log_likelihood << endl;
            cerr << "           perplexity:     "<< exp(-log_likelihood/validation_data_size) << endl;
			cerr << "           accuracy:       "<< validation_correct_labels/validation_data_size <<endl;
            writeVector(validation_argmaxes,argmax_file);
	    // If the validation perplexity decreases, halve the learning rate.

            //if (epoch > 0 && validation_correct_labels/validation_data_size < current_validation_accuracy && myParam.parameter_update != "ADA" && current_momentum < 0.)
			//
            if (epoch > 0 && log_likelihood < current_validation_ll && myParam.parameter_update != "ADA" && current_momentum < 0.)
            {
                current_learning_rate /= 2;
            }
            current_validation_ll = log_likelihood;
			current_validation_accuracy = validation_correct_labels/validation_data_size;

		}
		//Scaled the model back after validation
		nn.scale(1.0/myParam.hidden1_dropout_value,
			1.0/myParam.hidden2_dropout_value,
			1.0/myParam.hidden3_dropout_value);
    }
    return 0;
}


//==============================================================================
//         Copyright 2003 - 2011 LASMEA UMR 6602 CNRS/Univ. Clermont II
//         Copyright 2009 - 2011 LRI    UMR 8623 CNRS/Univ Paris Sud XI
//         Copyright 2012 - 2014 MetaScale SAS
//
//          Distributed under the Boost Software License, Version 1.0.
//                 See accompanying file LICENSE.txt or copy at
//                     http://www.boost.org/LICENSE_1_0.txt
//==============================================================================
#ifndef BOOST_SIMD_ARITHMETIC_FUNCTIONS_SIMD_COMMON_DIVS_HPP_INCLUDED
#define BOOST_SIMD_ARITHMETIC_FUNCTIONS_SIMD_COMMON_DIVS_HPP_INCLUDED

#include <boost/simd/arithmetic/functions/divs.hpp>
#include <boost/simd/include/functions/simd/is_eqz.hpp>
#include <boost/simd/include/functions/simd/is_nez.hpp>
#include <boost/simd/include/functions/simd/divides.hpp>
#include <boost/simd/include/functions/simd/shift_right.hpp>
#include <boost/simd/include/functions/simd/plus.hpp>
#include <boost/simd/include/functions/simd/if_zero_else_one.hpp>
#include <boost/simd/include/functions/simd/bitwise_or.hpp>
#include <boost/simd/include/functions/simd/bitwise_xor.hpp>
#include <boost/simd/include/functions/simd/if_else.hpp>
#include <boost/simd/include/functions/simd/genmask.hpp>
#include <boost/simd/include/functions/simd/logical_and.hpp>
#include <boost/simd/include/constants/one.hpp>
#include <boost/simd/include/constants/valmin.hpp>
#include <boost/simd/include/constants/valmax.hpp>
#include <boost/simd/sdk/meta/scalar_of.hpp>
#include <boost/simd/sdk/meta/as_logical.hpp>
#include <boost/dispatch/attributes.hpp>

// perhaps divs for signed integral types must invoke correct Valmin entry and invoke  divfix
// also call simply divfix for unsigned
// also rdivide has to be defined as divs for float and as divfix for integers
namespace boost { namespace simd { namespace ext
{
  BOOST_DISPATCH_IMPLEMENT          ( divs_, tag::cpu_, (A0)(X)
                                    , ((simd_<floating_<A0>,X>))
                                      ((simd_<floating_<A0>,X>))
                                    )
  {
    typedef A0 result_type;
    BOOST_FORCEINLINE BOOST_SIMD_FUNCTOR_CALL_REPEAT(2)
    {
      return a0/a1;
    }
  };

  BOOST_DISPATCH_IMPLEMENT          ( divs_, tag::cpu_, (A0)(X)
                                    , ((simd_<uint_<A0>,X>))
                                      ((simd_<uint_<A0>,X>))
                                    )
  {
    typedef A0 result_type;
    BOOST_FORCEINLINE BOOST_SIMD_FUNCTOR_CALL_REPEAT(2)
    {
      typedef typename meta::as_logical<A0>::type bA0;

      const bA0 iseqza1 = is_eqz(a1);
      const A0 aa1 = if_else(iseqza1, One<A0>(), a1);
      const A0 aa0 = if_else(iseqza1, genmask(a0), a0);
      return aa0/aa1;
    }
  };

  BOOST_DISPATCH_IMPLEMENT          ( divs_, tag::cpu_, (A0)(X)
                                    , ((simd_<int_<A0>,X>))
                                      ((simd_<int_<A0>,X>))
                                    )
  {
    typedef A0 result_type;

    BOOST_FORCEINLINE BOOST_SIMD_FUNCTOR_CALL_REPEAT(2)
    {
      typedef typename meta::as_logical<A0>::type bA0;
      typedef typename meta::scalar_of<A0>::type sA0;

      const bA0 iseqza1 = is_eqz(a1);

      // replace valmin/-1 by (valmin+1)/-1
      A0 x = a0 + if_zero_else_one((a1 + One<A0>()) | (a0 + Valmin<A0>()));
      // negative -> valmin
      // positive -> valmax
      const A0 x2 = bitwise_xor(Valmax<A0>(), shrai(x, sizeof(sA0)*CHAR_BIT-1));

      x = if_else(logical_and(iseqza1, is_nez(x)), x2, x);
      const A0 y = if_else(iseqza1, One<A0>(), a1);
      return x/y;
    }
  };
} } }

#endif


// This file is part of libigl, a simple c++ geometry processing library.
//
// Copyright (C) 2015 Alec Jacobson <alecjacobson@gmail.com>
//
// This Source Code Form is subject to the terms of the Mozilla Public License
// v. 2.0. If a copy of the MPL was not distributed with this file, You can
// obtain one at http://mozilla.org/MPL/2.0/.
#include "planarize_quad_mesh.h"
#include "quad_planarity.h"
#include <Eigen/Sparse>
#include <Eigen/Eigenvalues>
#include <iostream>

namespace igl
{
  template <typename DerivedV, typename DerivedF>
  class PlanarizerShapeUp
  {
  protected:
    // number of faces, number of vertices
    long numV, numF;
    // references to the input faces and vertices
    const Eigen::MatrixBase<DerivedV> &Vin;
    const Eigen::MatrixBase<DerivedF> &Fin;

    // vector consisting of the vertex positions stacked: [x;y;z;x;y;z...]
    // vector consisting of a weight per face (currently all set to 1)
    // vector consisting of the projected face vertices (might be different for the same vertex belonging to different faces)
    Eigen::Matrix<typename DerivedV::Scalar, Eigen::Dynamic, 1> Vv, weightsSqrt, P;

    // Matrices as in the paper
    // Q: lhs matrix
    // Ni: matrix that subtracts the mean of a face from the 4 vertices of a face
    Eigen::SparseMatrix<typename DerivedV::Scalar > Q, Ni;
    Eigen::SimplicialLDLT<Eigen::SparseMatrix<typename DerivedV::Scalar > > solver;

    int maxIter;
    double threshold;
    const int ni = 4;

    // Matrix assemblers
    inline void assembleQ();
    inline void assembleP();
    inline void assembleNi();

    // Selects out of Vv the 4 vertices belonging to face fi
    inline void assembleSelector(int fi,
                          Eigen::SparseMatrix<typename DerivedV::Scalar > &S);

  public:
    // Init - assemble stacked vector and lhs matrix, factorize
    inline PlanarizerShapeUp(const Eigen::MatrixBase<DerivedV> &V_,
                             const Eigen::MatrixBase<DerivedF> &F_,
                             const int maxIter_,
                             const double &threshold_);
    // Planarization - output to Vout
    inline void planarize(Eigen::PlainObjectBase<DerivedV> &Vout);
  };
}

//Implementation

template <typename DerivedV, typename DerivedF>
inline igl::PlanarizerShapeUp<DerivedV, DerivedF>::PlanarizerShapeUp(const Eigen::MatrixBase<DerivedV> &V_,
                                                                     const Eigen::MatrixBase<DerivedF> &F_,
                                                                     const int maxIter_,
                                                                     const double &threshold_):
numV(V_.rows()),
numF(F_.rows()),
Vin(V_),
Fin(F_),
weightsSqrt(Eigen::Matrix<typename DerivedV::Scalar, Eigen::Dynamic, 1>::Ones(numF,1)),
maxIter(maxIter_),
threshold(threshold_)
{
  // assemble stacked vertex position vector
  Vv.setZero(3*numV,1);
  for (int i =0;i<numV;++i)
    Vv.segment(3*i,3) = Vin.row(i);
  // assemble and factorize lhs matrix
  assembleQ();
};

template <typename DerivedV, typename DerivedF>
inline void igl::PlanarizerShapeUp<DerivedV, DerivedF>::assembleQ()
{
  std::vector<Eigen::Triplet<typename DerivedV::Scalar> > tripletList;

  // assemble the Ni matrix
  assembleNi();

  for (int fi = 0; fi< numF; fi++)
  {
    Eigen::SparseMatrix<typename DerivedV::Scalar > Sfi;
    assembleSelector(fi, Sfi);

    // the final matrix per face
    Eigen::SparseMatrix<typename DerivedV::Scalar > Qi = weightsSqrt(fi)*Ni*Sfi;
    // put it in the correct block of Q
    // todo: this can be made faster by omitting the selector matrix
    for (int k=0; k<Qi.outerSize(); ++k)
      for (typename Eigen::SparseMatrix<typename DerivedV::Scalar >::InnerIterator it(Qi,k); it; ++it)
      {
        typename DerivedV::Scalar val = it.value();
        int row = it.row();
        int col = it.col();
        tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(row+3*ni*fi,col,val));
      }
  }

  Q.resize(3*ni*numF,3*numV);
  Q.setFromTriplets(tripletList.begin(), tripletList.end());
  // the actual lhs matrix is Q'*Q
  // prefactor that matrix
  solver.compute(Q.transpose()*Q);
  if(solver.info()!=Eigen::Success)
  {
    std::cerr << "Cholesky failed - PlanarizerShapeUp.cpp" << std::endl;
    assert(0);
  }
}

template <typename DerivedV, typename DerivedF>
inline void igl::PlanarizerShapeUp<DerivedV, DerivedF>::assembleNi()
{
  std::vector<Eigen::Triplet<typename DerivedV::Scalar>> tripletList;
  for (int ii = 0; ii< ni; ii++)
  {
    for (int jj = 0; jj< ni; jj++)
    {
      tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*ii+0,3*jj+0,-1./ni));
      tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*ii+1,3*jj+1,-1./ni));
      tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*ii+2,3*jj+2,-1./ni));
    }
    tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*ii+0,3*ii+0,1.));
    tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*ii+1,3*ii+1,1.));
    tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*ii+2,3*ii+2,1.));
  }
  Ni.resize(3*ni,3*ni);
  Ni.setFromTriplets(tripletList.begin(), tripletList.end());
}

//assumes V stacked [x;y;z;x;y;z...];
template <typename DerivedV, typename DerivedF>
inline void igl::PlanarizerShapeUp<DerivedV, DerivedF>::assembleSelector(int fi,
                                                                            Eigen::SparseMatrix<typename DerivedV::Scalar > &S)
{

  std::vector<Eigen::Triplet<typename DerivedV::Scalar>> tripletList;
  for (int fvi = 0; fvi< ni; fvi++)
  {
    int vi = Fin(fi,fvi);
    tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*fvi+0,3*vi+0,1.));
    tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*fvi+1,3*vi+1,1.));
    tripletList.push_back(Eigen::Triplet<typename DerivedV::Scalar>(3*fvi+2,3*vi+2,1.));
  }

  S.resize(3*ni,3*numV);
  S.setFromTriplets(tripletList.begin(), tripletList.end());

}

//project all faces to their closest planar face
template <typename DerivedV, typename DerivedF>
inline void igl::PlanarizerShapeUp<DerivedV, DerivedF>::assembleP()
{
  P.setZero(3*ni*numF);
  for (int fi = 0; fi< numF; fi++)
  {
    // todo: this can be made faster by omitting the selector matrix
    Eigen::SparseMatrix<typename DerivedV::Scalar > Sfi;
    assembleSelector(fi, Sfi);
    Eigen::SparseMatrix<typename DerivedV::Scalar > NSi = Ni*Sfi;

    Eigen::Matrix<typename DerivedV::Scalar, Eigen::Dynamic, 1> Vi = NSi*Vv;
    Eigen::Matrix<typename DerivedV::Scalar, Eigen::Dynamic, Eigen::Dynamic> CC(3,ni);
    for (int i = 0; i <ni; ++i)
      CC.col(i) = Vi.segment(3*i, 3);
    Eigen::Matrix<typename DerivedV::Scalar, 3, 3> C = CC*CC.transpose();

    // Alec: Doesn't compile
    Eigen::EigenSolver<Eigen::Matrix<typename DerivedV::Scalar, 3, 3>> es(C);
    // the real() is for compilation purposes
    Eigen::Matrix<typename DerivedV::Scalar, 3, 1> lambda = es.eigenvalues().real();
    Eigen::Matrix<typename DerivedV::Scalar, 3, 3> U = es.eigenvectors().real();
    int min_i;
    lambda.cwiseAbs().minCoeff(&min_i);
    U.col(min_i).setZero();
    Eigen::Matrix<typename DerivedV::Scalar, Eigen::Dynamic, Eigen::Dynamic> PP = U*U.transpose()*CC;
    for (int i = 0; i <ni; ++i)
     P.segment(3*ni*fi+3*i, 3) =  weightsSqrt[fi]*PP.col(i);

  }
}

template <typename DerivedV, typename DerivedF>
inline void igl::PlanarizerShapeUp<DerivedV, DerivedF>::planarize(Eigen::PlainObjectBase<DerivedV> &Vout)
{
  Eigen::Matrix<typename DerivedV::Scalar, Eigen::Dynamic, 1> planarity;
  Vout = Vin;

  for (int iter =0; iter<maxIter; ++iter)
  {
    igl::quad_planarity(Vout, Fin, planarity);
    typename DerivedV::Scalar nonPlanarity = planarity.cwiseAbs().maxCoeff();
    //std::cerr<<"iter #"<<iter<<": max non-planarity: "<<nonPlanarity<<std::endl;
    if (nonPlanarity<threshold)
      break;
    assembleP();
    Vv = solver.solve(Q.transpose()*P);
    if(solver.info()!=Eigen::Success)
    {
      std::cerr << "Linear solve failed - PlanarizerShapeUp.cpp" << std::endl;
      assert(0);
    }
    for (int i =0;i<numV;++i)
      Vout.row(i) << Vv.segment(3*i,3).transpose();
  }
  // set the mean of Vout to the mean of Vin
  Eigen::Matrix<typename DerivedV::Scalar, 1, 3> oldMean, newMean;
  oldMean = Vin.colwise().mean();
  newMean = Vout.colwise().mean();
  Vout.rowwise() += (oldMean - newMean);

};

template <typename DerivedV, typename DerivedF>
IGL_INLINE void igl::planarize_quad_mesh(const Eigen::MatrixBase<DerivedV> &Vin,
                                    const Eigen::MatrixBase<DerivedF> &Fin,
                                    const int maxIter,
                                    const double &threshold,
                                    Eigen::PlainObjectBase<DerivedV> &Vout)
{
  PlanarizerShapeUp<DerivedV, DerivedF> planarizer(Vin, Fin, maxIter, threshold);
  planarizer.planarize(Vout);
}

#ifdef IGL_STATIC_LIBRARY
// Explicit template instantiation
template void igl::planarize_quad_mesh<Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, int, double const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&);
#endif


//---------------------------------------------------------------------------//
// Copyright (c) 2021 Mikhail Komarov <nemo@nil.foundation>
// Copyright (c) 2021 Ilias Khairullin <ilias@nil.foundation>
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//---------------------------------------------------------------------------//

#ifndef CRYPTO3_PUBKEY_ELGAMAL_VERIFIABLE_HPP
#define CRYPTO3_PUBKEY_ELGAMAL_VERIFIABLE_HPP

#include <tuple>
#include <type_traits>
#include <iterator>
#include <vector>

#include <boost/assert.hpp>

#include <nil/crypto3/algebra/algorithms/pair.hpp>

#include <nil/crypto3/zk/algorithms/prove.hpp>
#include <nil/crypto3/zk/algorithms/verify.hpp>

#include <nil/crypto3/zk/snark/systems/ppzksnark/r1cs_gg_ppzksnark.hpp>

#include <nil/crypto3/pubkey/keys/private_key.hpp>
#include <nil/crypto3/pubkey/keys/verification_key.hpp>
#include <nil/crypto3/pubkey/operations/generate_keypair_op.hpp>
#include <nil/crypto3/pubkey/operations/encrypt_op.hpp>
#include <nil/crypto3/pubkey/operations/decrypt_op.hpp>
#include <nil/crypto3/pubkey/operations/verify_encryption_op.hpp>
#include <nil/crypto3/pubkey/operations/verify_decryption_op.hpp>
#include <nil/crypto3/pubkey/operations/rerandomize_op.hpp>

namespace nil {
    namespace crypto3 {
        namespace pubkey {
            template<typename Curve, std::size_t BlockBits = 4>
            class elgamal_verifiable {
                typedef elgamal_verifiable<Curve, BlockBits> self_type;
                static_assert(BlockBits > 0);

            public:
                typedef Curve curve_type;
                static constexpr std::size_t block_bits = BlockBits;

                typedef public_key<self_type> public_key_type;
                typedef private_key<self_type> private_key_type;
                typedef verification_key<self_type> verification_key_type;
                typedef std::tuple<public_key_type, private_key_type, verification_key_type> keypair_type;

                typedef zk::snark::r1cs_gg_ppzksnark<
                    Curve, zk::snark::r1cs_gg_ppzksnark_generator<Curve, zk::snark::proving_mode::encrypted_input>,
                    zk::snark::r1cs_gg_ppzksnark_prover<Curve, zk::snark::proving_mode::encrypted_input>,
                    zk::snark::r1cs_gg_ppzksnark_verifier_strong_input_consistency<
                        Curve, zk::snark::proving_mode::encrypted_input>,
                    zk::snark::proving_mode::encrypted_input>
                    proof_system_type;

                typedef std::pair<std::vector<typename Curve::template g1_type<>::value_type>,
                                  typename proof_system_type::proof_type>
                    cipher_type;
                typedef std::pair<std::vector<typename Curve::scalar_field_type::value_type>,
                                  typename Curve::template g1_type<>::value_type>
                    decipher_type;
            };

            template<typename Curve, std::size_t BlockBits>
            struct verification_key<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;
                typedef typename Curve::template g2_type<> g2_type;

                friend class decrypt_op<scheme_type>;
                friend class verify_decryption_op<scheme_type>;

                verification_key() = default;
                verification_key(const typename g2_type::value_type &rho_g2,
                                 const std::vector<typename g2_type::value_type> &rho_sv_g2,
                                 const std::vector<typename g2_type::value_type> &rho_rhov_g2) :
                    rho_g2(rho_g2),
                    rho_sv_g2(rho_sv_g2), rho_rhov_g2(rho_rhov_g2) {
                }
                verification_key(typename g2_type::value_type &&rho_g2,
                                 std::vector<typename g2_type::value_type> &&rho_sv_g2,
                                 std::vector<typename g2_type::value_type> &&rho_rhov_g2) :
                    rho_g2(std::move(rho_g2)),
                    rho_sv_g2(std::move(rho_sv_g2)), rho_rhov_g2(std::move(rho_rhov_g2)) {
                }

                // private:
                typename g2_type::value_type rho_g2;
                std::vector<typename g2_type::value_type> rho_sv_g2;
                std::vector<typename g2_type::value_type> rho_rhov_g2;
            };

            template<typename Curve, std::size_t BlockBits>
            struct public_key<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;

                typedef typename Curve::template g1_type<> g1_type;
                typedef typename Curve::template g2_type<> g2_type;

                public_key() = default;
                public_key &operator=(const public_key &other) = default;
                public_key(const public_key &other) = default;
                public_key(public_key &&other) = default;
                public_key(const typename g1_type::value_type &delta_g1,
                           const std::vector<typename g1_type::value_type> &delta_s_g1,
                           const std::vector<typename g1_type::value_type> &t_g1,
                           const std::vector<typename g2_type::value_type> &t_g2,
                           const typename g1_type::value_type &delta_sum_s_g1,
                           const typename g1_type::value_type &gamma_inverse_sum_s_g1) :
                    delta_g1(delta_g1),
                    delta_s_g1(delta_s_g1), t_g1(t_g1), t_g2(t_g2), delta_sum_s_g1(delta_sum_s_g1),
                    gamma_inverse_sum_s_g1(gamma_inverse_sum_s_g1) {
                }
                public_key(typename g1_type::value_type &&delta_g1,
                           std::vector<typename g1_type::value_type> &&delta_s_g1,
                           std::vector<typename g1_type::value_type> &&t_g1,
                           std::vector<typename g2_type::value_type> &&t_g2,
                           typename g1_type::value_type &&delta_sum_s_g1,
                           typename g1_type::value_type &&gamma_inverse_sum_s_g1) :
                    delta_g1(std::move(delta_g1)),
                    delta_s_g1(std::move(delta_s_g1)), t_g1(std::move(t_g1)), t_g2(std::move(t_g2)),
                    delta_sum_s_g1(std::move(delta_sum_s_g1)),
                    gamma_inverse_sum_s_g1(std::move(gamma_inverse_sum_s_g1)) {
                }

                bool operator==(const public_key &other) const {
                    return delta_g1 == other.delta_g1 && delta_s_g1 == other.delta_s_g1 && t_g1 == other.t_g1 &&
                           t_g2 == other.t_g2 && delta_sum_s_g1 == other.delta_sum_s_g1 &&
                           gamma_inverse_sum_s_g1 == other.gamma_inverse_sum_s_g1;
                }

                typename g1_type::value_type delta_g1;
                std::vector<typename g1_type::value_type> delta_s_g1;
                std::vector<typename g1_type::value_type> t_g1;
                std::vector<typename g2_type::value_type> t_g2;
                typename g1_type::value_type delta_sum_s_g1;
                typename g1_type::value_type gamma_inverse_sum_s_g1;
            };

            template<typename Curve, std::size_t BlockBits>
            struct private_key<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;
                typedef typename Curve::scalar_field_type scalar_field_type;

                friend class decrypt_op<scheme_type>;

                private_key() = default;
                private_key(const typename scalar_field_type::value_type &rho) : rho(rho) {
                }

                // private:
                typename scalar_field_type::value_type rho;
            };

            template<typename Curve, std::size_t BlockBits>
            struct generate_keypair_op<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;
                typedef typename scheme_type::proof_system_type proof_system_type;

                typedef typename scheme_type::public_key_type public_key_type;
                typedef typename scheme_type::private_key_type private_key_type;
                typedef typename scheme_type::verification_key_type verification_key_type;
                typedef typename scheme_type::keypair_type keypair_type;

                typedef typename Curve::scalar_field_type scalar_field_type;
                typedef typename Curve::template g1_type<> g1_type;
                typedef typename Curve::template g2_type<> g2_type;

                struct init_params_type {
                    const typename proof_system_type::keypair_type &gg_keypair;
                    std::size_t msg_size;
                };
                struct internal_accumulator_type {
                    const typename proof_system_type::keypair_type &gg_keypair;
                    std::size_t msg_size;
                    std::vector<typename scalar_field_type::value_type> rnd;
                };
                typedef keypair_type result_type;

                static inline internal_accumulator_type init_accumulator(const init_params_type &init_params) {
                    // TODO: check
                    BOOST_ASSERT_MSG(init_params.gg_keypair.second.gamma_ABC_g1.rest.size() > init_params.msg_size,
                                     "Array of gammas in vk should be longer than the message.");
                    return {init_params.gg_keypair, init_params.msg_size,
                            std::vector<typename scalar_field_type::value_type> {}};
                }

                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    std::move(first, last, std::back_inserter(acc.rnd));
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, InputRange range) {
                    update(acc, std::cbegin(range), std::cend(range));
                }

                static inline result_type process(internal_accumulator_type &acc) {
                    // TODO: check
                    BOOST_ASSERT_MSG(acc.rnd.size() >= 3 * acc.msg_size + 2,
                                     "Too few numbers in the source of randomness.");
                    auto rnd_iter = std::cbegin(acc.rnd);

                    typename scalar_field_type::value_type s_sum = scalar_field_type::value_type::zero();

                    std::vector<typename g1_type::value_type> delta_s_g1;
                    typename g1_type::value_type delta_sum_s_g1;
                    typename g1_type::value_type gamma_inverse_sum_s_g1 = acc.gg_keypair.second.gamma_g1;

                    typename scalar_field_type::value_type rho = *rnd_iter++;
                    typename g2_type::value_type rho_g2 = rho * g2_type::value_type::one();
                    std::vector<typename g2_type::value_type> rho_sv_g2;
                    std::vector<typename g2_type::value_type> rho_rhov_g2;

                    std::vector<typename g1_type::value_type> t_g1;
                    std::vector<typename g2_type::value_type> t_g2;

                    delta_s_g1.reserve(acc.msg_size);
                    rho_sv_g2.reserve(acc.msg_size);
                    rho_rhov_g2.reserve(acc.msg_size);
                    t_g1.reserve(acc.msg_size);
                    t_g2.reserve(acc.msg_size + 1);

                    typename scalar_field_type::value_type t = *rnd_iter++;
                    t_g2.emplace_back(t * g2_type::value_type::one());
                    delta_sum_s_g1 = t * acc.gg_keypair.second.delta_g1;

                    for (std::size_t i = 0; i < acc.msg_size; ++i) {
                        typename scalar_field_type::value_type s = *rnd_iter++;
                        typename scalar_field_type::value_type v = *rnd_iter++;
                        typename scalar_field_type::value_type sv = s * v;
                        t = *rnd_iter++;

                        delta_s_g1.emplace_back(s * acc.gg_keypair.second.delta_g1);
                        t_g1.emplace_back(t * acc.gg_keypair.second.gamma_ABC_g1.rest[i]);
                        t_g2.emplace_back(t * g2_type::value_type::one());
                        delta_sum_s_g1 = delta_sum_s_g1 + (s * t) * acc.gg_keypair.second.delta_g1;
                        gamma_inverse_sum_s_g1 = gamma_inverse_sum_s_g1 + s * acc.gg_keypair.second.gamma_g1;

                        rho_sv_g2.emplace_back(sv * g2_type::value_type::one());
                        rho_rhov_g2.emplace_back(v * rho_g2);
                    }
                    gamma_inverse_sum_s_g1 = -gamma_inverse_sum_s_g1;

                    public_key_type pk(acc.gg_keypair.second.delta_g1, delta_s_g1, t_g1, t_g2, delta_sum_s_g1,
                                       gamma_inverse_sum_s_g1);
                    private_key_type sk(rho);
                    verification_key_type vk(rho_g2, rho_sv_g2, rho_rhov_g2);

                    return {pk, sk, vk};
                }
            };

            template<typename Curve, std::size_t BlockBits>
            struct encrypt_op<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;
                typedef typename scheme_type::proof_system_type proof_system_type;
                typedef typename scheme_type::public_key_type public_key_type;

                typedef typename Curve::scalar_field_type scalar_field_type;
                typedef typename Curve::template g1_type<> g1_type;

                struct init_params_type {
                    typename scalar_field_type::value_type r;
                    const public_key_type &pubkey;
                    const typename proof_system_type::keypair_type &gg_keypair;
                    // TODO: accumulate primary_input and auxiliary_input
                    const typename proof_system_type::primary_input_type &primary_input;
                    const typename proof_system_type::auxiliary_input_type &auxiliary_input;
                };
                struct internal_accumulator_type {
                    std::vector<typename scalar_field_type::value_type> plain_text;
                    typename scalar_field_type::value_type r;
                    const public_key_type &pubkey;
                    const typename proof_system_type::keypair_type &gg_keypair;
                    const typename proof_system_type::primary_input_type &primary_input;
                    const typename proof_system_type::auxiliary_input_type &auxiliary_input;
                };
                typedef typename scheme_type::cipher_type result_type;

                static inline internal_accumulator_type init_accumulator(const init_params_type &init_params) {
                    return {std::vector<typename scalar_field_type::value_type> {},
                            std::move(init_params.r),
                            init_params.pubkey,
                            init_params.gg_keypair,
                            init_params.primary_input,
                            init_params.auxiliary_input};
                }

                // TODO: process input data in place
                // TODO: use marshalling module instead of custom marshalling to process input data
                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    std::copy(first, last, std::back_inserter(acc.plain_text));
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, InputRange range) {
                    update(acc, std::cbegin(range), std::cend(range));
                }

                static inline result_type process(internal_accumulator_type &acc) {
                    // TODO: check
                    BOOST_ASSERT_MSG(acc.gg_keypair.second.gamma_ABC_g1.rest.size() > acc.plain_text.size(),
                                     "Array of gammas in vk should be longer than the plain text.");
                    BOOST_ASSERT_MSG(acc.primary_input.size() > acc.plain_text.size(),
                                     "Primary input should be longer than plain text.");
                    BOOST_ASSERT_MSG(acc.gg_keypair.second.gamma_ABC_g1.rest.size() == acc.primary_input.size(),
                                     "Number of gammas should be equal to the primary input size.");
                    BOOST_ASSERT_MSG(acc.plain_text.size() == acc.pubkey.delta_s_g1.size(),
                                     "Plain text size should be equal to the delta_s array size from pk.");
                    BOOST_ASSERT_MSG(acc.plain_text.size() == acc.pubkey.t_g1.size(),
                                     "Plain text size should be equal to the t_g1 array size from pk.");
                    BOOST_ASSERT_MSG(acc.plain_text.size() == acc.pubkey.t_g2.size() - 1,
                                     "Plain text size should be equal to the t_g2 array size from pk.");
                    for (std::size_t i = 0; i < acc.plain_text.size(); ++i) {
                        BOOST_ASSERT_MSG(acc.primary_input[i] == acc.plain_text[i],
                                         "Plain text should be a prefix of primary input.");
                    }

                    typename result_type::first_type ct_g1;
                    ct_g1.reserve(acc.plain_text.size() + 2);
                    ct_g1.emplace_back(acc.r * acc.pubkey.delta_g1);

                    typename g1_type::value_type sum_tm_g1 = acc.r * acc.pubkey.delta_sum_s_g1;

                    for (std::size_t i = 0; i < acc.plain_text.size(); ++i) {
                        ct_g1.emplace_back(acc.r * acc.pubkey.delta_s_g1[i] +
                                           acc.plain_text[i] * acc.gg_keypair.second.gamma_ABC_g1.rest[i]);
                        sum_tm_g1 = sum_tm_g1 + acc.plain_text[i] * acc.pubkey.t_g1[i];
                    }
                    ct_g1.emplace_back(sum_tm_g1);
                    auto proof = zk::prove<proof_system_type>(acc.gg_keypair.first, acc.pubkey, acc.primary_input,
                                                              acc.auxiliary_input, acc.r);

                    return {ct_g1, proof};
                }
            };

            template<typename Curve, std::size_t BlockBits>
            struct decrypt_op<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;
                typedef typename scheme_type::proof_system_type proof_system_type;
                typedef typename scheme_type::private_key_type private_key_type;
                typedef typename scheme_type::verification_key_type verification_key_type;

                typedef typename Curve::scalar_field_type scalar_field_type;
                typedef typename Curve::template g1_type<> g1_type;
                typedef typename Curve::gt_type gt_type;

                struct init_params_type {
                    const private_key_type &privkey;
                    const verification_key_type &vk;
                    const typename proof_system_type::keypair_type &gg_keypair;
                };
                struct internal_accumulator_type {
                    std::vector<typename g1_type::value_type> cipher_text;
                    const private_key_type &privkey;
                    const verification_key_type &vk;
                    const typename proof_system_type::keypair_type &gg_keypair;
                };
                typedef typename scheme_type::decipher_type result_type;

                static inline internal_accumulator_type init_accumulator(const init_params_type &init_params) {
                    return internal_accumulator_type {std::vector<typename g1_type::value_type> {}, init_params.privkey,
                                                      init_params.vk, init_params.gg_keypair};
                }

                // TODO: process input data in place
                // TODO: use marshalling module instead of custom marshalling to process input data
                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    std::copy(first, last, std::back_inserter(acc.cipher_text));
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, InputRange range) {
                    update(acc, std::cbegin(range), std::cend(range));
                }

                static inline result_type process(internal_accumulator_type &acc) {
                    // TODO: check
                    BOOST_ASSERT_MSG(
                        acc.gg_keypair.second.gamma_ABC_g1.rest.size() > acc.cipher_text.size() - 2,
                        "Array of gammas in vk should be longer than the cipher text (exclusive of 2 element in CT).");
                    BOOST_ASSERT_MSG(acc.cipher_text.size() - 2 == acc.vk.rho_sv_g2.size(),
                                     "Cipher text size should be equal to the rho_sv_g2 array size from vk (exclusive "
                                     "of 2 element in CT).");
                    BOOST_ASSERT_MSG(acc.cipher_text.size() - 2 == acc.vk.rho_rhov_g2.size(),
                                     "Cipher text size should be equal to the rho_rhov_g2 array size from vk "
                                     "(exclusive of 2 element in CT).");
                    std::vector<typename scalar_field_type::value_type> m_new;
                    m_new.reserve(acc.cipher_text.size() - 2);

                    for (size_t j = 1; j < acc.cipher_text.size() - 1; ++j) {
                        typename gt_type::value_type ci_sk_i =
                            algebra::pair_reduced<Curve>(acc.cipher_text[j], acc.vk.rho_rhov_g2[j - 1]);
                        typename gt_type::value_type c0_sk_0 =
                            algebra::pair_reduced<Curve>(acc.cipher_text[0], acc.vk.rho_sv_g2[j - 1])
                                .pow(acc.privkey.rho.data);
                        typename gt_type::value_type dec_tmp = ci_sk_i * c0_sk_0.inversed();
                        auto discrete_log = gt_type::value_type::one();
                        typename gt_type::value_type bruteforce = algebra::pair_reduced<Curve>(
                            acc.gg_keypair.second.gamma_ABC_g1.rest[j - 1], acc.vk.rho_rhov_g2[j - 1]);
                        std::size_t exp = 0;
                        bool deciphered = false;
                        do {
                            if (dec_tmp == discrete_log) {
                                m_new.template emplace_back(exp);
                                deciphered = true;
                                break;
                            }
                            discrete_log = discrete_log * bruteforce;
                        } while (exp++ < 1 << scheme_type::block_bits);
                        BOOST_ASSERT_MSG(deciphered, "Decryption failed.");
                    }

                    typename g1_type::value_type verify_c0 = acc.privkey.rho * acc.cipher_text[0];

                    return {m_new, verify_c0};
                }
            };

            template<typename Curve, std::size_t BlockBits>
            struct verify_encryption_op<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;
                typedef typename scheme_type::proof_system_type proof_system_type;
                typedef typename scheme_type::public_key_type public_key_type;

                typedef typename Curve::scalar_field_type scalar_field_type;
                typedef typename Curve::template g1_type<> g1_type;

                struct init_params_type {
                    const public_key_type &pubkey;
                    const typename proof_system_type::verification_key_type &gg_vk;
                    const typename proof_system_type::proof_type &proof;
                    const typename proof_system_type::primary_input_type &unencrypted_primary_input;
                };
                struct internal_accumulator_type {
                    const public_key_type &pubkey;
                    const typename proof_system_type::verification_key_type &gg_vk;
                    const typename proof_system_type::proof_type &proof;
                    const typename proof_system_type::primary_input_type &unencrypted_primary_input;
                    std::vector<typename g1_type::value_type> cipher_text;
                };
                typedef bool result_type;

                static inline internal_accumulator_type init_accumulator(const init_params_type &init_params) {
                    return internal_accumulator_type {init_params.pubkey, init_params.gg_vk, init_params.proof,
                                                      init_params.unencrypted_primary_input,
                                                      std::vector<typename g1_type::value_type> {}};
                }

                // TODO: process input data in place
                // TODO: use marshalling module instead of custom marshalling to process input data
                template<typename InputIterator>
                static inline void update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    std::copy(first, last, std::back_inserter(acc.cipher_text));
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, InputRange range) {
                    update(acc, std::cbegin(range), std::cend(range));
                }

                static inline result_type process(internal_accumulator_type &acc) {
                    return zk::verify<proof_system_type>(std::cbegin(acc.cipher_text), std::cend(acc.cipher_text),
                                                         acc.gg_vk, acc.pubkey, acc.unencrypted_primary_input,
                                                         acc.proof);
                }
            };

            template<typename Curve, std::size_t BlockBits>
            struct verify_decryption_op<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;
                typedef typename scheme_type::proof_system_type proof_system_type;
                typedef typename scheme_type::public_key_type public_key_type;
                typedef typename scheme_type::verification_key_type verification_key_type;

                typedef typename Curve::scalar_field_type scalar_field_type;
                typedef typename Curve::template g1_type<> g1_type;
                typedef typename Curve::template g2_type<> g2_type;
                typedef typename Curve::gt_type gt_type;

                struct init_params_type {
                    const verification_key_type &vk;
                    const typename proof_system_type::keypair_type &gg_keypair;
                    const typename g1_type::value_type &proof;
                };
                struct internal_accumulator_type {
                    const verification_key_type &vk;
                    const typename proof_system_type::keypair_type &gg_keypair;
                    const typename g1_type::value_type &proof;
                    std::vector<typename scalar_field_type::value_type> plain_text;
                    std::vector<typename g1_type::value_type> cipher_text;
                };
                typedef bool result_type;

                static inline internal_accumulator_type init_accumulator(const init_params_type &init_params) {
                    return internal_accumulator_type {init_params.vk, init_params.gg_keypair, init_params.proof,
                                                      std::vector<typename scalar_field_type::value_type> {},
                                                      std::vector<typename g1_type::value_type> {}};
                }

                // TODO: process input data in place
                // TODO: use marshalling module instead of custom marshalling to process input data
                template<typename InputIterator>
                static inline typename std::enable_if<
                    std::is_same<typename scalar_field_type::value_type,
                                 typename std::iterator_traits<InputIterator>::value_type>::value>::type
                    update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    std::copy(first, last, std::back_inserter(acc.plain_text));
                }

                template<typename InputIterator>
                static inline typename std::enable_if<
                    std::is_same<typename g1_type::value_type,
                                 typename std::iterator_traits<InputIterator>::value_type>::value>::type
                    update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    std::copy(first, last, std::back_inserter(acc.cipher_text));
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, InputRange range) {
                    update(acc, std::cbegin(range), std::cend(range));
                }

                static inline result_type process(internal_accumulator_type &acc) {
                    BOOST_ASSERT_MSG(
                        acc.plain_text.size() + 2 == acc.cipher_text.size(),
                        "Cipher text size should be equal to the plain text size (exclusive of 2 element in CT).");
                    BOOST_ASSERT_MSG(acc.gg_keypair.second.gamma_ABC_g1.rest.size() > acc.plain_text.size(),
                                     "Array of gammas in vk should be longer than the plain text.");
                    typename gt_type::value_type vm_gt =
                        algebra::pair_reduced<Curve>(acc.proof, g2_type::value_type::one());
                    typename gt_type::value_type new_c0_v0_gt =
                        algebra::pair_reduced<Curve>(acc.cipher_text[0], acc.vk.rho_g2);
                    bool ans = (vm_gt == new_c0_v0_gt);

                    for (size_t i = 1; i < acc.cipher_text.size() - 1; ++i) {
                        typename gt_type::value_type ci_v_nj_gt =
                            algebra::pair_reduced<Curve>(acc.cipher_text[i], acc.vk.rho_rhov_g2[i - 1]);
                        typename gt_type::value_type v_vj_gt =
                            algebra::pair_reduced<Curve>(acc.proof, acc.vk.rho_sv_g2[i - 1]);
                        typename gt_type::value_type verify_tmp = ci_v_nj_gt * v_vj_gt.inversed();
                        typename gt_type::value_type verify_msg =
                            algebra::pair_reduced<Curve>(acc.gg_keypair.second.gamma_ABC_g1.rest[i - 1],
                                                         acc.vk.rho_rhov_g2[i - 1])
                                .pow(acc.plain_text[i - 1].data);
                        bool ans_m = (verify_tmp == verify_msg);
                        ans &= ans_m;
                    }

                    return ans;
                }
            };

            template<typename Curve, std::size_t BlockBits>
            struct rerandomize_op<elgamal_verifiable<Curve, BlockBits>> {
                typedef elgamal_verifiable<Curve, BlockBits> scheme_type;
                typedef typename scheme_type::proof_system_type proof_system_type;
                typedef typename scheme_type::public_key_type public_key_type;

                typedef typename Curve::scalar_field_type scalar_field_type;
                typedef typename Curve::template g1_type<> g1_type;
                typedef typename Curve::template g2_type<> g2_type;
                typedef typename Curve::gt_type gt_type;

                struct init_params_type {
                    const public_key_type &pubkey;
                    const typename proof_system_type::keypair_type &gg_keypair;
                    const typename proof_system_type::proof_type &proof;
                };
                struct internal_accumulator_type {
                    const public_key_type &pubkey;
                    const typename proof_system_type::keypair_type &gg_keypair;
                    const typename proof_system_type::proof_type &proof;
                    std::vector<typename scalar_field_type::value_type> rnd;
                    std::vector<typename g1_type::value_type> cipher_text;
                };
                typedef typename scheme_type::cipher_type result_type;

                static inline internal_accumulator_type init_accumulator(const init_params_type &init_params) {
                    return internal_accumulator_type {init_params.pubkey, init_params.gg_keypair, init_params.proof,
                                                      std::vector<typename scalar_field_type::value_type> {},
                                                      std::vector<typename g1_type::value_type> {}};
                }

                // TODO: process input data in place
                // TODO: use marshalling module instead of custom marshalling to process input data
                template<typename InputIterator>
                static inline typename std::enable_if<
                    std::is_same<typename scalar_field_type::value_type,
                                 typename std::iterator_traits<InputIterator>::value_type>::value>::type
                    update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    std::copy(first, last, std::back_inserter(acc.rnd));
                }

                template<typename InputIterator>
                static inline typename std::enable_if<
                    std::is_same<typename g1_type::value_type,
                                 typename std::iterator_traits<InputIterator>::value_type>::value>::type
                    update(internal_accumulator_type &acc, InputIterator first, InputIterator last) {
                    std::copy(first, last, std::back_inserter(acc.cipher_text));
                }

                template<typename InputRange>
                static inline void update(internal_accumulator_type &acc, InputRange range) {
                    update(acc, std::cbegin(range), std::cend(range));
                }

                static inline result_type process(internal_accumulator_type &acc) {
                    BOOST_ASSERT_MSG(acc.rnd.size() >= 3, "Too few numbers in the source of randomness (at least 3).");
                    BOOST_ASSERT_MSG(acc.pubkey.delta_s_g1.size() == acc.cipher_text.size() - 2,
                                     "Cipher text size should be equal to the delta_s_g1 array size from pk (exclusive "
                                     "of 2 element in CT).");
                    BOOST_ASSERT_MSG(acc.pubkey.t_g1.size() == acc.cipher_text.size() - 2,
                                     "Cipher text size should be equal to the t_g1 array size from pk (exclusive of 2 "
                                     "element in CT).");
                    BOOST_ASSERT_MSG(acc.pubkey.t_g2.size() - 1 == acc.cipher_text.size() - 2,
                                     "Cipher text size should be equal to the t_g2 array size from pk (exclusive of 2 "
                                     "element in CT).");
                    std::vector<typename g1_type::value_type> ct_g1;
                    ct_g1.reserve(acc.cipher_text.size());

                    auto rnd_it = std::cbegin(acc.rnd);
                    typename scalar_field_type::value_type r = *rnd_it++;
                    typename scalar_field_type::value_type z1 = *rnd_it++;
                    typename scalar_field_type::value_type z2 = *rnd_it++;

                    typename scalar_field_type::value_type z1_inverse = z1.inversed();

                    ct_g1.emplace_back(acc.cipher_text.front() + r * acc.pubkey.delta_g1);
                    for (size_t i = 1; i < acc.cipher_text.size() - 1; ++i) {
                        ct_g1.emplace_back(acc.cipher_text[i] + r * acc.pubkey.delta_s_g1[i - 1]);
                    }
                    ct_g1.emplace_back(acc.cipher_text.back() + r * acc.pubkey.delta_sum_s_g1);

                    typename g1_type::value_type g1_A = z1 * acc.proof.g_A;
                    typename g2_type::value_type g2_B =
                        z1_inverse * acc.proof.g_B + z2 * acc.gg_keypair.second.delta_g2;
                    typename g1_type::value_type g1_C =
                        acc.proof.g_C + z2 * g1_A + r * acc.pubkey.gamma_inverse_sum_s_g1;

                    return std::make_pair(ct_g1, typename proof_system_type::proof_type {
                                                     std::move(g1_A), std::move(g2_B), std::move(g1_C)});
                }
            };
        }    // namespace pubkey
    }        // namespace crypto3
}    // namespace nil

#endif