#include <iostream>
#include <string>
#include <ctime>
#include <chrono>
#include <thread>

#include "MD5.h"

//DLU Includes:
#include "dCommonVars.h"
#include "dServer.h"
#include "dLogger.h"
#include "Database.h"
#include "dConfig.h"
#include "dpWorld.h"
#include "dZoneManager.h"
#include "Metrics.hpp"
#include "PerformanceManager.h"
#include "Diagnostics.h"

//RakNet includes:
#include "RakNetDefines.h"
#include "RakNetworkFactory.h"
#include "RakString.h"

//World includes:
#include <csignal>

#include "AuthPackets.h"
#include "PacketUtils.h"
#include "WorldPackets.h"
#include "UserManager.h"
#include "dMessageIdentifiers.h"
#include "CDClientManager.h"
#include "CDClientDatabase.h"
#include "GeneralUtils.h"
#include "ObjectIDManager.h"
#include "ZoneInstanceManager.h"
#include "dChatFilter.h"
#include "ClientPackets.h"
#include "CharacterComponent.h"

#include "EntityManager.h"
#include "Entity.h"
#include "Character.h"
#include "ChatPackets.h"
#include "GameMessageHandler.h"
#include "GameMessages.h"
#include "Mail.h"
#include "dLocale.h"
#include "TeamManager.h"
#include "SkillComponent.h"
#include "DestroyableComponent.h"
#include "Game.h"
#include "MasterPackets.h"
#include "Player.h"
#include "PropertyManagementComponent.h"

#include "ZCompression.h"

namespace Game {
	dLogger* logger;
	dServer* server;
	dZoneManager* zoneManager;
	dpWorld* physicsWorld;
	dChatFilter* chatFilter;
	dConfig* config;
	dLocale* locale;
	std::mt19937 randomEngine;

	RakPeerInterface* chatServer;
	SystemAddress chatSysAddr;
}

bool chatDisabled = false;
bool chatConnected = false;
bool worldShutdownSequenceStarted = false;
bool worldShutdownSequenceComplete = false;
void WorldShutdownSequence();

dLogger* SetupLogger(int zoneID, int instanceID);
void HandlePacketChat(Packet* packet);
void HandlePacket(Packet* packet);

struct tempSessionInfo {
	SystemAddress sysAddr;
	std::string hash;
};

std::map<std::string, tempSessionInfo> m_PendingUsers;
int instanceID = 0;
int g_CloneID = 0;
std::string databaseChecksum = "";

int main(int argc, char** argv) {
	Diagnostics::SetProcessName("World");
	Diagnostics::SetProcessFileName(argv[0]);
	Diagnostics::Initialize();

	// Triggers the shutdown sequence at application exit
	std::atexit(WorldShutdownSequence);
	
	signal(SIGINT, [](int)
	{
		WorldShutdownSequence();
	});

	int zoneID = 1000;
	int cloneID = 0;
	int maxClients = 8;
	int ourPort = 2007;

	//Check our arguments:
	for (int i = 0; i < argc; ++i) {
		std::string argument(argv[i]);

		if (argument == "-zone") zoneID = atoi(argv[i + 1]);
		if (argument == "-instance") instanceID = atoi(argv[i + 1]);
		if (argument == "-clone") cloneID = atoi(argv[i + 1]);
		if (argument == "-maxclients") maxClients = atoi(argv[i + 1]);
		if (argument == "-port") ourPort = atoi(argv[i + 1]);
	}

	//Create all the objects we need to run our service:
	Game::logger = SetupLogger(zoneID, instanceID);
	if (!Game::logger) return 0;
	
	Game::logger->SetLogToConsole(true); //We want this info to always be logged.
	Game::logger->Log("WorldServer", "Starting World server...\n");
	Game::logger->Log("WorldServer", "Version: %i.%i\n", PROJECT_VERSION_MAJOR, PROJECT_VERSION_MINOR);
	Game::logger->Log("WorldServer", "Compiled on: %s\n", __TIMESTAMP__);

#ifndef _DEBUG
	Game::logger->SetLogToConsole(false); //By default, turn it back off if not in debug.
#endif

	//Read our config:
	dConfig config("worldconfig.ini");
	Game::config = &config;
	Game::logger->SetLogToConsole(bool(std::stoi(config.GetValue("log_to_console"))));
	Game::logger->SetLogDebugStatements(config.GetValue("log_debug_statements") == "1");
	if (config.GetValue("disable_chat") == "1") chatDisabled = true;

	// Connect to CDClient
    try {
        CDClientDatabase::Connect("./res/CDServer.sqlite");
    } catch (CppSQLite3Exception& e) {
        Game::logger->Log("WorldServer", "Unable to connect to CDServer SQLite Database\n");
        Game::logger->Log("WorldServer", "Error: %s\n", e.errorMessage());
        Game::logger->Log("WorldServer", "Error Code: %i\n", e.errorCode());
        return -1;
    }
	
	CDClientManager::Instance()->Initialize();

	//Connect to the MySQL Database
	std::string mysql_host = config.GetValue("mysql_host");
	std::string mysql_database = config.GetValue("mysql_database");
	std::string mysql_username = config.GetValue("mysql_username");
	std::string mysql_password = config.GetValue("mysql_password");

	Diagnostics::SetProduceMemoryDump(config.GetValue("generate_dump") == "1");

	if (!config.GetValue("dump_folder").empty())
	{
		Diagnostics::SetOutDirectory(config.GetValue("dump_folder"));
	}

	try {
        Database::Connect(mysql_host, mysql_database, mysql_username, mysql_password);
    } catch (sql::SQLException& ex) {
		Game::logger->Log("WorldServer", "Got an error while connecting to the database: %s\n", ex.what());
		return 0;
    }

	//Find out the master's IP:
	std::string masterIP = "localhost";
	int masterPort = 1000;
	sql::PreparedStatement* stmt = Database::CreatePreppedStmt("SELECT ip, port FROM servers WHERE name='master';");
	auto res = stmt->executeQuery();
	while (res->next()) {
		masterIP = res->getString(1).c_str();
		masterPort = res->getInt(2);
	}

	delete res;
	delete stmt;

	ObjectIDManager::Instance()->Initialize();
	UserManager::Instance()->Initialize();
	LootGenerator::Instance();
	Game::chatFilter = new dChatFilter("./res/chatplus_en_us", bool(std::stoi(config.GetValue("dont_generate_dcf"))));

	Game::server = new dServer(masterIP, ourPort, instanceID, maxClients, false, true, Game::logger, masterIP, masterPort, ServerType::World, zoneID);

	//Connect to the chat server:
	int chatPort = 1501;
	if (config.GetValue("chat_server_port") != "") chatPort = std::atoi(config.GetValue("chat_server_port").c_str());
	
	auto chatSock = SocketDescriptor(uint16_t(ourPort + 2), 0);
	Game::chatServer = RakNetworkFactory::GetRakPeerInterface();
	Game::chatServer->Startup(1, 30, &chatSock, 1);
	Game::chatServer->Connect(masterIP.c_str(), chatPort, "3.25 ND1", 8);

	//Set up other things:
	Game::randomEngine = std::mt19937(time(0));
	Game::locale = new dLocale();

	//Run it until server gets a kill message from Master:
	auto lastTime = std::chrono::high_resolution_clock::now();
	auto t = std::chrono::high_resolution_clock::now();

	Packet* packet = nullptr;
	int framesSinceLastFlush = 0;
	int framesSinceMasterDisconnect = 0;
	int framesSinceChatDisconnect = 0;
	int framesSinceLastUsersSave = 0;
	int framesSinceLastSQLPing = 0;
	int framesSinceLastUser = 0;
	
	const float maxPacketProcessingTime = 1.5f; //0.015f;
	const int maxPacketsToProcess = 1024;

	bool ready = false;
	int framesSinceMasterStatus = 0;
	int framesSinceShutdownSequence = 0;
	int currentFramerate = highFrameRate;
	int physicsFramerate = highFrameRate;
	int physicsStepRate = 0;
	int physicsStepCount = 0;
	int ghostingStepCount = 0;
	auto ghostingLastTime = std::chrono::high_resolution_clock::now();

	PerformanceManager::SelectProfile(zoneID);

	//Load our level:
	if (zoneID != 0) {
		dpWorld::Instance().Initialize(zoneID);
		Game::physicsWorld = &dpWorld::Instance(); //just in case some old code references it
		dZoneManager::Instance()->Initialize(LWOZONEID(zoneID, instanceID, cloneID));
		g_CloneID = cloneID;

		// pre calculate the FDB checksum
		if (Game::config->GetValue("check_fdb") == "1") {
				std::ifstream fileStream;

				static const std::vector<std::string> aliases = {
					"res/CDServers.fdb",
					"res/cdserver.fdb",
					"res/CDClient.fdb",
					"res/cdclient.fdb",
				};
				
				for (const auto& file : aliases) {
					fileStream.open(file, std::ios::binary | std::ios::in);
					if (fileStream.is_open()) {
						break;
					}
				}

				const int bufferSize = 1024;
				MD5* md5 = new MD5();
				
				char fileStreamBuffer[1024] = {};

				while (!fileStream.eof()) {
					memset(fileStreamBuffer, 0, bufferSize);
					fileStream.read(fileStreamBuffer, bufferSize);
					md5->update(fileStreamBuffer, fileStream.gcount());
				}

				fileStream.close();

				const char* nullTerminateBuffer = "\0";
				md5->update(nullTerminateBuffer, 1); // null terminate the data
				md5->finalize();
				databaseChecksum = md5->hexdigest();
			
				delete md5;

				Game::logger->Log("WorldServer", "FDB Checksum calculated as: %s\n", databaseChecksum.c_str());
			}
	}

	while (true) {
		Metrics::StartMeasurement(MetricVariable::Frame);
		Metrics::StartMeasurement(MetricVariable::GameLoop);

		std::clock_t metricCPUTimeStart = std::clock();

		const auto currentTime = std::chrono::high_resolution_clock::now();
		float deltaTime = std::chrono::duration<float>(currentTime - lastTime).count();
		lastTime = currentTime;

		const auto occupied = UserManager::Instance()->GetUserCount() != 0;

		if (!ready)
		{
			currentFramerate = highFrameRate;
		}
		else
		{
			currentFramerate = PerformanceManager::GetServerFramerate();
		}
		
		physicsFramerate = PerformanceManager::GetPhysicsFramerate();
		physicsStepRate = PerformanceManager::GetPhysicsStepRate();

		//Warning if we ran slow
		if (deltaTime > currentFramerate) {
			Game::logger->Log("WorldServer", "We're running behind, dT: %f > %f (framerate)\n", deltaTime, currentFramerate);
		}

		//Check if we're still connected to master:
		if (!Game::server->GetIsConnectedToMaster()) {
			framesSinceMasterDisconnect++;

			if (framesSinceMasterDisconnect >= 30) {
				worldShutdownSequenceStarted = true;
			}
		}
		else framesSinceMasterDisconnect = 0;

		// Check if we're still connected to chat:
		if (!chatConnected) {
			framesSinceChatDisconnect++;

			// Attempt to reconnect every 30 seconds.
			if (framesSinceChatDisconnect >= 2000) {
				framesSinceChatDisconnect = 0;

				Game::chatServer->Connect(masterIP.c_str(), chatPort, "3.25 ND1", 8);
			}
		}
		else framesSinceChatDisconnect = 0;

		//In world we'd update our other systems here.

		if (zoneID != 0 && deltaTime > 0.0f) {
			Metrics::StartMeasurement(MetricVariable::Physics);
			if (physicsStepCount++ >= physicsStepRate) {
				dpWorld::Instance().StepWorld(deltaTime);
				physicsStepCount = 0;
			}
			Metrics::EndMeasurement(MetricVariable::Physics);

			Metrics::StartMeasurement(MetricVariable::UpdateEntities);
			EntityManager::Instance()->UpdateEntities(deltaTime);
			Metrics::EndMeasurement(MetricVariable::UpdateEntities);

			Metrics::StartMeasurement(MetricVariable::Ghosting);
			if (std::chrono::duration<float>(currentTime - ghostingLastTime).count() >= 1.0f) {
				EntityManager::Instance()->UpdateGhosting();
				ghostingLastTime = currentTime;
			}
			Metrics::EndMeasurement(MetricVariable::Ghosting);

			Metrics::StartMeasurement(MetricVariable::UpdateSpawners);
			dZoneManager::Instance()->Update(deltaTime);
			Metrics::EndMeasurement(MetricVariable::UpdateSpawners);
		}

		Metrics::StartMeasurement(MetricVariable::PacketHandling);

		//Check for packets here:
		packet = Game::server->ReceiveFromMaster();
		if (packet) { //We can get messages not handle-able by the dServer class, so handle them if we returned anything.
			HandlePacket(packet); 
			Game::server->DeallocateMasterPacket(packet);
		}

		//Handle our chat packets:
		packet = Game::chatServer->Receive();
		if (packet) {
			HandlePacketChat(packet);
			Game::chatServer->DeallocatePacket(packet);
		}

		//Handle world-specific packets:
		float timeSpent = 0.0f;

		UserManager::Instance()->DeletePendingRemovals();

		auto t1 = std::chrono::high_resolution_clock::now();
		for (int curPacket = 0; curPacket < maxPacketsToProcess && timeSpent < maxPacketProcessingTime; curPacket++) {
			packet = Game::server->Receive();
			if (packet) {
				auto t1 = std::chrono::high_resolution_clock::now();
				HandlePacket(packet);
				auto t2 = std::chrono::high_resolution_clock::now();

				timeSpent += std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
				Game::server->DeallocatePacket(packet);
				packet = nullptr;
			}
			else {
				break;
			}
		}

		Metrics::EndMeasurement(MetricVariable::PacketHandling);

		Metrics::StartMeasurement(MetricVariable::UpdateReplica);

		//Update our replica objects:
		Game::server->UpdateReplica();

		Metrics::EndMeasurement(MetricVariable::UpdateReplica);

		//Push our log every 15s:
		if (framesSinceLastFlush >= 1000) {
			Game::logger->Flush();
			framesSinceLastFlush = 0;
		} else framesSinceLastFlush++;

		if (zoneID != 0 && !occupied)
		{
			framesSinceLastUser++;

			//If we haven't had any players for a while, time out and shut down:
			if (framesSinceLastUser == (cloneID != 0 ? 4000 : 40000))
			{
				worldShutdownSequenceStarted = true;
			}
		}
		else
		{
			framesSinceLastUser = 0;
		}

		//Save all connected users every 10 minutes:
		if (framesSinceLastUsersSave >= 40000 && zoneID != 0) {
			UserManager::Instance()->SaveAllActiveCharacters();
			framesSinceLastUsersSave = 0;

			if (PropertyManagementComponent::Instance() != nullptr) {
				PropertyManagementComponent::Instance()->Save();
			}
		}
		else framesSinceLastUsersSave++;

		//Every 10 min we ping our sql server to keep it alive hopefully:
		if (framesSinceLastSQLPing >= 40000) {
			//Find out the master's IP for absolutely no reason:
			std::string masterIP;
			int masterPort;
			sql::PreparedStatement* stmt = Database::CreatePreppedStmt("SELECT ip, port FROM servers WHERE name='master';");
			auto res = stmt->executeQuery();
			while (res->next()) {
				masterIP = res->getString(1).c_str();
				masterPort = res->getInt(2);
			}

			delete res;
			delete stmt;

			framesSinceLastSQLPing = 0;
		}
		else framesSinceLastSQLPing++;

		Metrics::EndMeasurement(MetricVariable::GameLoop);

		Metrics::StartMeasurement(MetricVariable::Sleep);

		t += std::chrono::milliseconds(currentFramerate);
		std::this_thread::sleep_until(t);
		
		Metrics::EndMeasurement(MetricVariable::Sleep);

		if (!ready && Game::server->GetIsConnectedToMaster())
		{
			// Some delay is required here or else we crash the client?
			
			framesSinceMasterStatus++;

			if (framesSinceMasterStatus >= 200)
			{
				Game::logger->Log("WorldServer", "Finished loading world, ready up!\n");

				MasterPackets::SendWorldReady(Game::server, Game::server->GetZoneID(), Game::server->GetInstanceID());

				ready = true;
			}
		}

		if (worldShutdownSequenceStarted && !worldShutdownSequenceComplete)
		{
			if (framesSinceShutdownSequence == 0) {

				ChatPackets::SendSystemMessage(UNASSIGNED_SYSTEM_ADDRESS, u"Server shutting down...", true);
				
				for (auto i = 0; i < Game::server->GetReplicaManager()->GetParticipantCount(); ++i)
				{
					const auto& player = Game::server->GetReplicaManager()->GetParticipantAtIndex(i);

					auto* entity = Player::GetPlayer(player);

					if (entity != nullptr && entity->GetCharacter() != nullptr)
					{
						auto* skillComponent = entity->GetComponent<SkillComponent>();

						if (skillComponent != nullptr)
						{
							skillComponent->Reset();
						}

						entity->GetCharacter()->SaveXMLToDatabase();
					}
				}

				if (PropertyManagementComponent::Instance() != nullptr) {
					ChatPackets::SendSystemMessage(UNASSIGNED_SYSTEM_ADDRESS, u"Property data saved...", true);
					PropertyManagementComponent::Instance()->Save();
				}
				
				ChatPackets::SendSystemMessage(UNASSIGNED_SYSTEM_ADDRESS, u"Character data saved...", true);
			}

			framesSinceShutdownSequence++;

			if (framesSinceShutdownSequence == 100)
			{
				while (Game::server->GetReplicaManager()->GetParticipantCount() > 0)
				{
					const auto& player = Game::server->GetReplicaManager()->GetParticipantAtIndex(0);

					Game::server->Disconnect(player, SERVER_DISCON_KICK);
				}
				
				CBITSTREAM;
				PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_SHUTDOWN_RESPONSE);
				Game::server->SendToMaster(&bitStream);
			}
			
			if (framesSinceShutdownSequence == 300)
			{
				break;
			}
		}

		Metrics::AddMeasurement(MetricVariable::CPUTime, (1e6 * (1000.0 * (std::clock() - metricCPUTimeStart))) / CLOCKS_PER_SEC);
		Metrics::EndMeasurement(MetricVariable::Frame);
	}
	
	//Delete our objects here:
	if (Game::physicsWorld) Game::physicsWorld = nullptr;
	if (Game::zoneManager) delete Game::zoneManager;

	Game::logger->Log("Test", "Quitting\n");

	Metrics::Clear();
	Database::Destroy();
	delete Game::chatFilter;
	delete Game::server;
	delete Game::logger;

	worldShutdownSequenceComplete = true;

	exit(0);
}

dLogger * SetupLogger(int zoneID, int instanceID) {
	std::string logPath = "./logs/WorldServer_" + std::to_string(zoneID) + "_" + std::to_string(instanceID) + "_" + std::to_string(time(nullptr)) + ".log";
	bool logToConsole = false;
	bool logDebugStatements = false;
#ifdef _DEBUG
	logToConsole = true;
	logDebugStatements = true;
#endif

	return new dLogger(logPath, logToConsole, logDebugStatements);
}

void HandlePacketChat(Packet* packet) {
	if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) {
		Game::logger->Log("WorldServer", "Lost our connection to chat.\n");

		chatConnected = false;
	}

	if (packet->data[0] == ID_CONNECTION_REQUEST_ACCEPTED) {
		Game::logger->Log("WorldServer", "Established connection to chat\n");
		Game::chatSysAddr = packet->systemAddress;

		chatConnected = true;
	}

	if (packet->data[0] == ID_USER_PACKET_ENUM) {
		if (packet->data[1] == CHAT_INTERNAL) {
			switch (packet->data[3]) {
			case MSG_CHAT_INTERNAL_ROUTE_TO_PLAYER: {
				CINSTREAM;
				LWOOBJID playerID;
				inStream.Read(playerID);
				inStream.Read(playerID);

				auto player = EntityManager::Instance()->GetEntity(playerID);
				if (!player) return;

				auto sysAddr = player->GetSystemAddress();

				//Write our stream outwards:
				CBITSTREAM;
				for (int i = 0; i < inStream.GetNumberOfBytesUsed(); i++) {
					bitStream.Write(packet->data[i + 16]); //16 bytes == header + playerID to skip
				}

				SEND_PACKET; //send routed packet to player

				break;
			}

			case MSG_CHAT_INTERNAL_ANNOUNCEMENT: {
				CINSTREAM;
				LWOOBJID header;
				inStream.Read(header);

				RakNet::RakString title;
				RakNet::RakString msg;

				inStream.Read(title);
				inStream.Read(msg);

				//Send to our clients:
				AMFArrayValue args;
				auto* titleValue = new AMFStringValue();
				titleValue->SetStringValue(title.C_String());
				auto* messageValue = new AMFStringValue();
				messageValue->SetStringValue(msg.C_String());

				args.InsertValue("title", titleValue);
				args.InsertValue("message", messageValue);

				GameMessages::SendUIMessageServerToAllClients("ToggleAnnounce", &args);

				delete titleValue;
				delete messageValue;
				titleValue = nullptr;
				messageValue = nullptr;

				break;
			}

			case MSG_CHAT_INTERNAL_MUTE_UPDATE: {
				CINSTREAM;
				LWOOBJID playerId;
				time_t expire = 0;
				inStream.Read(playerId);
				inStream.Read(playerId);
				inStream.Read(expire);
				
				auto* entity = EntityManager::Instance()->GetEntity(playerId);

				if (entity != nullptr)
				{
					entity->GetParentUser()->SetMuteExpire(expire);

					entity->GetCharacter()->SendMuteNotice();
				}

				break;
			}

			case MSG_CHAT_INTERNAL_TEAM_UPDATE: {
				CINSTREAM;
				LWOOBJID header;
				inStream.Read(header);

				LWOOBJID teamID = 0;
				char lootOption = 0;
				char memberCount = 0;
				std::vector<LWOOBJID> members;

				inStream.Read(teamID);
				bool deleteTeam = inStream.ReadBit();

				if (deleteTeam)
				{
					TeamManager::Instance()->DeleteTeam(teamID);

					Game::logger->Log("WorldServer", "Deleting team (%llu)\n", teamID);

					break;
				}

				inStream.Read(lootOption);
				inStream.Read(memberCount);
				Game::logger->Log("WorldServer", "Updating team (%llu), (%i), (%i)\n", teamID, lootOption, memberCount);
				for (char i = 0; i < memberCount; i++)
				{
					LWOOBJID member = LWOOBJID_EMPTY;
					inStream.Read(member);
					members.push_back(member);

					Game::logger->Log("WorldServer", "Updating team member (%llu)\n", member);
				}

				TeamManager::Instance()->UpdateTeam(teamID, lootOption, members);

				break;
			}

			default:
				Game::logger->Log("WorldServer", "Received an unknown chat internal: %i\n", int(packet->data[3]));
			}
		}
	}
}

void HandlePacket(Packet* packet) {
	if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) {
		auto user = UserManager::Instance()->GetUser(packet->systemAddress);
		if (!user) return;

		auto c = user->GetLastUsedChar();
		if (!c) {
			UserManager::Instance()->DeleteUser(packet->systemAddress);
			return;
		}

		auto* entity = EntityManager::Instance()->GetEntity(c->GetObjectID());

		if (!entity)
		{
			entity = Player::GetPlayer(packet->systemAddress);
		}

		if (entity) {
			auto* skillComponent = entity->GetComponent<SkillComponent>();

			if (skillComponent != nullptr)
			{
				skillComponent->Reset();
			}

			entity->GetCharacter()->SaveXMLToDatabase();

			Game::logger->Log("WorldServer", "Deleting player %llu\n", entity->GetObjectID());

			EntityManager::Instance()->DestroyEntity(entity);

			CBITSTREAM;
			PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_REMOVED_NOTIFICATION);
			bitStream.Write(c->GetObjectID());
			Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
		}

		UserManager::Instance()->DeleteUser(packet->systemAddress);

		if (PropertyManagementComponent::Instance() != nullptr) {
			PropertyManagementComponent::Instance()->Save();
		}

		CBITSTREAM;
		PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_PLAYER_REMOVED);
		bitStream.Write((LWOMAPID)Game::server->GetZoneID());
		bitStream.Write((LWOINSTANCEID)instanceID);
		Game::server->SendToMaster(&bitStream);
	}

	if (packet->data[0] != ID_USER_PACKET_ENUM) return;
	if (packet->data[1] == SERVER) {
		if (packet->data[3] == MSG_SERVER_VERSION_CONFIRM) {
			AuthPackets::HandleHandshake(Game::server, packet);
		}
	}

	if (packet->data[1] == MASTER) {
		switch (packet->data[3]) {
			case MSG_MASTER_REQUEST_PERSISTENT_ID_RESPONSE: {
				uint64_t requestID = PacketUtils::ReadPacketU64(8, packet);
				uint32_t objectID = PacketUtils::ReadPacketU32(16, packet);
				ObjectIDManager::Instance()->HandleRequestPersistentIDResponse(requestID, objectID);
				break;
			}

			case MSG_MASTER_REQUEST_ZONE_TRANSFER_RESPONSE: {
				uint64_t requestID = PacketUtils::ReadPacketU64(8, packet);
				ZoneInstanceManager::Instance()->HandleRequestZoneTransferResponse(requestID, packet);
				break;
			}

			case MSG_MASTER_SESSION_KEY_RESPONSE: {
				//Read our session key and to which user it belongs:
				RakNet::BitStream inStream(packet->data, packet->length, false);
				uint64_t header = inStream.Read(header);
				uint32_t sessionKey = 0;
				std::string username;

				inStream.Read(sessionKey);
				username = PacketUtils::ReadString(12, packet, false);

				//Find them:
				auto it = m_PendingUsers.find(username);
				if (it == m_PendingUsers.end()) return;

				//Convert our key:
				std::string userHash = std::to_string(sessionKey);
				userHash = md5(userHash);

				//Verify it:
				if (userHash != it->second.hash) {
					Game::logger->Log("WorldServer", "SOMEONE IS TRYING TO HACK? SESSION KEY MISMATCH: ours: %s != master: %s\n", userHash.c_str(), it->second.hash.c_str());
					Game::server->Disconnect(it->second.sysAddr, SERVER_DISCON_INVALID_SESSION_KEY);
					return;
				}
				else {
					Game::logger->Log("WorldServer", "User %s authenticated with correct key.\n", username.c_str());

					UserManager::Instance()->DeleteUser(packet->systemAddress);

					//Create our user and send them in:
					UserManager::Instance()->CreateUser(it->second.sysAddr, username, userHash);

					auto zone = dZoneManager::Instance()->GetZone();
					if (zone) {
						float x = 0.0f;
						float y = 0.0f;
						float z = 0.0f;

						if (zone->GetZoneID().GetMapID() == 1100) {
							auto pos = zone->GetSpawnPos();
							x = pos.x;
							y = pos.y;
							z = pos.z;
						}

						WorldPackets::SendLoadStaticZone(it->second.sysAddr, x, y, z, zone->GetChecksum());
					}

					if (Game::server->GetZoneID() == 0) {
						//Since doing this reroute breaks the client's request, we have to call this manually.
						UserManager::Instance()->RequestCharacterList(it->second.sysAddr);
					}

					m_PendingUsers.erase(username);

					//Notify master:
					{
						CBITSTREAM;
						PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_PLAYER_ADDED);
						bitStream.Write((LWOMAPID)Game::server->GetZoneID());
						bitStream.Write((LWOINSTANCEID)instanceID);
						Game::server->SendToMaster(&bitStream);
					}
				}

				break;
			}
			case MSG_MASTER_AFFIRM_TRANSFER_REQUEST: {
				const uint64_t requestID = PacketUtils::ReadPacketU64(8, packet);

				Game::logger->Log("MasterServer", "Got affirmation request of transfer %llu\n", requestID);

				CBITSTREAM

				PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_AFFIRM_TRANSFER_RESPONSE);
				bitStream.Write(requestID);
				Game::server->SendToMaster(&bitStream);

				break;
			}

			case MSG_MASTER_SHUTDOWN: {
				worldShutdownSequenceStarted = true;
				Game::logger->Log("WorldServer", "Got shutdown request\n");
				break;
			}

			case MSG_MASTER_NEW_SESSION_ALERT: {
				RakNet::BitStream inStream(packet->data, packet->length, false);
				uint64_t header = inStream.Read(header);
				uint32_t sessionKey = inStream.Read(sessionKey);
				RakNet::RakString username;
				inStream.Read(username);

				//Find them:
				User* user = UserManager::Instance()->GetUser(username.C_String());
				if (!user) {
					Game::logger->Log("WorldServer", "Got new session alert for user %s, but they're not logged in.\n", username.C_String());
					return;
				}

				//Check the key:
				if (sessionKey != std::atoi(user->GetSessionKey().c_str())) {
					Game::logger->Log("WorldServer", "Got new session alert for user %s, but the session key is invalid.\n", username.C_String());
					Game::server->Disconnect(user->GetSystemAddress(), SERVER_DISCON_INVALID_SESSION_KEY);
					return;
				}
				break;
			}

			default:
				Game::logger->Log("WorldServer", "Unknown packet ID from master %i\n", int(packet->data[3]));
		}

		return;
	}

	if (packet->data[1] != WORLD) return;
	
	switch (packet->data[3]) {
		case MSG_WORLD_CLIENT_VALIDATION: {
			std::string username = PacketUtils::ReadString(0x08, packet, true);
			std::string sessionKey = PacketUtils::ReadString(74, packet, true);
			std::string clientDatabaseChecksum = PacketUtils::ReadString(packet->length - 33, packet, false);

			// sometimes client puts a null terminator at the end of the checksum and sometimes doesn't, weird
			clientDatabaseChecksum = clientDatabaseChecksum.substr(0, 32);

			// If the check is turned on, validate the client's database checksum.
			if (Game::config->GetValue("check_fdb") == "1" && !databaseChecksum.empty()) {
				uint32_t gmLevel = 0;
				auto* stmt = Database::CreatePreppedStmt("SELECT gm_level FROM accounts WHERE name=? LIMIT 1;");
				stmt->setString(1, username.c_str());
				
				auto* res = stmt->executeQuery();
				while (res->next()) {
					gmLevel = res->getInt(1);
				}

				delete stmt;
				delete res;

				// Developers may skip this check
				if (gmLevel < 8 && clientDatabaseChecksum != databaseChecksum) {
					Game::logger->Log("WorldServer", "Client's database checksum does not match the server's, aborting connection.\n");
					Game::server->Disconnect(packet->systemAddress, SERVER_DISCON_KICK);
					return;
				}
			}
			
			//Request the session info from Master:
			CBITSTREAM;
			PacketUtils::WriteHeader(bitStream, MASTER, MSG_MASTER_REQUEST_SESSION_KEY);
			PacketUtils::WriteString(bitStream, username, 64);
			Game::server->SendToMaster(&bitStream);

			//Insert info into our pending list
			tempSessionInfo info;
			info.sysAddr = SystemAddress(packet->systemAddress);
			info.hash = sessionKey;
			m_PendingUsers.insert(std::make_pair(username, info));
                            
			break;
		}

		case MSG_WORLD_CLIENT_CHARACTER_LIST_REQUEST: {
			//We need to delete the entity first, otherwise the char list could delete it while it exists in the world!
			if (Game::server->GetZoneID() != 0) {
				auto user = UserManager::Instance()->GetUser(packet->systemAddress);
				if (!user) return;
				EntityManager::Instance()->DestroyEntity(user->GetLastUsedChar()->GetEntity());
			}

			//This loops prevents users who aren't authenticated to double-request the char list, which
			//would make the login screen freeze sometimes. 
			if (m_PendingUsers.size() > 0) {
				for (auto it : m_PendingUsers) {
					if (it.second.sysAddr == packet->systemAddress) {
						return;
					}
				}
			}

			UserManager::Instance()->RequestCharacterList(packet->systemAddress);
			break;
		}

		case MSG_WORLD_CLIENT_GAME_MSG: {
            RakNet::BitStream bitStream(packet->data, packet->length, false);
                            
            uint64_t header;
            LWOOBJID objectID;
            uint16_t messageID;
                            
            bitStream.Read(header);
            bitStream.Read(objectID);
            bitStream.Read(messageID);
                            
            RakNet::BitStream dataStream;
            bitStream.Read(dataStream, bitStream.GetNumberOfUnreadBits());
                                    
            GameMessageHandler::HandleMessage(&dataStream, packet->systemAddress, objectID, GAME_MSG(messageID));
            break;
        }

		case MSG_WORLD_CLIENT_CHARACTER_CREATE_REQUEST: {
			UserManager::Instance()->CreateCharacter(packet->systemAddress, packet);
			break;
		}

		case MSG_WORLD_CLIENT_LOGIN_REQUEST: {
			RakNet::BitStream inStream(packet->data, packet->length, false);
			uint64_t header = inStream.Read(header);
							
			LWOOBJID playerID = 0;
			inStream.Read(playerID);
			playerID = GeneralUtils::ClearBit(playerID, OBJECT_BIT_CHARACTER);
			playerID = GeneralUtils::ClearBit(playerID, OBJECT_BIT_PERSISTENT);

			UserManager::Instance()->LoginCharacter(packet->systemAddress, static_cast<uint32_t>(playerID));
			break;
		}

		case MSG_WORLD_CLIENT_CHARACTER_DELETE_REQUEST: {
			UserManager::Instance()->DeleteCharacter(packet->systemAddress, packet);
			UserManager::Instance()->RequestCharacterList(packet->systemAddress);
			break;
		}
                        
		case MSG_WORLD_CLIENT_CHARACTER_RENAME_REQUEST: {
			UserManager::Instance()->RenameCharacter(packet->systemAddress, packet);
			break;
		}

		case MSG_WORLD_CLIENT_LEVEL_LOAD_COMPLETE: {
            Game::logger->Log("WorldServer", "Received level load complete from user.\n");
            User* user = UserManager::Instance()->GetUser(packet->systemAddress);
            if (user) {
                Character* c = user->GetLastUsedChar();
                if (c != nullptr) { 
                    std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName());
                    WorldPackets::SendCreateCharacter(packet->systemAddress, c->GetObjectID(), c->GetXMLData(), username, c->GetGMLevel());
                    WorldPackets::SendServerState(packet->systemAddress);

					Game::server->GetReplicaManager()->AddParticipant(packet->systemAddress);
                    
                    EntityInfo info {};
                    info.lot = 1;
					Entity* player = EntityManager::Instance()->CreateEntity(info, UserManager::Instance()->GetUser(packet->systemAddress));

					const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(dZoneManager::Instance()->GetZone()->GetWorldID());
					
					EntityManager::Instance()->ConstructEntity(player, UNASSIGNED_SYSTEM_ADDRESS, true);
					
					if (respawnPoint != NiPoint3::ZERO)
					{
						GameMessages::SendPlayerReachedRespawnCheckpoint(player, respawnPoint, NiQuaternion::IDENTITY);
					}
					
                    EntityManager::Instance()->ConstructAllEntities(packet->systemAddress);

					player->GetComponent<CharacterComponent>()->SetLastRocketConfig(u"");
					player->GetCharacter()->SetTargetScene("");

					// Fix the destroyable component
					auto* destroyableComponent = player->GetComponent<DestroyableComponent>();

					if (destroyableComponent != nullptr)
					{
						destroyableComponent->FixStats();
					}

					//Tell the player to generate BBB models, if any:
					if (g_CloneID != 0) {
						const auto& worldId = dZoneManager::Instance()->GetZone()->GetZoneID();

						const auto zoneId = Game::server->GetZoneID();
						const auto cloneId = g_CloneID;

						std::stringstream query;

						query << "SELECT id FROM PropertyTemplate WHERE mapID = " << std::to_string(zoneId) << ";";

						auto result = CDClientDatabase::ExecuteQuery(query.str());

						if (result.eof() || result.fieldIsNull(0)) {
							Game::logger->Log("WorldServer", "No property templates found for zone %d, not sending BBB\n", zoneId);
							goto noBBB;
						}

						//Check for BBB models:
						auto stmt = Database::CreatePreppedStmt("SELECT ugc_id FROM properties_contents WHERE lot=14 AND property_id=?");

						int templateId = result.getIntField(0);

						result.finalize();
						
						auto* propertyLookup = Database::CreatePreppedStmt("SELECT * FROM properties WHERE template_id = ? AND clone_id = ?;");

						propertyLookup->setInt(1, templateId);
						propertyLookup->setInt64(2, g_CloneID);

						auto* propertyEntry = propertyLookup->executeQuery();
						uint64_t propertyId = 0;

						if (propertyEntry->next()) {
							propertyId = propertyEntry->getUInt64(1);
						}

						delete propertyLookup;

						stmt->setUInt64(1, propertyId);
						auto res = stmt->executeQuery();
						while (res->next()) {
							Game::logger->Log("UGC", "Getting lxfml ugcID: " + std::to_string(res->getUInt(1)) + "\n");

							//Get lxfml:
							auto stmtL = Database::CreatePreppedStmt("SELECT lxfml from ugc where id=?");
							stmtL->setUInt(1, res->getUInt(1));

							auto lxres = stmtL->executeQuery();
							
							while (lxres->next()) {
								auto lxfml = lxres->getBlob(1);

								lxfml->seekg(0, std::ios::end);
								size_t lxfmlSize = lxfml->tellg();
								lxfml->seekg(0);

								//Send message:
								{
									LWOOBJID blueprintID = res->getUInt(1);
									blueprintID = GeneralUtils::SetBit(blueprintID, OBJECT_BIT_CHARACTER);
									blueprintID = GeneralUtils::SetBit(blueprintID, OBJECT_BIT_PERSISTENT);

									CBITSTREAM;
									PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_BLUEPRINT_SAVE_RESPONSE);
									bitStream.Write<LWOOBJID>(0); //always zero so that a check on the client passes
									bitStream.Write<unsigned int>(0);
									bitStream.Write<unsigned int>(1);
									bitStream.Write(blueprintID);

									bitStream.Write<uint32_t>(lxfmlSize + 9);

									//Write a fake sd0 header:
									bitStream.Write<unsigned char>(0x73); //s
									bitStream.Write<unsigned char>(0x64); //d
									bitStream.Write<unsigned char>(0x30); //0
									bitStream.Write<unsigned char>(0x01); //1
									bitStream.Write<unsigned char>(0xFF); //end magic

									bitStream.Write<uint32_t>(lxfmlSize);

									for (size_t i = 0; i < lxfmlSize; ++i)
										bitStream.Write<uint8_t>(lxfml->get());

									SystemAddress sysAddr = packet->systemAddress;
									SEND_PACKET;
									PacketUtils::SavePacket("lxfml packet " + std::to_string(res->getUInt(1)) + ".bin", (char*)bitStream.GetData(), bitStream.GetNumberOfBytesUsed());
								}
							}

							delete stmtL;
							delete lxres;
						}

						delete stmt;
						delete res;
					}

					noBBB:

					// Tell the client it's done loading:
					GameMessages::SendInvalidZoneTransferList(player, packet->systemAddress, u"https://forms.zohopublic.eu/virtualoffice204/form/DLUInGameSurvey/formperma/kpU-IL5v2-Wt41QcB5UFnYjzlLp-j2LEisF8e11PisU", u"", false, false);
					GameMessages::SendServerDoneLoadingAllObjects(player, packet->systemAddress);

					//Send the player it's mail count:
					//update: this might not be needed so im going to try disabling this here.
					//Mail::HandleNotificationRequest(packet->systemAddress, player->GetObjectID());

					//Notify chat that a player has loaded:
					{
						const auto& playerName = player->GetCharacter()->GetName();
						//RakNet::RakString playerName(player->GetCharacter()->GetName().c_str());

						CBITSTREAM;
						PacketUtils::WriteHeader(bitStream, CHAT_INTERNAL, MSG_CHAT_INTERNAL_PLAYER_ADDED_NOTIFICATION);
						bitStream.Write(player->GetObjectID());
						bitStream.Write<uint16_t>(playerName.size());
						for (size_t i = 0; i < playerName.size(); i++)
						{
							bitStream.Write(playerName[i]);
						}
						
						//bitStream.Write(playerName);
						
						auto zone = dZoneManager::Instance()->GetZone()->GetZoneID();
						bitStream.Write(zone.GetMapID());
						bitStream.Write(zone.GetInstanceID());
						bitStream.Write(zone.GetCloneID());
						bitStream.Write(player->GetParentUser()->GetMuteExpire());

						Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false);
					}
                }
				else {
					Game::logger->Log("WorldMain", "Couldn't find character to log in with for user %s (%i)!\n", user->GetUsername().c_str(), user->GetAccountID());
					Game::server->Disconnect(packet->systemAddress, SERVER_DISCON_CHARACTER_NOT_FOUND);
				}
            } else {
                Game::logger->Log("WorldMain", "Couldn't get user for level load complete!\n");
            }
            break;
        }

		case MSG_WORLD_CLIENT_POSITION_UPDATE: {
			ClientPackets::HandleClientPositionUpdate(packet->systemAddress, packet);
			break;
		}

		case MSG_WORLD_CLIENT_MAIL: {
			RakNet::BitStream bitStream(packet->data, packet->length, false);
			LWOOBJID space;
			bitStream.Read(space);
			Mail::HandleMailStuff(&bitStream, packet->systemAddress, UserManager::Instance()->GetUser(packet->systemAddress)->GetLastUsedChar()->GetEntity());
			break;
		}

		case MSG_WORLD_CLIENT_ROUTE_PACKET: {
			//Yeet to chat
			CINSTREAM;
			uint64_t header = 0;
			uint32_t size = 0;
			inStream.Read(header);
			inStream.Read(size);

			if (size > 20000) {
				Game::logger->Log("WorldServer", "Tried to route a packet with a read size > 20000, so likely a false packet.\n");
				return;
			}

			CBITSTREAM;

			PacketUtils::WriteHeader(bitStream, CHAT, packet->data[14]);

			//We need to insert the player's objectID so the chat server can find who originated this request:
			LWOOBJID objectID = 0;
			auto user = UserManager::Instance()->GetUser(packet->systemAddress);
			if (user) {
				objectID = user->GetLastUsedChar()->GetObjectID();
			}

			bitStream.Write(objectID);

			//Now write the rest of the data:
			auto data = inStream.GetData();
			for (uint32_t i = 0; i < size; ++i) {
				bitStream.Write(data[i+23]);
			}

			Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE_ORDERED, 0, Game::chatSysAddr, false);
			break;
		}

		case MSG_WORLD_CLIENT_STRING_CHECK: {
			ClientPackets::HandleChatModerationRequest(packet->systemAddress, packet);
			break;
		}

		case MSG_WORLD_CLIENT_GENERAL_CHAT_MESSAGE: {
			if (chatDisabled) {
				ChatPackets::SendMessageFail(packet->systemAddress);
			}
			else {
				ClientPackets::HandleChatMessage(packet->systemAddress, packet);
			}

			break;
		}

		case MSG_WORLD_CLIENT_HANDLE_FUNNESS: {
			//This means the client is running slower or faster than it should.
			//Could be insane lag, but I'mma just YEET them as it's usually speedhacking.
			//This is updated to now count the amount of times we've been caught "speedhacking" to kick with a delay
			//This is hopefully going to fix the random disconnects people face sometimes.
			if (Game::config->GetValue("disable_anti_speedhack") == "1") {
				return;
			}

			User* user = UserManager::Instance()->GetUser(packet->systemAddress);
			if (user) {
				user->UserOutOfSync();
			}
			else {
				Game::server->Disconnect(packet->systemAddress, SERVER_DISCON_KICK);
			}
			break;
		}

	default:
		Game::server->GetLogger()->Log("HandlePacket", "Unknown world packet received: %i\n", int(packet->data[3]));
	}
}

void WorldShutdownSequence()
{
	if (worldShutdownSequenceStarted || worldShutdownSequenceComplete)
	{
		return;
	}

	worldShutdownSequenceStarted = true;

	auto t = std::chrono::high_resolution_clock::now();
	auto ticks = 0;

	printf("Attempting to shutdown world, max 10 seconds...");;

	while (true)
	{
		if (worldShutdownSequenceStarted)
		{
			break;
		}

		t += std::chrono::milliseconds(highFrameRate);
		std::this_thread::sleep_until(t);

		ticks++;

		if (ticks == 600)
		{
			break;
		}
	}
}
