/* SPDX-FileCopyrightText: 2026 Project Tick
 * SPDX-FileContributor: Project Tick
 * SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-MeshMC-MMCO-Module-Exception-1.0
 *
 *   MeshMC - A Custom Launcher for Minecraft
 *   Copyright (C) 2026 Project Tick
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version, with the additional permission
 *   described in the MeshMC MMCO Module Exception 1.0.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 *   You should have received a copy of the MeshMC MMCO Module Exception 1.0
 *   along with this program.  If not, see <https://projecttick.org/licenses/>.
 */

#include <meta/VersionList.h>
#include <meta/Index.h>
#include "Component.h"

#include <QSaveFile>

#include "meta/Version.h"
#include "VersionFile.h"
#include "minecraft/PackProfile.h"
#include "FileSystem.h"
#include "OneSixVersionFormat.h"
#include "Application.h"

#include <assert.h>

Component::Component(PackProfile* parent, const QString& uid)
{
	assert(parent);
	m_parent = parent;

	m_uid = uid;
}

Component::Component(PackProfile* parent,
					 std::shared_ptr<Meta::Version> version)
{
	assert(parent);
	m_parent = parent;

	m_metaVersion = version;
	m_uid = version->uid();
	m_version = m_cachedVersion = version->version();
	m_cachedName = version->name();
	m_loaded = version->isLoaded();
}

Component::Component(PackProfile* parent, const QString& uid,
					 std::shared_ptr<VersionFile> file)
{
	assert(parent);
	m_parent = parent;

	m_file = file;
	m_uid = uid;
	m_cachedVersion = m_file->version;
	m_cachedName = m_file->name;
	m_loaded = true;
}

std::shared_ptr<Meta::Version> Component::getMeta()
{
	return m_metaVersion;
}

void Component::applyTo(LaunchProfile* profile)
{
	// do not apply disabled components
	if (!isEnabled()) {
		return;
	}
	auto vfile = getVersionFile();
	if (vfile) {
		vfile->applyTo(profile);
	} else {
		profile->applyProblemSeverity(getProblemSeverity());
	}
}

std::shared_ptr<class VersionFile> Component::getVersionFile() const
{
	if (m_metaVersion) {
		if (!m_metaVersion->isLoaded()) {
			m_metaVersion->load(Net::Mode::Online);
		}
		return m_metaVersion->data();
	} else {
		return m_file;
	}
}

std::shared_ptr<class Meta::VersionList> Component::getVersionList() const
{
	// FIXME: what if the metadata index isn't loaded yet?
	if (APPLICATION->metadataIndex()->hasUid(m_uid)) {
		return APPLICATION->metadataIndex()->get(m_uid);
	}
	return nullptr;
}

int Component::getOrder()
{
	if (m_orderOverride)
		return m_order;

	auto vfile = getVersionFile();
	if (vfile) {
		return vfile->order;
	}
	return 0;
}
void Component::setOrder(int order)
{
	m_orderOverride = true;
	m_order = order;
}
QString Component::getID()
{
	return m_uid;
}
QString Component::getName()
{
	if (!m_cachedName.isEmpty())
		return m_cachedName;
	return m_uid;
}
QString Component::getVersion()
{
	return m_cachedVersion;
}
QString Component::getFilename()
{
	return m_parent->patchFilePathForUid(m_uid);
}
QDateTime Component::getReleaseDateTime()
{
	if (m_metaVersion) {
		return m_metaVersion->time();
	}
	auto vfile = getVersionFile();
	if (vfile) {
		return vfile->releaseTime;
	}
	// FIXME: fake
	return QDateTime::currentDateTime();
}

bool Component::isEnabled()
{
	return !canBeDisabled() || !m_disabled;
}

bool Component::canBeDisabled()
{
	return isRemovable() && !m_dependencyOnly;
}

bool Component::setEnabled(bool state)
{
	bool intendedDisabled = !state;
	if (!canBeDisabled()) {
		intendedDisabled = false;
	}
	if (intendedDisabled != m_disabled) {
		m_disabled = intendedDisabled;
		emit dataChanged();
		return true;
	}
	return false;
}

bool Component::isCustom()
{
	return m_file != nullptr;
}

bool Component::isCustomizable()
{
	if (m_metaVersion) {
		if (getVersionFile()) {
			return true;
		}
	}
	return false;
}
bool Component::isRemovable()
{
	return !m_important;
}
bool Component::isRevertible()
{
	if (isCustom()) {
		if (APPLICATION->metadataIndex()->hasUid(m_uid)) {
			return true;
		}
	}
	return false;
}
bool Component::isMoveable()
{
	// HACK, FIXME: this was too dumb and wouldn't follow dependency constraints
	// anyway. For now hardcoded to 'true'.
	return true;
}
bool Component::isVersionChangeable()
{
	auto list = getVersionList();
	if (list) {
		if (!list->isLoaded()) {
			list->load(Net::Mode::Online);
		}
		return list->count() != 0;
	}
	return false;
}

void Component::setImportant(bool state)
{
	if (m_important != state) {
		m_important = state;
		emit dataChanged();
	}
}

ProblemSeverity Component::getProblemSeverity() const
{
	auto file = getVersionFile();
	if (file) {
		return file->getProblemSeverity();
	}
	return ProblemSeverity::Error;
}

const QList<PatchProblem> Component::getProblems() const
{
	auto file = getVersionFile();
	if (file) {
		return file->getProblems();
	}
	return {{ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.")}};
}

void Component::setVersion(const QString& version)
{
	if (version == m_version) {
		return;
	}
	m_version = version;
	if (m_loaded) {
		// we are loaded and potentially have state to invalidate
		if (m_file) {
			// we have a file... explicit version has been changed and there is
			// nothing else to do.
		} else {
			// we don't have a file, therefore we are loaded with metadata
			m_cachedVersion = version;
			// see if the meta version is loaded
			auto metaVersion =
				APPLICATION->metadataIndex()->get(m_uid, version);
			if (metaVersion->isLoaded()) {
				// if yes, we can continue with that.
				m_metaVersion = metaVersion;
			} else {
				// if not, we need loading
				m_metaVersion.reset();
				m_loaded = false;
			}
			updateCachedData();
		}
	} else {
		// not loaded... assume it will be sorted out later by the update task
	}
	emit dataChanged();
}

bool Component::customize()
{
	if (isCustom()) {
		return false;
	}

	auto filename = getFilename();
	if (!FS::ensureFilePathExists(filename)) {
		return false;
	}
	// FIXME: get rid of this try-catch.
	try {
		QSaveFile jsonFile(filename);
		if (!jsonFile.open(QIODevice::WriteOnly)) {
			return false;
		}
		auto vfile = getVersionFile();
		if (!vfile) {
			return false;
		}
		auto document = OneSixVersionFormat::versionFileToJson(vfile);
		jsonFile.write(document.toJson());
		if (!jsonFile.commit()) {
			return false;
		}
		m_file = vfile;
		m_metaVersion.reset();
		emit dataChanged();
	} catch (const Exception& error) {
		qWarning() << "Version could not be loaded:" << error.cause();
	}
	return true;
}

bool Component::revert()
{
	if (!isCustom()) {
		// already not custom
		return true;
	}
	auto filename = getFilename();
	bool result = true;
	// just kill the file and reload
	if (QFile::exists(filename)) {
		result = QFile::remove(filename);
	}
	if (result) {
		// file gone...
		m_file.reset();

		// check local cache for metadata...
		auto version = APPLICATION->metadataIndex()->get(m_uid, m_version);
		if (version->isLoaded()) {
			m_metaVersion = version;
		} else {
			m_metaVersion.reset();
			m_loaded = false;
		}
		emit dataChanged();
	}
	return result;
}

/**
 * deep inspecting compare for requirement sets
 * By default, only uids are compared for set operations.
 * This compares all fields of the Require structs in the sets.
 */
static bool deepCompare(const std::set<Meta::Require>& a,
						const std::set<Meta::Require>& b)
{
	// NOTE: this needs to be rewritten if the type of Meta::RequireSet changes
	if (a.size() != b.size()) {
		return false;
	}
	for (const auto& reqA : a) {
		const auto& iter2 = b.find(reqA);
		if (iter2 == b.cend()) {
			return false;
		}
		const auto& reqB = *iter2;
		if (!reqA.deepEquals(reqB)) {
			return false;
		}
	}
	return true;
}

void Component::updateCachedData()
{
	auto file = getVersionFile();
	if (file) {
		bool changed = false;
		if (m_cachedName != file->name) {
			m_cachedName = file->name;
			changed = true;
		}
		if (m_cachedVersion != file->version) {
			m_cachedVersion = file->version;
			changed = true;
		}
		if (m_cachedVolatile != file->m_volatile) {
			m_cachedVolatile = file->m_volatile;
			changed = true;
		}
		if (!deepCompare(m_cachedRequires, file->requirements)) {
			m_cachedRequires = file->requirements;
			changed = true;
		}
		if (!deepCompare(m_cachedConflicts, file->conflicts)) {
			m_cachedConflicts = file->conflicts;
			changed = true;
		}
		if (changed) {
			emit dataChanged();
		}
	} else {
		// in case we removed all the metadata
		m_cachedRequires.clear();
		m_cachedConflicts.clear();
		emit dataChanged();
	}
}
