/*
 * This file is part of the Monero P2Pool <https://github.com/SChernykh/p2pool>
 * Copyright (c) 2021-2022 SChernykh <https://github.com/SChernykh>
 *
 * This program 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, version 3.
 *
 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "common.h"
#include "block_cache.h"
#include "pool_block.h"
#include "p2p_server.h"

static constexpr char log_category_prefix[] = "BlockCache ";

static constexpr uint32_t BLOCK_SIZE = 96 * 1024;
static constexpr uint32_t NUM_BLOCKS = 4608;
static constexpr uint32_t CACHE_SIZE = BLOCK_SIZE * NUM_BLOCKS;
static constexpr char cache_name[] = "p2pool.cache";

namespace p2pool {

struct BlockCache::Impl : public nocopy_nomove
{
#if defined(__linux__) || defined(__unix__) || defined(_POSIX_VERSION) || defined(__MACH__)

	Impl()
	{
		m_fd = open(cache_name, O_RDWR | O_CREAT, static_cast<mode_t>(0600));
		if (m_fd == -1) {
			LOGERR(1, "couldn't open/create " << cache_name);
			return;
		}

		int result = lseek(m_fd, static_cast<off_t>(CACHE_SIZE) - 1, SEEK_SET);
		if (result == -1) {
			LOGERR(1, "lseek failed");
			close(m_fd);
			m_fd = -1;
			return;
		}

		result = write(m_fd, "", 1);
		if (result != 1) {
			LOGERR(1, "write failed");
			close(m_fd);
			m_fd = -1;
			return;
		}

		void* map = mmap(0, CACHE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
		if (map == MAP_FAILED) {
			LOGERR(1, "mmap failed");
			close(m_fd);
			m_fd = -1;
			return;
		}

		m_data = reinterpret_cast<uint8_t*>(map);
	}

	~Impl()
	{
		if (m_data) munmap(m_data, CACHE_SIZE);
		if (m_fd != -1) close(m_fd);
	}

	void flush()
	{
		if (m_data) {
			msync(m_data, CACHE_SIZE, MS_SYNC);
		}
	}

	int m_fd = -1;

#elif defined(_WIN32)

	Impl()
	{
		m_file = CreateFile(cache_name, GENERIC_ALL, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_HIDDEN, NULL);
		if (m_file == INVALID_HANDLE_VALUE) {
			LOGERR(1, "couldn't open " << cache_name << ", error " << static_cast<uint32_t>(GetLastError()));
			return;
		}

		if (SetFilePointer(m_file, CACHE_SIZE, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) {
			LOGERR(1, "SetFilePointer failed, error " << static_cast<uint32_t>(GetLastError()));
			CloseHandle(m_file);
			m_file = INVALID_HANDLE_VALUE;
			return;
		}

		if (!SetEndOfFile(m_file)) {
			LOGERR(1, "SetEndOfFile failed, error " << static_cast<uint32_t>(GetLastError()));
			CloseHandle(m_file);
			m_file = INVALID_HANDLE_VALUE;
			return;
		}

		m_map = CreateFileMapping(m_file, NULL, PAGE_READWRITE, 0, CACHE_SIZE, NULL);
		if (!m_map) {
			LOGERR(1, "CreateFileMapping failed, error " << static_cast<uint32_t>(GetLastError()));
			CloseHandle(m_file);
			m_file = INVALID_HANDLE_VALUE;
			return;
		}

		m_data = reinterpret_cast<uint8_t*>(MapViewOfFile(m_map, FILE_MAP_ALL_ACCESS, 0, 0, 0));
		if (!m_data) {
			LOGERR(1, "MapViewOfFile failed, error " << static_cast<uint32_t>(GetLastError()));
			CloseHandle(m_map);
			CloseHandle(m_file);
			m_map = 0;
			m_file = INVALID_HANDLE_VALUE;
		}
	}

	~Impl()
	{
		if (m_data) UnmapViewOfFile(m_data);
		if (m_map) CloseHandle(m_map);
		if (m_file != INVALID_HANDLE_VALUE) CloseHandle(m_file);
	}

	void flush()
	{
		if (m_data) {
			FlushViewOfFile(m_data, 0);
			FlushFileBuffers(m_file);
		}
	}

	HANDLE m_file = INVALID_HANDLE_VALUE;
	HANDLE m_map = 0;

#else
	// Not implemented on other platforms
	void flush() {}
#endif

	uint8_t* m_data = nullptr;
};

BlockCache::BlockCache()
	: m_impl(new Impl())
	, m_flushRunning(0)
	, m_storeIndex(0)
{
}

BlockCache::~BlockCache()
{
	delete m_impl;
}

void BlockCache::store(const PoolBlock& block)
{
	const size_t n1 = block.m_mainChainData.size();
	const size_t n2 = block.m_sideChainData.size();

	if (!m_impl->m_data || (sizeof(uint32_t) + n1 + n2 > BLOCK_SIZE)) {
		return;
	}

	uint8_t* data = m_impl->m_data + (static_cast<size_t>((m_storeIndex++) % NUM_BLOCKS) * BLOCK_SIZE);

	*reinterpret_cast<uint32_t*>(data) = static_cast<uint32_t>(n1 + n2);
	memcpy(data + sizeof(uint32_t), block.m_mainChainData.data(), n1);
	memcpy(data + sizeof(uint32_t) + n1, block.m_sideChainData.data(), n2);
}

void BlockCache::load_all(SideChain& side_chain, P2PServer& server)
{
	if (!m_impl->m_data) {
		return;
	}

	LOGINFO(1, "loading cached blocks");

	PoolBlock block;
	uint32_t blocks_loaded = 0;

	for (uint64_t i = 0; i < NUM_BLOCKS; ++i) {
		const uint8_t* data = m_impl->m_data + i * BLOCK_SIZE;
		const uint32_t n = *reinterpret_cast<const uint32_t*>(data);

		if (!n || (n + sizeof(uint32_t) > BLOCK_SIZE)) {
			continue;
		}

		if (block.deserialize(data + sizeof(uint32_t), n, side_chain) == 0) {
			server.add_cached_block(block);
			++blocks_loaded;
		}
	}

	LOGINFO(1, "loaded " << blocks_loaded << " cached blocks");
}

void BlockCache::flush()
{
	if (m_flushRunning.exchange(1) == 0) {
		m_impl->flush();
		m_flushRunning.store(0);
	}
}

} // namespace p2pool
