/*
 * Darkflame Universe
 * Copyright 2019
 */

#include <sstream>
#include <string>

#include "MissionComponent.h"
#include "Logger.h"
#include "CDClientManager.h"
#include "CDMissionTasksTable.h"
#include "InventoryComponent.h"
#include "GameMessages.h"
#include "Game.h"
#include "Amf3.h"
#include "dZoneManager.h"
#include "Mail.h"
#include "MissionPrerequisites.h"
#include "AchievementCacheKey.h"
#include "eMissionState.h"

 // MARK: Mission Component

std::unordered_map<AchievementCacheKey, std::vector<uint32_t>> MissionComponent::m_AchievementCache = {};

//! Initializer
MissionComponent::MissionComponent(Entity* parent) : Component(parent) {
	m_LastUsedMissionOrderUID = Game::zoneManager->GetUniqueMissionIdStartingValue();
}

//! Destructor
MissionComponent::~MissionComponent() {
	for (const auto& mission : m_Missions) {
		delete mission.second;
	}

	this->m_Missions.clear();
}


Mission* MissionComponent::GetMission(const uint32_t missionId) const {
	if (m_Missions.count(missionId) == 0) {
		return nullptr;
	}

	const auto& index = m_Missions.find(missionId);

	if (index == m_Missions.end()) {
		return nullptr;
	}

	return index->second;
}


eMissionState MissionComponent::GetMissionState(const uint32_t missionId) const {
	auto* mission = GetMission(missionId);

	if (mission == nullptr) {
		return CanAccept(missionId) ? eMissionState::AVAILABLE : eMissionState::UNKNOWN;
	}

	return mission->GetMissionState();
}


const std::unordered_map<uint32_t, Mission*>& MissionComponent::GetMissions() const {
	return m_Missions;
}


bool MissionComponent::CanAccept(const uint32_t missionId) const {
	return MissionPrerequisites::CanAccept(missionId, m_Missions);
}


void MissionComponent::AcceptMission(const uint32_t missionId, const bool skipChecks) {
	if (!skipChecks && !CanAccept(missionId)) {
		return;
	}

	// If this is a daily mission, it may already be "accepted"
	auto* mission = this->GetMission(missionId);

	if (mission != nullptr) {
		if (mission->GetClientInfo().repeatable) {
			mission->Accept();
			if (mission->IsMission()) mission->SetUniqueMissionOrderID(++m_LastUsedMissionOrderUID);
		}

		return;
	}

	mission = new Mission(this, missionId);

	if (mission->IsMission()) mission->SetUniqueMissionOrderID(++m_LastUsedMissionOrderUID);

	mission->Accept();

	this->m_Missions.insert_or_assign(missionId, mission);
}

void MissionComponent::CompleteMission(const uint32_t missionId, const bool skipChecks, const bool yieldRewards) {
	// Get the mission first
	auto* mission = this->GetMission(missionId);

	if (mission == nullptr) {
		AcceptMission(missionId, skipChecks);

		mission = this->GetMission(missionId);

		if (mission == nullptr) {
			return;
		}
	}

	//If this mission is not repeatable, and already completed, we stop here.
	if (mission->IsComplete() && !mission->IsRepeatable()) {
		return;
	}

	mission->Complete(yieldRewards);
}

void MissionComponent::RemoveMission(uint32_t missionId) {
	auto* mission = this->GetMission(missionId);

	if (mission == nullptr) {
		return;
	}

	delete mission;

	m_Missions.erase(missionId);
}

void MissionComponent::Progress(eMissionTaskType type, int32_t value, LWOOBJID associate, const std::string& targets, int32_t count, bool ignoreAchievements) {
	std::vector<uint32_t> acceptedAchievements;
	if (count > 0 && !ignoreAchievements) {
		acceptedAchievements = LookForAchievements(type, value, true, associate, targets, count);
	}

	for (const auto& [id, mission] : m_Missions) {
		if (!mission || std::find(acceptedAchievements.begin(), acceptedAchievements.end(), mission->GetMissionId()) != acceptedAchievements.end()) continue;

		if (mission->IsAchievement() && ignoreAchievements) continue;

		if (mission->IsComplete()) continue;

		mission->Progress(type, value, associate, targets, count);
	}
}

void MissionComponent::ForceProgress(const uint32_t missionId, const uint32_t taskId, const int32_t value, const bool acceptMission) {
	auto* mission = GetMission(missionId);

	if (mission == nullptr) {
		if (!acceptMission) {
			return;
		}

		AcceptMission(missionId);

		mission = GetMission(missionId);

		if (mission == nullptr) {
			return;
		}
	}

	for (auto* element : mission->GetTasks()) {
		if (element->GetClientInfo().uid != taskId) continue;

		element->AddProgress(value);
	}

	if (!mission->IsComplete()) {
		mission->CheckCompletion();
	}
}

void MissionComponent::ForceProgressTaskType(const uint32_t missionId, const uint32_t taskType, const int32_t value, const bool acceptMission) {
	auto* mission = GetMission(missionId);

	if (mission == nullptr) {
		if (!acceptMission) {
			return;
		}

		CDMissions missionInfo;

		if (!GetMissionInfo(missionId, missionInfo)) {
			return;
		}

		if (missionInfo.isMission) {
			return;
		}

		AcceptMission(missionId);

		mission = GetMission(missionId);

		if (mission == nullptr) {
			return;
		}
	}

	for (auto* element : mission->GetTasks()) {
		if (element->GetType() != static_cast<eMissionTaskType>(taskType)) continue;

		element->AddProgress(value);
	}

	if (!mission->IsComplete()) {
		mission->CheckCompletion();
	}
}

void MissionComponent::ForceProgressValue(uint32_t missionId, uint32_t taskType, int32_t value, bool acceptMission) {
	auto* mission = GetMission(missionId);

	if (mission == nullptr) {
		if (!acceptMission) {
			return;
		}

		CDMissions missionInfo;

		if (!GetMissionInfo(missionId, missionInfo)) {
			return;
		}

		if (missionInfo.isMission) {
			return;
		}

		AcceptMission(missionId);

		mission = GetMission(missionId);

		if (mission == nullptr) {
			return;
		}
	}

	for (auto* element : mission->GetTasks()) {
		if (element->GetType() != static_cast<eMissionTaskType>(taskType) || !element->InAllTargets(value)) continue;

		element->AddProgress(1);
	}

	if (!mission->IsComplete()) {
		mission->CheckCompletion();
	}
}

bool MissionComponent::GetMissionInfo(uint32_t missionId, CDMissions& result) {
	auto* missionsTable = CDClientManager::GetTable<CDMissionsTable>();

	const auto missions = missionsTable->Query([=](const CDMissions& entry) {
		return entry.id == static_cast<int>(missionId);
		});

	if (missions.empty()) {
		return false;
	}

	result = missions[0];

	return true;
}

#define MISSION_NEW_METHOD

const std::vector<uint32_t> MissionComponent::LookForAchievements(eMissionTaskType type, int32_t value, bool progress, LWOOBJID associate, const std::string& targets, int32_t count) {
#ifdef MISSION_NEW_METHOD
	// Query for achievments, using the cache
	const auto& result = QueryAchievements(type, value, targets);

	std::vector<uint32_t> acceptedAchievements;

	for (const uint32_t missionID : result) {
		// Check if we already have this achievement
		if (GetMission(missionID) != nullptr) {
			continue;
		}

		// Check if we can accept this achievement
		if (!MissionPrerequisites::CanAccept(missionID, m_Missions)) {
			continue;
		}

		// Instantiate new mission and accept it
		auto* instance = new Mission(this, missionID);

		m_Missions.insert_or_assign(missionID, instance);

		if (instance->IsMission()) instance->SetUniqueMissionOrderID(++m_LastUsedMissionOrderUID);

		instance->Accept();

		acceptedAchievements.push_back(missionID);

		if (progress) {
			// Progress mission to bring it up to speed
			instance->Progress(type, value, associate, targets, count);
		}
	}

	return acceptedAchievements;
#else
	auto* missionTasksTable = CDClientManager::GetTable<CDMissionTasksTable>();
	auto* missionsTable = CDClientManager::GetTable<CDMissionsTable>();

	auto tasks = missionTasksTable->Query([=](const CDMissionTasks& entry) {
		return entry.taskType == static_cast<unsigned>(type);
		});

	std::vector<uint32_t> acceptedAchievements;

	for (const auto& task : tasks) {
		if (GetMission(task.id) != nullptr) {
			continue;
		}

		const auto missionEntries = missionsTable->Query([=](const CDMissions& entry) {
			return entry.id == static_cast<int>(task.id) && !entry.isMission;
			});

		if (missionEntries.empty()) {
			continue;
		}

		const auto mission = missionEntries[0];

		if (mission.isMission || !MissionPrerequisites::CanAccept(mission.id, m_Missions)) {
			continue;
		}

		if (task.target != value && task.targetGroup != targets) {
			auto stream = std::istringstream(task.targetGroup);
			std::string token;

			auto found = false;

			while (std::getline(stream, token, ',')) {
				try {
					const auto target = std::stoul(token);

					found = target == value;

					if (found) {
						break;
					}
				} catch (std::invalid_argument& exception) {
					LOG("Failed to parse target (%s): (%s)!", token.c_str(), exception.what());
				}
			}

			if (!found) {
				continue;
			}
		}

		auto* instance = new Mission(this, mission.id);

		m_Missions.insert_or_assign(mission.id, instance);

		if (instance->IsMission()) instance->SetUniqueMissionOrderID(++m_LastUsedMissionOrderUID);

		instance->Accept();

		acceptedAchievements.push_back(mission.id);

		if (progress) {
			instance->Progress(type, value, associate, targets, count);
		}
	}

	return acceptedAchievements;
#endif
}

const std::vector<uint32_t>& MissionComponent::QueryAchievements(eMissionTaskType type, int32_t value, const std::string targets) {
	// Create a hash which represent this query for achievements
	AchievementCacheKey toFind;
	toFind.SetType(type);
	toFind.SetValue(value);
	toFind.SetTargets(targets);

	const auto& iter = m_AchievementCache.find(toFind);

	// Check if this query is cached
	if (iter != m_AchievementCache.end()) {
		return iter->second;
	}

	// Find relevent tables
	auto* missionTasksTable = CDClientManager::GetTable<CDMissionTasksTable>();
	auto* missionsTable = CDClientManager::GetTable<CDMissionsTable>();

	std::vector<uint32_t> result;

	// Loop through all mission tasks, might cache this task check later
	for (const auto& task : missionTasksTable->GetEntries()) {
		if (task.taskType != static_cast<uint32_t>(type)) {
			continue;
		}

		// Seek the assosicated mission
		auto foundMission = false;

		const auto& mission = missionsTable->GetByMissionID(task.id, foundMission);

		if (!foundMission || mission.isMission) {
			continue;
		}

		// Compare the easy values
		if (task.target == value || task.targetGroup == targets) {
			result.push_back(mission.id);

			continue;
		}

		// Compare the target group, array separated by ','
		auto stream = std::istringstream(task.targetGroup);
		std::string token;

		while (std::getline(stream, token, ',')) {
			try {
				if (std::stoi(token) == value) {
					result.push_back(mission.id);

					continue;
				}
			} catch (std::invalid_argument& exception) {
				// Ignored
			}
		}
	}
	// Insert into cache
	m_AchievementCache.insert_or_assign(toFind, result);
	return m_AchievementCache.find(toFind)->second;
}

bool MissionComponent::RequiresItem(const LOT lot) {
	auto query = CDClientDatabase::CreatePreppedStmt(
		"SELECT type FROM Objects WHERE id = ?;");
	query.bind(1, static_cast<int>(lot));

	auto result = query.execQuery();

	if (result.eof()) {
		return false;
	}

	if (!result.fieldIsNull("type")) {
		const auto type = std::string(result.getStringField("type"));

		result.finalize();

		if (type == "Powerup") {
			return true;
		}
	}

	result.finalize();

	for (const auto& pair : m_Missions) {
		auto* mission = pair.second;

		if (mission->IsComplete()) {
			continue;
		}

		for (auto* task : mission->GetTasks()) {
			if (task->IsComplete() || task->GetType() != eMissionTaskType::GATHER) {
				continue;
			}

			if (!task->InAllTargets(lot)) {
				continue;
			}

			return true;
		}
	}

	const auto required = LookForAchievements(eMissionTaskType::GATHER, lot, false);

	return !required.empty();
}


void MissionComponent::LoadFromXml(const tinyxml2::XMLDocument& doc) {
	auto* mis = doc.FirstChildElement("obj")->FirstChildElement("mis");

	if (mis == nullptr) return;

	auto* cur = mis->FirstChildElement("cur");
	auto* done = mis->FirstChildElement("done");

	auto* doneM = done->FirstChildElement();

	while (doneM) {
		int missionId;

		doneM->QueryAttribute("id", &missionId);

		auto* mission = new Mission(this, missionId);

		mission->LoadFromXmlDone(*doneM);

		doneM = doneM->NextSiblingElement();

		m_Missions.insert_or_assign(missionId, mission);
	}

	auto* currentM = cur->FirstChildElement();

	uint32_t missionOrder{};
	while (currentM) {
		int missionId;

		currentM->QueryAttribute("id", &missionId);

		auto* mission = m_Missions.contains(missionId) ? m_Missions[missionId] : new Mission(this, missionId);

		mission->LoadFromXmlCur(*currentM);

		if (currentM->QueryAttribute("o", &missionOrder) == tinyxml2::XML_SUCCESS && mission->IsMission()) {
			mission->SetUniqueMissionOrderID(missionOrder);
			if (missionOrder > m_LastUsedMissionOrderUID) m_LastUsedMissionOrderUID = missionOrder;
		}

		currentM = currentM->NextSiblingElement();

		m_Missions.insert_or_assign(missionId, mission);
	}
}


void MissionComponent::UpdateXml(tinyxml2::XMLDocument& doc) {
	auto shouldInsertMis = false;

	auto* obj = doc.FirstChildElement("obj");

	auto* mis = obj->FirstChildElement("mis");

	if (mis == nullptr) {
		mis = doc.NewElement("mis");

		shouldInsertMis = true;
	}

	mis->DeleteChildren();

	auto* done = doc.NewElement("done");
	auto* cur = doc.NewElement("cur");

	for (const auto& pair : m_Missions) {
		auto* mission = pair.second;

		if (mission) {
			const auto completions = mission->GetCompletions();

			auto* m = doc.NewElement("m");

			if (completions > 0) {
				mission->UpdateXmlDone(*m);

				done->LinkEndChild(m);

				if (mission->IsComplete()) continue;

				m = doc.NewElement("m");
			}

			if (mission->IsMission()) m->SetAttribute("o", mission->GetUniqueMissionOrderID());

			mission->UpdateXmlCur(*m);

			cur->LinkEndChild(m);
		}
	}

	mis->InsertFirstChild(done);
	mis->InsertEndChild(cur);

	if (shouldInsertMis) {
		obj->LinkEndChild(mis);
	}
}

void MissionComponent::AddCollectible(int32_t collectibleID) {
	// Check if this collectible is already in the list
	if (HasCollectible(collectibleID)) {
		return;
	}

	m_Collectibles.push_back(collectibleID);
}

bool MissionComponent::HasCollectible(int32_t collectibleID) {
	return std::find(m_Collectibles.begin(), m_Collectibles.end(), collectibleID) != m_Collectibles.end();
}

bool MissionComponent::HasMission(uint32_t missionId) {
	return GetMission(missionId) != nullptr;
}

void MissionComponent::ResetMission(const int32_t missionId) {
	auto* mission = GetMission(missionId);

	if (!mission) return;

	m_Missions.erase(missionId);
	GameMessages::SendResetMissions(m_Parent, m_Parent->GetSystemAddress(), missionId);
}
