#pragma once
// used: [ext] json parser implementation
#include <json.hpp>

namespace C::JSON
{
	/* @section: [internal] */
	// write single variable to buffer
	inline void WriteBuffer(nlohmann::json& entry, const VariableObject_t& variable)
	{
		// write is different for variable types
		switch (variable.uTypeHash)
		{
		case FNV1A::HashConst("bool"):
		{
			entry = *variable.GetStorage<bool>();
			break;
		}
		case FNV1A::HashConst("int"):
		{
			entry = *variable.GetStorage<int>();
			break;
		}
		case FNV1A::HashConst("unsigned int"):
		{
			entry = *variable.GetStorage<unsigned int>();
			break;
		}
		case FNV1A::HashConst("float"):
		{
			entry = *variable.GetStorage<float>();
			break;
		}
		case FNV1A::HashConst("char[]"):
		{
			entry = std::string(*variable.GetStorage<char[], false>());
			break;
		}
		case FNV1A::HashConst("bool[]"):
		{
			const std::size_t nArraySize = variable.nStorageSize / sizeof(bool);
			const auto& arrValues = *variable.GetStorage<bool[], false>();

			std::vector<bool> vecBools(nArraySize);
			for (std::size_t i = 0U; i < nArraySize; i++)
				vecBools[i] = arrValues[i];

			entry = vecBools;
			break;
		}
		case FNV1A::HashConst("int[]"):
		{
			const std::size_t nArraySize = variable.nStorageSize / sizeof(int);
			const auto& arrValues = *variable.GetStorage<int[], false>();

			std::vector<int> vecInts(nArraySize);
			for (std::size_t i = 0U; i < nArraySize; i++)
				vecInts[i] = arrValues[i];

			entry = vecInts;
			break;
		}
		case FNV1A::HashConst("unsigned int[]"):
		{
			const std::size_t nArraySize = variable.nStorageSize / sizeof(unsigned int);
			const auto& arrValues = *variable.GetStorage<unsigned int[], false>();

			std::vector<unsigned int> vecUInts(nArraySize);
			for (std::size_t i = 0U; i < nArraySize; i++)
				vecUInts[i] = arrValues[i];

			entry = vecUInts;
			break;
		}
		case FNV1A::HashConst("float[]"):
		{
			const std::size_t nArraySize = variable.nStorageSize / sizeof(float);
			const auto& arrValues = *variable.GetStorage<float[], false>();

			std::vector<float> vecFloats(nArraySize);
			for (std::size_t i = 0U; i < nArraySize; i++)
				vecFloats[i] = arrValues[i];

			entry = vecFloats;
			break;
		}
		case FNV1A::HashConst("char[][]"):
		{
			// @test: very ugh
			std::string strValue(variable.nStorageSize, '\0');
			CRT::MemoryCopy(strValue.data(), variable.GetStorage<char*[], false>(), variable.nStorageSize);
			entry = strValue;
			break;
		}
		default:
		{
			[[maybe_unused]] bool bFoundUserType = false;
			//const std::uint8_t* pVariableStorage = variable.GetStorage<const std::uint8_t, false>();

			// lookup for user-defined data type
			for (const UserDataType_t& userType : vecUserTypes)
			{
				if (userType.uTypeHash == variable.uTypeHash)
				{
					// write size
					entry[CS_XOR("size")] = variable.GetSerializationSize();

					nlohmann::json members = {};

					// write data
					// @todo: it would be so fucking neatful if we could rework this to proceed recursive call instead
					for (const UserDataMember_t& userMember : userType.vecMembers)
					{
						nlohmann::json currentMember = {};
						currentMember[CS_XOR("name")] = userMember.uNameHash;
						currentMember[CS_XOR("type")] = userMember.uTypeHash;

						// @todo: call user defined 'to_json' callback | or again, remake to deal with recursive call instead

						members.emplace_back(std::move(currentMember));
					}

					entry[CS_XOR("members")] = members;

					bFoundUserType = true;
					break;
				}
			}

			CS_ASSERT(bFoundUserType); // value type is not defined
			break;
		}
		}
	}

	// read single variable from buffer
	inline void ReadBuffer(nlohmann::json& entry, VariableObject_t& variable)
	{
		switch (variable.uTypeHash)
		{
		case FNV1A::HashConst("bool"):
		{
			const bool bValue = entry.get<bool>();
			variable.SetStorage(&bValue);
			break;
		}
		case FNV1A::HashConst("int"):
		{
			const int iValue = entry.get<int>();
			variable.SetStorage(&iValue);
			break;
		}
		case FNV1A::HashConst("unsigned int"):
		{
			const unsigned int uValue = entry.get<unsigned int>();
			variable.SetStorage(&uValue);
			break;
		}
		case FNV1A::HashConst("float"):
		{
			const float flValue = entry.get<float>();
			variable.SetStorage(&flValue);
			break;
		}
		case FNV1A::HashConst("char[]"):
		{
			const std::string strValue = entry.get<std::string>();
			CS_ASSERT((strValue.size() + 1U) * sizeof(char) <= variable.nStorageSize); // source size is bigger than destination size
			variable.SetStorage(strValue.c_str());
			break;
		}
		case FNV1A::HashConst("bool[]"):
		{
			const auto& vecBools = entry.get<std::vector<bool>>();
			CS_ASSERT(vecBools.size() * sizeof(bool) <= variable.nStorageSize); // source size is bigger than destination size

			bool* arrValues = *variable.GetStorage<bool*, false>();
			for (std::size_t i = 0U; i < vecBools.size(); i++)
				arrValues[i] = vecBools[i];

			break;
		}
		case FNV1A::HashConst("int[]"):
		{
			const auto& vecInts = entry.get<std::vector<int>>();
			CS_ASSERT(vecInts.size() * sizeof(int) <= variable.nStorageSize); // source size is bigger than destination size

			int* arrValues = *variable.GetStorage<int*, false>();
			for (std::size_t i = 0U; i < vecInts.size(); i++)
				arrValues[i] = vecInts[i];

			break;
		}
		case FNV1A::HashConst("unsigned int[]"):
		{
			const auto& vecUInts = entry.get<std::vector<unsigned int>>();
			CS_ASSERT(vecUInts.size() * sizeof(unsigned int) <= variable.nStorageSize); // source size is bigger than destination size

			unsigned int* arrValues = *variable.GetStorage<unsigned int*, false>();
			for (std::size_t i = 0U; i < vecUInts.size(); i++)
				arrValues[i] = vecUInts[i];

			break;
		}
		case FNV1A::HashConst("float[]"):
		{
			const auto& vecFloats = entry.get<std::vector<float>>();
			CS_ASSERT(vecFloats.size() * sizeof(float) <= variable.nStorageSize); // source size is bigger than destination size

			float* arrValues = *variable.GetStorage<float*, false>();
			for (std::size_t i = 0U; i < vecFloats.size(); i++)
				arrValues[i] = vecFloats[i];

			break;
		}
		case FNV1A::HashConst("char[][]"):
		{
			// @test: very ugh
			const std::string strValue = entry.get<std::string>();
			CS_ASSERT((strValue.size() + 1U) * sizeof(char) <= variable.nStorageSize); // source size is bigger than destination size
			variable.SetStorage(strValue.data());
			break;
		}
		default:
		{
			[[maybe_unused]] bool bFoundUserType = false;
			//std::uint8_t* pVariableStorage = variable.GetStorage<std::uint8_t, false>();

			// lookup for user-defined data type
			for (const UserDataType_t& userType : vecUserTypes)
			{
				if (userType.uTypeHash == variable.uTypeHash)
				{
					// read size
					const std::size_t nSourceSerializationSize = entry[CS_XOR("size")].get<std::size_t>();

					nlohmann::json members = entry[CS_XOR("members")];

					// read data
					// @todo: it would be so fucking neatful if we could rework this to proceed recursive call instead
					// @todo: instead we must loop through all "members" and them look for same in 'vecMembers'
					for (const UserDataMember_t& userMember : userType.vecMembers)
					{
						const FNV1A_t uMemberNameHash = members[CS_XOR("name")].get<FNV1A_t>();
						const FNV1A_t uMemberTypeHash = members[CS_XOR("type")].get<FNV1A_t>();

						// verify source and destination variable name and data type
						if (uMemberNameHash != userMember.uNameHash || uMemberTypeHash != userMember.uTypeHash)
						{
							// @todo: instead we can skip up to the next matched name and type hashes variable and continue read from there
							CS_ASSERT(false); // source and destination structure mismatch
							break;
						}

						// @todo: call user defined 'from_json' callback | or again, remake to deal with recursive call instead
					}

					bFoundUserType = true;
					break;
				}
			}

			CS_ASSERT(bFoundUserType); // value type is not defined
			break;
		}
		}
	}

	/* @section: main */
	inline bool SaveVariable(const wchar_t* wszFilePath, const VariableObject_t& variable)
	{
		const HANDLE hFileOut = ::CreateFileW(wszFilePath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
		if (hFileOut == INVALID_HANDLE_VALUE)
			return false;

		FILE* hFileStream = nullptr;
		::_wfopen_s(&hFileStream, wszFilePath, CS_XOR(L"r"));

		if (hFileStream == nullptr)
			return false;

		nlohmann::json root = nlohmann::json::parse(hFileStream);
		::fclose(hFileStream);

		char szKeyBuffer[CRT::IntegerToString_t<FNV1A_t, 16U>::MaxCount()] = {};
		const char* szVariableHash = CRT::IntegerToString(variable.uNameHash, szKeyBuffer, sizeof(szKeyBuffer), 16);

		WriteBuffer(root[szVariableHash], variable);

		// write re-serialized configuration to file
		const std::string strSerialized = root.dump(4);
		const BOOL bWritten = ::WriteFile(hFileOut, strSerialized.data(), strSerialized.size(), nullptr, nullptr);
		::CloseHandle(hFileOut);

		return bWritten;
	}

	inline bool LoadVariable(const wchar_t* wszFilePath, VariableObject_t& variable)
	{
		FILE* hFileStream = nullptr;
		::_wfopen_s(&hFileStream, wszFilePath, CS_XOR(L"r+"));

		if (hFileStream == nullptr)
			return false;

		nlohmann::json root = nlohmann::json::parse(hFileStream, nullptr, false);

		char szHashBuffer[CRT::IntegerToString_t<FNV1A_t, 16U>::MaxCount()] = {};
		const char* szVariableHash = CRT::IntegerToString(variable.uNameHash, szHashBuffer, sizeof(szHashBuffer), 16);

		if (root.contains(szVariableHash))
			ReadBuffer(root[szVariableHash], variable);
		else // @todo: we should check for version at first
		{
			WriteBuffer(root[szVariableHash], variable);

			// overwrite version
			szVariableHash = CRT::IntegerToString(FNV1A::HashConst("version"), szHashBuffer, sizeof(szHashBuffer), 16);
			root[szVariableHash] = CS_VERSION;

			// @todo: reserialize and write to file
		}

		::fclose(hFileStream);
		return true;
	}

	inline bool RemoveVariable(const wchar_t* wszFilePath, const VariableObject_t& variable)
	{
		// @todo:
		return false;
	}

	inline bool SaveFile(const wchar_t* wszFilePath)
	{
		const HANDLE hFileOut = ::CreateFileW(wszFilePath, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
		if (hFileOut == INVALID_HANDLE_VALUE)
			return false;

		char szHashBuffer[_MAX_ULTOSTR_BASE16_COUNT] = {};
		const char* szVariableHash = CRT::IntegerToString(FNV1A::HashConst("version"), szHashBuffer, sizeof(szHashBuffer), 16);

		nlohmann::json root;

		// put current cheat build number
		WriteBuffer(root[szVariableHash], VariableObject_t{ FNV1A::HashConst("version"), FNV1A::HashConst("int"), CS_VERSION });

		for (const auto& variable : vecVariables)
		{
			szVariableHash = CRT::IntegerToString(variable.uNameHash, szHashBuffer, sizeof(szHashBuffer), 16);

			WriteBuffer(root[szVariableHash], variable);
		}

		// write serialized configuration to file
		const std::string strSerialized = root.dump(4);
		const BOOL bWritten = ::WriteFile(hFileOut, strSerialized.data(), strSerialized.size(), nullptr, nullptr);
		::CloseHandle(hFileOut);

		return bWritten;
	}

	inline bool LoadFile(const wchar_t* wszFilePath)
	{
		FILE* hFileStream = nullptr;
		::_wfopen_s(&hFileStream, wszFilePath, CS_XOR(L"r+"));

		if (hFileStream == nullptr)
			return false;

		nlohmann::json root = nlohmann::json::parse(hFileStream, nullptr, false);

		// @todo: implement version adaptation mechanism like so: if file has variable but src doesn't - remove from file, if src has variable but file doesn't - add it to file + probably with menu notification and ask for this

		char szHashBuffer[CRT::IntegerToString_t<FNV1A_t, 16U>::MaxCount()] = {};
		const char* szVariableHash = CRT::IntegerToString(FNV1A::HashConst("version"), szHashBuffer, sizeof(szHashBuffer), 16);

		// get cheat version at time when configuration has been saved
		auto& version = root[szVariableHash];

		for (auto& variable : vecVariables)
		{
			szVariableHash = CRT::IntegerToString(variable.uNameHash, szHashBuffer, sizeof(szHashBuffer), 16);

			// check is variable not found
			if (!root.contains(szVariableHash))
			{
				// add variable to save | assert if version matches to current
				continue;
			}

			ReadBuffer(root[szVariableHash], variable);
		}

		// @todo: check is configuration version older than current cheat version
		//if (toml::get<int>(version) != CS_VERSION)
		//{
		//	// manually update version number
		//	version = CS_VERSION;
		//
		//	// re-serialize toml configuration and write to file
		//	const std::string strSerialized = toml::format(root);
		//	if (!::WriteFile(hFileOut, strSerialized.data(), strSerialized.size(), nullptr, nullptr))
		//		L_PRINT(LOG_WARNING) << CS_XOR("failed to re-serialize configuration file: \"") << wszFileName << CS_XOR("\"");
		//
		//	::CloseHandle(hFileOut);
		//}

		::fclose(hFileStream);
		return true;
	}
}
