/* 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/>.
 *
 *  This file incorporates work covered by the following copyright and
 *  permission notice:
 *
 * Copyright 2013-2021 MultiMC Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <QFile>
#include <QCryptographicHash>
#include <Version.h>
#include <QDir>
#include <QJsonDocument>
#include <QJsonArray>
#include <QDebug>
#include <QSaveFile>
#include <QUuid>
#include <QTimer>

#include "Exception.h"
#include "minecraft/OneSixVersionFormat.h"
#include "FileSystem.h"
#include "meta/Index.h"
#include "minecraft/MinecraftInstance.h"
#include "Json.h"

#include "PackProfile.h"
#include "PackProfile_p.h"
#include "ComponentUpdateTask.h"

#include "Application.h"

PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel()
{
	d.reset(new PackProfileData);
	d->m_instance = instance;
	d->m_saveTimer.setSingleShot(true);
	d->m_saveTimer.setInterval(5000);
	d->interactionDisabled = instance->isRunning();
	connect(d->m_instance, &BaseInstance::runningStatusChanged, this,
			&PackProfile::disableInteraction);
	connect(&d->m_saveTimer, &QTimer::timeout, this,
			&PackProfile::save_internal);
}

PackProfile::~PackProfile()
{
	saveNow();
}

// BEGIN: component file format

static const int currentComponentsFileVersion = 1;

static QJsonObject componentToJsonV1(ComponentPtr component)
{
	QJsonObject obj;
	// critical
	obj.insert("uid", component->m_uid);
	if (!component->m_version.isEmpty()) {
		obj.insert("version", component->m_version);
	}
	if (component->m_dependencyOnly) {
		obj.insert("dependencyOnly", true);
	}
	if (component->m_important) {
		obj.insert("important", true);
	}
	if (component->m_disabled) {
		obj.insert("disabled", true);
	}

	// cached
	if (!component->m_cachedVersion.isEmpty()) {
		obj.insert("cachedVersion", component->m_cachedVersion);
	}
	if (!component->m_cachedName.isEmpty()) {
		obj.insert("cachedName", component->m_cachedName);
	}
	Meta::serializeRequires(obj, &component->m_cachedRequires,
							"cachedRequires");
	Meta::serializeRequires(obj, &component->m_cachedConflicts,
							"cachedConflicts");
	if (component->m_cachedVolatile) {
		obj.insert("cachedVolatile", true);
	}
	return obj;
}

static ComponentPtr componentFromJsonV1(PackProfile* parent,
										const QString& componentJsonPattern,
										const QJsonObject& obj)
{
	// critical
	auto uid = Json::requireString(obj.value("uid"));
	auto filePath = componentJsonPattern.arg(uid);
	auto component = new Component(parent, uid);
	component->m_version = Json::ensureString(obj.value("version"));
	component->m_dependencyOnly =
		Json::ensureBoolean(obj.value("dependencyOnly"), false);
	component->m_important = Json::ensureBoolean(obj.value("important"), false);

	// cached
	// TODO @RESILIENCE: ignore invalid values/structure here?
	component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion"));
	component->m_cachedName = Json::ensureString(obj.value("cachedName"));
	Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires");
	Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
	component->m_cachedVolatile =
		Json::ensureBoolean(obj.value("volatile"), false);
	bool disabled = Json::ensureBoolean(obj.value("disabled"), false);
	component->setEnabled(!disabled);
	return component;
}

// Save the given component container data to a file
static bool savePackProfile(const QString& filename,
							const ComponentContainer& container)
{
	QJsonObject obj;
	obj.insert("formatVersion", currentComponentsFileVersion);
	QJsonArray orderArray;
	for (auto component : container) {
		orderArray.append(componentToJsonV1(component));
	}
	obj.insert("components", orderArray);
	QSaveFile outFile(filename);
	if (!outFile.open(QFile::WriteOnly)) {
		qCritical() << "Couldn't open" << outFile.fileName()
					<< "for writing:" << outFile.errorString();
		return false;
	}
	auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented);
	if (outFile.write(data) != data.size()) {
		qCritical() << "Couldn't write all the data into" << outFile.fileName()
					<< "because:" << outFile.errorString();
		return false;
	}
	if (!outFile.commit()) {
		qCritical() << "Couldn't save" << outFile.fileName()
					<< "because:" << outFile.errorString();
	}
	return true;
}

// Read the given file into component containers
static bool loadPackProfile(PackProfile* parent, const QString& filename,
							const QString& componentJsonPattern,
							ComponentContainer& container)
{
	QFile componentsFile(filename);
	if (!componentsFile.exists()) {
		qWarning()
			<< "Components file doesn't exist. This should never happen.";
		return false;
	}
	if (!componentsFile.open(QFile::ReadOnly)) {
		qCritical() << "Couldn't open" << componentsFile.fileName()
					<< " for reading:" << componentsFile.errorString();
		qWarning() << "Ignoring overriden order";
		return false;
	}

	// and it's valid JSON
	QJsonParseError error;
	QJsonDocument doc =
		QJsonDocument::fromJson(componentsFile.readAll(), &error);
	if (error.error != QJsonParseError::NoError) {
		qCritical() << "Couldn't parse" << componentsFile.fileName() << ":"
					<< error.errorString();
		qWarning() << "Ignoring overriden order";
		return false;
	}

	// and then read it and process it if all above is true.
	try {
		auto obj = Json::requireObject(doc);
		// check order file version.
		auto version = Json::requireInteger(obj.value("formatVersion"));
		if (version != currentComponentsFileVersion) {
			throw JSONValidationError(
				QObject::tr("Invalid component file version, expected %1")
					.arg(currentComponentsFileVersion));
		}
		auto orderArray = Json::requireArray(obj.value("components"));
		for (auto item : orderArray) {
			auto obj =
				Json::requireObject(item, "Component must be an object.");
			container.append(
				componentFromJsonV1(parent, componentJsonPattern, obj));
		}
	} catch (const JSONValidationError& err) {
		qCritical() << "Couldn't parse" << componentsFile.fileName()
					<< ": bad file format";
		container.clear();
		return false;
	}
	return true;
}

// END: component file format

// BEGIN: save/load logic

void PackProfile::saveNow()
{
	if (saveIsScheduled()) {
		d->m_saveTimer.stop();
		save_internal();
	}
}

bool PackProfile::saveIsScheduled() const
{
	return d->dirty;
}

void PackProfile::buildingFromScratch()
{
	d->loaded = true;
	d->dirty = true;
}

void PackProfile::scheduleSave()
{
	if (!d->loaded) {
		qDebug() << "Component list should never save if it didn't "
					"successfully load, instance:"
				 << d->m_instance->name();
		return;
	}
	if (!d->dirty) {
		d->dirty = true;
		qDebug() << "Component list save is scheduled for"
				 << d->m_instance->name();
	}
	d->m_saveTimer.start();
}

QString PackProfile::componentsFilePath() const
{
	return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json");
}

QString PackProfile::patchesPattern() const
{
	return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json");
}

QString PackProfile::patchFilePathForUid(const QString& uid) const
{
	return patchesPattern().arg(uid);
}

void PackProfile::save_internal()
{
	qDebug() << "Component list save performed now for"
			 << d->m_instance->name();
	auto filename = componentsFilePath();
	savePackProfile(filename, d->components);
	d->dirty = false;
}

bool PackProfile::load()
{
	auto filename = componentsFilePath();
	QFile componentsFile(filename);

	// migrate old config to new one, if needed
	if (!componentsFile.exists()) {
		if (!migratePreComponentConfig()) {
			// FIXME: the user should be notified...
			qCritical()
				<< "Failed to convert old pre-component config for instance"
				<< d->m_instance->name();
			return false;
		}
	}

	// load the new component list and swap it with the current one...
	ComponentContainer newComponents;
	if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) {
		qCritical() << "Failed to load the component config for instance"
					<< d->m_instance->name();
		return false;
	} else {
		// FIXME: actually use fine-grained updates, not this...
		beginResetModel();
		// disconnect all the old components
		for (auto component : d->components) {
			disconnect(component.get(), &Component::dataChanged, this,
					   &PackProfile::componentDataChanged);
		}
		d->components.clear();
		d->componentIndex.clear();
		for (auto component : newComponents) {
			if (d->componentIndex.contains(component->m_uid)) {
				qWarning() << "Ignoring duplicate component entry"
						   << component->m_uid;
				continue;
			}
			connect(component.get(), &Component::dataChanged, this,
					&PackProfile::componentDataChanged);
			d->components.append(component);
			d->componentIndex[component->m_uid] = component;
		}
		endResetModel();
		d->loaded = true;
		return true;
	}
}

void PackProfile::reload(Net::Mode netmode)
{
	// Do not reload when the update/resolve task is running. It is in control.
	if (d->m_updateTask) {
		return;
	}

	// flush any scheduled saves to not lose state
	saveNow();

	// FIXME: differentiate when a reapply is required by propagating state from
	// components
	invalidateLaunchProfile();

	if (load()) {
		resolve(netmode);
	}
}

Task::Ptr PackProfile::getCurrentTask()
{
	return d->m_updateTask;
}

void PackProfile::resolve(Net::Mode netmode)
{
	auto updateTask = new ComponentUpdateTask(
		ComponentUpdateTask::Mode::Resolution, netmode, this);
	d->m_updateTask.reset(updateTask);
	connect(updateTask, &ComponentUpdateTask::succeeded, this,
			&PackProfile::updateSucceeded);
	connect(updateTask, &ComponentUpdateTask::failed, this,
			&PackProfile::updateFailed);
	d->m_updateTask->start();
}

void PackProfile::updateSucceeded()
{
	qDebug() << "Component list update/resolve task succeeded for"
			 << d->m_instance->name();
	d->m_updateTask.reset();
	invalidateLaunchProfile();
}

void PackProfile::updateFailed(const QString& error)
{
	qDebug() << "Component list update/resolve task failed for"
			 << d->m_instance->name() << "Reason:" << error;
	d->m_updateTask.reset();
	invalidateLaunchProfile();
}

// NOTE this is really old stuff, and only needs to be used when loading the old
// hardcoded component-unaware format (loadPreComponentConfig).
static void upgradeDeprecatedFiles(QString root, QString instanceName)
{
	auto versionJsonPath = FS::PathCombine(root, "version.json");
	auto customJsonPath = FS::PathCombine(root, "custom.json");
	auto mcJson = FS::PathCombine(root, "patches", "net.minecraft.json");

	QString sourceFile;
	QString renameFile;

	// convert old crap.
	if (QFile::exists(customJsonPath)) {
		sourceFile = customJsonPath;
		renameFile = versionJsonPath;
	} else if (QFile::exists(versionJsonPath)) {
		sourceFile = versionJsonPath;
	}
	if (!sourceFile.isEmpty() && !QFile::exists(mcJson)) {
		if (!FS::ensureFilePathExists(mcJson)) {
			qWarning() << "Couldn't create patches folder for" << instanceName;
			return;
		}
		if (!renameFile.isEmpty() && QFile::exists(renameFile)) {
			if (!QFile::rename(renameFile, renameFile + ".old")) {
				qWarning() << "Couldn't rename" << renameFile << "to"
						   << renameFile + ".old" << "in" << instanceName;
				return;
			}
		}
		auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false);
		ProfileUtils::removeLwjglFromPatch(file);
		file->uid = "net.minecraft";
		file->version = file->minecraftVersion;
		file->name = "Minecraft";

		Meta::Require needsLwjgl;
		needsLwjgl.uid = "org.lwjgl";
		file->requirements.insert(needsLwjgl);

		if (!ProfileUtils::saveJsonFile(
				OneSixVersionFormat::versionFileToJson(file), mcJson)) {
			return;
		}
		if (!QFile::rename(sourceFile, sourceFile + ".old")) {
			qWarning() << "Couldn't rename" << sourceFile << "to"
					   << sourceFile + ".old" << "in" << instanceName;
			return;
		}
	}
}

/*
 * Migrate old layout to the component based one...
 * - Part of the version information is taken from `instance.cfg` (fed to this
 * class from outside).
 * - Part is taken from the old order.json file.
 * - Part is loaded from loose json files in the instance's `patches` directory.
 */
bool PackProfile::migratePreComponentConfig()
{
	// upgrade the very old files from the beginnings of MeshMC
	upgradeDeprecatedFiles(d->m_instance->instanceRoot(),
						   d->m_instance->name());

	QList<ComponentPtr> components;
	QSet<QString> loaded;

	auto addBuiltinPatch =
		[&](const QString& uid, bool asDependency, const QString& emptyVersion,
			const Meta::Require& req, const Meta::Require& conflict) {
			auto jsonFilePath = FS::PathCombine(d->m_instance->instanceRoot(),
												"patches", uid + ".json");
			auto intendedVersion = d->getOldConfigVersion(uid);
			// load up the base minecraft patch
			ComponentPtr component;
			if (QFile::exists(jsonFilePath)) {
				if (intendedVersion.isEmpty()) {
					intendedVersion = emptyVersion;
				}
				auto file =
					ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false);
				// fix uid
				file->uid = uid;
				// if version is missing, add it from the outside.
				if (file->version.isEmpty()) {
					file->version = intendedVersion;
				}
				// if this is a dependency (LWJGL), mark it also as volatile
				if (asDependency) {
					file->m_volatile = true;
				}
				// insert requirements if needed
				if (!req.uid.isEmpty()) {
					file->requirements.insert(req);
				}
				// insert conflicts if needed
				if (!conflict.uid.isEmpty()) {
					file->conflicts.insert(conflict);
				}
				// FIXME: @QUALITY do not ignore return value
				ProfileUtils::saveJsonFile(
					OneSixVersionFormat::versionFileToJson(file), jsonFilePath);
				component = new Component(this, uid, file);
				component->m_version = intendedVersion;
			} else if (!intendedVersion.isEmpty()) {
				auto metaVersion =
					APPLICATION->metadataIndex()->get(uid, intendedVersion);
				component = new Component(this, metaVersion);
			} else {
				return;
			}
			component->m_dependencyOnly = asDependency;
			component->m_important = !asDependency;
			components.append(component);
		};
	// TODO: insert depends and conflicts here if these are customized files...
	Meta::Require reqLwjgl;
	reqLwjgl.uid = "org.lwjgl";
	reqLwjgl.suggests = "2.9.1";
	Meta::Require conflictLwjgl3;
	conflictLwjgl3.uid = "org.lwjgl3";
	Meta::Require nullReq;
	addBuiltinPatch("org.lwjgl", true, "2.9.1", nullReq, conflictLwjgl3);
	addBuiltinPatch("net.minecraft", false, QString(), reqLwjgl, nullReq);

	// first, collect all other file-based patches and load them
	QMap<QString, ComponentPtr> loadedComponents;
	QDir patchesDir(FS::PathCombine(d->m_instance->instanceRoot(), "patches"));
	for (auto info :
		 patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files)) {
		// parse the file
		qDebug() << "Reading" << info.fileName();
		auto file = ProfileUtils::parseJsonFile(info, true);

		// correct missing or wrong uid based on the file name
		QString uid = info.completeBaseName();

		// ignore builtins, they've been handled already
		if (uid == "net.minecraft")
			continue;
		if (uid == "org.lwjgl")
			continue;

		// handle horrible corner cases
		if (uid.isEmpty()) {
			// if you have a file named '.json', make it just go away.
			// FIXME: @QUALITY do not ignore return value
			QFile::remove(info.absoluteFilePath());
			continue;
		}
		file->uid = uid;
		// FIXME: @QUALITY do not ignore return value
		ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file),
								   info.absoluteFilePath());

		auto component = new Component(this, file->uid, file);
		auto version = d->getOldConfigVersion(file->uid);
		if (!version.isEmpty()) {
			component->m_version = version;
		}
		loadedComponents[file->uid] = component;
	}
	// try to load the other 'hardcoded' patches (forge, liteloader), if they
	// weren't loaded from files
	auto loadSpecial = [&](const QString& uid, int order) {
		auto patchVersion = d->getOldConfigVersion(uid);
		if (!patchVersion.isEmpty() && !loadedComponents.contains(uid)) {
			auto patch = new Component(
				this, APPLICATION->metadataIndex()->get(uid, patchVersion));
			patch->setOrder(order);
			loadedComponents[uid] = patch;
		}
	};
	loadSpecial("net.minecraftforge", 5);
	loadSpecial("com.mumfrey.liteloader", 10);

	// load the old order.json file, if present
	ProfileUtils::PatchOrder userOrder;
	ProfileUtils::readOverrideOrders(
		FS::PathCombine(d->m_instance->instanceRoot(), "order.json"),
		userOrder);

	// now add all the patches by user sort order
	for (auto uid : userOrder) {
		// ignore builtins
		if (uid == "net.minecraft")
			continue;
		if (uid == "org.lwjgl")
			continue;
		// ordering has a patch that is gone?
		if (!loadedComponents.contains(uid)) {
			continue;
		}
		components.append(loadedComponents.take(uid));
	}

	// is there anything left to sort? - this is used when there are leftover
	// components that aren't part of the order.json
	if (!loadedComponents.isEmpty()) {
		// inserting into multimap by order number as key sorts the patches and
		// detects duplicates
		QMultiMap<int, ComponentPtr> files;
		auto iter = loadedComponents.begin();
		while (iter != loadedComponents.end()) {
			files.insert((*iter)->getOrder(), *iter);
			iter++;
		}

		// then just extract the patches and put them in the list
		for (auto order : files.keys()) {
			const auto& values = files.values(order);
			for (auto& value : values) {
				// TODO: put back the insertion of problem messages here, so the
				// user knows about the id duplication
				components.append(value);
			}
		}
	}
	// new we have a complete list of components...
	return savePackProfile(componentsFilePath(), components);
}

// END: save/load

void PackProfile::appendComponent(ComponentPtr component)
{
	insertComponent(d->components.size(), component);
}

void PackProfile::insertComponent(size_t index, ComponentPtr component)
{
	auto id = component->getID();
	if (id.isEmpty()) {
		qWarning() << "Attempt to add a component with empty ID!";
		return;
	}
	if (d->componentIndex.contains(id)) {
		qWarning() << "Attempt to add a component that is already present!";
		return;
	}
	beginInsertRows(QModelIndex(), index, index);
	d->components.insert(index, component);
	d->componentIndex[id] = component;
	endInsertRows();
	connect(component.get(), &Component::dataChanged, this,
			&PackProfile::componentDataChanged);
	scheduleSave();
}

void PackProfile::componentDataChanged()
{
	auto objPtr = qobject_cast<Component*>(sender());
	if (!objPtr) {
		qWarning()
			<< "PackProfile got dataChenged signal from a non-Component!";
		return;
	}
	if (objPtr->getID() == "net.minecraft") {
		emit minecraftChanged();
	}
	// figure out which one is it... in a seriously dumb way.
	int index = 0;
	for (auto component : d->components) {
		if (component.get() == objPtr) {
			emit dataChanged(
				createIndex(index, 0),
				createIndex(index, columnCount(QModelIndex()) - 1));
			scheduleSave();
			return;
		}
		index++;
	}
	qWarning() << "PackProfile got dataChenged signal from a Component which "
				  "does not belong to it!";
}

bool PackProfile::remove(const int index)
{
	auto patch = getComponent(index);
	if (!patch->isRemovable()) {
		qWarning() << "Patch" << patch->getID() << "is non-removable";
		return false;
	}

	if (!removeComponent_internal(patch)) {
		qCritical() << "Patch" << patch->getID() << "could not be removed";
		return false;
	}

	beginRemoveRows(QModelIndex(), index, index);
	d->components.removeAt(index);
	d->componentIndex.remove(patch->getID());
	endRemoveRows();
	invalidateLaunchProfile();
	scheduleSave();
	return true;
}

bool PackProfile::remove(const QString id)
{
	int i = 0;
	for (auto patch : d->components) {
		if (patch->getID() == id) {
			return remove(i);
		}
		i++;
	}
	return false;
}

bool PackProfile::customize(int index)
{
	auto patch = getComponent(index);
	if (!patch->isCustomizable()) {
		qDebug() << "Patch" << patch->getID() << "is not customizable";
		return false;
	}
	if (!patch->customize()) {
		qCritical() << "Patch" << patch->getID() << "could not be customized";
		return false;
	}
	invalidateLaunchProfile();
	scheduleSave();
	return true;
}

bool PackProfile::revertToBase(int index)
{
	auto patch = getComponent(index);
	if (!patch->isRevertible()) {
		qDebug() << "Patch" << patch->getID() << "is not revertible";
		return false;
	}
	if (!patch->revert()) {
		qCritical() << "Patch" << patch->getID() << "could not be reverted";
		return false;
	}
	invalidateLaunchProfile();
	scheduleSave();
	return true;
}

Component* PackProfile::getComponent(const QString& id)
{
	auto iter = d->componentIndex.find(id);
	if (iter == d->componentIndex.end()) {
		return nullptr;
	}
	return (*iter).get();
}

Component* PackProfile::getComponent(int index)
{
	if (index < 0 || index >= d->components.size()) {
		return nullptr;
	}
	return d->components[index].get();
}

QVariant PackProfile::data(const QModelIndex& index, int role) const
{
	if (!index.isValid())
		return QVariant();

	int row = index.row();
	int column = index.column();

	if (row < 0 || row >= d->components.size())
		return QVariant();

	auto patch = d->components.at(row);

	switch (role) {
		case Qt::CheckStateRole: {
			switch (column) {
				case NameColumn: {
					return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
				}
				default:
					return QVariant();
			}
		}
		case Qt::DisplayRole: {
			switch (column) {
				case NameColumn:
					return patch->getName();
				case VersionColumn: {
					if (patch->isCustom()) {
						return QString("%1 (Custom)").arg(patch->getVersion());
					} else {
						return patch->getVersion();
					}
				}
				default:
					return QVariant();
			}
		}
		case Qt::DecorationRole: {
			switch (column) {
				case NameColumn: {
					auto severity = patch->getProblemSeverity();
					switch (severity) {
						case ProblemSeverity::Warning:
							return "warning";
						case ProblemSeverity::Error:
							return "error";
						default:
							return QVariant();
					}
				}
				default: {
					return QVariant();
				}
			}
		}
	}
	return QVariant();
}

bool PackProfile::setData(const QModelIndex& index, const QVariant& value,
						  int role)
{
	if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index)) {
		return false;
	}

	if (role == Qt::CheckStateRole) {
		auto component = d->components[index.row()];
		if (component->setEnabled(!component->isEnabled())) {
			return true;
		}
	}
	return false;
}

QVariant PackProfile::headerData(int section, Qt::Orientation orientation,
								 int role) const
{
	if (orientation == Qt::Horizontal) {
		if (role == Qt::DisplayRole) {
			switch (section) {
				case NameColumn:
					return tr("Name");
				case VersionColumn:
					return tr("Version");
				default:
					return QVariant();
			}
		}
	}
	return QVariant();
}

// FIXME: zero precision mess
Qt::ItemFlags PackProfile::flags(const QModelIndex& index) const
{
	if (!index.isValid()) {
		return Qt::NoItemFlags;
	}

	Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;

	int row = index.row();

	if (row < 0 || row >= d->components.size()) {
		return Qt::NoItemFlags;
	}

	auto patch = d->components.at(row);
	// TODO: this will need fine-tuning later...
	if (patch->canBeDisabled() && !d->interactionDisabled) {
		outFlags |= Qt::ItemIsUserCheckable;
	}
	return outFlags;
}

int PackProfile::rowCount(const QModelIndex& parent) const
{
	return d->components.size();
}

int PackProfile::columnCount(const QModelIndex& parent) const
{
	return NUM_COLUMNS;
}

void PackProfile::move(const int index, const MoveDirection direction)
{
	int theirIndex;
	if (direction == MoveUp) {
		theirIndex = index - 1;
	} else {
		theirIndex = index + 1;
	}

	if (index < 0 || index >= d->components.size())
		return;
	if (theirIndex >= rowCount())
		theirIndex = rowCount() - 1;
	if (theirIndex == -1)
		theirIndex = rowCount() - 1;
	if (index == theirIndex)
		return;
	int togap = theirIndex > index ? theirIndex + 1 : theirIndex;

	auto from = getComponent(index);
	auto to = getComponent(theirIndex);

	if (!from || !to || !to->isMoveable() || !from->isMoveable()) {
		return;
	}
	beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap);
	d->components.swapItemsAt(index, theirIndex);
	endMoveRows();
	invalidateLaunchProfile();
	scheduleSave();
}

void PackProfile::invalidateLaunchProfile()
{
	d->m_profile.reset();
}

void PackProfile::installJarMods(QStringList selectedFiles)
{
	installJarMods_internal(selectedFiles);
}

void PackProfile::installCustomJar(QString selectedFile)
{
	installCustomJar_internal(selectedFile);
}

bool PackProfile::installEmpty(const QString& uid, const QString& name)
{
	QString patchDir =
		FS::PathCombine(d->m_instance->instanceRoot(), "patches");
	if (!FS::ensureFolderPathExists(patchDir)) {
		return false;
	}
	auto f = std::make_shared<VersionFile>();
	f->name = name;
	f->uid = uid;
	f->version = "1";
	QString patchFileName = FS::PathCombine(patchDir, uid + ".json");
	QFile file(patchFileName);
	if (!file.open(QFile::WriteOnly)) {
		qCritical() << "Error opening" << file.fileName()
					<< "for reading:" << file.errorString();
		return false;
	}
	file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
	file.close();

	appendComponent(new Component(this, f->uid, f));
	scheduleSave();
	invalidateLaunchProfile();
	return true;
}

bool PackProfile::removeComponent_internal(ComponentPtr patch)
{
	bool ok = true;
	// first, remove the patch file. this ensures it's not used anymore
	auto fileName = patch->getFilename();
	if (fileName.size()) {
		QFile patchFile(fileName);
		if (patchFile.exists() && !patchFile.remove()) {
			qCritical() << "File" << fileName << "could not be removed because:"
						<< patchFile.errorString();
			return false;
		}
	}

	// FIXME: we need a generic way of removing local resources, not just jar
	// mods...
	auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool {
		if (!jarMod->isLocal()) {
			return true;
		}
		QStringList jar, temp1, temp2, temp3;
		jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3,
								   d->m_instance->jarmodsPath().absolutePath());
		QFileInfo finfo(jar[0]);
		if (finfo.exists()) {
			QFile jarModFile(jar[0]);
			if (!jarModFile.remove()) {
				qCritical()
					<< "File" << jar[0] << "could not be removed because:"
					<< jarModFile.errorString();
				return false;
			}
			return true;
		}
		return true;
	};

	auto vFile = patch->getVersionFile();
	if (vFile) {
		auto& jarMods = vFile->jarMods;
		for (auto& jarmod : jarMods) {
			ok &= preRemoveJarMod(jarmod);
		}
	}
	return ok;
}

bool PackProfile::installJarMods_internal(QStringList filepaths)
{
	QString patchDir =
		FS::PathCombine(d->m_instance->instanceRoot(), "patches");
	if (!FS::ensureFolderPathExists(patchDir)) {
		return false;
	}

	if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir())) {
		return false;
	}

	for (auto filepath : filepaths) {
		QFileInfo sourceInfo(filepath);
		auto uuid = QUuid::createUuid();
		QString id = uuid.toString().remove('{').remove('}');
		QString target_filename = id + ".jar";
		QString target_id = "org.projecttick.jarmod." + id;
		QString target_name = sourceInfo.completeBaseName() + " (jar mod)";
		QString finalPath =
			FS::PathCombine(d->m_instance->jarModsDir(), target_filename);

		QFileInfo targetInfo(finalPath);
		if (targetInfo.exists()) {
			return false;
		}

		if (!QFile::copy(sourceInfo.absoluteFilePath(),
						 QFileInfo(finalPath).absoluteFilePath())) {
			return false;
		}

		auto f = std::make_shared<VersionFile>();
		auto jarMod = std::make_shared<Library>();
		jarMod->setRawName(
			GradleSpecifier("org.projecttick.jarmods:" + id + ":1"));
		jarMod->setFilename(target_filename);
		jarMod->setDisplayName(sourceInfo.completeBaseName());
		jarMod->setHint("local");
		f->jarMods.append(jarMod);
		f->name = target_name;
		f->uid = target_id;
		QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");

		QFile file(patchFileName);
		if (!file.open(QFile::WriteOnly)) {
			qCritical() << "Error opening" << file.fileName()
						<< "for reading:" << file.errorString();
			return false;
		}
		file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
		file.close();

		appendComponent(new Component(this, f->uid, f));
	}
	scheduleSave();
	invalidateLaunchProfile();
	return true;
}

bool PackProfile::installCustomJar_internal(QString filepath)
{
	QString patchDir =
		FS::PathCombine(d->m_instance->instanceRoot(), "patches");
	if (!FS::ensureFolderPathExists(patchDir)) {
		return false;
	}

	QString libDir = d->m_instance->getLocalLibraryPath();
	if (!FS::ensureFolderPathExists(libDir)) {
		return false;
	}

	auto specifier = GradleSpecifier("org.projecttick:customjar:1");
	QFileInfo sourceInfo(filepath);
	QString target_filename = specifier.getFileName();
	QString target_id = specifier.artifactId();
	QString target_name = sourceInfo.completeBaseName() + " (custom jar)";
	QString finalPath = FS::PathCombine(libDir, target_filename);

	QFileInfo jarInfo(finalPath);
	if (jarInfo.exists()) {
		if (!QFile::remove(finalPath)) {
			return false;
		}
	}
	if (!QFile::copy(filepath, finalPath)) {
		return false;
	}

	auto f = std::make_shared<VersionFile>();
	auto jarMod = std::make_shared<Library>();
	jarMod->setRawName(specifier);
	jarMod->setDisplayName(sourceInfo.completeBaseName());
	jarMod->setHint("local");
	f->mainJar = jarMod;
	f->name = target_name;
	f->uid = target_id;
	QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");

	QFile file(patchFileName);
	if (!file.open(QFile::WriteOnly)) {
		qCritical() << "Error opening" << file.fileName()
					<< "for reading:" << file.errorString();
		return false;
	}
	file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
	file.close();

	appendComponent(new Component(this, f->uid, f));

	scheduleSave();
	invalidateLaunchProfile();
	return true;
}

std::shared_ptr<LaunchProfile> PackProfile::getProfile() const
{
	if (!d->m_profile) {
		try {
			auto profile = std::make_shared<LaunchProfile>();
			for (auto file : d->components) {
				qDebug() << "Applying" << file->getID()
						 << (file->getProblemSeverity() ==
									 ProblemSeverity::Error
								 ? "ERROR"
								 : "GOOD");
				file->applyTo(profile.get());
			}
			d->m_profile = profile;
		} catch (const Exception& error) {
			qWarning() << "Couldn't apply profile patches because: "
					   << error.cause();
		}
	}
	return d->m_profile;
}

void PackProfile::setOldConfigVersion(const QString& uid,
									  const QString& version)
{
	if (version.isEmpty()) {
		return;
	}
	d->m_oldConfigVersions[uid] = version;
}

bool PackProfile::setComponentVersion(const QString& uid,
									  const QString& version, bool important)
{
	auto iter = d->componentIndex.find(uid);
	if (iter != d->componentIndex.end()) {
		ComponentPtr component = *iter;
		// set existing
		if (component->revert()) {
			component->setVersion(version);
			component->setImportant(important);
			return true;
		}
		return false;
	} else {
		// add new
		auto component = new Component(this, uid);
		component->m_version = version;
		component->m_important = important;
		appendComponent(component);
		return true;
	}
}

QString PackProfile::getComponentVersion(const QString& uid) const
{
	const auto iter = d->componentIndex.find(uid);
	if (iter != d->componentIndex.end()) {
		return (*iter)->getVersion();
	}
	return QString();
}

void PackProfile::disableInteraction(bool disable)
{
	if (d->interactionDisabled != disable) {
		d->interactionDisabled = disable;
		auto size = d->components.size();
		if (size) {
			emit dataChanged(index(0), index(size - 1));
		}
	}
}
