#include "VanityUtilities.h"

#include "DestroyableComponent.h"
#include "EntityManager.h"
#include "GameMessages.h"
#include "InventoryComponent.h"
#include "PhantomPhysicsComponent.h"
#include "ProximityMonitorComponent.h"
#include "ScriptComponent.h"
#include "dCommonVars.h"
#include "dConfig.h"
#include "dServer.h"
#include "tinyxml2.h"
#include "Game.h"
#include "Logger.h"
#include "BinaryPathFinder.h"
#include "EntityInfo.h"
#include "Spawner.h"
#include "dZoneManager.h"
#include "ObjectIDManager.h"
#include "Level.h"

#include <fstream>

std::vector<VanityNPC> VanityUtilities::m_NPCs = {};
std::vector<VanityParty> VanityUtilities::m_Parties = {};
std::vector<std::string> VanityUtilities::m_PartyPhrases = {};

void VanityUtilities::SpawnVanity() {
	if (Game::config->GetValue("disable_vanity") == "1") {
		return;
	}

	const uint32_t zoneID = Game::server->GetZoneID();

	for (const auto& npc : m_NPCs) {
		if (npc.m_ID == LWOOBJID_EMPTY) continue;
		if (npc.m_LOT == 176){
			Game::zoneManager->RemoveSpawner(npc.m_ID);
		} else{
			auto* entity = Game::entityManager->GetEntity(npc.m_ID);
			if (!entity) continue;
			entity->Smash(LWOOBJID_EMPTY, eKillType::VIOLENT);
		}
	}

	m_NPCs.clear();
	m_Parties.clear();
	m_PartyPhrases.clear();

	ParseXML((BinaryPathFinder::GetBinaryDir() / "vanity/NPC.xml").string());

	// Loop through all parties
	for (const auto& party : m_Parties) {
		const auto chance = party.m_Chance;
		const auto zone = party.m_Zone;

		if (zone != Game::server->GetZoneID()) {
			continue;
		}

		float rate = GeneralUtils::GenerateRandomNumber<float>(0, 1);
		if (chance < rate) {
			continue;
		}

		// Copy m_NPCs into a new vector
		std::vector<VanityNPC> npcList = m_NPCs;
		std::vector<uint32_t> taken = {};

		LOG("Spawning party with %i locations", party.m_Locations.size());

		// Loop through all locations
		for (const auto& location : party.m_Locations) {
		rate = GeneralUtils::GenerateRandomNumber<float>(0, 1);
			if (0.75f < rate) {
				continue;
			}

			// Get a random NPC
			auto npcIndex = GeneralUtils::GenerateRandomNumber<uint32_t>(0, npcList.size() - 1);

			while (std::find(taken.begin(), taken.end(), npcIndex) != taken.end()) {
				npcIndex = GeneralUtils::GenerateRandomNumber<uint32_t>(0, npcList.size() - 1);
			}

			auto& npc = npcList[npcIndex];
			// Skip spawners
			if (npc.m_LOT == 176) continue;
			
			taken.push_back(npcIndex);

			LOG("ldf size is %i", npc.ldf.size());
			if (npc.ldf.empty()) {
				npc.ldf = {
					new LDFData<std::vector<std::u16string>>(u"syncLDF", { u"custom_script_client" }),
					new LDFData<std::u16string>(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua")
				};
			}

			// Spawn the NPC
			if (npc.m_LOT == 176){
				npc.m_ID = SpawnSpawner(npc.m_LOT, location.m_Position, location.m_Rotation, npc.ldf);
			} else {
				auto* npcEntity = SpawnNPC(npc.m_LOT, npc.m_Name, location.m_Position, location.m_Rotation, npc.m_Equipment, npc.ldf);
				if (!npc.m_Phrases.empty()) {
					npcEntity->SetVar<std::vector<std::string>>(u"chats", m_PartyPhrases);
					SetupNPCTalk(npcEntity);
				}
			}
		}
		return;
	}

	// Loop through all NPCs
	for (auto& npc : m_NPCs) {
		if (npc.m_Locations.find(Game::server->GetZoneID()) == npc.m_Locations.end())
			continue;

		const std::vector<VanityNPCLocation>& locations = npc.m_Locations.at(Game::server->GetZoneID());

		// Pick a random location
		const auto& location = locations[GeneralUtils::GenerateRandomNumber<int>(
			static_cast<size_t>(0), static_cast<size_t>(locations.size() - 1))];

		float rate = GeneralUtils::GenerateRandomNumber<float>(0, 1);
		if (location.m_Chance < rate) {
			continue;
		}

		if (npc.ldf.empty()) {
			npc.ldf = {
				new LDFData<std::vector<std::u16string>>(u"syncLDF", { u"custom_script_client" }),
				new LDFData<std::u16string>(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua")
			};
		}
		if (npc.m_LOT == 176){
			npc.m_ID = SpawnSpawner(npc.m_LOT, location.m_Position, location.m_Rotation, npc.ldf);
		} else {
			// Spawn the NPC
			auto* npcEntity = SpawnNPC(npc.m_LOT, npc.m_Name, location.m_Position, location.m_Rotation, npc.m_Equipment, npc.ldf);
			if (!npcEntity) continue;
			npc.m_ID = npcEntity->GetObjectID();
			if (!npc.m_Phrases.empty()){
				npcEntity->SetVar<std::vector<std::string>>(u"chats", npc.m_Phrases);

				auto* scriptComponent = npcEntity->GetComponent<ScriptComponent>();

				if (scriptComponent && !npc.m_Script.empty()) {
					scriptComponent->SetScript(npc.m_Script);
					scriptComponent->SetSerialized(false);

					for (const auto& npc : npc.m_Flags) {
						npcEntity->SetVar<bool>(GeneralUtils::ASCIIToUTF16(npc.first), npc.second);
					}
				}
				SetupNPCTalk(npcEntity);
			}
		}
	}

	if (zoneID == 1200) {
		{
			EntityInfo info;
			info.lot = 8139;
			info.pos = { 259.5f, 246.4f, -705.2f };
			info.rot = { 0.0f, 0.0f, 1.0f, 0.0f };
			info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();

			info.settings = { new LDFData<bool>(u"hasCustomText", true),
				new LDFData<std::string>(u"customText", ParseMarkdown((BinaryPathFinder::GetBinaryDir() / "vanity/TESTAMENT.md").string())) };

			auto* entity = Game::entityManager->CreateEntity(info);

			Game::entityManager->ConstructEntity(entity);
		}
	}
}

LWOOBJID VanityUtilities::SpawnSpawner(LOT lot, const NiPoint3& position, const NiQuaternion& rotation, const std::vector<LDFBaseData*>& ldf){
	SceneObject obj;
	obj.lot = lot;
	// guratantee we have no collisions
	do {
		obj.id = ObjectIDManager::GenerateObjectID();
	} while(Game::zoneManager->GetSpawner(obj.id));
	obj.position = position;
	obj.rotation = rotation;
	obj.settings = ldf;
	Level::MakeSpawner(obj);
	return obj.id;
}

Entity* VanityUtilities::SpawnNPC(LOT lot, const std::string& name, const NiPoint3& position, const NiQuaternion& rotation, const std::vector<LOT>& inventory, const std::vector<LDFBaseData*>& ldf) {
	EntityInfo info;
	info.lot = lot;
	info.pos = position;
	info.rot = rotation;
	info.spawnerID = Game::entityManager->GetZoneControlEntity()->GetObjectID();
	info.settings = ldf;

	auto* entity = Game::entityManager->CreateEntity(info);
	entity->SetVar(u"npcName", name);
	if (entity->GetVar<bool>(u"noGhosting")) entity->SetIsGhostingCandidate(false);

	auto* inventoryComponent = entity->GetComponent<InventoryComponent>();

	if (inventoryComponent && !inventory.empty()) {
		inventoryComponent->SetNPCItems(inventory);
	}

	auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();

	if (destroyableComponent != nullptr) {
		destroyableComponent->SetIsGMImmune(true);
		destroyableComponent->SetMaxHealth(0);
		destroyableComponent->SetHealth(0);
	}

	Game::entityManager->ConstructEntity(entity);

	return entity;
}

void VanityUtilities::ParseXML(const std::string& file) {
	// Read the entire file
	std::ifstream xmlFile(file);
	std::string xml((std::istreambuf_iterator<char>(xmlFile)), std::istreambuf_iterator<char>());

	// Parse the XML
	tinyxml2::XMLDocument doc;
	doc.Parse(xml.c_str(), xml.size());

	// Read the NPCs
	auto* npcs = doc.FirstChildElement("npcs");

	if (npcs == nullptr) {
		LOG("Failed to parse NPCs");
		return;
	}

	for (auto* party = npcs->FirstChildElement("party"); party != nullptr; party = party->NextSiblingElement("party")) {
		// Get 'zone' as uint32_t and 'chance' as float
		uint32_t zone = 0;
		float chance = 0.0f;

		if (party->Attribute("zone") != nullptr) {
			zone = std::stoul(party->Attribute("zone"));
		}

		if (party->Attribute("chance") != nullptr) {
			chance = std::stof(party->Attribute("chance"));
		}

		VanityParty partyInfo;
		partyInfo.m_Zone = zone;
		partyInfo.m_Chance = chance;

		auto* locations = party->FirstChildElement("locations");

		if (locations == nullptr) {
			LOG("Failed to parse party locations");
			continue;
		}

		for (auto* location = locations->FirstChildElement("location"); location != nullptr;
			location = location->NextSiblingElement("location")) {
			// Get the location data
			auto* x = location->Attribute("x");
			auto* y = location->Attribute("y");
			auto* z = location->Attribute("z");
			auto* rw = location->Attribute("rw");
			auto* rx = location->Attribute("rx");
			auto* ry = location->Attribute("ry");
			auto* rz = location->Attribute("rz");

			if (x == nullptr || y == nullptr || z == nullptr || rw == nullptr || rx == nullptr || ry == nullptr
				|| rz == nullptr) {
				LOG("Failed to parse party location data");
				continue;
			}

			VanityNPCLocation locationData;
			locationData.m_Position = { std::stof(x), std::stof(y), std::stof(z) };
			locationData.m_Rotation = { std::stof(rw), std::stof(rx), std::stof(ry), std::stof(rz) };
			locationData.m_Chance = 1.0f;

			partyInfo.m_Locations.push_back(locationData);
		}

		m_Parties.push_back(partyInfo);
	}

	auto* partyPhrases = npcs->FirstChildElement("partyphrases");

	if (partyPhrases == nullptr) {
		LOG("No party phrases found");
	} else {
		for (auto* phrase = partyPhrases->FirstChildElement("phrase"); phrase != nullptr;
			phrase = phrase->NextSiblingElement("phrase")) {
			// Get the phrase
			auto* text = phrase->GetText();
	
			if (text == nullptr) {
				LOG("Failed to parse party phrase");
				continue;
			}
	
			m_PartyPhrases.push_back(text);
		}
	}

	for (auto* npc = npcs->FirstChildElement("npc"); npc != nullptr; npc = npc->NextSiblingElement("npc")) {
		// Get the NPC name
		auto* name = npc->Attribute("name");

		if (!name) name = "";

		// Get the NPC lot
		auto* lot = npc->Attribute("lot");

		if (lot == nullptr) {
			LOG("Failed to parse NPC lot");
			continue;
		}

		// Get the equipment
		auto* equipment = npc->FirstChildElement("equipment");
		std::vector<LOT> inventory;

		if (equipment) {
			auto* text = equipment->GetText();

			if (text != nullptr) {
				std::string equipmentString(text);

				std::vector<std::string> splitEquipment = GeneralUtils::SplitString(equipmentString, ',');

				for (auto& item : splitEquipment) {
					inventory.push_back(std::stoi(item));
				}
			}
		}


		// Get the phrases
		auto* phrases = npc->FirstChildElement("phrases");

		std::vector<std::string> phraseList = {};

		if (phrases) {
			for (auto* phrase = phrases->FirstChildElement("phrase"); phrase != nullptr;
				phrase = phrase->NextSiblingElement("phrase")) {
				// Get the phrase
				auto* text = phrase->GetText();
				if (text == nullptr) {
					LOG("Failed to parse NPC phrase");
					continue;
				}
				phraseList.push_back(text);
			}
		}

		// Get the script
		auto* scriptElement = npc->FirstChildElement("script");

		std::string scriptName = "";

		if (scriptElement != nullptr) {
			auto* scriptNameAttribute = scriptElement->Attribute("name");
			if (scriptNameAttribute) scriptName = scriptNameAttribute;
		}

		auto* ldfElement = npc->FirstChildElement("ldf");
		std::vector<std::u16string> keys = {};

		std::vector<LDFBaseData*> ldf = {};
		if(ldfElement) {
			for (auto* entry = ldfElement->FirstChildElement("entry"); entry != nullptr;
				entry = entry->NextSiblingElement("entry")) {
				// Get the ldf data
				auto* data = entry->Attribute("data");
				if (!data) continue;

				LDFBaseData* ldfData = LDFBaseData::DataFromString(data);
				keys.push_back(ldfData->GetKey());
				ldf.push_back(ldfData);
			}
		}
		if (!keys.empty()) ldf.push_back(new LDFData<std::vector<std::u16string>>(u"syncLDF", keys));

		VanityNPC npcData;
		npcData.m_Name = name;
		npcData.m_LOT = std::stoi(lot);
		npcData.m_Equipment = inventory;
		npcData.m_Phrases = phraseList;
		npcData.m_Script = scriptName;
		npcData.ldf = ldf;

		// Get flags
		auto* flags = npc->FirstChildElement("flags");

		if (flags != nullptr) {
			for (auto* flag = flags->FirstChildElement("flag"); flag != nullptr;
				flag = flag->NextSiblingElement("flag")) {
				// Get the flag name
				auto* name = flag->Attribute("name");

				if (name == nullptr) {
					LOG("Failed to parse NPC flag name");
					continue;
				}

				// Get the flag value
				auto* value = flag->Attribute("value");

				if (value == nullptr) {
					LOG("Failed to parse NPC flag value");
					continue;
				}

				npcData.m_Flags[name] = std::stoi(value);
			}
		}

		// Get the zones
		for (auto* zone = npc->FirstChildElement("zone"); zone != nullptr; zone = zone->NextSiblingElement("zone")) {
			// Get the zone ID
			auto* zoneID = zone->Attribute("id");

			if (zoneID == nullptr) {
				LOG("Failed to parse NPC zone ID");
				continue;
			}

			// Get the locations
			auto* locations = zone->FirstChildElement("locations");

			if (locations == nullptr) {
				LOG("Failed to parse NPC locations");
				continue;
			}

			for (auto* location = locations->FirstChildElement("location"); location != nullptr;
				location = location->NextSiblingElement("location")) {
				// Get the location data
				auto* x = location->Attribute("x");
				auto* y = location->Attribute("y");
				auto* z = location->Attribute("z");
				auto* rw = location->Attribute("rw");
				auto* rx = location->Attribute("rx");
				auto* ry = location->Attribute("ry");
				auto* rz = location->Attribute("rz");

				if (x == nullptr || y == nullptr || z == nullptr || rw == nullptr || rx == nullptr || ry == nullptr
					|| rz == nullptr) {
					LOG("Failed to parse NPC location data");
					continue;
				}

				VanityNPCLocation locationData;
				locationData.m_Position = { std::stof(x), std::stof(y), std::stof(z) };
				locationData.m_Rotation = { std::stof(rw), std::stof(rx), std::stof(ry), std::stof(rz) };
				locationData.m_Chance = 1.0f;

				if (location->Attribute("chance") != nullptr) {
					locationData.m_Chance = std::stof(location->Attribute("chance"));
				}

				const auto& it = npcData.m_Locations.find(std::stoi(zoneID));

				if (it != npcData.m_Locations.end()) {
					it->second.push_back(locationData);
				} else {
					std::vector<VanityNPCLocation> locations;
					locations.push_back(locationData);
					npcData.m_Locations.insert(std::make_pair(std::stoi(zoneID), locations));
				}
			}
		}

		m_NPCs.push_back(npcData);
	}
}

VanityNPC* VanityUtilities::GetNPC(const std::string& name) {
	for (size_t i = 0; i < m_NPCs.size(); i++) {
		if (m_NPCs[i].m_Name == name) {
			return &m_NPCs[i];
		}
	}

	return nullptr;
}

std::string VanityUtilities::ParseMarkdown(const std::string& file) {
	// This function will read the file and return the content formatted as ASCII text.

	// Read the file into a string
	std::ifstream t(file);

	// If the file does not exist, return an empty string.
	if (!t.good()) {
		return "";
	}

	std::stringstream buffer;
	buffer << t.rdbuf();
	std::string fileContents = buffer.str();

	// Loop through all lines in the file.
	// Replace all instances of the markdown syntax with the corresponding HTML.
	// Only care about headers
	std::stringstream output;
	std::string line;
	std::stringstream ss;
	ss << fileContents;
	while (std::getline(ss, line)) {

#define TOSTRING(x) #x

#ifndef STRINGIFY
#define STRINGIFY(x) TOSTRING(x)
#endif
		// Replace "__TIMESTAMP__" with the __TIMESTAMP__
		GeneralUtils::ReplaceInString(line, "__TIMESTAMP__", __TIMESTAMP__);
		// Replace "__VERSION__" with the PROJECT_VERSION
		GeneralUtils::ReplaceInString(line, "__VERSION__", Game::projectVersion);
		// Replace "__SOURCE__" with SOURCE
		GeneralUtils::ReplaceInString(line, "__SOURCE__", Game::config->GetValue("source"));
		// Replace "__LICENSE__" with LICENSE
		GeneralUtils::ReplaceInString(line, "__LICENSE__", "AGPL-3.0");

		if (line.find("##") != std::string::npos) {
			// Add "&lt;font size=&apos;18&apos; color=&apos;#000000&apos;&gt;" before the header
			output << "<font size=\"14\" color=\"#000000\">";
			// Add the header without the markdown syntax
			output << line.substr(3);

			output << "</font>";
		} else if (line.find("#") != std::string::npos) {
			// Add "&lt;font size=&apos;18&apos; color=&apos;#000000&apos;&gt;" before the header
			output << "<font size=\"18\" color=\"#000000\">";
			// Add the header without the markdown syntax
			output << line.substr(2);

			output << "</font>";
		} else {
			output << line;
		}

		output << "\n";
	}

	return output.str();
}

void VanityUtilities::SetupNPCTalk(Entity* npc) {
	npc->AddCallbackTimer(15.0f, [npc]() { NPCTalk(npc); });

	npc->SetProximityRadius(20.0f, "talk");
}

void VanityUtilities::NPCTalk(Entity* npc) {
	auto* proximityMonitorComponent = npc->GetComponent<ProximityMonitorComponent>();

	if (!proximityMonitorComponent->GetProximityObjects("talk").empty()) {
		const auto& chats = npc->GetVar<std::vector<std::string>>(u"chats");

		if (chats.empty()) {
			return;
		}

		const auto& selected
			= chats[GeneralUtils::GenerateRandomNumber<int32_t>(0, static_cast<int32_t>(chats.size() - 1))];

		GameMessages::SendNotifyClientZoneObject(
			npc->GetObjectID(), u"sendToclient_bubble", 0, 0, npc->GetObjectID(), selected, UNASSIGNED_SYSTEM_ADDRESS);
	}

	Game::entityManager->SerializeEntity(npc);

	const float nextTime = GeneralUtils::GenerateRandomNumber<float>(15, 60);

	npc->AddCallbackTimer(nextTime, [npc]() { NPCTalk(npc); });
}