#include "migration.hpp"

#include <QDebug>

namespace ZShell::config {

namespace {

// A stored value is "missing" if the path does not resolve to an entry.
// Note: default-constructed QJsonValue is Null, not Undefined.
bool isMissing(const QJsonValue& v) {
	return v.isUndefined() || v.isNull();
}

QJsonValue valueAt(const QJsonObject& obj, const QStringList& path) {
	QJsonObject cur = obj;

	for (int i = 0; i < path.size() - 1; ++i) {
		if (!cur.value(path.at(i)).isObject()) return QJsonValue::Undefined;
		cur = cur.value(path.at(i)).toObject();
	}

	const QString key = path.last();
	if (!cur.contains(key)) return QJsonValue::Undefined;
	return cur.value(key);
}

QJsonObject placeAt(QJsonObject obj, QStringList path, const QJsonValue& value) {
	const QString head = path.takeFirst();

	if (path.isEmpty()) {
		obj[head] = value;
		return obj;
	}

	QJsonObject child = obj.value(head).isObject() ? obj.value(head).toObject()
												   : QJsonObject();
	obj[head] = placeAt(child, path, value);
	return obj;
}

// Removes `path` from `obj`. Returns true if a value was removed.
// Parent objects left empty are removed as well.
bool removeAt(QJsonObject& obj, const QStringList& path) {
	const QString head = path.first();

	if (path.size() == 1) {
		if (!obj.contains(head)) return false;
		obj.remove(head);
		return true;
	}

	if (!obj.value(head).isObject()) return false;

	QJsonObject child = obj.value(head).toObject();
	const bool removed = removeAt(child, path.mid(1));

	if (child.isEmpty())
		obj.remove(head);
	else
		obj[head] = child;

	return removed;
}

} // namespace

const QList<ConfigMigrationRule>& ConfigMigrations::rules() {
	static const QList<ConfigMigrationRule> s_rules = {
		migrate(
			"general.color.scheduleHyprsunset", "display.nightlight.schedule"),
		migrate(
			"general.color.scheduleHyprsunsetStart",
			"display.nightlight.scheduleStart"),
		migrate(
			"general.color.scheduleHyprsunsetEnd",
			"display.nightlight.scheduleEnd"),
		migrate("general.color.scheduleDark", "colors.scheduleDark"),
		migrate("general.color.scheduleDarkStart", "colors.scheduleDarkStart"),
		migrate("general.color.scheduleDarkEnd", "colors.scheduleDarkEnd"),
		migrate("general.color.schemeGeneration", "colors.schemeGen"),
		migrate(
			"general.color.useNativeNightlight",
			"display.nightlight.useNative"),
		migrate(
			"general.color.nativeFadeDuration",
			"display.nightlight.fadeDuration"),
		migrate("general.color.hyprsunsetTemp", "display.nightlight.temp"),
		migrate("general.color.neovimColors", "colors.neovimColors"),
		migrate("general.color.mode", "colors.mode"),
		migrate("general.color.smart", "colors.smart"),
	};
	return s_rules;
}

QJsonObject ConfigMigrations::apply(const QJsonObject& json) {
	QJsonObject obj = json;

	for (const auto& rule : rules()) {
		const QStringList from =
			rule.from.split(QLatin1Char('.'), Qt::SkipEmptyParts);
		const QStringList to =
			rule.to.split(QLatin1Char('.'), Qt::SkipEmptyParts);

		if (from.isEmpty() || to.isEmpty()) continue;

		const QJsonValue oldValue = valueAt(obj, from);
		if (isMissing(oldValue)) continue;

		const bool targetExists = !isMissing(valueAt(obj, to));
		if (!targetExists) obj = placeAt(obj, to, oldValue);

		if (removeAt(obj, from)) {
			qInfo() << "Config migration:" << rule.from << "->" << rule.to
					<< (targetExists ? "(target already set, old value dropped)"
									 : "(value moved)");
		}
	}

	return obj;
}

} // namespace ZShell::config
