/* 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 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
 * Copyright 2021 Petr Mrazek <peterix@gmail.com>
 *
 * 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 "ATLPackInstallTask.h"

#include <QtConcurrent/QtConcurrent>
#include <QRegularExpression>

#include "MMCZip.h"
#include "minecraft/OneSixVersionFormat.h"
#include "Version.h"
#include "net/ChecksumValidator.h"
#include "FileSystem.h"
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
#include "settings/INISettingsObject.h"
#include "meta/Index.h"
#include "meta/Version.h"
#include "meta/VersionList.h"

#include "BuildConfig.h"
#include "Application.h"

namespace ATLauncher
{

	PackInstallTask::PackInstallTask(UserInteractionSupport* support,
									 QString pack, QString version)
	{
		m_support = support;
		m_pack = pack;
		m_version_name = version;
	}

	bool PackInstallTask::abort()
	{
		if (abortable) {
			return jobPtr->abort();
		}
		return false;
	}

	void PackInstallTask::executeTask()
	{
		qDebug() << "PackInstallTask::executeTask: "
				 << QThread::currentThreadId();
		auto* netJob =
			new NetJob("ATLauncher::VersionFetch", APPLICATION->network());
		auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL +
								 "packs/%1/versions/%2/Configs.json")
							 .arg(m_pack)
							 .arg(m_version_name);
		netJob->addNetAction(
			Net::Download::makeByteArray(QUrl(searchUrl), &response));
		jobPtr = netJob;
		jobPtr->start();

		QObject::connect(netJob, &NetJob::succeeded, this,
						 &PackInstallTask::onDownloadSucceeded);
		QObject::connect(netJob, &NetJob::failed, this,
						 &PackInstallTask::onDownloadFailed);
	}

	void PackInstallTask::onDownloadSucceeded()
	{
		qDebug() << "PackInstallTask::onDownloadSucceeded: "
				 << QThread::currentThreadId();
		jobPtr.reset();

		QJsonParseError parse_error;
		QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
		if (parse_error.error != QJsonParseError::NoError) {
			qWarning() << "Error while parsing JSON response from FTB at "
					   << parse_error.offset
					   << " reason: " << parse_error.errorString();
			qWarning() << response;
			return;
		}

		auto obj = doc.object();

		ATLauncher::PackVersion version;
		try {
			ATLauncher::loadVersion(version, obj);
		} catch (const JSONValidationError& e) {
			emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
			return;
		}
		m_version = version;

		auto vlist = APPLICATION->metadataIndex()->get("net.minecraft");
		if (!vlist) {
			emitFailed(tr("Failed to get local metadata index for %1")
						   .arg("net.minecraft"));
			return;
		}

		auto ver = vlist->getVersion(m_version.minecraft);
		if (!ver) {
			emitFailed(tr("Failed to get local metadata index for '%1' v%2")
						   .arg("net.minecraft")
						   .arg(m_version.minecraft));
			return;
		}
		ver->load(Net::Mode::Online);
		minecraftVersion = ver;

		if (m_version.noConfigs) {
			downloadMods();
		} else {
			installConfigs();
		}
	}

	void PackInstallTask::onDownloadFailed(QString reason)
	{
		qDebug() << "PackInstallTask::onDownloadFailed: "
				 << QThread::currentThreadId();
		emitFailed(reason);
		jobPtr.reset();
	}

	QString PackInstallTask::getDirForModType(ModType type, QString raw)
	{
		switch (type) {
			// Mod types that can either be ignored at this stage, or ignored
			// completely.
			case ModType::Root:
			case ModType::Extract:
			case ModType::Decomp:
			case ModType::TexturePackExtract:
			case ModType::ResourcePackExtract:
			case ModType::MCPC:
				return Q_NULLPTR;
			case ModType::Forge:
				// Forge detection happens later on, if it cannot be detected it
				// will install a jarmod component.
			case ModType::Jar:
				return "jarmods";
			case ModType::Mods:
				return "mods";
			case ModType::Flan:
				return "Flan";
			case ModType::Dependency:
				return FS::PathCombine("mods", m_version.minecraft);
			case ModType::Ic2Lib:
				return FS::PathCombine("mods", "ic2");
			case ModType::DenLib:
				return FS::PathCombine("mods", "denlib");
			case ModType::Coremods:
				return "coremods";
			case ModType::Plugins:
				return "plugins";
			case ModType::TexturePack:
				return "texturepacks";
			case ModType::ResourcePack:
				return "resourcepacks";
			case ModType::ShaderPack:
				return "shaderpacks";
			case ModType::Millenaire:
				qWarning() << "Unsupported mod type: " + raw;
				return Q_NULLPTR;
			case ModType::Unknown:
				emitFailed(tr("Unknown mod type: %1").arg(raw));
				return Q_NULLPTR;
		}

		return Q_NULLPTR;
	}

	QString PackInstallTask::getVersionForLoader(QString uid)
	{
		if (m_version.loader.recommended || m_version.loader.latest ||
			m_version.loader.choose) {
			auto vlist = APPLICATION->metadataIndex()->get(uid);
			if (!vlist) {
				emitFailed(
					tr("Failed to get local metadata index for %1").arg(uid));
				return Q_NULLPTR;
			}

			if (!vlist->isLoaded()) {
				vlist->load(Net::Mode::Online);
			}

			if (m_version.loader.recommended || m_version.loader.latest) {
				for (int i = 0; i < vlist->versions().size(); i++) {
					auto version = vlist->versions().at(i);
					auto reqs = version->requirements();

					// filter by minecraft version, if the loader depends on a
					// certain version. not all mod loaders depend on a given
					// Minecraft version, so we won't do this filtering for
					// those loaders.
					if (m_version.loader.type != "fabric") {
						auto iter =
							std::find_if(reqs.begin(), reqs.end(),
										 [](const Meta::Require& req) {
											 return req.uid == "net.minecraft";
										 });
						if (iter == reqs.end())
							continue;
						if (iter->equalsVersion != m_version.minecraft)
							continue;
					}

					if (m_version.loader.recommended) {
						// first recommended build we find, we use.
						if (!version->isRecommended())
							continue;
					}

					return version->descriptor();
				}

				emitFailed(tr("Failed to find version for %1 loader")
							   .arg(m_version.loader.type));
				return Q_NULLPTR;
			} else if (m_version.loader.choose) {
				// Fabric Loader doesn't depend on a given Minecraft version.
				if (m_version.loader.type == "fabric") {
					return m_support->chooseVersion(vlist, Q_NULLPTR);
				}

				return m_support->chooseVersion(vlist, m_version.minecraft);
			}
		}

		if (m_version.loader.version == Q_NULLPTR ||
			m_version.loader.version.isEmpty()) {
			emitFailed(tr("No loader version set for modpack!"));
			return Q_NULLPTR;
		}

		return m_version.loader.version;
	}

	QString PackInstallTask::detectLibrary(VersionLibrary library)
	{
		// Try to detect what the library is
		if (!library.server.isEmpty() &&
			library.server.split("/").length() >= 3) {
			auto lastSlash = library.server.lastIndexOf("/");
			auto locationAndVersion = library.server.mid(0, lastSlash);
			auto fileName = library.server.mid(lastSlash + 1);

			lastSlash = locationAndVersion.lastIndexOf("/");
			auto location = locationAndVersion.mid(0, lastSlash);
			auto version = locationAndVersion.mid(lastSlash + 1);

			lastSlash = location.lastIndexOf("/");
			auto group = location.mid(0, lastSlash).replace("/", ".");
			auto artefact = location.mid(lastSlash + 1);

			return group + ":" + artefact + ":" + version;
		}

		if (library.file.contains("-")) {
			auto lastSlash = library.file.lastIndexOf("-");
			auto name = library.file.mid(0, lastSlash);
			auto version = library.file.mid(lastSlash + 1).remove(".jar");

			if (name == QString("guava")) {
				return "com.google.guava:guava:" + version;
			} else if (name == QString("commons-lang3")) {
				return "org.apache.commons:commons-lang3:" + version;
			}
		}

		return "org.projecttick.atlauncher:" + library.md5 + ":1";
	}

	bool PackInstallTask::createLibrariesComponent(
		QString instanceRoot, std::shared_ptr<PackProfile> profile)
	{
		if (m_version.libraries.isEmpty()) {
			return true;
		}

		QList<GradleSpecifier> exempt;
		for (const auto& componentUid : componentsToInstall.keys()) {
			auto componentVersion = componentsToInstall.value(componentUid);

			for (const auto& library : componentVersion->data()->libraries) {
				GradleSpecifier lib(library->rawName());
				exempt.append(lib);
			}
		}

		{
			for (const auto& library : minecraftVersion->data()->libraries) {
				GradleSpecifier lib(library->rawName());
				exempt.append(lib);
			}
		}

		auto uuid = QUuid::createUuid();
		auto id = uuid.toString().remove('{').remove('}');
		auto target_id = "org.projecttick.atlauncher." + id;

		auto patchDir = FS::PathCombine(instanceRoot, "patches");
		if (!FS::ensureFolderPathExists(patchDir)) {
			return false;
		}
		auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");

		auto f = std::make_shared<VersionFile>();
		f->name = m_pack + " " + m_version_name + " (libraries)";

		for (const auto& lib : m_version.libraries) {
			auto libName = detectLibrary(lib);
			GradleSpecifier libSpecifier(libName);

			bool libExempt = false;
			for (const auto& existingLib : exempt) {
				if (libSpecifier.matchName(existingLib)) {
					// If the pack specifies a newer version of the lib, use
					// that!
					libExempt = Version(libSpecifier.version()) >=
								Version(existingLib.version());
				}
			}
			if (libExempt)
				continue;

			auto library = std::make_shared<Library>();
			library->setRawName(libName);

			switch (lib.download) {
				case DownloadType::Server:
					library->setAbsoluteUrl(
						BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url);
					break;
				case DownloadType::Direct:
					library->setAbsoluteUrl(lib.url);
					break;
				case DownloadType::Browser:
				case DownloadType::Unknown:
					emitFailed(tr("Unknown or unsupported download type: %1")
								   .arg(lib.download_raw));
					return false;
			}

			f->libraries.append(library);
		}

		if (f->libraries.isEmpty()) {
			return true;
		}

		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();

		profile->appendComponent(new Component(profile.get(), target_id, f));
		return true;
	}

	bool
	PackInstallTask::createPackComponent(QString instanceRoot,
										 std::shared_ptr<PackProfile> profile)
	{
		if (m_version.mainClass == QString() &&
			m_version.extraArguments == QString()) {
			return true;
		}

		auto uuid = QUuid::createUuid();
		auto id = uuid.toString().remove('{').remove('}');
		auto target_id = "org.projecttick.atlauncher." + id;

		auto patchDir = FS::PathCombine(instanceRoot, "patches");
		if (!FS::ensureFolderPathExists(patchDir)) {
			return false;
		}
		auto patchFileName = FS::PathCombine(patchDir, target_id + ".json");

		QStringList mainClasses;
		QStringList tweakers;
		for (const auto& componentUid : componentsToInstall.keys()) {
			auto componentVersion = componentsToInstall.value(componentUid);

			if (componentVersion->data()->mainClass != QString("")) {
				mainClasses.append(componentVersion->data()->mainClass);
			}
			tweakers.append(componentVersion->data()->addTweakers);
		}

		auto f = std::make_shared<VersionFile>();
		f->name = m_pack + " " + m_version_name;
		if (m_version.mainClass != QString() &&
			!mainClasses.contains(m_version.mainClass)) {
			f->mainClass = m_version.mainClass;
		}

		// Parse out tweakers
		auto args = m_version.extraArguments.split(" ");
		QString previous;
		for (auto arg : args) {
			if (arg.startsWith("--tweakClass=") || previous == "--tweakClass") {
				auto tweakClass = arg.remove("--tweakClass=");
				if (tweakers.contains(tweakClass))
					continue;

				f->addTweakers.append(tweakClass);
			}
			previous = arg;
		}

		if (f->mainClass == QString() && f->addTweakers.isEmpty()) {
			return true;
		}

		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();

		profile->appendComponent(new Component(profile.get(), target_id, f));
		return true;
	}

	void PackInstallTask::installConfigs()
	{
		qDebug() << "PackInstallTask::installConfigs: "
				 << QThread::currentThreadId();
		setStatus(tr("Downloading configs..."));
		jobPtr = new NetJob(tr("Config download"), APPLICATION->network());

		auto path =
			QString("Configs/%1/%2.zip").arg(m_pack).arg(m_version_name);
		auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL +
						   "packs/%1/versions/%2/Configs.zip")
					   .arg(m_pack)
					   .arg(m_version_name);
		auto entry =
			APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path);
		entry->setStale(true);

		auto dl = Net::Download::makeCached(url, entry);
		if (!m_version.configs.sha1.isEmpty()) {
			auto rawSha1 =
				QByteArray::fromHex(m_version.configs.sha1.toLatin1());
			dl->addValidator(
				new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
		}
		jobPtr->addNetAction(dl);
		archivePath = entry->getFullPath();

		connect(jobPtr.get(), &NetJob::succeeded, this, [&]() {
			abortable = false;
			extractConfigs();
			jobPtr.reset();
		});
		connect(jobPtr.get(), &NetJob::failed, [&](QString reason) {
			abortable = false;
			emitFailed(reason);
			jobPtr.reset();
		});
		connect(jobPtr.get(), &NetJob::progress,
				[&](qint64 current, qint64 total) {
					abortable = true;
					setProgress(current, total);
				});

		jobPtr->start();
	}

	void PackInstallTask::extractConfigs()
	{
		qDebug() << "PackInstallTask::extractConfigs: "
				 << QThread::currentThreadId();
		setStatus(tr("Extracting configs..."));

		QDir extractDir(m_stagingPath);

		QString extractPath = extractDir.absolutePath() + "/minecraft";
		QString archivePathCopy = archivePath;
		m_extractFuture = QtConcurrent::run(
			QThreadPool::globalInstance(), [archivePathCopy, extractPath]() {
				return MMCZip::extractDir(archivePathCopy, extractPath);
			});
		connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished,
				this, [&]() { downloadMods(); });
		connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled,
				this, [&]() { emitAborted(); });
		m_extractFutureWatcher.setFuture(m_extractFuture);
	}

	void PackInstallTask::downloadMods()
	{
		qDebug() << "PackInstallTask::installMods: "
				 << QThread::currentThreadId();

		QVector<ATLauncher::VersionMod> optionalMods;
		for (const auto& mod : m_version.mods) {
			if (mod.optional) {
				optionalMods.push_back(mod);
			}
		}

		// Select optional mods, if pack contains any
		QVector<QString> selectedMods;
		if (!optionalMods.isEmpty()) {
			setStatus(tr("Selecting optional mods..."));
			selectedMods = m_support->chooseOptionalMods(optionalMods);
		}

		setStatus(tr("Downloading mods..."));

		jarmods.clear();
		jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
		for (const auto& mod : m_version.mods) {
			// skip non-client mods
			if (!mod.client)
				continue;

			// skip optional mods that were not selected
			if (mod.optional && !selectedMods.contains(mod.name))
				continue;

			QString url;
			switch (mod.download) {
				case DownloadType::Server:
					url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url;
					break;
				case DownloadType::Browser:
					emitFailed(tr("Unsupported download type: %1")
								   .arg(mod.download_raw));
					return;
				case DownloadType::Direct:
					url = mod.url;
					break;
				case DownloadType::Unknown:
					emitFailed(
						tr("Unknown download type: %1").arg(mod.download_raw));
					return;
			}

			QFileInfo fileName(mod.file);
			auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." +
							 fileName.suffix();

			if (mod.type == ModType::Extract ||
				mod.type == ModType::TexturePackExtract ||
				mod.type == ModType::ResourcePackExtract) {
				auto entry = APPLICATION->metacache()->resolveEntry(
					"ATLauncherPacks", cacheName);
				entry->setStale(true);
				modsToExtract.insert(entry->getFullPath(), mod);

				auto dl = Net::Download::makeCached(url, entry);
				if (!mod.md5.isEmpty()) {
					auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
					dl->addValidator(new Net::ChecksumValidator(
						QCryptographicHash::Md5, rawMd5));
				}
				jobPtr->addNetAction(dl);
			} else if (mod.type == ModType::Decomp) {
				auto entry = APPLICATION->metacache()->resolveEntry(
					"ATLauncherPacks", cacheName);
				entry->setStale(true);
				modsToDecomp.insert(entry->getFullPath(), mod);

				auto dl = Net::Download::makeCached(url, entry);
				if (!mod.md5.isEmpty()) {
					auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
					dl->addValidator(new Net::ChecksumValidator(
						QCryptographicHash::Md5, rawMd5));
				}
				jobPtr->addNetAction(dl);
			} else {
				auto relpath = getDirForModType(mod.type, mod.type_raw);
				if (relpath == Q_NULLPTR)
					continue;

				auto entry = APPLICATION->metacache()->resolveEntry(
					"ATLauncherPacks", cacheName);
				entry->setStale(true);

				auto dl = Net::Download::makeCached(url, entry);
				if (!mod.md5.isEmpty()) {
					auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1());
					dl->addValidator(new Net::ChecksumValidator(
						QCryptographicHash::Md5, rawMd5));
				}
				jobPtr->addNetAction(dl);

				auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath,
											mod.file);
				qDebug() << "Will download" << url << "to" << path;
				modsToCopy[entry->getFullPath()] = path;

				if (mod.type == ModType::Forge) {
					auto vlist =
						APPLICATION->metadataIndex()->get("net.minecraftforge");
					if (vlist) {
						auto ver = vlist->getVersion(mod.version);
						if (ver) {
							ver->load(Net::Mode::Online);
							componentsToInstall.insert("net.minecraftforge",
													   ver);
							continue;
						}
					}

					qDebug() << "Jarmod: " + path;
					jarmods.push_back(path);
				}

				if (mod.type == ModType::Jar) {
					qDebug() << "Jarmod: " + path;
					jarmods.push_back(path);
				}
			}
		}

		connect(jobPtr.get(), &NetJob::succeeded, this,
				&PackInstallTask::onModsDownloaded);
		connect(jobPtr.get(), &NetJob::failed, [&](QString reason) {
			abortable = false;
			emitFailed(reason);
			jobPtr.reset();
		});
		connect(jobPtr.get(), &NetJob::progress,
				[&](qint64 current, qint64 total) {
					abortable = true;
					setProgress(current, total);
				});

		jobPtr->start();
	}

	void PackInstallTask::onModsDownloaded()
	{
		abortable = false;

		qDebug() << "PackInstallTask::onModsDownloaded: "
				 << QThread::currentThreadId();

		if (!modsToExtract.empty() || !modsToDecomp.empty() ||
			!modsToCopy.empty()) {
			auto modsToExtractCopy = modsToExtract;
			auto modsToDecompCopy = modsToDecomp;
			auto modsToCopyCopy = modsToCopy;
			m_modExtractFuture = QtConcurrent::run(
				QThreadPool::globalInstance(),
				[this, modsToExtractCopy, modsToDecompCopy, modsToCopyCopy]() {
					return this->extractMods(modsToExtractCopy,
											 modsToDecompCopy, modsToCopyCopy);
				});
			connect(&m_modExtractFutureWatcher,
					&QFutureWatcher<QStringList>::finished, this,
					&PackInstallTask::onModsExtracted);
			connect(&m_modExtractFutureWatcher,
					&QFutureWatcher<QStringList>::canceled, this,
					[&]() { emitAborted(); });
			m_modExtractFutureWatcher.setFuture(m_modExtractFuture);
		} else {
			install();
		}
	}

	void PackInstallTask::onModsExtracted()
	{
		qDebug() << "PackInstallTask::onModsExtracted: "
				 << QThread::currentThreadId();
		if (m_modExtractFuture.result()) {
			install();
		} else {
			emitFailed(tr("Failed to extract mods..."));
		}
	}

	bool
	PackInstallTask::extractMods(const QMap<QString, VersionMod>& toExtract,
								 const QMap<QString, VersionMod>& toDecomp,
								 const QMap<QString, QString>& toCopy)
	{
		qDebug() << "PackInstallTask::extractMods: "
				 << QThread::currentThreadId();

		setStatus(tr("Extracting mods..."));
		for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) {
			auto& modPath = iter.key();
			auto& mod = iter.value();

			QString extractToDir;
			if (mod.type == ModType::Extract) {
				extractToDir =
					getDirForModType(mod.extractTo, mod.extractTo_raw);
			} else if (mod.type == ModType::TexturePackExtract) {
				extractToDir = FS::PathCombine("texturepacks", "extracted");
			} else if (mod.type == ModType::ResourcePackExtract) {
				extractToDir = FS::PathCombine("resourcepacks", "extracted");
			}

			QDir extractDir(m_stagingPath);
			auto extractToPath = FS::PathCombine(extractDir.absolutePath(),
												 "minecraft", extractToDir);

			QString folderToExtract = "";
			if (mod.type == ModType::Extract) {
				folderToExtract = mod.extractFolder;
				folderToExtract.remove(QRegularExpression("^/"));
			}

			qDebug() << "Extracting " + mod.file + " to " + extractToDir;
			if (!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) {
				// assume error
				return false;
			}
		}

		for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) {
			auto& modPath = iter.key();
			auto& mod = iter.value();
			auto extractToDir =
				getDirForModType(mod.decompType, mod.decompType_raw);

			QDir extractDir(m_stagingPath);
			auto extractToPath =
				FS::PathCombine(extractDir.absolutePath(), "minecraft",
								extractToDir, mod.decompFile);

			qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir;
			if (!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) {
				qWarning() << "Failed to extract" << mod.decompFile;
				return false;
			}
		}

		for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) {
			auto& from = iter.key();
			auto& to = iter.value();
			FS::copy fileCopyOperation(from, to);
			if (!fileCopyOperation()) {
				qWarning() << "Failed to copy" << from << "to" << to;
				return false;
			}
		}
		return true;
	}

	void PackInstallTask::install()
	{
		qDebug() << "PackInstallTask::install: " << QThread::currentThreadId();
		setStatus(tr("Installing modpack"));

		auto instanceConfigPath =
			FS::PathCombine(m_stagingPath, "instance.cfg");
		auto instanceSettings =
			std::make_shared<INISettingsObject>(instanceConfigPath);
		instanceSettings->suspendSave();
		instanceSettings->registerSetting("InstanceType", "Legacy");
		instanceSettings->set("InstanceType", "OneSix");

		MinecraftInstance instance(m_globalSettings, instanceSettings,
								   m_stagingPath);
		auto components = instance.getPackProfile();
		components->buildingFromScratch();

		// Use a component to add libraries BEFORE Minecraft
		if (!createLibrariesComponent(instance.instanceRoot(), components)) {
			emitFailed(tr("Failed to create libraries component"));
			return;
		}

		// Minecraft
		components->setComponentVersion("net.minecraft", m_version.minecraft,
										true);

		// Loader
		if (m_version.loader.type == QString("forge")) {
			auto version = getVersionForLoader("net.minecraftforge");
			if (version == Q_NULLPTR)
				return;

			components->setComponentVersion("net.minecraftforge", version,
											true);
		} else if (m_version.loader.type == QString("fabric")) {
			auto version = getVersionForLoader("net.fabricmc.fabric-loader");
			if (version == Q_NULLPTR)
				return;

			components->setComponentVersion("net.fabricmc.fabric-loader",
											version, true);
		} else if (m_version.loader.type == QString("neoforge")) {
			auto version = getVersionForLoader("net.neoforged");
			if (version == Q_NULLPTR)
				return;

			components->setComponentVersion("net.neoforged", version, true);
		} else if (m_version.loader.type == QString("quilt")) {
			auto version = getVersionForLoader("org.quiltmc.quilt-loader");
			if (version == Q_NULLPTR)
				return;

			components->setComponentVersion("org.quiltmc.quilt-loader", version,
											true);
		} else if (m_version.loader.type != QString()) {
			emitFailed(tr("Unknown loader type: ") + m_version.loader.type);
			return;
		}

		for (const auto& componentUid : componentsToInstall.keys()) {
			auto version = componentsToInstall.value(componentUid);
			components->setComponentVersion(componentUid, version->version());
		}

		components->installJarMods(jarmods);

		// Use a component to fill in the rest of the data
		// todo: use more detection
		if (!createPackComponent(instance.instanceRoot(), components)) {
			emitFailed(tr("Failed to create pack component"));
			return;
		}

		components->saveNow();

		instance.setName(m_instName);
		instance.setIconKey(m_instIcon);
		instanceSettings->resumeSave();

		jarmods.clear();
		emitSucceeded();
	}

} // namespace ATLauncher
