/******************************************************************************
    Copyright (C) 2023 by Dennis Sädtler <dennis@obsproject.com>

    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 2 of the License, or
    (at your option) any later version.

    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 <http://www.gnu.org/licenses/>.
******************************************************************************/

#include <cinttypes>

#include <util/cf-parser.h>

#include <QDir>
#include <QFile>
#include <QTimer>
#include <QMetaEnum>
#include <QDirIterator>
#include <QGuiApplication>
#include <QRandomGenerator>

#include "qt-wrappers.hpp"
#include "obs-app.hpp"
#include "obs-app-theming.hpp"
#include "obs-proxy-style.hpp"
#include "platform.hpp"

#include "ui-config.h"

using namespace std;

struct CFParser {
	cf_parser cfp = {};
	~CFParser() { cf_parser_free(&cfp); }
	operator cf_parser *() { return &cfp; }
	cf_parser *operator->() { return &cfp; }
};

static OBSTheme *ParseThemeMeta(const QString &path)
{
	QFile themeFile(path);
	if (!themeFile.open(QIODeviceBase::ReadOnly))
		return nullptr;

	OBSTheme *meta = nullptr;
	const QByteArray data = themeFile.readAll();
	CFParser cfp;
	int ret;

	if (!cf_parser_parse(cfp, data.constData(), QT_TO_UTF8(path)))
		return nullptr;

	if (cf_token_is(cfp, "@") || cf_go_to_token(cfp, "@", nullptr)) {
		while (cf_next_token(cfp)) {
			if (cf_token_is(cfp, "OBSThemeMeta"))
				break;

			if (!cf_go_to_token(cfp, "@", nullptr))
				return nullptr;
		}

		if (!cf_next_token(cfp))
			return nullptr;

		if (!cf_token_is(cfp, "{"))
			return nullptr;

		meta = new OBSTheme();

		for (;;) {
			if (!cf_next_token(cfp)) {
				delete meta;
				return nullptr;
			}

			ret = cf_token_is_type(cfp, CFTOKEN_NAME, "name",
					       nullptr);
			if (ret != PARSE_SUCCESS)
				break;

			string name(cfp->cur_token->str.array,
				    cfp->cur_token->str.len);

			ret = cf_next_token_should_be(cfp, ":", ";", nullptr);
			if (ret != PARSE_SUCCESS)
				continue;

			if (!cf_next_token(cfp)) {
				delete meta;
				return nullptr;
			}

			ret = cf_token_is_type(cfp, CFTOKEN_STRING, "value",
					       ";");

			if (ret != PARSE_SUCCESS)
				continue;

			BPtr str = cf_literal_to_str(cfp->cur_token->str.array,
						     cfp->cur_token->str.len);

			if (str) {
				if (name == "dark")
					meta->isDark = strcmp(str, "true") == 0;
				else if (name == "extends")
					meta->extends = str;
				else if (name == "author")
					meta->author = str;
				else if (name == "id")
					meta->id = str;
				else if (name == "name")
					meta->name = str;
			}

			if (!cf_go_to_token(cfp, ";", nullptr)) {
				delete meta;
				return nullptr;
			}
		}
	}

	if (meta) {
		auto filepath = filesystem::u8path(path.toStdString());
		meta->isBaseTheme = filepath.extension() == ".obt";
		meta->filename = filepath.stem();

		if (meta->id.isEmpty() || meta->name.isEmpty() ||
		    (!meta->isBaseTheme && meta->extends.isEmpty())) {
			/* Theme is invalid */
			delete meta;
			meta = nullptr;
		} else {
			meta->location = absolute(filepath);
			meta->isHighContrast = path.endsWith(".oha");
			meta->isVisible = !path.contains("System");
		}
	}

	return meta;
}

static bool ParseVarName(CFParser &cfp, QString &value)
{
	int ret;

	ret = cf_next_token_should_be(cfp, "(", ";", nullptr);
	if (ret != PARSE_SUCCESS)
		return false;
	ret = cf_next_token_should_be(cfp, "-", ";", nullptr);
	if (ret != PARSE_SUCCESS)
		return false;
	ret = cf_next_token_should_be(cfp, "-", ";", nullptr);
	if (ret != PARSE_SUCCESS)
		return false;
	if (!cf_next_token(cfp))
		return false;

	value = QString::fromUtf8(cfp->cur_token->str.array,
				  cfp->cur_token->str.len);

	ret = cf_next_token_should_be(cfp, ")", ";", nullptr);
	if (ret != PARSE_SUCCESS)
		return false;

	return !value.isEmpty();
}

static QColor ParseColor(CFParser &cfp)
{
	const char *array;
	uint32_t color = 0;
	QColor res(QColor::Invalid);

	if (cf_token_is(cfp, "#")) {
		if (!cf_next_token(cfp))
			return res;

		color = strtol(cfp->cur_token->str.array, nullptr, 16);
	} else if (cf_token_is(cfp, "rgb")) {
		int ret = cf_next_token_should_be(cfp, "(", ";", nullptr);
		if (ret != PARSE_SUCCESS || !cf_next_token(cfp))
			return res;

		array = cfp->cur_token->str.array;
		color |= strtol(array, nullptr, 10) << 16;

		ret = cf_next_token_should_be(cfp, ",", ";", nullptr);
		if (ret != PARSE_SUCCESS || !cf_next_token(cfp))
			return res;

		array = cfp->cur_token->str.array;
		color |= strtol(array, nullptr, 10) << 8;

		ret = cf_next_token_should_be(cfp, ",", ";", nullptr);
		if (ret != PARSE_SUCCESS || !cf_next_token(cfp))
			return res;

		array = cfp->cur_token->str.array;
		color |= strtol(array, nullptr, 10);

		ret = cf_next_token_should_be(cfp, ")", ";", nullptr);
		if (ret != PARSE_SUCCESS)
			return res;
	} else if (cf_token_is(cfp, "bikeshed")) {
		color |= QRandomGenerator::global()->bounded(INT8_MAX) << 16;
		color |= QRandomGenerator::global()->bounded(INT8_MAX) << 8;
		color |= QRandomGenerator::global()->bounded(INT8_MAX);
	}

	res = color;
	return res;
}

static bool ParseCalc(CFParser &cfp, QStringList &calc,
		      vector<OBSThemeVariable> &vars)
{
	int ret = cf_next_token_should_be(cfp, "(", ";", nullptr);
	if (ret != PARSE_SUCCESS)
		return false;
	if (!cf_next_token(cfp))
		return false;

	while (!cf_token_is(cfp, ")")) {
		if (cf_token_is(cfp, ";"))
			break;

		if (cf_token_is(cfp, "calc")) {
			/* Internal calc's do not have proper names.
			 * They are anonymous variables */
			OBSThemeVariable var;
			QStringList subcalc;

			var.name = QString("__unnamed_%1")
					   .arg(QRandomGenerator::global()
							->generate64());

			if (!ParseCalc(cfp, subcalc, vars))
				return false;

			var.type = OBSThemeVariable::Calc;
			var.value = subcalc;
			calc << var.name;
			vars.push_back(std::move(var));
		} else if (cf_token_is(cfp, "var")) {
			QString value;
			if (!ParseVarName(cfp, value))
				return false;

			calc << value;
		} else {
			calc << QString::fromUtf8(cfp->cur_token->str.array,
						  cfp->cur_token->str.len);
		}

		if (!cf_next_token(cfp))
			return false;
	}

	return !calc.isEmpty();
}

static vector<OBSThemeVariable> ParseThemeVariables(const char *themeData)
{
	CFParser cfp;
	int ret;

	std::vector<OBSThemeVariable> vars;

	if (!cf_parser_parse(cfp, themeData, nullptr))
		return vars;

	if (!cf_token_is(cfp, "@") && !cf_go_to_token(cfp, "@", nullptr))
		return vars;

	while (cf_next_token(cfp)) {
		if (cf_token_is(cfp, "OBSThemeVars"))
			break;

		if (!cf_go_to_token(cfp, "@", nullptr))
			return vars;
	}

	if (!cf_next_token(cfp))
		return {};

	if (!cf_token_is(cfp, "{"))
		return {};

	for (;;) {
		if (!cf_next_token(cfp))
			return vars;

		if (!cf_token_is(cfp, "-"))
			return vars;

		ret = cf_next_token_should_be(cfp, "-", ";", nullptr);
		if (ret != PARSE_SUCCESS)
			continue;

		if (!cf_next_token(cfp))
			return vars;

		ret = cf_token_is_type(cfp, CFTOKEN_NAME, "key", nullptr);
		if (ret != PARSE_SUCCESS)
			break;

		QString key = QString::fromUtf8(cfp->cur_token->str.array,
						cfp->cur_token->str.len);
		OBSThemeVariable var;
		var.name = key;

#ifdef _WIN32
		const QString osPrefix = "os_win_";
#elif __APPLE__
		const QString osPrefix = "os_mac_";
#else
		const QString osPrefix = "os_lin_";
#endif

		if (key.startsWith(osPrefix) &&
		    key.length() > osPrefix.length()) {
			var.name = key.sliced(osPrefix.length());
		}

		ret = cf_next_token_should_be(cfp, ":", ";", nullptr);
		if (ret != PARSE_SUCCESS)
			continue;

		if (!cf_next_token(cfp))
			return vars;

		if (cfp->cur_token->type == CFTOKEN_NUM) {
			const char *ch = cfp->cur_token->str.array;
			const char *end = ch + cfp->cur_token->str.len;
			double f = os_strtod(ch);

			var.value = f;
			var.type = OBSThemeVariable::Number;

			/* Look for a suffix and mark variable as size if it exists */
			while (ch < end) {
				if (!isdigit(*ch) && !isspace(*ch) &&
				    *ch != '.') {
					var.suffix =
						QString::fromUtf8(ch, end - ch);
					var.type = OBSThemeVariable::Size;
					break;
				}
				ch++;
			}
		} else if (cf_token_is(cfp, "rgb") || cf_token_is(cfp, "#") ||
			   cf_token_is(cfp, "bikeshed")) {
			QColor color = ParseColor(cfp);
			if (!color.isValid())
				continue;

			var.value = color;
			var.type = OBSThemeVariable::Color;
		} else if (cf_token_is(cfp, "var")) {
			QString value;

			if (!ParseVarName(cfp, value))
				continue;

			var.value = value;
			var.type = OBSThemeVariable::Alias;
		} else if (cf_token_is(cfp, "calc")) {
			QStringList calc;

			if (!ParseCalc(cfp, calc, vars))
				continue;

			var.type = OBSThemeVariable::Calc;
			var.value = calc;
		} else {
			var.type = OBSThemeVariable::String;
			BPtr strVal =
				cf_literal_to_str(cfp->cur_token->str.array,
						  cfp->cur_token->str.len);
			var.value = QString::fromUtf8(strVal.Get());
		}

		if (!cf_next_token(cfp))
			return vars;

		if (cf_token_is(cfp, "!") &&
		    cf_next_token_should_be(cfp, "editable", nullptr,
					    nullptr) == PARSE_SUCCESS) {
			if (var.type == OBSThemeVariable::Calc ||
			    var.type == OBSThemeVariable::Alias) {
				blog(LOG_WARNING,
				     "Variable of calc/alias type cannot be editable: %s",
				     QT_TO_UTF8(var.name));
			} else {
				var.editable = true;
			}
		}

		vars.push_back(std::move(var));

		if (!cf_token_is(cfp, ";") &&
		    !cf_go_to_token(cfp, ";", nullptr))
			return vars;
	}

	return vars;
}

void OBSApp::FindThemes()
{
	string themeDir;
	unique_ptr<OBSTheme> theme;

	QStringList filters;
	filters << "*.obt" // OBS Base Theme
		<< "*.ovt" // OBS Variant Theme
		<< "*.oha" // OBS High-contrast Adjustment layer
		;

	GetDataFilePath("themes/", themeDir);
	QDirIterator it(QString::fromStdString(themeDir), filters, QDir::Files);
	while (it.hasNext()) {
		theme.reset(ParseThemeMeta(it.next()));
		if (theme && !themes.contains(theme->id))
			themes[theme->id] = std::move(*theme);
	}

	themeDir.resize(1024);
	if (GetConfigPath(themeDir.data(), themeDir.capacity(),
			  "obs-studio/themes/") > 0) {
		QDirIterator it(QT_UTF8(themeDir.c_str()), filters,
				QDir::Files);

		while (it.hasNext()) {
			theme.reset(ParseThemeMeta(it.next()));
			if (theme && !themes.contains(theme->id))
				themes[theme->id] = std::move(*theme);
		}
	}

	/* Build dependency tree for all themes, removing ones that have items missing. */
	QSet<QString> invalid;

	for (OBSTheme &theme : themes) {
		if (theme.extends.isEmpty()) {
			if (!theme.isBaseTheme) {
				blog(LOG_ERROR,
				     R"(Theme "%s" is not base, but does not specify parent!)",
				     QT_TO_UTF8(theme.id));
				invalid.insert(theme.id);
			}

			continue;
		}

		QString parentId = theme.extends;
		while (!parentId.isEmpty()) {
			OBSTheme *parent = GetTheme(parentId);
			if (!parent) {
				blog(LOG_ERROR,
				     R"(Theme "%s" is missing ancestor "%s"!)",
				     QT_TO_UTF8(theme.id),
				     QT_TO_UTF8(parentId));
				invalid.insert(theme.id);
				break;
			}

			if (theme.isBaseTheme && !parent->isBaseTheme) {
				blog(LOG_ERROR,
				     R"(Ancestor "%s" of base theme "%s" is not a base theme!)",
				     QT_TO_UTF8(parent->id),
				     QT_TO_UTF8(theme.id));
				invalid.insert(theme.id);
				break;
			}

			if (parent->id == theme.id ||
			    theme.dependencies.contains(parent->id)) {
				blog(LOG_ERROR,
				     R"(Dependency chain of "%s" ("%s") contains recursion!)",
				     QT_TO_UTF8(theme.id),
				     QT_TO_UTF8(parent->id));
				invalid.insert(theme.id);
				break;
			}

			/* Mark this theme as a variant of first parent that is a base theme. */
			if (!theme.isBaseTheme && parent->isBaseTheme &&
			    theme.parent.isEmpty())
				theme.parent = parent->id;

			theme.dependencies.push_front(parent->id);
			parentId = parent->extends;

			if (parentId.isEmpty() && !parent->isBaseTheme) {
				blog(LOG_ERROR,
				     R"(Final ancestor of "%s" ("%s") is not a base theme!)",
				     QT_TO_UTF8(theme.id),
				     QT_TO_UTF8(parent->id));
				invalid.insert(theme.id);
				break;
			}
		}
	}

	for (const QString &name : invalid) {
		themes.remove(name);
	}
}

static bool ResolveVariable(const QHash<QString, OBSThemeVariable> &vars,
			    OBSThemeVariable &var)
{
	const OBSThemeVariable *varPtr = &var;
	const OBSThemeVariable *realVar = varPtr;

	while (realVar->type == OBSThemeVariable::Alias) {
		QString newKey = realVar->value.toString();

		if (!vars.contains(newKey)) {
			blog(LOG_ERROR,
			     R"(Variable "%s" (aliased by "%s") does not exist!)",
			     QT_TO_UTF8(newKey), QT_TO_UTF8(var.name));
			return false;
		}

		const OBSThemeVariable &newVar = vars[newKey];
		realVar = &newVar;
	}

	if (realVar != varPtr)
		var = *realVar;

	return true;
}

static QString EvalCalc(const QHash<QString, OBSThemeVariable> &vars,
			const OBSThemeVariable &var, const int recursion = 0);

static OBSThemeVariable
ParseCalcVariable(const QHash<QString, OBSThemeVariable> &vars,
		  const QString &value, const int recursion = 0)
{
	OBSThemeVariable var;
	const QByteArray utf8 = value.toUtf8();
	const char *data = utf8.constData();

	if (isdigit(*data)) {
		double f = os_strtod(data);
		var.type = OBSThemeVariable::Number;
		var.value = f;

		const char *dataEnd = data + utf8.size();
		while (data < dataEnd) {
			if (*data && !isdigit(*data) && *data != '.') {
				var.suffix =
					QString::fromUtf8(data, dataEnd - data);
				var.type = OBSThemeVariable::Size;
				break;
			}

			data++;
		}
	} else {
		/* Treat value as an alias/key and resolve it */
		var.type = OBSThemeVariable::Alias;
		var.value = value;
		ResolveVariable(vars, var);

		/* Handle nested calc()s */
		if (var.type == OBSThemeVariable::Calc) {
			QString val = EvalCalc(vars, var, recursion + 1);
			var = ParseCalcVariable(vars, val);
		}

		/* Only number or size would be valid here */
		if (var.type != OBSThemeVariable::Number &&
		    var.type != OBSThemeVariable::Size) {
			blog(LOG_ERROR,
			     "calc() operand is not a size or number: %s",
			     QT_TO_UTF8(var.value.toString()));
			throw invalid_argument("Operand not of numeric type");
		}
	}

	return var;
}

static QString EvalCalc(const QHash<QString, OBSThemeVariable> &vars,
			const OBSThemeVariable &var, const int recursion)
{
	if (recursion >= 10) {
		/* Abort after 10 levels of recursion */
		blog(LOG_ERROR, "Maximum calc() recursion levels hit!");
		return "'Invalid expression'";
	}

	QStringList args = var.value.toStringList();
	if (args.length() != 3) {
		blog(LOG_ERROR,
		     "calc() had invalid number of arguments: %lld (%s)",
		     args.length(), QT_TO_UTF8(args.join(", ")));
		return "'Invalid expression'";
	}

	QString &opt = args[1];
	if (opt != '*' && opt != '+' && opt != '-' && opt != '/') {
		blog(LOG_ERROR, "Unknown/invalid calc() operator: %s",
		     QT_TO_UTF8(opt));
		return "'Invalid expression'";
	}

	OBSThemeVariable val1, val2;
	try {
		val1 = ParseCalcVariable(vars, args[0], recursion);
		val2 = ParseCalcVariable(vars, args[2], recursion);
	} catch (...) {
		return "'Invalid expression'";
	}

	/* Ensure that suffixes match (if any) */
	if (!val1.suffix.isEmpty() && !val2.suffix.isEmpty() &&
	    val1.suffix != val2.suffix) {
		blog(LOG_ERROR,
		     "calc() requires suffixes to match or only one to be present! %s != %s",
		     QT_TO_UTF8(val1.suffix), QT_TO_UTF8(val2.suffix));
		return "'Invalid expression'";
	}

	double val = numeric_limits<double>::quiet_NaN();
	double d1 = val1.userValue.isValid() ? val1.userValue.toDouble()
					     : val1.value.toDouble();
	double d2 = val2.userValue.isValid() ? val2.userValue.toDouble()
					     : val2.value.toDouble();

	if (!isfinite(d1) || !isfinite(d2)) {
		blog(LOG_ERROR,
		     "calc() received at least one invalid value:"
		     " op1: %f, op2: %f",
		     d1, d2);
		return "'Invalid expression'";
	}

	if (opt == "+")
		val = d1 + d2;
	else if (opt == "-")
		val = d1 - d2;
	else if (opt == "*")
		val = d1 * d2;
	else if (opt == "/")
		val = d1 / d2;

	if (!isnormal(val)) {
		blog(LOG_ERROR,
		     "Invalid calc() math resulted in non-normal number:"
		     " %f %s %f = %f",
		     d1, QT_TO_UTF8(opt), d2, val);
		return "'Invalid expression'";
	}

	bool isInteger = ceill(val) == val;
	QString result = QString::number(val, 'f', isInteger ? 0 : -1);

	/* Carry-over suffix */
	if (!val1.suffix.isEmpty())
		result += val1.suffix;
	else if (!val2.suffix.isEmpty())
		result += val2.suffix;

	return result;
}

static qsizetype FindEndOfOBSMetadata(const QString &content)
{
	/* Find end of last OBS-specific section and strip it, kinda jank but should work */
	qsizetype end = 0;

	for (auto section : {"OBSThemeMeta", "OBSThemeVars", "OBSTheme"}) {
		qsizetype idx = content.indexOf(section, 0);
		if (idx > end) {
			end = content.indexOf('}', idx) + 1;
		}
	}

	return end;
}

static QString PrepareQSS(const QHash<QString, OBSThemeVariable> &vars,
			  const QStringList &contents)
{
	QString stylesheet;
	QString needleTemplate("var(--%1)");

	for (const QString &content : contents) {
		qsizetype offset = FindEndOfOBSMetadata(content);
		if (offset >= 0) {
			stylesheet += "\n";
			stylesheet += content.sliced(offset);
		}
	}

	for (const OBSThemeVariable &var_ : vars) {
		OBSThemeVariable var(var_);

		if (!ResolveVariable(vars, var))
			continue;

		QString needle = needleTemplate.arg(var_.name);
		QString replace;

		QVariant value = var.userValue.isValid() ? var.userValue
							 : var.value;

		if (var.type == OBSThemeVariable::Color) {
			replace = value.value<QColor>().name(QColor::HexRgb);
		} else if (var.type == OBSThemeVariable::Calc) {
			replace = EvalCalc(vars, var);
		} else if (var.type == OBSThemeVariable::Size ||
			   var.type == OBSThemeVariable::Number) {
			double val = value.toDouble();
			bool isInteger = ceill(val) == val;
			replace = QString::number(val, 'f', isInteger ? 0 : -1);

			if (!var.suffix.isEmpty())
				replace += var.suffix;
		} else {
			replace = value.toString();
		}

		stylesheet = stylesheet.replace(needle, replace);
	}

	return stylesheet;
}

template<typename T> static void FillEnumMap(QHash<QString, T> &map)
{
	QMetaEnum meta = QMetaEnum::fromType<T>();

	int numKeys = meta.keyCount();
	for (int i = 0; i < numKeys; i++) {
		const char *key = meta.key(i);
		QString keyName(key);
		map[keyName.toLower()] = static_cast<T>(meta.keyToValue(key));
	}
}

static QPalette PreparePalette(const QHash<QString, OBSThemeVariable> &vars,
			       const QPalette &defaultPalette)
{
	static QHash<QString, QPalette::ColorRole> roleMap;
	static QHash<QString, QPalette::ColorGroup> groupMap;

	if (roleMap.empty())
		FillEnumMap<QPalette::ColorRole>(roleMap);
	if (groupMap.empty())
		FillEnumMap<QPalette::ColorGroup>(groupMap);

	QPalette pal(defaultPalette);

	for (const OBSThemeVariable &var_ : vars) {
		if (!var_.name.startsWith("palette_"))
			continue;
		if (var_.name.count("_") < 1 || var_.name.count("_") > 2)
			continue;

		OBSThemeVariable var(var_);
		if (!ResolveVariable(vars, var) ||
		    var.type != OBSThemeVariable::Color)
			continue;

		/* Determine role and optionally group based on name.
		 * Format is: palette_<role>[_<group>] */
		QPalette::ColorRole role = QPalette::NoRole;
		QPalette::ColorGroup group = QPalette::All;

		QStringList parts = var_.name.split("_");
		if (parts.length() >= 2) {
			QString key = parts[1].toLower();
			if (!roleMap.contains(key)) {
				blog(LOG_WARNING,
				     "Palette role \"%s\" is not valid!",
				     QT_TO_UTF8(parts[1]));
				continue;
			}
			role = roleMap[key];
		}

		if (parts.length() == 3) {
			QString key = parts[2].toLower();
			if (!groupMap.contains(key)) {
				blog(LOG_WARNING,
				     "Palette group \"%s\" is not valid!",
				     QT_TO_UTF8(parts[2]));
				continue;
			}
			group = groupMap[key];
		}

		QVariant value = var.userValue.isValid() ? var.userValue
							 : var.value;

		QColor color = value.value<QColor>().name(QColor::HexRgb);
		pal.setColor(group, role, color);
	}

	return pal;
}

OBSTheme *OBSApp::GetTheme(const QString &name)
{
	if (!themes.contains(name))
		return nullptr;

	return &themes[name];
}

bool OBSApp::SetTheme(const QString &name)
{
	OBSTheme *theme = GetTheme(name);
	if (!theme)
		return false;

	if (themeWatcher) {
		themeWatcher->blockSignals(true);
		themeWatcher->removePaths(themeWatcher->files());
	}

	setStyleSheet("");
	currentTheme = theme;

	QStringList contents;
	QHash<QString, OBSThemeVariable> vars;
	/* Build list of themes to load (in order) */
	QStringList themeIds(theme->dependencies);
	themeIds << theme->id;

	/* Find and add high contrast adjustment layer if available */
	if (HighContrastEnabled()) {
		for (const OBSTheme &theme_ : themes) {
			if (!theme_.isHighContrast)
				continue;
			if (theme_.parent != theme->id)
				continue;
			themeIds << theme_.id;
			break;
		}
	}

	QStringList filenames;
	for (const QString &themeId : themeIds) {
		OBSTheme *cur = GetTheme(themeId);

		QFile file(cur->location);
		filenames << file.fileName();

		if (!file.open(QIODeviceBase::ReadOnly))
			return false;
		const QByteArray content = file.readAll();

		for (OBSThemeVariable &var :
		     ParseThemeVariables(content.constData())) {
			vars[var.name] = std::move(var);
		}

		contents.emplaceBack(content.constData());
	}

	const QString stylesheet = PrepareQSS(vars, contents);
	const QPalette palette = PreparePalette(vars, defaultPalette);
	setPalette(palette);
	setStyleSheet(stylesheet);

#ifdef _DEBUG
	/* Write resulting QSS to file in config dir "themes" folder. */
	string filename("obs-studio/themes/");
	filename += theme->id.toStdString();
	filename += ".out";

	filesystem::path debugOut;
	char configPath[512];
	if (GetConfigPath(configPath, sizeof(configPath), filename.c_str())) {
		debugOut = absolute(filesystem::u8path(configPath));
		filesystem::create_directories(debugOut.parent_path());
	}

	QFile debugFile(debugOut);
	if (debugFile.open(QIODeviceBase::WriteOnly)) {
		debugFile.write(stylesheet.toUtf8());
		debugFile.flush();
	}
#endif

#ifdef __APPLE__
	SetMacOSDarkMode(theme->isDark);
#endif

	emit StyleChanged();

	if (themeWatcher) {
		themeWatcher->addPaths(filenames);
		/* Give it 250 ms before re-enabling the watcher to prevent too
		 * many reloads when edited with an auto-saving IDE. */
		QTimer::singleShot(250, this,
				   [&] { themeWatcher->blockSignals(false); });
	}

	return true;
}

void OBSApp::themeFileChanged(const QString &path)
{
	themeWatcher->blockSignals(true);
	blog(LOG_INFO, "Theme file \"%s\" changed, reloading...",
	     QT_TO_UTF8(path));
	SetTheme(currentTheme->id);
}

static map<string, string> themeMigrations = {
	{"Yami", DEFAULT_THEME},
	{"Grey", "com.obsproject.Yami.Grey"},
	{"Rachni", "com.obsproject.Yami.Rachni"},
	{"Light", "com.obsproject.Yami.Light"},
	{"Dark", "com.obsproject.Yami.Classic"},
	{"Acri", "com.obsproject.Yami.Acri"},
	{"System", "com.obsproject.System"},
};

bool OBSApp::InitTheme()
{
	defaultPalette = palette();
#if !defined(_WIN32) && !defined(__APPLE__)
	setStyle(new OBSProxyStyle("Fusion"));
#else
	setStyle(new OBSProxyStyle());
#endif

	/* Set search paths for custom 'theme:' URI prefix */
	string searchDir;
	if (GetDataFilePath("themes", searchDir)) {
		auto installSearchDir = filesystem::u8path(searchDir);
		QDir::addSearchPath("theme", absolute(installSearchDir));
	}

	char userDir[512];
	if (GetConfigPath(userDir, sizeof(userDir), "obs-studio/themes")) {
		auto configSearchDir = filesystem::u8path(userDir);
		QDir::addSearchPath("theme", absolute(configSearchDir));
	}

	/* Load list of themes and read their metadata */
	FindThemes();

	if (config_get_bool(globalConfig, "Appearance", "AutoReload")) {
		/* Set up Qt file watcher to automatically reload themes */
		themeWatcher = new QFileSystemWatcher(this);
		connect(themeWatcher.get(), &QFileSystemWatcher::fileChanged,
			this, &OBSApp::themeFileChanged);
	}

	/* Migrate old theme config key */
	if (config_has_user_value(globalConfig, "General", "CurrentTheme3") &&
	    !config_has_user_value(globalConfig, "Appearance", "Theme")) {
		const char *old = config_get_string(globalConfig, "General",
						    "CurrentTheme3");

		if (themeMigrations.count(old)) {
			config_set_string(globalConfig, "Appearance", "Theme",
					  themeMigrations[old].c_str());
		}
	}

	QString themeName =
		config_get_string(globalConfig, "Appearance", "Theme");

	if (themeName.isEmpty() || !GetTheme(themeName)) {
		if (!themeName.isEmpty()) {
			blog(LOG_WARNING,
			     "Loading theme \"%s\" failed, falling back to "
			     "default theme (\"%s\").",
			     QT_TO_UTF8(themeName), DEFAULT_THEME);
		}
#ifdef _WIN32
		themeName = HighContrastEnabled() ? "com.obsproject.System"
						  : DEFAULT_THEME;
#else
		themeName = DEFAULT_THEME;
#endif
	}

	if (!SetTheme(themeName)) {
		blog(LOG_ERROR,
		     "Loading default theme \"%s\" failed, falling back to "
		     "system theme as last resort.",
		     QT_TO_UTF8(themeName));
		return SetTheme("com.obsproject.System");
	}

	return true;
}
