/*
    PIP - Platform Independent Primitives
    Plugin helpers
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef MICRO_PIP

#  include "piplugin.h"

#  include "pidir.h"
#  include "pifile.h"
#  include "piincludes_p.h"

//! \class PIPluginLoader piplugin.h
//! \~\details
//! \~english \section PIPluginLoader_sec0 Synopsis
//! \~russian \section PIPluginLoader_sec0 Краткий обзор
//!
//! \~english
//! This file provides several macro to define plugin and %PIPluginLoader - class
//! to load and check plugin.
//!
//! \~russian
//! Этот файл предоставляет несколько макросов для объявления плагина и
//! %PIPluginLoader - класс для его загрузки и проверки.
//!
//!
//! \~english \section PIPluginLoader_sec1 Plugin side
//! \~russian \section PIPluginLoader_sec1 Сторона плагина
//! \~english
//! Plugin is a shared library that can be loaded in run-time.
//! This is only \a PIP_PLUGIN macro necessary to define plugin.
//! If you want to set and check some version, use macro
//! \a PIP_PLUGIN_SET_USER_VERSION(version). Also you can
//! define a function to merge static sections between application
//! and plugin with macro \a PIP_PLUGIN_STATIC_SECTION_MERGE. Before
//! merge, you should set pointers to that sections with macro
//! \a PIP_PLUGIN_ADD_STATIC_SECTION(type, ptr).
//!
//! \~russian
//! Плагин - это разделяемая библиотека, которая может быть
//! загружена динамически. Необходимым является только макрос \a PIP_PLUGIN.
//! Можно установить версию для дополнительной проверки с помощью
//! макроса \a PIP_PLUGIN_SET_USER_VERSION(version). Также
//! можно объявить метод для слияния статических секций между
//! приложением и плагином с помощью макроса \a PIP_PLUGIN_STATIC_SECTION_MERGE.
//! Перед слиянием необходимо установить указатели для секций с
//! помощью макроса \a PIP_PLUGIN_ADD_STATIC_SECTION(type, ptr).
//!
//!
//! \~english \section PIPluginLoader_sec2 Application side
//! \~russian \section PIPluginLoader_sec2 Сторона приложения
//! \~english
//! Application should use class \a PIPluginLoader to load
//! plugin. Main function is \a load(PIString name).
//! "name" is base name of library, %PIPluginLoader
//! try to use sevaral names, \<name\>, lib\<name\> and
//! "dll", "so" and "dylib" extensions, depends on system.\n
//! For example:
//!
//! \~russian
//! Приложение должно использовать класс \a PIPluginLoader
//! для загрузки плагина. Основной метод - это \a load(PIString name).
//! "name" является базовым именем для поиска библиотеки.
//! %PIPluginLoader попробует несколько имен, \<name\>, lib\<name\> и
//! расширения "dll", "so" и "dylib", в зависимости от системы.\n
//! Например:
//!
//! \~\code
//! PIPluginLoader l;
//! l.load("foo");
//! \endcode
//!
//! \~english
//! On Windows, try to open "foo", "libfoo", "foo.dll" and
//! "libfoo.dll".\n
//! If you using user version check, you should set it
//! with macro \a PIP_PLUGIN_SET_USER_VERSION(version).
//! When plugin is successfully loaded and checked,
//! you can load your custom symbols with function
//! \a resolve(name), similar to PILibrary.
//! \note You should use \a PIP_PLUGIN_EXPORT and [C-linkage](https://en.cppreference.com/w/cpp/language/language_linkage)
//! with functions you want to use with \a resolve(name)!
//!
//! \~russian
//! На Windows будут опробованы "foo", "libfoo", "foo.dll" и
//! "libfoo.dll".\n
//! Если нужна проверка пользовательской версии, установите
//! ей с помощью макроса \a PIP_PLUGIN_SET_USER_VERSION(version).
//! Когда плагин будет успешно загружен и проверен, можно будет
//! получать из него свои методы с помощью \a resolve(name),
//! аналогично PILibrary.
//! \note Необходимо использовать \a PIP_PLUGIN_EXPORT и [C-linkage](https://en.cppreference.com/w/cpp/language/language_linkage)
//! для методов, получаемых через \a resolve(name)!
//!
//!
//! \~english \section PIPluginLoader_sec3 Static sections
//! \~russian \section PIPluginLoader_sec3 Статические секции
//! \~english
//! Macro \a PIP_PLUGIN_STATIC_SECTION_MERGE defines function
//! with arguments (int type, void * from, void * to), so you
//! can leave this macro as declaration or define its body next:
//!
//! \~russian
//! Макрос \a PIP_PLUGIN_STATIC_SECTION_MERGE объявляет метод
//! с аргументами (int type, void * from, void * to), поэтому
//! можно оставить его как объявление, или реализовать тут же:
//!
//! \~\code
//! PIP_PLUGIN_STATIC_SECTION_MERGE() {
//!		switch (type) {
//!		...
//!		}
//! }
//! \endcode
//!
//! \~english
//! \note If you using singletones, remember that cpp-defined
//! singletones in shared libraries are single for whole application,
//! including plugins! But if you use h-defined singletones or
//! static linking, there are many objects in application and you
//! should merge their content with this macro.
//!
//! Anyway, if this is macro \a PIP_PLUGIN_STATIC_SECTION_MERGE, it
//! called once while loading plugin with "from" - plugin side
//! and "to" - application side, and second (optionally) on method
//! \a mergeStatic() with "from" - application side and "to" - plugin side.\n
//! First direction allow you to copy all defined static content from plugin
//! to application, and second - after loading all plugins (for example)
//! to copy static content from application (and all other plugins) to plugin.
//!
//! \~russian
//! \note При использовании синглтонов надо помнить, что реализованные
//! в cpp синглтоны в разделяемых библиотеках едины для всего приложения,
//! включая плагины! Однако реализованные в h-файлах синглтоны, либо
//! из статических библиотек, не являются реальными одиночками,
//! и для каждого плагина и приложения будет свой объект. В этом случае
//! можно провести слияние статических секций.
//!
//! В любом случае, если используется макрос \a PIP_PLUGIN_STATIC_SECTION_MERGE,
//! то он вызывается первый раз во время загрузки плагина с "from" - областью плагина,
//! "to" - областью приложения, и второй раз (необязательно) при вызове
//! \a mergeStatic() с "from" - областью приложения и "to" - областью плагина.\n
//! Первый вызов позволяет скопировать статические секции из плагина в приложение,
//! второй - из приложения (и всех остальных плагинов) в плагин.
//!
//! \~english \section PIPluginLoader_sec4 Examples
//! \~russian \section PIPluginLoader_sec4 Примеры
//! \~english Simple plugin:
//! \~russian Простой плагин:
//! \~\code
//! #include <piplugin.h>
//!
//! PIP_PLUGIN
//!
//! extern "C" {
//! 	PIP_PLUGIN_EXPORT void myFunc() {
//! 		piCout << "Hello plugin!";
//! 	}
//! }
//! \endcode
//!
//! \~english Application:
//! \~russian Приложение:
//! \~\code
//! #include <piplugin.h>
//! int main() {
//! 	PIPluginLoader pl;
//! 	pl.load("your_lib");
//! 	if (pl.isLoaded()) {
//! 		typedef void(*MyFunc)();
//! 		MyFunc f = (MyFunc)pl.resolve("myFunc");
//! 		if (f) f();
//! 	}
//! 	return 0;
//! }
//! \endcode
//!
//! \~english Complex plugin:
//! \~russian Сложный плагин:
//! \~\code
//! #include <piplugin.h>
//!
//! PIStringList global_list;
//!
//! PIP_PLUGIN
//! PIP_PLUGIN_SET_USER_VERSION("1.0.0")
//! PIP_PLUGIN_ADD_STATIC_SECTION(1, &global_list)
//!
//! STATIC_INITIALIZER_BEGIN
//! 	global_list << "plugin_init";
//! STATIC_INITIALIZER_END
//!
//! PIP_PLUGIN_STATIC_SECTION_MERGE {
//! 	PIStringList * sfrom = (PIStringList*)from, * sto = (PIStringList*)to;
//! 	*sto << *sfrom;
//! 	sto->removeDuplicates();
//! }
//! \endcode
//!
//! \~english Application:
//! \~russian Приложение:
//! \~\code
//! #include <piplugin.h>
//!
//! PIStringList global_list;
//!
//! PIP_PLUGIN_SET_USER_VERSION("1.0.0");
//! PIP_PLUGIN_ADD_STATIC_SECTION(1, &global_list);
//!
//! int main() {
//! 	global_list << "app";
//! 	PIPluginLoader pl;
//! 	pl.load("your_lib");
//! 	pl.mergeStatic();
//! 	piCout << "list =" << global_list;
//! 	return 0;
//! }
//! \endcode
//!

#  define STR_WF(s) #s
#  define STR(s)    STR_WF(s)


PIPluginInfo::PIPluginInfo() {}


void PIPluginInfo::setUserVersion(const PIString & v) {
	user_version = v;
}


void PIPluginInfo::setStaticSection(int type, void * ptr) {
	static_sections[type] = ptr;
}


PIString PIPluginInfo::userVersion() const {
	return user_version;
}


PIMap<int, void *> PIPluginInfo::staticSections() const {
	return static_sections;
}


PIPluginInfoStorage::PIPluginInfoStorage() {
	enterPlugin(0);
}


PIPluginInfo * PIPluginInfoStorage::currentInfo() {
	return info[current];
}


PIPluginInfo * PIPluginInfoStorage::pluginInfo(void * p) {
	return info.value(p, nullptr);
}


PIPluginInfo * PIPluginInfoStorage::applicationInfo() {
	return info[0];
}


PIPluginInfo * PIPluginInfoStorage::enterPlugin(void * p) {
	current = p;
	PIPluginInfo *& i(info[p]);
	if (!i) i = new PIPluginInfo();
	return i;
}


void PIPluginInfoStorage::unloadPlugin(void * p) {
	if (current == p) current = 0;
	PIPluginInfo *& i(info[p]);
	if (i) delete i;
	info.remove(p);
}


PIPluginInfoStorage * PIPluginInfoStorage::instance() {
	static PIPluginInfoStorage ret;
	return &ret;
}


PIPluginLoader::PIPluginLoader(const PIString & name) {
	func_loader_version = nullptr;
	func_static_merge   = nullptr;
	error               = Unknown;
	loaded              = false;
	messages            = true;
	if (!name.isEmpty()) load(name);
}


PIPluginLoader::~PIPluginLoader() {
	unload();
}


//! \~\details
//! \~english
//! Loader try prefix "lib" and suffix ".dll",
//! ".so" or ".dylib", depends on platform.
//! \~russian
//! Загрузчик пробует приставку "lib" и окончания
//! ".dll", ".so" или ".dylib", в зависимости от системы
bool PIPluginLoader::load(const PIString & name) {
	unload();
	PIPluginInfo * ai = PIPluginInfoStorage::instance()->applicationInfo();
	PIPluginInfo * pi = PIPluginInfoStorage::instance()->enterPlugin(this);
	PIString fname    = findLibrary(name);
	if (fname.isEmpty()) {
		error     = NoSuchFile;
		error_str = "Load plugin \"" + lib.path() + "\" error: can`t find lib for \"" + name + "\"";
		if (messages) piCout << error_str;
		return false;
	}
	if (!lib.load(fname)) {
		unload();
		error     = LibraryLoadError;
		error_str = "Load plugin \"" + lib.path() + "\" error: can`t load lib: " + lib.lastError();
		if (messages) piCout << error_str;
		return false;
	}
	// piCout << "loading" << lib.path() << "...";
	func_loader_version = (FunctionLoaderVersion)lib.resolve(STR(__PIP_PLUGIN_LOADER_VERSION_FUNC__));
	if (!func_loader_version) {
		unload();
		error     = MissingSymbols;
		error_str = "Load plugin \"" + lib.path() + "\" error: can`t find " + STR(__PIP_PLUGIN_LOADER_VERSION_FUNC__);
		if (messages) piCout << error_str;
		return false;
	}
	if (__PIP_PLUGIN_LOADER_VERSION__ != func_loader_version()) {
		unload();
		error     = InvalidLoaderVersion;
		error_str = "Load plugin \"" + lib.path() +
		            "\" error: invalid loader version: application = " + PIString::fromNumber(func_loader_version()) +
		            ", plugin = " + PIString::fromNumber(__PIP_PLUGIN_LOADER_VERSION__);
		if (messages) piCout << error_str;
		return false;
	}
	if (ai->userVersion().size_s() > 1) {
		PIString pversion = pi->userVersion(), lversion = ai->userVersion();
		if (pversion != lversion) {
			unload();
			error = InvalidUserVersion;
			error_str =
				"Load plugin \"" + lib.path() + "\" error: invalid user version: application = " + lversion + ", plugin = " + pversion;
			if (messages) piCout << error_str;
			return false;
		}
	}
	func_static_merge = (FunctionStaticMerge)lib.resolve(STR(__PIP_PLUGIN_STATIC_MERGE_FUNC__));
	if (func_static_merge) {
		auto pss = pi->staticSections(), lss = ai->staticSections();
		// piCout << lss.keys() << pss.keys();
		auto it = lss.makeIterator();
		while (it.next()) {
			if (!pss.contains(it.key())) continue;
			void *from = pss.value(it.key()), *to = it.value();
			if (from != to) func_static_merge(it.key(), from, to);
		}
	}
	loaded = true;
	error  = NoError;
	return true;
}


void PIPluginLoader::unload() {
	lib.unload();
	loaded = false;
	error_str.clear();
	error = Unknown;
	PIPluginInfoStorage::instance()->unloadPlugin(this);
}


bool PIPluginLoader::isLoaded() const {
	return loaded;
}


PIPluginLoader::Error PIPluginLoader::lastError() const {
	return error;
}


PIString PIPluginLoader::lastErrorText() const {
	return error_str;
}


void PIPluginLoader::setMessages(bool yes) {
	messages = yes;
}


bool PIPluginLoader::isMessages() const {
	return messages;
}


PIString PIPluginLoader::libPath() {
	return lib.path();
}


void * PIPluginLoader::resolve(const char * name) {
	if (!loaded) return nullptr;
	return lib.resolve(name);
}


//! \~\details
//! \~english
//! Call PIP_PLUGIN_STATIC_SECTION_MERGE function
//! on every common type, with "from" - application scope,
//! "to" - plugin scope
//! \~russian
//!
void PIPluginLoader::mergeStatic() {
	if (!loaded || !func_static_merge) return;
	PIPluginInfo * ai = PIPluginInfoStorage::instance()->applicationInfo();
	PIPluginInfo * pi = PIPluginInfoStorage::instance()->pluginInfo(this);
	auto pss = pi->staticSections(), lss = ai->staticSections();
	auto it = lss.makeIterator();
	while (it.next()) {
		if (!pss.contains(it.key())) continue;
		void *from = it.value(), *to = pss.value(it.key());
		if (from != to) func_static_merge(it.key(), from, to);
	}
}


PIStringList PIPluginLoader::pluginsDirectories(const PIString & name) {
	static PIStringList dl({".", "./plugins", "../PlugIns"});
	PIString ret;
	for (const auto & d: dl) {
		PIString dp = d + "/" + name;
		if (PIDir::isExists(dp)) ret += dp;
	}
	return ret;
}


PIString PIPluginLoader::findLibrary(const PIString & path) {
	static const PIStringList prefixes({"", "lib"});
	static const PIStringList suffixes({"", libExtension()});
	PIFile::FileInfo fi(path);
	PIString dir = fi.dir(), name = fi.name();
	for (const auto & p: prefixes) {
		for (const auto & s: suffixes) {
			PIString fn = dir + p + name + s;
			if (PIFile::isExists(fn)) return fn;
		}
	}
	return PIString();
}


PIString PIPluginLoader::libExtension() {
	return
#  ifdef WINDOWS
		".dll"
#  elif defined(MAC_OS)
		".dylib"
#  else
		".so"
#  endif
		;
}


#endif // MICRO_PIP
