/* Copyright (c) 2013-2015 Jeffrey Pfau
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ConfigController.h"

#include "ActionMapper.h"
#include "CoreController.h"

#include <QDir>
#include <QMenu>

#include <mgba/feature/commandline.h>
#ifdef M_CORE_GB
#include <mgba/internal/gb/overrides.h>
#endif

static const mOption s_frontendOptions[] = {
	{ "ecard", true, '\0' },
	{ "mb", true, '\0' },
#ifdef ENABLE_SCRIPTING
	{ "script", true, '\0' },
#endif
	{ 0 }
};

using namespace QGBA;

ConfigOption::ConfigOption(const QString& name, QObject* parent)
	: QObject(parent)
	, m_name(name)
{
}

void ConfigOption::connect(std::function<void(const QVariant&)> slot, QObject* parent) {
	m_slots[parent] = std::move(slot);
	QObject::connect(parent, &QObject::destroyed, this, [this, parent]() {
		m_slots.remove(parent);
	});
}

std::shared_ptr<Action> ConfigOption::addValue(const QString& text, const QVariant& value, ActionMapper* actions, const QString& menu) {
	std::shared_ptr<Action> action;
	auto function = [this, value]() {
		emit valueChanged(value);
	};
	QString name = QString("%1.%2").arg(m_name).arg(value.toString());
	if (actions) {
		action = actions->addAction(text, name, function, menu);
	} else {
		action = std::make_shared<Action>(function, name, text, this);
	}
	action->setExclusive();
	std::weak_ptr<Action> weakAction(action);
	QObject::connect(action.get(), &QObject::destroyed, this, [this, weakAction = std::move(weakAction), value]() {
		if (weakAction.expired()) {
			return;
		}
		std::shared_ptr<Action> action(weakAction.lock());
		m_actions.removeAll(std::make_pair(action, value));
	});
	m_actions.append(std::make_pair(action, value));
	return action;
}

std::shared_ptr<Action> ConfigOption::addValue(const QString& text, const char* value, ActionMapper* actions, const QString& menu) {
	return addValue(text, QString(value), actions, menu);
}

std::shared_ptr<Action> ConfigOption::addBoolean(const QString& text, ActionMapper* actions, const QString& menu) {
	std::shared_ptr<Action> action;
	auto function = [this](bool value) {
		emit valueChanged(value);
	};
	if (actions) {
		action = actions->addBooleanAction(text, m_name, function, menu);
	} else {
		action = std::make_shared<Action>(function, m_name, text, this);
	}

	std::weak_ptr<Action> weakAction(action);
	QObject::connect(action.get(), &QObject::destroyed, this, [this, weakAction]() {
		if (weakAction.expired()) {
			return;
		}
		std::shared_ptr<Action> action(weakAction.lock());
		m_actions.removeAll(std::make_pair(action, QVariant(1)));
	});
	m_actions.append(std::make_pair(action, QVariant(1)));

	return action;
}

void ConfigOption::setValue(bool value) {
	setValue(QVariant(value));
}

void ConfigOption::setValue(int value) {
	setValue(QVariant(value));
}

void ConfigOption::setValue(unsigned value) {
	setValue(QVariant(value));
}

void ConfigOption::setValue(const char* value) {
	setValue(QVariant(QString(value)));
}

void ConfigOption::setValue(const QVariant& value) {
	for (std::pair<std::shared_ptr<Action>, QVariant>& action : m_actions) {
		action.first->setActive(value == action.second);
	}
	for (std::function<void(const QVariant&)>& slot : m_slots.values()) {
		slot(value);
	}
}

QString ConfigController::s_configDir;

ConfigController::ConfigController(QObject* parent)
	: QObject(parent)
{
	QString fileName = configDir();
	fileName.append(QDir::separator());
	fileName.append("qt.ini");
	m_settings = std::make_unique<QSettings>(fileName, QSettings::IniFormat);

	mCoreConfigInit(&m_config, PORT);

	m_opts.audioSync = CoreController::AUDIO_SYNC;
	m_opts.videoSync = CoreController::VIDEO_SYNC;
	m_opts.fpsTarget = 60;
	m_opts.audioBuffers = 1536;
	m_opts.sampleRate = 44100;
	m_opts.volume = 0x100;
	m_opts.logLevel = mLOG_WARN | mLOG_ERROR | mLOG_FATAL;
	m_opts.rewindEnable = false;
	m_opts.rewindBufferCapacity = 300;
	m_opts.rewindBufferInterval = 1;
	m_opts.useBios = true;
	m_opts.suspendScreensaver = true;
	m_opts.lockAspectRatio = true;
	m_opts.interframeBlending = false;
	mCoreConfigLoad(&m_config);
	mCoreConfigLoadDefaults(&m_config, &m_opts);
#ifdef M_CORE_GB
	mCoreConfigSetDefaultIntValue(&m_config, "sgb.borders", 1);
	mCoreConfigSetDefaultIntValue(&m_config, "gb.colors", GB_COLORS_CGB);
#endif
	mCoreConfigMap(&m_config, &m_opts);

	mSubParserGraphicsInit(&m_subparsers[0], &m_graphicsOpts);

	m_subparsers[1].usage = "Frontend options:\n"
	    "  --ecard FILE  Scan an e-Reader card in the first loaded game\n"
	    "                Can be passed multiple times for multiple cards\n"
	    "  --mb FILE     Boot a multiboot image with FILE inserted into the ROM slot"
#ifdef ENABLE_SCRIPTING
	    "\n  --script FILE Script file to load on start"
#endif
	    ;

	m_subparsers[1].parse = nullptr;
	m_subparsers[1].parseLong = [](struct mSubParser* parser, const char* option, const char* arg) {
		ConfigController* self = static_cast<ConfigController*>(parser->opts);
		QString optionName(QString::fromUtf8(option));
		if (optionName == QLatin1String("ecard")) {
			QStringList ecards;
			if (self->m_argvOptions.contains(optionName)) {
				ecards = self->m_argvOptions[optionName].toStringList();
			}
			ecards.append(QString::fromUtf8(arg));
			self->m_argvOptions[optionName] = ecards;
			return true;
		}
		if (optionName == QLatin1String("mb")) {
			self->m_argvOptions[optionName] = QString::fromUtf8(arg);
			return true;
		}
#ifdef ENABLE_SCRIPTING
		if (optionName == QLatin1String("script")) {
			QStringList scripts;
			if (self->m_argvOptions.contains(optionName)) {
				scripts = self->m_argvOptions[optionName].toStringList();
			}
			scripts.append(QString::fromUtf8(arg));
			self->m_argvOptions[optionName] = scripts;
			return true;
		}
#endif
		return false;
	};
	m_subparsers[1].apply = nullptr;
	m_subparsers[1].extraOptions = nullptr;
	m_subparsers[1].longOptions = s_frontendOptions;
	m_subparsers[1].opts = this;
}

ConfigController::~ConfigController() {
	mCoreConfigDeinit(&m_config);
	mCoreConfigFreeOpts(&m_opts);

	if (m_parsed) {
		mArgumentsDeinit(&m_args);
	}
}

bool ConfigController::parseArguments(int argc, char* argv[]) {
	if (m_parsed) {
		return false;
	}

	if (mArgumentsParse(&m_args, argc, argv, m_subparsers.data(), m_subparsers.size())) {
		mCoreConfigFreeOpts(&m_opts);
		mArgumentsApply(&m_args, m_subparsers.data(), m_subparsers.size(), &m_config);
		mCoreConfigMap(&m_config, &m_opts);
		m_parsed = true;
		return true;
	}
	return false;
}

ConfigOption* ConfigController::addOption(const char* key) {
	QString optionName(key);

	if (m_optionSet.contains(optionName)) {
		return m_optionSet[optionName];
	}
	ConfigOption* newOption = new ConfigOption(optionName, this);
	m_optionSet[optionName] = newOption;
	connect(newOption, &ConfigOption::valueChanged, this, [this, key](const QVariant& value) {
		setOption(key, value);
	});
	return newOption;
}

void ConfigController::updateOption(const char* key) {
	if (!key) {
		return;
	}

	QString optionName(key);

	if (!m_optionSet.contains(optionName)) {
		return;
	}
	m_optionSet[optionName]->setValue(mCoreConfigGetValue(&m_config, key));
}

QString ConfigController::getOption(const char* key, const QVariant& defaultVal) const {
	const char* val = mCoreConfigGetValue(&m_config, key);
	if (val) {
		return QString(val);
	}
	return defaultVal.toString();
}

QString ConfigController::getOption(const QString& key, const QVariant& defaultVal) const {
	return getOption(key.toUtf8().constData(), defaultVal);
}

QVariant ConfigController::getQtOption(const QString& key, const QString& group) const {
	if (!group.isNull()) {
		m_settings->beginGroup(group);
	}
	QVariant value = m_settings->value(key);
	if (!group.isNull()) {
		m_settings->endGroup();
	}
	return value;
}

QVariant ConfigController::getArgvOption(const QString& key) const {
	return m_argvOptions.value(key);
}

QVariant ConfigController::takeArgvOption(const QString& key) {
		return m_argvOptions.take(key);
}

void ConfigController::saveOverride(const Override& override) {
	override.save(overrides());
	write();
}

void ConfigController::setOption(const char* key, bool value) {
	mCoreConfigSetIntValue(&m_config, key, value);
	QString optionName(key);
	if (m_optionSet.contains(optionName)) {
		m_optionSet[optionName]->setValue(value);
	}
}

void ConfigController::setOption(const char* key, int value) {
	mCoreConfigSetIntValue(&m_config, key, value);
	QString optionName(key);
	if (m_optionSet.contains(optionName)) {
		m_optionSet[optionName]->setValue(value);
	}
}

void ConfigController::setOption(const char* key, unsigned value) {
	mCoreConfigSetUIntValue(&m_config, key, value);
	QString optionName(key);
	if (m_optionSet.contains(optionName)) {
		m_optionSet[optionName]->setValue(value);
	}
}

void ConfigController::setOption(const char* key, const char* value) {
	mCoreConfigSetValue(&m_config, key, value);
	QString optionName(key);
	if (m_optionSet.contains(optionName)) {
		m_optionSet[optionName]->setValue(value);
	}
}

void ConfigController::setOption(const char* key, const QVariant& value) {
	if (value.type() == QVariant::Bool) {
		setOption(key, value.toBool());
		return;
	}
	QString stringValue(value.toString());
	setOption(key, stringValue.toUtf8().constData());
}

void ConfigController::setQtOption(const QString& key, const QVariant& value, const QString& group) {
	if (!group.isNull()) {
		m_settings->beginGroup(group);
	}
	m_settings->setValue(key, value);
	if (!group.isNull()) {
		m_settings->endGroup();
	}
}

QStringList ConfigController::getMRU(ConfigController::MRU mruType) const {
	QStringList mru;
	m_settings->beginGroup(mruName(mruType));
	for (int i = 0; i < MRU_LIST_SIZE; ++i) {
		QString item = m_settings->value(QString::number(i)).toString();
		if (item.isNull()) {
			continue;
		}
		mru.append(item);
	}
	m_settings->endGroup();
	return mru;
}

void ConfigController::setMRU(const QStringList& mru, ConfigController::MRU mruType) {
	int i = 0;
	m_settings->beginGroup(mruName(mruType));
	for (const QString& item : mru) {
		m_settings->setValue(QString::number(i), item);
		++i;
		if (i >= MRU_LIST_SIZE) {
			break;
		}
	}
	for (; i < MRU_LIST_SIZE; ++i) {
		m_settings->remove(QString::number(i));
	}
	m_settings->endGroup();
}

constexpr const char* ConfigController::mruName(ConfigController::MRU mru) {
	switch (mru) {
	case MRU::ROM:
		return "mru";
	case MRU::Script:
		return "recentScripts";
	}
	Q_UNREACHABLE();
}

void ConfigController::write() {
	mCoreConfigSave(&m_config);
	m_settings->sync();

	mCoreConfigFreeOpts(&m_opts);
	mCoreConfigMap(&m_config, &m_opts);
}

void ConfigController::makePortable() {
	mCoreConfigMakePortable(&m_config, nullptr);

	QString fileName(configDir());
	fileName.append(QDir::separator());
	fileName.append("qt.ini");
	auto settings2 = std::make_unique<QSettings>(fileName, QSettings::IniFormat);
	for (const auto& key : m_settings->allKeys()) {
		settings2->setValue(key, m_settings->value(key));
	}
	m_settings = std::move(settings2);
}

void ConfigController::usage(const char* arg0) const {
	::usage(arg0, nullptr, nullptr, m_subparsers.data(), m_subparsers.size());
}

bool ConfigController::isPortable() {
	return mCoreConfigIsPortable();
}

const QString& ConfigController::configDir() {
	if (s_configDir.isNull()) {
		char path[PATH_MAX];
		mCoreConfigDirectory(path, sizeof(path));
		s_configDir = QString::fromUtf8(path);
	}
	return s_configDir;
}

const QString& ConfigController::cacheDir() {
	return configDir();
}
