#include "SH_plugins_loader.h"
#include "SH_runtime.h"
#include <QMetaEnum>
#include <QStandardPaths>
#include <QMetaObject>
#include <QLibrary>
#include <pifile.h>
#include <piqt.h>


// SHPluginsLoader::PluginInfo

QString SHPluginsLoader::PluginInfo::key() const {
	if (!plugin) return QString();
	return plugin->name() + QString::number(plugin->type());
}


QString SHPluginsLoader::PluginInfo::displayType() const {
	if (!plugin) return QString();
	QMetaEnum tme = getMetaEnum<SHPluginBase>("PluginType");
	return tme.valueToKey(plugin->type());
}


QString SHPluginsLoader::PluginInfo::fullName() const {
	return manifest.name + " (" + displayType() + ")";
}


// SHPluginsLoader::PluginLibrary

bool SHPluginsLoader::PluginLibrary::isLoaded() const {
	if (!library) return false;
	return library->isLoaded();
}


bool SHPluginsLoader::PluginLibrary::isDisabled() const {
	if (!parent) return false;
	return parent->disabled.contains(manifest.name);
}


bool SHPluginsLoader::PluginLibrary::isError() const {
	return !load_error.isEmpty();
}


bool SHPluginsLoader::PluginLibrary::load(bool force) {
	unload();
	if (!parent) return false;
	if (!force && parent->disabled.contains(manifest.name))
		return false;
	if (!manifest.checkVersions()) {
		load_error = QString("Invalid version");
		return false;
	}
	library = new PIPluginLoader();
	if (!library->load(Q2PIString(manifest.libpath))) {
		load_error = PI2QString(library->lastErrorText());
		piDeleteSafety(library);
		return false;
	}
	typedef QList<SHPluginFactoryBase*>(*PluginListGetFunction)();
	PluginListGetFunction cplfunc = reinterpret_cast<PluginListGetFunction>(library->resolve("getPluginList"));
	if (!cplfunc) {
		qDebug() << "[SHPluginsLoader::loadPlugin] Can`t resolve \"getPluginList()\" function in plugin" << manifest.libpath << "! Maybe you forgot \"SHS_PLUGIN\" macro?";
		load_error = PI2QString(library->lastErrorText());
		piDeleteSafety(library);
		return false;
	}
	auto facts = cplfunc();
	int count = 0;
	for (auto * f: facts) {
		if (!f) continue;
		SHPluginBase * p = f->createPlugin();
		if (!p) continue;
		p->_rt = RUNTIME;
		PluginInfo pi;
		pi.plugin = p;
		pi.manifest = manifest;
		pi.flags = p->flags();
		pi.type = p->type();
		if (parent->registerPlugin(pi)) {
			++count;
			plugins << pi;
		} else {
			delete p;
		}
	}
	qDebug() << "[SHPluginsLoader::loadPlugin]" << (manifest.isBuiltin() ? QString("Builtin") : manifest.file) << ": loaded" << count << "plugins";
	return true;

}


void SHPluginsLoader::PluginLibrary::unload() {
	load_error.clear();
	if (library)
		qDebug() << "[SHPluginsLoader::unloadPlugin]" << (manifest.isBuiltin() ? QString("Builtin") : manifest.file);
	for (auto p: plugins) {
		if (parent && p.plugin) {
			emit parent->aboutUnloadPlugin(p.plugin);
			parent->plugin_by_id.remove(p.plugin->id());
		}
		piDeleteSafety(p.plugin);
	}
	piDeleteSafety(library);
	plugins.clear();
}


// SHPluginsLoader

SHPluginsLoader::SHPluginsLoader(): QObject() {
	QString cache_path = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/SHS/v" + QString::number(SHS_VERSION_MAJOR) + "/";
	web_dir = cache_path + "plugins";
	QDir wd(web_dir);
	if (!wd.exists()) wd.mkpath(".");
	disabled_filepath = Q2PIString(cache_path) + "disabled.bin";
	disabled = piDeserialize<QStringList>(PIFile::readAll(disabled_filepath));
	//qDebug() << web_dir;
}


SHPluginsLoader::~SHPluginsLoader() {
	unloadPlugins();
}


void SHPluginsLoader::loadPlugins() {
	unloadPlugins();
	for (const PluginInfo & i: builtinPlugins()) {
		registerPlugin(i);
	}
	readManifests();
	for (const auto & m: plugin_libraries)
		loadPlugin(m.second.manifest);
	emit loaded();
}


SHPluginsLoader::PluginInfo SHPluginsLoader::getPlugin(const QString & name, SHPluginBase::PluginType type) const {
	return plugin_by_id.value(SHPluginBase::getID(name, type));
}


SHPluginsLoader::PluginInfo SHPluginsLoader::getPlugin(uint id) const {
	return plugin_by_id.value(id);
}


PIVector<SHPluginsLoader::PluginInfo> SHPluginsLoader::getPlugins(SHPluginBase::PluginType type) const {
	PIVector<SHPluginsLoader::PluginInfo> ret;
	auto it = plugin_by_id.makeIterator();
	while (it.next()) {
		if ((it.value().type == type) && it.value().plugin)
			ret << it.value();
	}
	return ret;
}


PIVector<SHPluginsLoader::PluginInfo> SHPluginsLoader::loadedPlugins() const {
	return plugin_by_id.values();
}


void SHPluginsLoader::readManifests() {
	QString prev_cd = QDir::currentPath();
	PIScopeExitCall exit_call([prev_cd]{QDir::setCurrent(prev_cd);});
	QDir::setCurrent(QApplication::applicationDirPath());
	QStringList dirs = PI2QStringList(PIPluginLoader::pluginsDirectories("SH_Plugins"));
	dirs << web_dir;
	//if (dirs.isEmpty()) {
	//	qDebug() << "[SHPluginsLoader::loadPlugins] Can`t find \"SH_Plugins\" directory!";
	//	return false;
	//}
	for (QString d: dirs) {
		QFileInfoList fl = QDir(d).entryInfoList({"*.manifest"}, QDir::NoDotAndDotDot | QDir::Files);
		for (const QFileInfo & f: fl) {
			SHPluginManifest m;
			if (m.read(f.absoluteFilePath())) {
				auto & pl(plugin_libraries[m.name]);
				pl.parent = this;
				pl.manifest = m;
			}
		}
	}
}


void SHPluginsLoader::addBuiltinPlugin(SHPluginBase * p) {
	if (!p) return;
	for (const auto & i: builtinPlugins()) {
		if ((i.plugin->name() == p->name()) && (i.plugin->type() == p->type())) {
			delete p;
			return;
		}
	}
	//qDebug() << "add builtin plugin" << p->name();
	p->_rt = RUNTIME;
	PluginInfo dbp;
	dbp.plugin = p;
	dbp.flags = p->flags();
	dbp.type = p->type();
	dbp.manifest.fillBuiltin(p);
	builtinPlugins() << dbp;
}


void SHPluginsLoader::unloadPlugins() {
	if (plugin_by_id.size_s() > builtinPlugins().size())
		qDebug() << "[SHPluginsLoader::unloadPlugins]";
	emit aboutUnload();
	auto it = plugin_libraries.makeIterator();
	while (it.next()) {
		it.value().unload();
	}
	plugin_by_id.clear();
	plugin_libraries.clear();
}


void SHPluginsLoader::unloadPlugin(const QString & name) {
	if (name.isEmpty()) return;
	if (!plugin_libraries.contains(name)) return;
	plugin_libraries[name].unload();
}


void SHPluginsLoader::removePlugin(const QString & name) {
	plugin_libraries.remove(name);
}


void SHPluginsLoader::enablePlugin(const QString & name) {
	if (!disabled.contains(name)) return;
	disabled.removeAll(name);
	saveDisabled();
	loadPlugin(name);
}


void SHPluginsLoader::disablePlugin(const QString & name) {
	if (disabled.contains(name)) return;
	disabled << name;
	saveDisabled();
	unloadPlugin(name);
}


bool SHPluginsLoader::loadPlugin(const SHPluginManifest & manifest, bool force) {
	return loadPlugin(manifest.name, force);
}


bool SHPluginsLoader::loadPlugin(const QString & name, bool force) {
	if (name.isEmpty()) return false;
	if (!plugin_libraries.contains(name)) return false;
	return plugin_libraries[name].load(force);
}


bool SHPluginsLoader::registerPlugin(const SHPluginsLoader::PluginInfo & pi) {
	auto id = pi.plugin->id();
	if (plugin_by_id.contains(id)) {
		qDebug() << "[SHPluginsLoader::registerPlugin] Plugin" << pi.fullName() << "already registered! " << pi.manifest.file << " ignored";
		return false;
	}
	plugin_by_id[pi.plugin->id()] = pi;
	return true;
}


void SHPluginsLoader::saveDisabled() {
	PIFile::writeAll(disabled_filepath, piSerialize(disabled));
}


QList<SHPluginsLoader::PluginInfo> & SHPluginsLoader::builtinPlugins() {
	static QList<SHPluginsLoader::PluginInfo> ret;
	return ret;
}
