#include "configobject.hpp"

#include <QJsonArray>
#include <QMetaObject>
#include <QStringList>
#include <QVariant>

namespace ZShell::config {

namespace {

bool isStringArray(const QJsonArray& arr) {
	for (const auto& v : arr) {
		if (!v.isString()) return false;
	}
	return true;
}

bool jsonValueMatchesType(const QJsonValue& val, QMetaType type) {
	switch (val.type()) {
	case QJsonValue::Bool:
		return type.id() == QMetaType::Bool;

	case QJsonValue::Double:
		switch (type.id()) {
		case QMetaType::Int:
		case QMetaType::UInt:
		case QMetaType::LongLong:
		case QMetaType::ULongLong:
		case QMetaType::Double:
		case QMetaType::Float:
			return true;
		default:
			return false;
		}

	case QJsonValue::String:
		return type.id() == QMetaType::QString;

	case QJsonValue::Array:
		if (type.id() == QMetaType::QStringList)
			return isStringArray(val.toArray());
		return type.id() == QMetaType::QVariantList;

	case QJsonValue::Object:
	case QJsonValue::Null:
	case QJsonValue::Undefined:
	default:
		return false;
	}
}

} // namespace

ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {}

void ConfigObject::loadFromJson(const QJsonValue& json) {
	const auto obj = json.toObject();
	const auto* meta = metaObject();

	QSet<QString> known;
	QSet<QString> invalid;

	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		auto prop = meta->property(i);
		const auto key = QString::fromUtf8(prop.name());

		known.insert(key);

		if (!obj.contains(key)) continue;

		const auto jsonVal = obj.value(key);

		if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
			if (auto* const node = prop.read(this).value<ConfigNode*>()) {
				node->loadFromJson(jsonVal);
				continue;
			}
		}

		if (!prop.isWritable()) continue;

		if (!jsonValueMatchesType(jsonVal, prop.metaType())) {
			qWarning() << "Config: type mismatch for" << key << "in"
					   << meta->className() << "- expected"
					   << prop.metaType().name() << "got value" << jsonVal
					   << "- resetting to default";
			invalid.insert(key);
			continue;
		}

		if (prop.metaType().id() == QMetaType::QStringList) {
			QStringList list;
			const auto jsonArr = jsonVal.toArray();
			for (const auto& v : jsonArr)
				list.append(v.toString());
			prop.write(this, QVariant::fromValue(list));
			m_loadedKeys.insert(key);
			continue;
		}

		if (prop.metaType().id() == QMetaType::QVariantList) {
			prop.write(this, jsonVal.toVariant());
			m_loadedKeys.insert(key);
			continue;
		}

		prop.write(this, jsonVal.toVariant());
		m_loadedKeys.insert(key);
	}

	m_extras = {};
	for (auto it = obj.begin(); it != obj.end(); ++it) {
		if (!known.contains(it.key()) || invalid.contains(it.key())) {
			m_extras.insert(it.key(), it.value());
		}
	}

	materializeDefaults();
}

QJsonValue ConfigObject::toJson() const {
	QJsonObject obj;
	const auto* meta = metaObject();

	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		const auto prop = meta->property(i);

		if (!prop.isReadable()) continue;

		const auto key = QString::fromUtf8(prop.name());

		const auto value = prop.read(this);

		if (prop.metaType().flags().testFlag(QMetaType::PointerToQObject)) {
			if (auto* const node = value.value<ConfigNode*>()) {
				const auto childJson = node->toJson();
				if (!childJson.isUndefined()) obj.insert(key, childJson);
				continue;
			}
		}

		if (!m_loadedKeys.contains(key)) continue;

		if (!prop.isWritable()) continue;

		if (prop.metaType().id() == QMetaType::QStringList) {
			QJsonArray arr;
			const auto strList = value.toStringList();
			for (const auto& s : strList)
				arr.append(s);
			obj.insert(key, arr);
			continue;
		}

		if (prop.metaType().id() == QMetaType::QVariantList) {
			obj.insert(key, QJsonArray::fromVariantList(value.toList()));
			continue;
		}

		obj.insert(key, QJsonValue::fromVariant(value));
	}

	if (obj.isEmpty()) return QJsonValue::Undefined;

	return obj;
}

void ConfigObject::clearLoadedKeys() {
	m_loadedKeys.clear();
	m_extras = {};

	const auto* meta = metaObject();
	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		auto prop = meta->property(i);
		if (auto* const node = prop.read(this).value<ConfigNode*>())
			node->clearLoadedKeys();
	}
}

QStringList ConfigObject::identityKeys() const {
	return {};
}

QStringList ConfigObject::unknownKeys() const {
	auto keys = m_extras.keys();

	const auto* meta = metaObject();
	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		const auto prop = meta->property(i);
		auto* const node = prop.read(this).value<ConfigNode*>();
		if (!node) continue;

		const auto childKeys = node->unknownKeys();
		for (const auto& childKey : childKeys)
			keys.append(joinPath(QString::fromUtf8(prop.name()), childKey));
	}

	return keys;
}

QList<ConfigNode*> ConfigObject::childNodes() const {
	QList<ConfigNode*> nodes;

	const auto* meta = metaObject();
	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		const auto prop = meta->property(i);

		if (auto* const node = prop.read(this).value<ConfigNode*>())
			nodes.append(node);
	}

	return nodes;
}

void ConfigObject::materializeDefaults() {
	const auto* meta = metaObject();

	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		const auto prop = meta->property(i);
		const auto key = QString::fromUtf8(prop.name());

		if (auto* const node = prop.read(this).value<ConfigNode*>()) {
			node->materializeDefaults();
			continue;
		}

		if (!prop.isWritable()) continue;

		if (m_global) continue;

		m_loadedKeys.insert(key);
	}
}

void ConfigObject::syncValuesFromGlobal() {
	const auto* meta = metaObject();

	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		auto prop = meta->property(i);
		const auto key = QString::fromUtf8(prop.name());

		if (auto* const node = prop.read(this).value<ConfigNode*>()) {
			if (auto* const globalNode =
					prop.read(m_global).value<ConfigNode*>())
				node->syncFromGlobal(globalNode);
			continue;
		}

		if (!prop.isWritable()) continue;

		if (!m_loadedKeys.contains(key)) {
			auto val = prop.read(m_global);
			prop.write(this, val);
			m_loadedKeys.remove(key);
		}
	}
}

void ConfigObject::resyncFromGlobal() {
	if (!m_global) return;

	const auto* meta = metaObject();
	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		auto prop = meta->property(i);
		const auto key = QString::fromUtf8(prop.name());

		if (auto* const node = prop.read(this).value<ConfigNode*>()) {
			node->resyncFromGlobal();
			continue;
		}

		if (!prop.isWritable()) continue;

		if (!m_loadedKeys.contains(key)) {
			prop.write(this, prop.read(m_global));
			m_loadedKeys.remove(key);
		}
	}
}

bool ConfigObject::isPropertyLoaded(const QString& name) const {
	return m_loadedKeys.contains(name);
}

bool ConfigObject::isGlobalOnly(const QString& name) const {
	return m_globalOnlyKeys.contains(name);
}

QStringList ConfigObject::globalOnlyKeys() const {
	return {m_globalOnlyKeys.begin(), m_globalOnlyKeys.end()};
}

void ConfigObject::resetOption(const QString& name) {
	m_loadedKeys.remove(name);

	const int idx = metaObject()->indexOfProperty(name.toUtf8().constData());
	if (idx < 0) return;

	const auto prop = metaObject()->property(idx);

	if (auto* const node = prop.read(this).value<ConfigNode*>()) {
		node->clearLoadedKeys();
		node->resyncFromGlobal();
		return;
	}

	if (m_global && prop.isWritable()) prop.write(this, prop.read(m_global));
}

void ConfigObject::onGlobalPropertiesChanged(
	const QMap<QString, QVariant>& changed) {
	for (auto it = changed.begin(); it != changed.end(); ++it) {
		if (m_loadedKeys.contains(it.key()) || isGlobalOnly(it.key())) continue;

		int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData());
		if (idx >= 0) {
			metaObject()->property(idx).write(this, it.value());
			m_loadedKeys.remove(it.key());
		}
	}
}

QString ConfigObject::childPath(const ConfigNode* child) const {
	const auto* meta = metaObject();
	for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
		const auto prop = meta->property(i);

		if (prop.read(this).value<ConfigNode*>() == child)
			return QString::fromUtf8(prop.name());
	}

	return {};
}

void ConfigObject::markPropertyLoaded(const QString& name) {
	m_loadedKeys.insert(name);
}

void ConfigObject::markGlobalOnly(const QString& name) {
	m_globalOnlyKeys.insert(name);
}

} // namespace ZShell::config
