/*
	This file is part of cpp-ethereum.

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

	cpp-ethereum is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with cpp-ethereum.  If not, see <http://www.gnu.org/licenses/>.
*/
/** @file ProofOfWork.cpp
 * @author Gav Wood <i@gavwood.com>
 * @date 2014
 */

#include <boost/detail/endian.hpp>
#include <boost/filesystem.hpp>
#include <chrono>
#include <array>
#include <thread>
#include <random>
#include <thread>
#include <libdevcore/Guards.h>
#include <libdevcore/Log.h>
#include <libdevcrypto/CryptoPP.h>
#include <libdevcrypto/FileSystem.h>
#include <libdevcore/Common.h>
#if ETH_ETHASHCL
#include <libethash-cl/ethash_cl_miner.h>
#define ETHASH_REVISION REVISION
#define ETHASH_DATASET_BYTES_INIT DATASET_BYTES_INIT
#define ETHASH_DATASET_BYTES_GROWTH DATASET_BYTES_GROWTH
#define ETHASH_CACHE_BYTES_INIT CACHE_BYTES_INIT
#define ETHASH_CACHE_BYTES_GROWTH CACHE_BYTES_GROWTH
#define ETHASH_DAGSIZE_BYTES_INIT DAGSIZE_BYTES_INIT
#define ETHASH_DAG_GROWTH DAG_GROWTH
#define ETHASH_EPOCH_LENGTH EPOCH_LENGTH
#define ETHASH_MIX_BYTES MIX_BYTES
#define ETHASH_HASH_BYTES HASH_BYTES
#define ETHASH_DATASET_PARENTS DATASET_PARENTS
#define ETHASH_CACHE_ROUNDS CACHE_ROUNDS
#define ETHASH_ACCESSES ACCESSES
#undef REVISION
#undef DATASET_BYTES_INIT
#undef DATASET_BYTES_GROWTH
#undef CACHE_BYTES_INIT
#undef CACHE_BYTES_GROWTH
#undef DAGSIZE_BYTES_INIT
#undef DAG_GROWTH
#undef EPOCH_LENGTH
#undef MIX_BYTES
#undef HASH_BYTES
#undef DATASET_PARENTS
#undef CACHE_ROUNDS
#undef ACCESSES
#endif
#include "BlockInfo.h"
#include "Ethasher.h"
#include "ProofOfWork.h"
using namespace std;
using namespace std::chrono;

namespace dev
{
namespace eth
{

bool EthashCPU::verify(BlockInfo const& _header)
{
	return Ethasher::verify(_header);
}

std::pair<MineInfo, EthashCPU::Proof> EthashCPU::mine(BlockInfo const& _header, unsigned _msTimeout, bool _continue, bool _turbo)
{
	Ethasher::Miner m(_header);

	std::pair<MineInfo, Proof> ret;
	auto tid = std::this_thread::get_id();
	static std::mt19937_64 s_eng((time(0) + *reinterpret_cast<unsigned*>(m_last.data()) + std::hash<decltype(tid)>()(tid)));
	uint64_t tryNonce = (uint64_t)(u64)(m_last = Nonce::random(s_eng));

	h256 boundary = u256((bigint(1) << 256) / _header.difficulty);
	ret.first.requirement = log2((double)(u256)boundary);

	// 2^ 0      32      64      128      256
	//   [--------*-------------------------]
	//
	// evaluate until we run out of time
	auto startTime = std::chrono::steady_clock::now();
	if (!_turbo)
		std::this_thread::sleep_for(std::chrono::milliseconds(_msTimeout * 90 / 100));
	double best = 1e99;	// high enough to be effectively infinity :)
	Proof result;
	unsigned hashCount = 0;
	for (; (std::chrono::steady_clock::now() - startTime) < std::chrono::milliseconds(_msTimeout) && _continue; tryNonce++, hashCount++)
	{
		h256 val(m.mine(tryNonce));
		best = std::min<double>(best, log2((double)(u256)val));
		if (val <= boundary)
		{
			ret.first.completed = true;
			assert(Ethasher::eval(_header, (Nonce)(u64)tryNonce).value == val);
			result.mixHash = m.lastMixHash();
			result.nonce = u64(tryNonce);
			BlockInfo test = _header;
			assignResult(result, test);
			assert(verify(test));
			break;
		}
	}
	ret.first.hashes = hashCount;
	ret.first.best = best;
	ret.second = result;

	if (ret.first.completed)
	{
		BlockInfo test = _header;
		assignResult(result, test);
		assert(verify(test));
	}

	return ret;
}

#if ETH_ETHASHCL

/*
struct ethash_cl_search_hook
{
	// reports progress, return true to abort
	virtual bool found(uint64_t const* nonces, uint32_t count) = 0;
	virtual bool searched(uint64_t start_nonce, uint32_t count) = 0;
};

class ethash_cl_miner
{
public:
	ethash_cl_miner();

	bool init(ethash_params const& params, const uint8_t seed[32], unsigned workgroup_size = 64);

	void hash(uint8_t* ret, uint8_t const* header, uint64_t nonce, unsigned count);
	void search(uint8_t const* header, uint64_t target, search_hook& hook);
};
*/

struct EthashCLHook: public ethash_cl_search_hook
{
	virtual bool found(uint64_t const* _nonces, uint32_t _count)
	{
		Guard l(x_all);
		for (unsigned i = 0; i < _count; ++i)
			found.push_back((Nonce)(u64)_nonces[i]);
		if (abort)
		{
			aborted = true;
			return true;
		}
		return false;
	}

	virtual bool searched(uint64_t _startNonce, uint32_t _count)
	{
		Guard l(x_all);
		total += _count;
		last = _startNonce + _count;
		if (abort)
		{
			aborted = true;
			return true;
		}
		return false;
	}

	vector<Nonce> fetchFound() { vector<Nonce> ret; Guard l(x_all); std::swap(ret, found); return ret; }
	uint64_t fetchTotal() { Guard l(x_all); auto ret = total; total = 0; return ret; }

	Mutex x_all;
	vector<Nonce> found;
	uint64_t total;
	uint64_t last;
	bool abort = false;
	bool aborted = false;
};

EthashCL::EthashCL():
	m_miner(new ethash_cl_miner),
	m_hook(new EthashCLHook)
{
}

EthashCL::~EthashCL()
{
	m_hook->abort = true;
	for (unsigned timeout = 0; timeout < 100 && !m_hook->aborted; ++timeout)
		std::this_thread::sleep_for(chrono::milliseconds(30));
	if (!m_hook->aborted)
		cwarn << "Couldn't abort. Abandoning OpenCL process.";
}

bool EthashCL::verify(BlockInfo const& _header)
{
	return Ethasher::verify(_header);
}

std::pair<MineInfo, Ethash::Proof> EthashCL::mine(BlockInfo const& _header, unsigned _msTimeout, bool, bool)
{
	if (m_lastHeader.seedHash() != _header.seedHash())
	{
		m_miner->init(Ethasher::params(_header), _header.seedHash().data());
		// TODO: reinit probably won't work when seed changes.
	}
	if (m_lastHeader != _header)
	{
		static std::random_device s_eng;
		uint64_t tryNonce = (uint64_t)(u64)(m_last = Nonce::random(s_eng));
		m_miner->search(_header.headerHash(WithoutNonce).data(), tryNonce, *m_hook);
	}
	m_lastHeader = _header;

	std::this_thread::sleep_for(chrono::milliseconds(_msTimeout));
	auto found = m_hook->fetchFound();
	if (!found.empty())
	{
		h256 mixHash; // ?????
		return std::make_pair(MineInfo{0.0, 1e99, 0, true}, EthashCL::Proof((Nonce)(u64)found[0], mixHash));
	}
	return std::make_pair(MineInfo{0.0, 1e99, 0, false}, EthashCL::Proof());
}

#endif

}
}