/**
 * @file       Benchmark.cpp
 *
 * @brief      Benchmarking tests for Zerocoin.
 *
 * @author     Ian Miers, Christina Garman and Matthew Green
 * @date       June 2013
 *
 * @copyright  Copyright 2013 Ian Miers, Christina Garman and Matthew Green
 * @license    This project is released under the MIT license.
 **/
// Copyright (c) 2017-2018 The PIVX developers

#include <boost/test/unit_test.hpp>
#include <string>
#include <iostream>
#include <fstream>
// #include <curses.h>
#include <exception>
#include <cstdlib>
#include <sys/time.h>
#include "streams.h"
#include "libzerocoin/ParamGeneration.h"
#include "libzerocoin/Denominations.h"
#include "libzerocoin/Coin.h"
#include "libzerocoin/CoinSpend.h"
#include "libzerocoin/Accumulator.h"
#include "test_agrarian.h"

using namespace std;
using namespace libzerocoin;

#define COLOR_STR_GREEN   "\033[32m"
#define COLOR_STR_NORMAL  "\033[0m"
#define COLOR_STR_RED     "\033[31m"

#define TESTS_COINS_TO_ACCUMULATE   50

// Global test counters
uint32_t    ggNumTests        = 0;
uint32_t    ggSuccessfulTests = 0;

// Global coin array
PrivateCoin    *ggCoins[TESTS_COINS_TO_ACCUMULATE];

// Global params
ZerocoinParams *gg_Params;

//////////
// Utility routines
//////////

class Timer
{
	timeval timer[2];

public:

	timeval start()
	{
		gettimeofday(&this->timer[0], NULL);
		return this->timer[0];
	}

	timeval stop()
	{
		gettimeofday(&this->timer[1], NULL);
		return this->timer[1];
	}

	int duration() const
	{
		int secs(this->timer[1].tv_sec - this->timer[0].tv_sec);
		int usecs(this->timer[1].tv_usec - this->timer[0].tv_usec);

		if(usecs < 0)
		{
			--secs;
			usecs += 1000000;
		}

		return static_cast<int>(secs * 1000 + usecs / 1000.0 + 0.5);
	}
};

// Global timer
Timer timer;

void
gLogTestResult(string testName, bool (*testPtr)())
{
	string colorGreen(COLOR_STR_GREEN);
	string colorNormal(COLOR_STR_NORMAL);
	string colorRed(COLOR_STR_RED);

	cout << "Testing if " << testName << "..." << endl;

	bool testResult = testPtr();

	if (testResult == true) {
		cout << "\t" << colorGreen << "[PASS]"  << colorNormal << endl;
		ggSuccessfulTests++;
	} else {
		cout << colorRed << "\t[FAIL]" << colorNormal << endl;
	}

	ggNumTests++;
}

CBigNum
gGetTestModulus()
{
	static CBigNum testModulus(0);

	// TODO: should use a hard-coded RSA modulus for testing
	if (!testModulus) {
		CBigNum p, q;
		p = CBigNum::generatePrime(1024, false);
		q = CBigNum::generatePrime(1024, false);
		testModulus = p * q;
	}

	return testModulus;
}

//////////
// Test routines
//////////


bool
Testb_GenRSAModulus()
{
	CBigNum result = gGetTestModulus();

	if (!result) {
		return false;
	}
	else {
		return true;
	}
}

bool
Testb_CalcParamSizes()
{
	bool result = true;
#if 0

	uint32_t pLen, qLen;

	try {
		calculateGroupParamLengths(4000, 80, &pLen, &qLen);
		if (pLen < 1024 || qLen < 256) {
			result = false;
		}
		calculateGroupParamLengths(4000, 96, &pLen, &qLen);
		if (pLen < 2048 || qLen < 256) {
			result = false;
		}
		calculateGroupParamLengths(4000, 112, &pLen, &qLen);
		if (pLen < 3072 || qLen < 320) {
			result = false;
		}
		calculateGroupParamLengths(4000, 120, &pLen, &qLen);
		if (pLen < 3072 || qLen < 320) {
			result = false;
		}
		calculateGroupParamLengths(4000, 128, &pLen, &qLen);
		if (pLen < 3072 || qLen < 320) {
			result = false;
		}
	} catch (exception &e) {
		result = false;
	}
#endif

	return result;
}

bool
Testb_GenerateGroupParams()
{
	uint32_t pLen = 1024, qLen = 256, count;
	IntegerGroupParams group;

	for (count = 0; count < 1; count++) {

		try {
			group = deriveIntegerGroupParams(calculateSeed(gGetTestModulus(), "test", ZEROCOIN_DEFAULT_SECURITYLEVEL, "TEST GROUP"), pLen, qLen);
		} catch (std::runtime_error e) {
			cout << "Caught exception " << e.what() << endl;
			return false;
		}

		// Now perform some simple tests on the resulting parameters
		if ((uint32_t)group.groupOrder.bitSize() < qLen || (uint32_t)group.modulus.bitSize() < pLen) {
			return false;
		}

		CBigNum c = group.g.pow_mod(group.groupOrder, group.modulus);
		//cout << "g^q mod p = " << c << endl;
		if (!(c.isOne())) return false;

		// Try at multiple parameter sizes
		pLen = pLen * 1.5;
		qLen = qLen * 1.5;
	}

	return true;
}

bool
Testb_ParamGen()
{
	bool result = true;

	try {
		timer.start();
		// Instantiating testParams runs the parameter generation code
		ZerocoinParams testParams(gGetTestModulus(),ZEROCOIN_DEFAULT_SECURITYLEVEL);
		timer.stop();

		cout << "\tPARAMGEN ELAPSED TIME: " << timer.duration() << " ms\t" << timer.duration()*0.001 << " s" << endl;
	} catch (runtime_error e) {
		cout << e.what() << endl;
		result = false;
	}

	return result;
}

bool
Testb_Accumulator()
{
	// This test assumes a list of coins were generated during
	// the Testb_MintCoin() test.
	if (ggCoins[0] == NULL) {
		return false;
	}
	try {
		// Accumulate the coin list from first to last into one accumulator
            Accumulator accOne(&gg_Params->accumulatorParams,libzerocoin::CoinDenomination::ZQ_ONE);
            Accumulator accTwo(&gg_Params->accumulatorParams,libzerocoin::CoinDenomination::ZQ_ONE);
            Accumulator accThree(&gg_Params->accumulatorParams,libzerocoin::CoinDenomination::ZQ_ONE);
            Accumulator accFour(&gg_Params->accumulatorParams,libzerocoin::CoinDenomination::ZQ_ONE);
		AccumulatorWitness wThree(gg_Params, accThree, ggCoins[0]->getPublicCoin());

		for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
			accOne += ggCoins[i]->getPublicCoin();
			accTwo += ggCoins[TESTS_COINS_TO_ACCUMULATE - (i+1)]->getPublicCoin();
			accThree += ggCoins[i]->getPublicCoin();
			wThree += ggCoins[i]->getPublicCoin();
			if(i != 0) {
				accFour += ggCoins[i]->getPublicCoin();
			}
		}

		// Compare the accumulated results
		if (accOne.getValue() != accTwo.getValue() || accOne.getValue() != accThree.getValue()) {
			cout << "Accumulators don't match" << endl;
			return false;
		}

		if(accFour.getValue() != wThree.getValue()) {
			cout << "Witness math not working," << endl;
			return false;
		}

		// Verify that the witness is correct
		if (!wThree.VerifyWitness(accThree, ggCoins[0]->getPublicCoin()) ) {
			cout << "Witness not valid" << endl;
			return false;
		}

	} catch (runtime_error e) {
		cout << e.what() << endl;
        return false;
	}

	return true;
}

bool
Testb_MintCoin()
{
	try {
		// Generate a list of coins
		timer.start();
		for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
            ggCoins[i] = new PrivateCoin(gg_Params,CoinDenomination::ZQ_ONE);
		}
		timer.stop();
	} catch (exception &e) {
		return false;
	}

	cout << "\tMINT ELAPSED TIME:\n\t\tTotal: " << timer.duration() << " ms\t" << timer.duration()*0.001 << " s\n\t\tPer Coin: " << timer.duration()/TESTS_COINS_TO_ACCUMULATE << " ms\t" << (timer.duration()/TESTS_COINS_TO_ACCUMULATE)*0.001 << " s" << endl;

	return true;
}

bool
Testb_MintAndSpend()
{
	try {
		// This test assumes a list of coins were generated in Testb_MintCoin()
		if (ggCoins[0] == NULL)
		{
			// No coins: mint some.
			Testb_MintCoin();
			if (ggCoins[0] == NULL) {
				return false;
			}
		}

		// Accumulate the list of generated coins into a fresh accumulator.
		// The first one gets marked as accumulated for a witness, the
		// others just get accumulated normally.
        Accumulator acc(&gg_Params->accumulatorParams,CoinDenomination::ZQ_ONE);
		AccumulatorWitness wAcc(gg_Params, acc, ggCoins[0]->getPublicCoin());

		timer.start();
		for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
			acc += ggCoins[i]->getPublicCoin();
		}
		timer.stop();

		cout << "\tACCUMULATOR ELAPSED TIME:\n\t\tTotal: " << timer.duration() << " ms\t" << timer.duration()*0.001 << " s\n\t\tPer Element: " << timer.duration()/TESTS_COINS_TO_ACCUMULATE << " ms\t" << (timer.duration()/TESTS_COINS_TO_ACCUMULATE)*0.001 << " s" << endl;

		timer.start();
		for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
			wAcc +=ggCoins[i]->getPublicCoin();
		}
		timer.stop();

		cout << "\tWITNESS ELAPSED TIME: \n\t\tTotal: " << timer.duration() << " ms\t" << timer.duration()*0.001 << " s\n\t\tPer Element: " << timer.duration()/TESTS_COINS_TO_ACCUMULATE << " ms\t" << (timer.duration()/TESTS_COINS_TO_ACCUMULATE)*0.001 << " s" << endl;

		// Now spend the coin
		timer.start();
		CoinSpend spend(gg_Params, gg_Params, *(ggCoins[0]), acc, 0, wAcc, 0, SpendType::SPEND); //(0) presstab
		timer.stop();

		cout << "\tSPEND ELAPSED TIME: " << timer.duration() << " ms\t" << timer.duration()*0.001 << " s" << endl;

		// Serialize the proof and deserialize into newSpend
		CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);

		timer.start();
		ss << spend;
		timer.stop();

		CoinSpend newSpend(gg_Params, gg_Params, ss);

		cout << "\tSERIALIZE ELAPSED TIME: " << timer.duration() << " ms\t" << timer.duration()*0.001 << " s" << endl;

		// Finally, see if we can verify the deserialized proof (return our result)
		timer.start();
		bool ret = newSpend.Verify(acc);
		timer.stop();

		cout << "\tSPEND VERIFY ELAPSED TIME: " << timer.duration() << " ms\t" << timer.duration()*0.001 << " s" << endl;

		return ret;
	} catch (runtime_error &e) {
		cout << e.what() << endl;
		return false;
	}

	return false;
}

void
Testb_RunAllTests()
{
	// Make a new set of parameters from a random RSA modulus
	gg_Params = new ZerocoinParams(gGetTestModulus());

	ggNumTests = ggSuccessfulTests = 0;
	for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
		ggCoins[i] = NULL;
	}

	// Run through all of the Zerocoin tests
	gLogTestResult("an RSA modulus can be generated", Testb_GenRSAModulus);
	gLogTestResult("parameter sizes are correct", Testb_CalcParamSizes);
	gLogTestResult("group/field parameters can be generated", Testb_GenerateGroupParams);
	gLogTestResult("parameter generation is correct", Testb_ParamGen);
	gLogTestResult("coins can be minted", Testb_MintCoin);
	gLogTestResult("the accumulator works", Testb_Accumulator);
	gLogTestResult("a minted coin can be spent", Testb_MintAndSpend);

	// Summarize test results
	if (ggSuccessfulTests < ggNumTests) {
		cout << endl << "ERROR: SOME TESTS FAILED" << endl;
	}

	// Clear any generated coins
	for (uint32_t i = 0; i < TESTS_COINS_TO_ACCUMULATE; i++) {
		delete ggCoins[i];
	}

	cout << ggSuccessfulTests << " out of " << ggNumTests << " tests passed." << endl << endl;
	delete gg_Params;
}

BOOST_FIXTURE_TEST_SUITE(benchmark_zerocoin, TestingSetup)

BOOST_AUTO_TEST_CASE(benchmark_test)
{
	cout << "libzerocoin v" << ZEROCOIN_VERSION_STRING << " benchmark utility." << endl << endl;

	Testb_RunAllTests();
}
BOOST_AUTO_TEST_SUITE_END()

