#include "ComponentUpdateTask.h"

#include "ComponentList_p.h"
#include "ComponentList.h"
#include "Component.h"
#include <Env.h>
#include <meta/Index.h>
#include <meta/VersionList.h>
#include <meta/Version.h>
#include "ComponentUpdateTask_p.h"
#include <cassert>
#include <Version.h>
#include "net/Mode.h"
#include "OneSixVersionFormat.h"

/*
 * This is responsible for loading the components of a component list AND resolving dependency issues between them
 */

/*
 * FIXME: the 'one shot async task' nature of this does not fit the intended usage
 * Really, it should be a reactor/state machine that receives input from the application
 * and dynamically adapts to changing requirements...
 *
 * The reactor should be the only entry into manipulating the ComponentList.
 * See: https://en.wikipedia.org/wiki/Reactor_pattern
 */

/*
 * Or make this operate on a snapshot of the ComponentList state, then merge results in as long as the snapshot and ComponentList didn't change?
 * If the component list changes, start over.
 */

ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList* list, QObject* parent)
	: Task(parent)
{
	d.reset(new ComponentUpdateTaskData);
	d->m_list = list;
	d->mode = mode;
	d->netmode = netmode;
}

ComponentUpdateTask::~ComponentUpdateTask()
{
}

void ComponentUpdateTask::executeTask()
{
	qDebug() << "Loading components";
	loadComponents();
}

namespace
{
enum class LoadResult
{
	LoadedLocal,
	RequiresRemote,
	Failed
};

LoadResult composeLoadResult(LoadResult a, LoadResult b)
{
	if (a < b)
	{
		return b;
	}
	return a;
}

static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
{
	if(component->m_loaded)
	{
		qDebug() << component->getName() << "is already loaded";
		return LoadResult::LoadedLocal;
	}

	LoadResult result = LoadResult::Failed;
	auto customPatchFilename = component->getFilename();
	if(QFile::exists(customPatchFilename))
	{
		// if local file exists...

		// check for uid problems inside...
		bool fileChanged = false;
		auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false);
		if(file->uid != component->m_uid)
		{
			file->uid = component->m_uid;
			fileChanged = true;
		}
		if(fileChanged)
		{
			// FIXME: @QUALITY do not ignore return value
			ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename);
		}

		component->m_file = file;
		component->m_loaded = true;
		result = LoadResult::LoadedLocal;
	}
	else
	{
		auto metaVersion = ENV.metadataIndex()->get(component->m_uid, component->m_version);
		component->m_metaVersion = metaVersion;
		if(metaVersion->isLoaded())
		{
			component->m_loaded = true;
			result = LoadResult::LoadedLocal;
		}
		else
		{
			metaVersion->load(netmode);
			loadTask = metaVersion->getCurrentTask();
			if(loadTask)
				result = LoadResult::RequiresRemote;
			else if (metaVersion->isLoaded())
				result = LoadResult::LoadedLocal;
			else
				result = LoadResult::Failed;
		}
	}
	return result;
}

// FIXME: dead code. determine if this can still be useful?
/*
static LoadResult loadComponentList(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
{
	if(component->m_loaded)
	{
		qDebug() << component->getName() << "is already loaded";
		return LoadResult::LoadedLocal;
	}

	LoadResult result = LoadResult::Failed;
	auto metaList = ENV.metadataIndex()->get(component->m_uid);
	if(metaList->isLoaded())
	{
		component->m_loaded = true;
		result = LoadResult::LoadedLocal;
	}
	else
	{
		metaList->load(netmode);
		loadTask = metaList->getCurrentTask();
		result = LoadResult::RequiresRemote;
	}
	return result;
}
*/

static LoadResult loadIndex(shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
{
	// FIXME: DECIDE. do we want to run the update task anyway?
	if(ENV.metadataIndex()->isLoaded())
	{
		qDebug() << "Index is already loaded";
		return LoadResult::LoadedLocal;
	}
	ENV.metadataIndex()->load(netmode);
	loadTask = ENV.metadataIndex()->getCurrentTask();
	if(loadTask)
	{
		return LoadResult::RequiresRemote;
	}
	// FIXME: this is assuming the load succeeded... did it really?
	return LoadResult::LoadedLocal;
}
}

void ComponentUpdateTask::loadComponents()
{
	LoadResult result = LoadResult::LoadedLocal;
	size_t taskIndex = 0;
	size_t componentIndex = 0;
	d->remoteLoadSuccessful = true;
	// load the main index (it is needed to determine if components can revert)
	{
		// FIXME: tear out as a method? or lambda?
		shared_qobject_ptr<Task> indexLoadTask;
		auto singleResult = loadIndex(indexLoadTask, d->netmode);
		result = composeLoadResult(result, singleResult);
		if(indexLoadTask)
		{
			qDebug() << "Remote loading is being run for metadata index";
			RemoteLoadStatus status;
			status.type = RemoteLoadStatus::Type::Index;
			d->remoteLoadStatusList.append(status);
			connect(indexLoadTask.get(), &Task::succeeded, [=]()
			{
				remoteLoadSucceeded(taskIndex);
			});
			connect(indexLoadTask.get(), &Task::failed, [=](const QString & error)
			{
				remoteLoadFailed(taskIndex, error);
			});
			taskIndex++;
		}
	}
	// load all the components OR their lists...
	for (auto component: d->m_list->d->components)
	{
		shared_qobject_ptr<Task> loadTask;
		LoadResult singleResult;
		RemoteLoadStatus::Type loadType;
		// FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, ignore all that...
#if 0
		switch(d->mode)
		{
			case Mode::Launch:
			{
				singleResult = loadComponent(component, loadTask, d->netmode);
				loadType = RemoteLoadStatus::Type::Version;
				break;
			}
			case Mode::Resolution:
			{
				singleResult = loadComponentList(component, loadTask, d->netmode);
				loadType = RemoteLoadStatus::Type::List;
				break;
			}
		}
#else
		singleResult = loadComponent(component, loadTask, d->netmode);
		loadType = RemoteLoadStatus::Type::Version;
#endif
		if(singleResult == LoadResult::LoadedLocal)
		{
			component->updateCachedData();
		}
		result = composeLoadResult(result, singleResult);
		if (loadTask)
		{
			qDebug() << "Remote loading is being run for" << component->getName();
			connect(loadTask.get(), &Task::succeeded, [=]()
			{
				remoteLoadSucceeded(taskIndex);
			});
			connect(loadTask.get(), &Task::failed, [=](const QString & error)
			{
				remoteLoadFailed(taskIndex, error);
			});
			RemoteLoadStatus status;
			status.type = loadType;
			status.componentListIndex = componentIndex;
			d->remoteLoadStatusList.append(status);
			taskIndex++;
		}
		componentIndex++;
	}
	d->remoteTasksInProgress = taskIndex;
	switch(result)
	{
		case LoadResult::LoadedLocal:
		{
			// Everything got loaded. Advance to dependency resolution.
			resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline);
			break;
		}
		case LoadResult::RequiresRemote:
		{
			// we wait for signals.
			break;
		}
		case LoadResult::Failed:
		{
			emitFailed(tr("Some component metadata load tasks failed."));
			break;
		}
	}
}

namespace
{
	struct RequireEx : public Meta::Require
	{
		size_t indexOfFirstDependee = 0;
	};
	struct RequireCompositionResult
	{
		bool ok;
		RequireEx outcome;
	};
	using RequireExSet = std::set<RequireEx>;
}

static RequireCompositionResult composeRequirement(const RequireEx & a, const RequireEx & b)
{
	assert(a.uid == b.uid);
	RequireEx out;
	out.uid = a.uid;
	out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee);
	if(a.equalsVersion.isEmpty())
	{
		out.equalsVersion = b.equalsVersion;
	}
	else if (b.equalsVersion.isEmpty())
	{
		out.equalsVersion = a.equalsVersion;
	}
	else if (a.equalsVersion == b.equalsVersion)
	{
		out.equalsVersion = a.equalsVersion;
	}
	else
	{
		// FIXME: mark error as explicit version conflict
		return {false, out};
	}

	if(a.suggests.isEmpty())
	{
		out.suggests = b.suggests;
	}
	else if (b.suggests.isEmpty())
	{
		out.suggests = a.suggests;
	}
	else
	{
		Version aVer(a.suggests);
		Version bVer(b.suggests);
		out.suggests = (aVer < bVer ? b.suggests : a.suggests);
	}
	return {true, out};
}

// gather the requirements from all components, finding any obvious conflicts
static bool gatherRequirementsFromComponents(const ComponentContainer & input, RequireExSet & output)
{
	bool succeeded = true;
	size_t componentNum = 0;
	for(auto component: input)
	{
		auto &componentRequires = component->m_cachedRequires;
		for(const auto & componentRequire: componentRequires)
		{
			auto found = std::find_if(output.cbegin(), output.cend(), [componentRequire](const Meta::Require & req){
				return req.uid == componentRequire.uid;
			});

			RequireEx componenRequireEx;
			componenRequireEx.uid = componentRequire.uid;
			componenRequireEx.suggests = componentRequire.suggests;
			componenRequireEx.equalsVersion = componentRequire.equalsVersion;
			componenRequireEx.indexOfFirstDependee = componentNum;

			if(found != output.cend())
			{
				// found... process it further
				auto result = composeRequirement(componenRequireEx, *found);
				if(result.ok)
				{
					output.erase(componenRequireEx);
					output.insert(result.outcome);
				}
				else
				{
					qCritical()
						<< "Conflicting requirements:"
						<< componentRequire.uid
						<< "versions:"
						<< componentRequire.equalsVersion
						<< ";"
						<< (*found).equalsVersion;
				}
				succeeded &= result.ok;
			}
			else
			{
				// not found, accumulate
				output.insert(componenRequireEx);
			}
		}
		componentNum++;
	}
	return succeeded;
}

/// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps)
static void getTrivialRemovals(const ComponentContainer & components, const RequireExSet & reqs, QStringList &toRemove)
{
	for(const auto & component: components)
	{
		if(!component->m_dependencyOnly)
			continue;
		if(!component->m_cachedVolatile)
			continue;
		RequireEx reqNeedle;
		reqNeedle.uid = component->m_uid;
		const auto iter = reqs.find(reqNeedle);
		if(iter == reqs.cend())
		{
			toRemove.append(component->m_uid);
		}
	}
}

/**
 * handles:
 * - trivial addition (there is an unmet requirement and it can be trivially met by adding something)
 * - trivial version conflict of dependencies == explicit version required and installed is different
 *
 * toAdd - set of requirements than mean adding a new component
 * toChange - set of requirements that mean changing version of an existing component
 */
static bool getTrivialComponentChanges(const ComponentIndex & index, const RequireExSet & input, RequireExSet & toAdd, RequireExSet & toChange)
{
	enum class Decision
	{
		Undetermined,
		Met,
		Missing,
		VersionNotSame,
		LockedVersionNotSame
	} decision = Decision::Undetermined;

	QString reqStr;
	bool succeeded = true;
	// list the composed requirements and say if they are met or unmet
	for(auto & req: input)
	{
		do
		{
			if(req.equalsVersion.isEmpty())
			{
				reqStr = QString("Req: %1").arg(req.uid);
				if(index.contains(req.uid))
				{
					decision = Decision::Met;
				}
				else
				{
					toAdd.insert(req);
					decision = Decision::Missing;
				}
				break;
			}
			else
			{
				reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion);
				const auto & compIter = index.find(req.uid);
				if(compIter == index.cend())
				{
					toAdd.insert(req);
					decision = Decision::Missing;
					break;
				}
				auto & comp = (*compIter);
				if(comp->getVersion() != req.equalsVersion)
				{
					if(comp->m_dependencyOnly)
					{
						decision = Decision::VersionNotSame;
					}
					else
					{
						decision = Decision::LockedVersionNotSame;
					}
					break;
				}
				decision = Decision::Met;
			}
		} while(false);
		switch(decision)
		{
			case Decision::Undetermined:
				qCritical() << "No decision for" << reqStr;
				succeeded = false;
				break;
			case Decision::Met:
				qDebug() << reqStr << "Is met.";
				break;
			case Decision::Missing:
				qDebug() << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee;
				toAdd.insert(req);
				break;
			case Decision::VersionNotSame:
				qDebug() << reqStr << "already has different version that can be changed.";
				toChange.insert(req);
				break;
			case Decision::LockedVersionNotSame:
				qDebug() << reqStr << "already has different version that cannot be changed.";
				succeeded = false;
				break;
		}
	}
	return succeeded;
}

// FIXME, TODO: decouple dependency resolution from loading
// FIXME: This works directly with the ComponentList internals. It shouldn't! It needs richer data types than ComponentList uses.
// FIXME: throw all this away and use a graph
void ComponentUpdateTask::resolveDependencies(bool checkOnly)
{
	qDebug() << "Resolving dependencies";
	/*
	 * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways:
	 * 1. There are conflicting dependencies on the same uid with different exact version numbers
	 *    -> hard error
	 * 2. A dependency has non-matching exact version number
	 *    -> hard error
	 * 3. A dependency is entirely missing and needs to be injected before the dependee(s)
	 *    -> requirements are injected
	 *
	 * NOTE: this is a placeholder and should eventually be replaced with something 'serious'
	 */
	auto & components = d->m_list->d->components;
	auto & componentIndex = d->m_list->d->componentIndex;

	RequireExSet allRequires;
	QStringList toRemove;
	do
	{
		allRequires.clear();
		toRemove.clear();
		if(!gatherRequirementsFromComponents(components, allRequires))
		{
			emitFailed(tr("Conflicting requirements detected during dependency checking!"));
			return;
		}
		getTrivialRemovals(components, allRequires, toRemove);
		if(!toRemove.isEmpty())
		{
			qDebug() << "Removing obsolete components...";
			for(auto & remove : toRemove)
			{
				qDebug() << "Removing" << remove;
				d->m_list->remove(remove);
			}
		}
	} while (!toRemove.isEmpty());
	RequireExSet toAdd;
	RequireExSet toChange;
	bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange);
	if(!succeeded)
	{
		emitFailed(tr("Instance has conflicting dependencies."));
		return;
	}
	if(checkOnly)
	{
		if(toAdd.size() || toChange.size())
		{
			emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch."));
		}
		else
		{
			emitSucceeded();
		}
		return;
	}

	bool recursionNeeded = false;
	if(toAdd.size())
	{
		// add stuff...
		for(auto &add: toAdd)
		{
			ComponentPtr component = new Component(d->m_list, add.uid);
			if(!add.equalsVersion.isEmpty())
			{
				// exact version
				qDebug() << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee;
				component->m_version = add.equalsVersion;
			}
			else
			{
				// version needs to be decided
				qDebug() << "Adding" << add.uid << "at position" << add.indexOfFirstDependee;
// ############################################################################################################
// HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded.
				if(!add.suggests.isEmpty())
				{
					component->m_version = add.suggests;
				}
				else
				{
					if(add.uid == "org.lwjgl")
					{
						component->m_version = "2.9.1";
					}
					else if (add.uid == "org.lwjgl3")
					{
						component->m_version = "3.1.2";
					}
				}
// HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded.
// ############################################################################################################
			}
			component->m_dependencyOnly = true;
			// FIXME: this should not work directly with the component list
			d->m_list->insertComponent(add.indexOfFirstDependee, component);
			componentIndex[add.uid] = component;
		}
		recursionNeeded = true;
	}
	if(toChange.size())
	{
		// change a version of something that exists
		for(auto &change: toChange)
		{
			// FIXME: this should not work directly with the component list
			qDebug() << "Setting version of " << change.uid << "to" << change.equalsVersion;
			auto component = componentIndex[change.uid];
			component->setVersion(change.equalsVersion);
		}
		recursionNeeded = true;
	}

	if(recursionNeeded)
	{
		loadComponents();
	}
	else
	{
		emitSucceeded();
	}
}

void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex)
{
	auto &taskSlot = d->remoteLoadStatusList[taskIndex];
	if(taskSlot.finished)
	{
		qWarning() << "Got multiple results from remote load task" << taskIndex;
		return;
	}
	qDebug() << "Remote task" << taskIndex << "succeeded";
	taskSlot.succeeded = false;
	taskSlot.finished = true;
	d->remoteTasksInProgress --;
	// update the cached data of the component from the downloaded version file.
	if (taskSlot.type == RemoteLoadStatus::Type::Version)
	{
		auto component = d->m_list->getComponent(taskSlot.componentListIndex);
		component->m_loaded = true;
		component->updateCachedData();
	}
	checkIfAllFinished();
}


void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg)
{
	auto &taskSlot = d->remoteLoadStatusList[taskIndex];
	if(taskSlot.finished)
	{
		qWarning() << "Got multiple results from remote load task" << taskIndex;
		return;
	}
	qDebug() << "Remote task" << taskIndex << "failed: " << msg;
	d->remoteLoadSuccessful = false;
	taskSlot.succeeded = false;
	taskSlot.finished = true;
	taskSlot.error = msg;
	d->remoteTasksInProgress --;
	checkIfAllFinished();
}

void ComponentUpdateTask::checkIfAllFinished()
{
	if(d->remoteTasksInProgress)
	{
		// not yet...
		return;
	}
	if(d->remoteLoadSuccessful)
	{
		// nothing bad happened... clear the temp load status and proceed with looking at dependencies
		d->remoteLoadStatusList.clear();
		resolveDependencies(d->mode == Mode::Launch);
	}
	else
	{
		// remote load failed... report error and bail
		QStringList allErrorsList;
		for(auto & item: d->remoteLoadStatusList)
		{
			if(!item.succeeded)
			{
				allErrorsList.append(item.error);
			}
		}
		auto allErrors = allErrorsList.join("\n");
		emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors));
		d->remoteLoadStatusList.clear();
	}
}