#include "UnrealProject.h"

#if WITH_EDITOR

#include "Metrics.hpp"
//#include <append>
#include <timer>

#include <fstream>
#include <unordered_map>
#include <unordered_set>
#include <chrono>
#include <algorithm>

#if PLATFORM_SPECIFIC_WIN == 0
#include "AllowWindowsPlatformTypes.h"
#include <Windows.h>
#endif

using namespace jlib;
using namespace std;
using namespace std::chrono;

#include "Events.hpp"

#include <type_traits>

namespace Metrics
{
	ofstream writeStream;
	timer writeTime;
	uint32_t lastWriteTime;

	unordered_map<uint8_t, PlayerHandle*> players;

	uint32_t CurrentTime()
	{
		return uint32_t(writeTime.milliseconds());
	}
	uint32_t TimeOffset()
	{
		const uint32_t currentTime = uint32_t(writeTime.milliseconds());
		const uint32_t offset = currentTime - lastWriteTime;
		lastWriteTime = currentTime;
		return offset;
	}
	wstring FormatNumber(uint16_t number, size_t desiredLength)
	{
		wstring result = wstring() + number;
		while (result.size() < desiredLength)
			result = wstring(L"0") + result;
		return result;
	}

	template<typename Event> void WriteEvent(const Event& event)
	{
		writeStream.write((const char*)&event, streamsize(sizeof(Event)));
	}
	void WriteBinary(void* addr, size_t size)
	{
		writeStream.write((const char*)addr, size);
	}

	void Clear()
	{
		for (auto iter = players.begin(); iter != players.end(); iter++)
			delete iter->second;
		players.clear();
	}
}

bool Metrics::StartSession(const wstring& filePath)
{
	EndSession();

	time_t timePoint = system_clock::to_time_t(std::chrono::system_clock::now());
	const char* timeData = ctime(&timePoint);
	wstring dateTime = convert_cstring_type<wchar_t, char>(timeData);

	wstring dateTimeFiltered;
	for (size_t i = 0; i < dateTime.size(); i++)
	{
		if (dateTime[i] == L'\n' || dateTime[i] == L'\r')
			continue;
		dateTimeFiltered += dateTime[i];
	}
	dateTime = dateTimeFiltered;

	wstring sessionName = L"MetricsSession";
#if PLATFORM_SPECIFIC_WIN == 0
	sessionName.clear();
	sessionName.resize(256);
	DWORD length = 256;
	GetComputerName(&sessionName[0], &length);
	sessionName.resize(length);
	wstring sessionNameFiltered;
	for (size_t i = 0; i < sessionName.size(); i++)
	{
		wchar_t character = sessionName[i];
		if ((character >= L'0' && character <= L'9') || (character >= L'a' && character <= L'z') || (character >= L'A' && character <= L'Z') || character == L'-' || character == L'_')
			sessionNameFiltered += character;
	}
	sessionName = sessionNameFiltered;
#endif

	wstring fileName = sessionName + L" - " + dateTime + L".hms";
	replace(fileName.begin(), fileName.end(), L':', L'-');
	writeStream.open(convert_string_type<char, wchar_t>(filePath + fileName), ios::out | ios::binary | ios::trunc);
	writeTime.restart();
	lastWriteTime = 0;
	
	// Write the header
	writeStream.write("HMS", 3);

	return writeStream.is_open();
}
bool Metrics::EndSession()
{
	if (writeStream.is_open())
	{
		WriteEvent(SessionEnd(TimeOffset()));

		Clear();
		writeStream.close();
		return true;
	}

	Clear();
	return false;
}

struct Metrics::PlayerHandleSet
{
	static void Enable(PlayerHandle& handle, uint8_t id, uint8_t team)
	{
		handle.m_id = id;
		handle.m_isValid = true;
		handle.m_team = team;
	}
};

void Metrics::SetMapData(float minX, float maxX, float minY, float maxY)
{
	if (!writeStream.is_open())
		return;
	
	WriteEvent(MapData(minX, maxX, minY, maxY));
}
void Metrics::SetMapImage(uint8_t* ptr, uint32_t width, uint32_t height)
{
	if (!writeStream.is_open())
		return;

	WriteEvent(MapImage(width, height));
	WriteBinary(ptr, width * height * 4);
}

Metrics::PlayerHandle::PlayerHandle() : m_isValid(false), m_id(-1), m_team(-1), m_updateTime(0), m_alive(false), m_health(INT_MAX), m_mana(INT_MAX), m_maxHealth(INT_MAX), m_maxMana(INT_MAX), m_level(255), m_playerX(FLT_MAX), m_playerY(FLT_MAX)
{
	
}

void Metrics::PlayerHandle::OnPlayerSpawn(float playerX, float playerY)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (m_alive)
		return;
	m_alive = true;

	m_playerX = playerX;
	m_playerY = playerY;

	WriteEvent(PlayerSpawn(TimeOffset(), m_id, playerX, playerY));
}
void Metrics::PlayerHandle::OnPlayerUpdate(float playerX, float playerY)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (!m_alive)
		return;

	// No need to write an event if the player hasnt moved
	if (fabsf(playerX - m_playerX) < 0.01f && fabsf(playerY - m_playerY) < 0.01f)
		return;

	// Only update position 30 times per second
	const uint32_t currentTime = CurrentTime();
	if (currentTime - m_updateTime < 33)
		return;
	m_updateTime = currentTime;

	m_playerX = playerX;
	m_playerY = playerY;

	WriteEvent(PlayerUpdate(TimeOffset(), m_id, playerX, playerY));
}
void Metrics::PlayerHandle::OnPlayerDie(uint8_t source)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (!m_alive)
		return;
	m_alive = false;

	WriteEvent(PlayerDie(TimeOffset(), m_id, source));
}
void Metrics::PlayerHandle::OnPlayerCast(uint32_t ability)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (!m_alive)
		return;
	
	WriteEvent(PlayerCast(TimeOffset(), m_id, ability));
}
void Metrics::PlayerHandle::OnPlayerHealth(int32_t health)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (!m_alive)
		return;

	// No need to write an event if the player hasnt lost/gained health
	if (m_health == health)
		return;

	m_health = health;

	WriteEvent(PlayerHealth(TimeOffset(), m_id, health));
}
void Metrics::PlayerHandle::OnPlayerMana(int32_t mana)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (!m_alive)
		return;

	// No need to write an event if the player hasnt lost/gained mana
	if (m_mana == mana)
		return;

	m_mana = mana;

	WriteEvent(PlayerMana(TimeOffset(), m_id, mana));
}
void Metrics::PlayerHandle::OnPlayerMaxHealth(int32_t maxHealth)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (!m_alive)
		return;

	// No need to write an event if the player hasnt changed max health
	if (m_maxHealth == maxHealth)
		return;

	m_maxHealth = maxHealth;

	WriteEvent(PlayerMaxHealth(TimeOffset(), m_id, maxHealth));
}
void Metrics::PlayerHandle::OnPlayerMaxMana(int32_t maxMana)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (!m_alive)
		return;

	// No need to write an event if the player hasnt changed max mana
	if (m_maxMana == maxMana)
		return;

	m_maxMana = maxMana;

	WriteEvent(PlayerMaxMana(TimeOffset(), m_id, maxMana));
}
void Metrics::PlayerHandle::OnPlayerDealDamage(uint32_t ability, int32_t damage)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (!m_alive)
		return;

	WriteEvent(PlayerDealDamage(TimeOffset(), m_id, ability, damage));
}
void Metrics::PlayerHandle::OnPlayerLevel(uint8_t level)
{
	if (!writeStream.is_open() || !m_isValid)
		return;

	if (m_level == level)
		return;

	m_level = level;

	WriteEvent(PlayerLevel(TimeOffset(), m_id, level));
}

bool Metrics::PlayerHandle::IsValid() const
{
	return m_isValid;
}
uint8_t Metrics::PlayerHandle::Id() const
{
	return m_id;
}
uint8_t Metrics::PlayerHandle::Team() const
{
	return m_team;
}

Metrics::PlayerHandle& Metrics::RegisterPlayer(const wstring& name, uint8_t team)
{
	static PlayerHandle errorHandle;
	if (!writeStream.is_open())
		return errorHandle;

	const uint8_t id = uint8_t(players.size() + 1);

	PlayerHandle* newHandle = new PlayerHandle();
	PlayerHandleSet::Enable(*newHandle, id, team);

	players.emplace(id, newHandle);

	wstring copy = name;
	if (copy.size() > 128)
		copy.resize(128);
	else if (copy.empty())
		copy = L"UNKNOWN PLAYER";

	WriteEvent(PlayerName(id, (uint16_t)copy.size()));
	WriteBinary((void*)copy.data(), copy.size() * sizeof(wstring::value_type));

	WriteEvent(PlayerTeam(id, team));

	return *newHandle;
}
void Metrics::RegisterAbility(uint32_t ability, const std::wstring& name)
{
	if (!writeStream.is_open())
		return;
	
	wstring copy = name;
	if (copy.size() > 128)
		copy.resize(128);
	else if (copy.empty())
		copy = L"UNKNOWN ABILITY";
	
	WriteEvent(AbilityName(ability, (uint16_t)copy.size()));
	WriteBinary((void*)copy.data(), copy.size() * sizeof(wstring::value_type));
}

#endif