//
// C++ Implementation: pluginmanager
//
// Description: 
//
//
// Author: Benjamin Mesing <bensmail@gmx.net>, (C) 2004
//
// Copyright: See COPYING file that comes with this distribution
//
//

#include <iostream>
#include <algorithm>
#include <cassert>

#include <qapplication.h>
#include <QDir>
#include <qstringlist.h>
#include <QListWidget>
#include <qlabel.h>
#include <QMainWindow>
#include <qstatusbar.h>
#include <QTranslator>
#include <QtDebug>

#include <dlfcn.h>	// for dl*()

#include "extalgorithm.h"

// NPlugin
#include "pluginmanager.h"
#include "plugincontainer.h"
#include "ipluginuser.h"
#include "iprovider.h"
#include "plugincontroldlgmeditator.h"

// NXml
#include "xmldata.h"

// NUtil
#include "progressdisplaydlg.h"

#include "helpers.h"

namespace NPlugin 
{




PluginManager::PluginManager(vector<string> directories, IProvider* pProvider)
	: _settingsVersion("0.2")
{
	_pProvider = pProvider;
 	_pProgressDlg = 0;
	_directories = directories;
	_pControlDialog = 0;
}


PluginManager::~PluginManager()
{
}

/////////////////////////////////////////////////////
// IPluginUser Interface
/////////////////////////////////////////////////////

void PluginManager::addPlugin(Plugin* pPlugin)
{
	informAddPlugin(pPlugin);
}

void PluginManager::removePlugin(Plugin* pPlugin)
{
	informRemovePlugin(pPlugin);
}


/////////////////////////////////////////////////////
// Other functions
/////////////////////////////////////////////////////

void PluginManager::loadPlugins()
{
	NUtil::ProgressDisplayDlg dlg(_pProvider->mainWindow(), "PluginProgressDlg", true);
	_pProgressDlg = &dlg;
	dlg.show();
	vector<PluginData> plugins = getAvailablePlugins();
	for (vector<PluginData>::const_iterator it = plugins.begin(); it != plugins.end(); ++it)
	{
		// if the plugin is not disabled and no such plugin is already loaded
		if (_disabledPlugins.find(it->name())==_disabledPlugins.end()  &&  it->pPlugin==0)
		{
			dlg.setText("Loading " + toQString(it->name()));
			dlg.setProgress(0);
			qApp->processEvents();
			loadPlugin(it->directory, it->name()); 
			dlg.setProgressRange(0, 100, false);
			dlg.setProgress(100);
			dlg.setText("");
			qApp->processEvents();
		}
	}
	_pProgressDlg = 0;
}



vector<PluginContainer*> PluginManager::getLoadedPlugins() const
{
	vector<PluginContainer*> result;
	for (map<string, PluginData>::const_iterator it = _plugins.begin(); 
		it != _plugins.end(); ++it)
	{
		if (it->second.pPlugin != 0)
			result.push_back(it->second.pPlugin);
	}
	return result;
}


// This function first collects the plugins available in the plugin directories.
// Afterwards it checks the loaded plugins and searches those in the ones collected 
// above where it appends the load specific information. If a loaded plugin is
// not available in the available ones it will be appended at the result.
vector<PluginManager::PluginData> PluginManager::getAvailablePlugins() const
{
	vector<PluginData> result;
	set<string> collectedPlugins;
	// iterate through the plugins and add the loaded plugins to result and loadedPlugins
	{
		for (map<string, PluginData>::const_iterator it = _plugins.begin();
			it != _plugins.end(); ++it )
		{
			// if the plugin is loaded
			if (it->second.pPlugin != 0)
			{
				result.push_back(it->second);
				collectedPlugins.insert(it->first);
			}
		}
	}

	for (vector<string>::const_iterator it = _directories.begin(); it != _directories.end(); ++it)
	{
		const string& directory = *it;
		dlerror();	// clear error messages
		QDir pluginDir(toQString(directory), "lib*.so");	// only care for library files
		QStringList pluginFiles = pluginDir.entryList(QDir::Files);
		// try to load the library and fetch the creator function for each file 
		// in the current directory, if both is possible the plugin is available
		for (QStringList::iterator jt = pluginFiles.begin(); jt != pluginFiles.end(); ++jt)
		{
			qStrDebug("opening plugin file " + pluginDir.absolutePath() + "/" + *jt);
			PluginData pluginData;
			string libraryFile = toString(*jt);
			string pluginName = libraryFile.substr(3,libraryFile.length() - 6);	// remove "lib" and ".so"
			// skip this library file if the plugin was already found
			if (collectedPlugins.find(pluginName) != collectedPlugins.end())
				continue;
			void* libraryHandle = dlopen((directory + libraryFile).c_str(), RTLD_NOW);
			checkDlError();
			if (libraryHandle)	// loading was possible
			{
				typedef PluginInformation (*GetPluginDataFkt)();
				GetPluginDataFkt getInformation = GetPluginDataFkt(dlsym(libraryHandle, "get_pluginInformation"));
				// if the function could be received this libary seems to contain a plugin
				if (getInformation != 0)
				{
					pluginData.directory = directory;
					if (getInformation != 0)
						pluginData.information = getInformation();
					result.push_back(pluginData);
					// this prevents a reload of the plugin information, if the plugin is also 
					// existent in a plugin path later in the list
					collectedPlugins.insert(pluginData.name());
				}
				dlclose(libraryHandle);	// close the library again
			}
		}
		dlerror();	// dismiss possible error messages
	}
	return result;
}

PluginContainer* PluginManager::loadPlugin(
	const string& directory, const string& pluginName)
{
	if (isLoaded(toQString(pluginName)))
		return 0;
	typedef PluginContainer* (*NewPluginFkt)();
	typedef PluginInformation (*GetPluginDataFkt)();
	
	// holds if a progress dialog was available when calling this method
	bool progressDialogAvailable = (_pProgressDlg != 0);
	if (!progressDialogAvailable)
	{
		_pProgressDlg = new NUtil::ProgressDisplayDlg(_pProvider->mainWindow(), "PluginProgressDlg", true);
// 		_pProgressDlg->show();
	}
	
	PluginContainer* pPlugin = 0;
	dlerror();	// dismiss pending errors
	void* libraryHandle = dlopen((directory + "lib" + pluginName + ".so").c_str(), RTLD_LAZY);
	if (!checkDlError()) // if no error occured
	{
		assert(libraryHandle != 0);
		PluginData pluginData;
		
		// get the function for creating the plugin
		NewPluginFkt func = NewPluginFkt(dlsym(libraryHandle, ("new_" + pluginName).c_str() ));
		if (func != 0)	// if no error occured on load (i.e. the symbol was present)
		{
			// setup translation
			QString locale = QLocale::system().name();
			qDebug() << "Translation dir " << _translationDir.path();
			QTranslator* pTranslator = new QTranslator();
			if (pTranslator->load(_translationDir.filePath( toQString(pluginName) + "_" + locale))) 
			{
				qDebug() << "Loaded translation " << toQString(pluginName) + "_" + locale;
				QApplication::installTranslator(pTranslator);
				pluginData.pTranslator = pTranslator;
			}
			else 
			{
				delete pTranslator;
				pTranslator = 0;
				// warn if we have non-english and no translation
				if (!locale.startsWith("en_"))
					qWarning() << tr("No translation for plugin ") << toQString(pluginName) << 
							tr(" found for current locale ") << locale;
			}
		
			pPlugin = func();	// fetch the plugin
 			qDebug() << "Loaded plugin: " << pPlugin->title();
			pluginData.directory = directory;
			pluginData.libraryHandle = libraryHandle;
			GetPluginDataFkt getInformation = GetPluginDataFkt(
				dlsym(libraryHandle, "get_pluginInformation"));
			if (getInformation != 0)
				pluginData.information = getInformation();
			if (pPlugin)	// if loading was successful, add the plugin to the map
				// and remove it from the disbled plugins if necessary
			{
				pPlugin->addPluginUser(this);
				if (pPlugin->init(_pProvider))	// if the container initializes successfully
				{
					pluginData.pPlugin = pPlugin;


					
					map<string, const QDomElement>::const_iterator it = _pluginSettings.find(pPlugin->name());
					if ( it != _pluginSettings.end() )
						pPlugin->loadSettings(it->second);
					_plugins[pluginName] = pluginData;
					
				}
				else
				{
					QApplication::removeTranslator(pluginData.pTranslator);
					delete pluginData.pTranslator;
					pluginData.pTranslator = 0;
					delete pPlugin;
					pPlugin = 0;
				}
			}
		}
		else	// an error while trying to load the lib<pluginName>.so file
		{
			dlclose(libraryHandle);
			dlerror();	// dismiss possible errors
		}
	}
	if (!progressDialogAvailable)
	{
		delete _pProgressDlg;
		_pProgressDlg = 0;
	}
	return pPlugin;
}


void PluginManager::unloadPlugin(PluginContainer* pPlugin)
{
	QString pluginTitle = pPlugin->title();
	map<string, PluginData>::iterator it = _plugins.find(pPlugin->name());
	PluginData& pluginData = it->second;
	assert(pluginData.pPlugin != 0);
	delete pluginData.pTranslator;
	pluginData.pTranslator = 0;
	// add the settings of the plugin in _pluginSettings
	{
		NXml::XmlData data("SettingsDocument");
		pPlugin->saveSettings(data, data.root());
		// if the plugin has settings
		if (!data.root().firstChild().isNull())
			// can not use operator[] because this would have violated the const specification
			_pluginSettings.insert( 
				make_pair(pPlugin->name(), data.root().firstChild().toElement()) );
	}
	delete pPlugin;
	pluginData.pPlugin = 0;
	dlclose(pluginData.libraryHandle);
	_pProvider->statusBar()->showMessage("Unloaded plugin " + pluginTitle, 3000);
}

void PluginManager::unloadPlugin(const string& pluginName)
{
	PluginToDataMap::iterator it = _plugins.find(pluginName);
	unloadPlugin(it->second.pPlugin);
}

void PluginManager::saveSettings(NXml::XmlData& outData, QDomElement parent) const
{
	// take a look at the pluginmanager.dtd file for the XML structure
	QDomElement pluginManager = outData.addElement(parent, "PluginManager");
	outData.addAttribute(pluginManager, _settingsVersion, "settingsVersion");
	
	QDomElement disabledPlugins = outData.addElement(pluginManager, "DisabledPlugins");
	QDomElement pluginSettings = outData.addElement(pluginManager, "PluginSettings");


	vector<PluginData> availablePlugins = getAvailablePlugins();
	for (vector<PluginData>::const_iterator it = availablePlugins.begin(); 
		it != availablePlugins.end(); ++it)
	{
		const PluginData& pluginData = *it;
		// if the plugin was loaded
		if (pluginData.pPlugin != 0)
		{
			qDebug() << "Saving " << pluginData.name().c_str();
			pluginData.pPlugin->saveSettings(outData, pluginSettings);
		}
		// the plugin was not loaded
		else
		{
			QDomElement plugin = outData.addElement(disabledPlugins, "Plugin");
			{
				outData.addAttribute(plugin, pluginData.name(), "name");
			}
			// store the loaded settings for the disabled plugins if available
			map<string, const QDomElement>::const_iterator jt = _pluginSettings.find(pluginData.name());
			if ( jt != _pluginSettings.end() )
			{
				outData.importNode(jt->second, pluginSettings, true);
			}
		}
	}
}

QDomElement PluginManager::loadSettings(QDomElement source) 
{
	if (source.tagName() != "PluginManager")
		return source;
	// take a look at the pluginmanager.dtd file for the XML structure
	QDomElement disabledPlugins = NXml::getFirstElement(source.firstChild());
	{
		QDomElement plugin = NXml::getFirstElement(disabledPlugins.firstChild());
		while (!plugin.isNull())
		{
			string disabledPlugin;
			NXml::getAttribute(plugin, disabledPlugin, "name");
			_disabledPlugins.insert(disabledPlugin);
			plugin = NXml::getNextElement(plugin);
		}
	}
	// the node which holds all settings of the different plugins
	QDomElement pluginSettings = NXml::getNextElement(disabledPlugins);
	{
		QDomElement settings = NXml::getFirstElement(pluginSettings.firstChild());
		while (!settings.isNull())
		{
// 			qDebug() << "Loaded " << settings.tagName() << " settings";
			// the settings for the current plugin
			_pluginSettings.insert( make_pair(toString(settings.tagName()),settings) );
			settings = NXml::getNextElement(settings);
		}
	}
	return NXml::getNextElement(source);
}


void PluginManager::showControlDialog(QWidget* pParent)
{
	PluginControlDlgMeditator mediator(this);
	mediator.displayDialog(pParent);
}

bool PluginManager::checkDlError() const
{
	const char* error = dlerror();
	if (error)
	{
		cerr << "Dynamic Library error: " << error <<endl;
		return true;
	}
	return false;
}

NUtil::IProgressObserver* PluginManager::progressObserver()
{
	return _pProgressDlg;
}


bool PluginManager::isLoaded(QString pluginName) const
{
	PluginToDataMap::const_iterator it = _plugins.find(toString(pluginName));
	if ( it != _plugins.end() && it->second.pPlugin != 0 )
		return true;
	return false;
}


void PluginManager::setTranslationDir(const QDir& dir)
{
	_translationDir = dir;
}

}	// namespace NPlugin
