/*******************************************************************************************************
 DkPluginManager.cpp
 Created on:	20.05.2013

 nomacs is a fast and small image viewer with the capability of synchronizing multiple instances

 Copyright (C) 2011-2013 Markus Diem <markus@nomacs.org>
 Copyright (C) 2011-2013 Stefan Fiel <stefan@nomacs.org>
 Copyright (C) 2011-2013 Florian Kleber <florian@nomacs.org>

 This file is part of nomacs.

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

 nomacs 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 "DkPluginManager.h"
#include "DkSettings.h"
#include "DkActionManager.h"
#include "DkUpdater.h"
#include "DkUtils.h"
#include "DkDependencyResolver.h"
#include "DkTimer.h"

#pragma warning(push, 0)	// no warnings from includes - begin
#include <QWidget>
#include <QSlider>
#include <QSpinBox>
#include <QTextEdit>
#include <QProgressDialog>
#include <QTabWidget>
#include <QPluginLoader>
#include <QHeaderView>
#include <QSortFilterProxyModel>
#include <QScrollBar>
#include <QTableView>
#include <QXmlStreamReader>
#include <QVBoxLayout>
#include <QPushButton>
#include <QDebug>
#include <QDir>
#include <QSettings>
#include <QMessageBox>
#include <QLineEdit>
#include <QSortFilterProxyModel>
#include <QMouseEvent>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkProxyFactory>
#include <QAction>
#include <QMenu>
#include <QJsonValue>
#pragma warning(pop)		// no warnings from includes - end

#ifdef QT_NO_DEBUG_OUTPUT
#pragma warning(disable: 4127)		// no 'conditional expression is constant' if qDebug() messages are removed
#endif

namespace nmc {

// DkLibrary --------------------------------------------------------------------
DkLibrary::DkLibrary(const QString& name) {
	mName = name;
}

QString DkLibrary::fullPath() const {
	return mFullPath;
}

QString DkLibrary::name() const {
	return mName;
}

bool DkLibrary::isLoaded() const {

	if (mLib)
		return mLib->isLoaded();

	return false;
}

bool DkLibrary::load() {

	QString suffix;
	QString prefix;

#if defined(Q_OS_LINUX) // TODO: add your operating system if libs are prefixed
	prefix = "lib";
#endif

	mLib = QSharedPointer<QLibrary>(new QLibrary());

	for (const QString& libPath : QCoreApplication::libraryPaths()) {

		QString fullPath = libPath + QDir::separator() + prefix + mName + suffix;
		mLib->setFileName(fullPath);
		mLib->load();

		if (mLib->isLoaded()) {
			mFullPath = fullPath;
			break;
		}

		// just needed for windows dlls
		if (QFileInfo(fullPath).exists())
			mFullPath = fullPath;
	}

	// if we could find the library but not load it, there is probably a dependency missing...
	if (!mLib->isLoaded() && !mFullPath.isEmpty()) {

		mDependencies = loadDependencies();

		mLib->setFileName(mFullPath);
		return mLib->load();
	}
	else if (!mLib->isLoaded())
		return false;

	return true;
}

bool DkLibrary::uninstall() {

	if (mLib)
		mLib->unload();

	return QFile::remove(fullPath());
}

QVector<DkLibrary> DkLibrary::loadDependencies() const {
	
	QVector<DkLibrary> dependencies;
	DkDllDependency d(mFullPath);

	if (!d.findDependencies()) {
		qDebug() << "sorry, but I could not find dependencies of" << mName;
		return dependencies;
	}

	// debug:
	//qInfo() << name() << "has these dependencies: " << d.dependencies();
	
	QStringList fd = d.filteredDependencies();

	for (const QString& n : fd) {
		DkLibrary lib(n);
		if (lib.load()) {
			dependencies << lib;
			//qInfo() << lib.name() << "loaded for" << name();
		}
		else
			qWarning() << "could not load" << lib.name() << "which is needed for" << name();
	}

	return dependencies;
}

// DkPluginContainer --------------------------------------------------------------------
DkPluginContainer::DkPluginContainer(const QString& pluginPath) {

	mPluginPath = pluginPath;
	mLoader = QSharedPointer<QPluginLoader>(new QPluginLoader(mPluginPath));
	loadJson();
}

DkPluginContainer::~DkPluginContainer() {
}

bool operator<(const QSharedPointer<DkPluginContainer>& l, const QSharedPointer<DkPluginContainer>& r) {

	if (!l || !r)
		return false;

	return l->pluginName() < r->pluginName();
}

void DkPluginContainer::setActive(bool active) {
	
	mActive = active;

	DkPluginInterface* p = plugin();
	if (p && p->interfaceType() == DkPluginInterface::interface_viewport) {

		DkViewPortInterface* vPlugin = pluginViewPort();

		if (!vPlugin)
			return;

		vPlugin->setVisible(false);
	}
}

bool DkPluginContainer::isActive() const {
	return mActive;
}

bool DkPluginContainer::isLoaded() const {
	return mLoader->isLoaded();
}

bool DkPluginContainer::load() {

	DkTimer dt;

	if (!isValid()) {
		
		// inform that we have found a dll that does not fit what we expect
		if (!mPluginPath.contains("opencv") && !mPluginPath.contains("Read"))
#ifdef Q_OS_WIN
			if (mPluginPath.contains("dll"))
#endif
			qInfo() << "Invalid: " << mPluginPath;
		return false;
	}
	else {

		QString fn = QFileInfo(mLoader->fileName()).fileName();

#ifdef Q_OS_WIN
		// ok - load it's dependencies first
		DkLibrary l(fn);
		l.load();
#endif

		if (!mLoader->load()) {
			qWarning() << "Could not load:" << fn;
			qInfo() << "name: " << mPluginName;
			qInfo() << "modified: " << mDateModified.toString("dd-MM-yyyy");
			qInfo() << "error: " << mLoader->errorString();
			return false;
		}
	}

	if (pluginViewPort())
		mType = type_viewport;
	else if (batchPlugin()) {
		// load the settings
		mType = type_batch;
		batchPlugin()->loadSettings();
	}
	else if (plugin())
		mType = type_simple;
	else {
		qWarning() << "could not initialize: " << mPluginPath << "unknown interface";
		return false;
	}

	if (mType != type_unknown) {
		// init actions
		plugin()->createActions(DkUtils::getMainWindow());
		createMenu();
	}

	qInfo() << mPluginPath << "loaded in" << dt;
	return true;

}

bool DkPluginContainer::uninstall() {

	mLoader->unload();
	// NOTE: dependencies are not removed yet -> they might be used by other plugins

	return QFile::remove(mPluginPath);
}

void DkPluginContainer::createMenu() {

	DkPluginInterface* p = plugin();

	// empty menu if we do not have any actions
	if (!p || p->pluginActions().empty())
		return;

	mPluginMenu = new QMenu(pluginName(), DkUtils::getMainWindow());

	for (auto action : p->pluginActions()) {
		mPluginMenu->addAction(action);
		connect(action, SIGNAL(triggered()), this, SLOT(run()), Qt::UniqueConnection);
	}
}

void DkPluginContainer::loadJson() {

	QJsonObject metaData = mLoader->metaData();
	QStringList keys = metaData.keys();

	for (const QString& key : keys) {

		if (key == "MetaData")
			loadMetaData(metaData.value(key));
		else if (key == "IID" && metaData.value(key).toString().contains("com.nomacs.ImageLounge"))
			mIsValid = true;
#ifndef _DEBUG	// warn if we have a debug & are not in debug ourselves
		else if (key == "debug") {
			bool isDebug = metaData.value(key).toBool();
			if (isDebug)
				qWarning() << "I cannot load a debug dll since I am compiled in release!";
		}
#endif

		//qDebug() << key << "|" << metaData.value(key);
	}
}

void DkPluginContainer::loadMetaData(const QJsonValue& val) {

	// we expect something like this...
	//{
	//		"PluginName" 	: [ "BinarizationPlugin" ],
	//		"AuthorName" 	: [ "Markus Diem" ],
	//		"Company"		: [ "Computer Vision Lab" ],
	//		"DateCreated" 	: [ "04.02.2016" ],
	//		"DateModified" 	: [ "04.02.2016" ],
	//		"Description"	: [ "Document Binarization Plugin" ],
	//		"StatusTip" 	: [ "Applies e.g. the Su et al. binarzation to an image." ]
	//}

	QJsonObject metaData = val.toObject();
	QStringList keys = metaData.keys();

	for (const QString& key : keys) {

		if (key == "PluginName")
			mPluginName = metaData.value(key).toString();
		else if (key == "AuthorName")
			mAuthorName = metaData.value(key).toString();
		else if (key == "Company")
			mCompany = metaData.value(key).toString();
		else if (key == "DateCreated")
			mDateCreated = QDate::fromString(metaData.value(key).toString(), "yyyy-MM-dd");
		else if (key == "DateModified")
			mDateModified = QDate::fromString(metaData.value(key).toString(), "yyyy-MM-dd");
		else if (key == "Description")
			mDescription = metaData.value(key).toString();
		else if (key == "Tagline")
			mTagline = metaData.value(key).toString();
		else if (key == "Version")
			mVersion = metaData.value(key).toString();
		else if (key == "PluginId") {
			mId = metaData.value(key).toString();
		}
		else
			qWarning() << "unknown key" << key << "|" << metaData.value(key);

		//qDebug() << "parsing:" << key << "|" << metaData.value(key);
	}

	if (!isValid() && !keys.empty()) {
		qWarning() << "invalid plugin - missing the PluginName in the json metadata...";
	}

}

void DkPluginContainer::run() {

	DkPluginInterface* p = plugin();

	if (p && p->interfaceType() == DkPluginInterface::interface_viewport) {

		DkViewPortInterface* vPlugin = pluginViewPort();
		mActive = true;

		if(!vPlugin)
			return;

		if (!vPlugin->getViewPort())
			vPlugin->createViewPort(vPlugin->getMainWindow());

		if (!vPlugin->getViewPort()) {
			qWarning() << "NULL viewport detected in" << mPluginName;
			return;
		}
		
		vPlugin->setVisible(true);

		connect(vPlugin->getViewPort(), SIGNAL(showToolbar(QToolBar*, bool)), vPlugin->getMainWindow(), SLOT(showToolbar(QToolBar*, bool)));
		emit runPlugin(vPlugin, false);
	}
	else if ((p && p->interfaceType() == DkPluginInterface::interface_basic) || p->interfaceType() == DkPluginInterface::interface_batch) {

		QAction* a = qobject_cast<QAction*>(QObject::sender());

		if (a)
			emit runPlugin(this, a->data().toString());
	}
	else
		qWarning() << "plugin with illegal interface detected in DkPluginContainer::run()";

}

bool DkPluginContainer::isValid() const {
	return mIsValid;
}

QString DkPluginContainer::pluginPath() const{
	return mPluginPath;
}

QString DkPluginContainer::pluginName() const {
	return mPluginName;
}

QString DkPluginContainer::authorName() const {
	return mAuthorName;
}

QString DkPluginContainer::company() const {
	return mCompany;
}

QString DkPluginContainer::version() const {
	return mVersion;
}

QString DkPluginContainer::description() const {
	return mDescription;
}

QString DkPluginContainer::fullDescription() const {

	QString trAuthor = tr("Author:");
	QString trCompany = tr("Company:");
	QString trCreated = tr("Created:");
	QString trModified = tr("Last Modified:");

	QString fs;

	fs += "<h3>" + pluginName() + "</h3>";

	if (!tagline().isEmpty())
		fs += "<i>" + tagline() + "</i>";
	fs += "<p>" + mDescription + "</p>";
	fs += "<b>" + trAuthor + "</b> " + mAuthorName + "<br>";

	if (!company().isEmpty())
		fs += "<b>" + trCompany + "</b> " + company() + "<br>";

	fs += "<b>" + trCreated		+ "</b> " + mDateCreated.toString(Qt::LocalDate) + "<br>";
	fs += "<b>" + trModified	+ "</b> " + mDateModified.toString(Qt::LocalDate) + "<br>";

	return fs;
}

QString DkPluginContainer::tagline() const {
	return mTagline;
}

QString DkPluginContainer::id() const {
	return mId;
}

QDate DkPluginContainer::dateCreated() const {
	return mDateCreated;
}

QDate DkPluginContainer::dateModified() const {
	return mDateModified;
}

QMenu * DkPluginContainer::pluginMenu() const {
	return mPluginMenu;
}

QSharedPointer<QPluginLoader> DkPluginContainer::loader() const {
	return mLoader;
}

DkPluginInterface* DkPluginContainer::plugin() const {

	// is everything fine here??
	if (!mLoader)
		return 0;

	DkPluginInterface* pi = qobject_cast<DkPluginInterface*>(mLoader->instance());

	if (!pi && pluginViewPort())
		return pluginViewPort();
	else if (!pi && batchPlugin())
		return batchPlugin();

	return pi;
}

DkBatchPluginInterface* DkPluginContainer::batchPlugin() const {

	// is everything fine here??
	if (!mLoader)
		return 0;

	return qobject_cast<DkBatchPluginInterface*>(mLoader->instance());
}

DkViewPortInterface* DkPluginContainer::pluginViewPort() const {

	// is everything fine here??
	if (!mLoader)
		return 0;

	return qobject_cast<DkViewPortInterface*>(mLoader->instance());
}

QString DkPluginContainer::actionNameToRunId(const QString & actionName) const {

	if (!plugin())
		return QString();

	QList<QAction*> actions = plugin()->pluginActions();
	for (const QAction* a : actions) {
		if (a->text() == actionName)
			return a->data().toString();
	}

	return QString();
}

/**********************************************************************************
* Plugin manager dialog
**********************************************************************************/
DkPluginManagerDialog::DkPluginManagerDialog(QWidget* parent) : QDialog(parent) {
	init();
}

DkPluginManagerDialog::~DkPluginManagerDialog() {}

/**
* initialize plugin manager dialog - set sizes
 **/
void DkPluginManagerDialog::init() {

	dialogWidth = 700;
	dialogHeight = 500;

	setWindowTitle(tr("Plugin Manager"));
	setMinimumSize(dialogWidth, dialogHeight);
	createLayout();
}

/*
* create plugin manager dialog layout
 **/
void DkPluginManagerDialog::createLayout() {

	tableWidgetInstalled = new DkPluginTableWidget(this);

	QPushButton* buttonClose = new QPushButton(tr("&Close"));
	connect(buttonClose, SIGNAL(clicked()), this, SLOT(closePressed()));
	buttonClose->setDefault(true);

	QWidget* dummy = new QWidget(this);
	QHBoxLayout* hLayout = new QHBoxLayout(dummy);
	hLayout->setAlignment(Qt::AlignRight);
	hLayout->addWidget(buttonClose);


	QVBoxLayout* layout = new QVBoxLayout(this);
	layout->addWidget(tableWidgetInstalled);
	layout->addWidget(dummy);
}


void DkPluginManagerDialog::closePressed() {

	close();
}

void DkPluginManagerDialog::showEvent(QShowEvent* ev) {

	qDebug() << "show event called...";
	DkPluginManager::instance().loadPlugins();

	tableWidgetInstalled->clearTableFilters();
	tableWidgetInstalled->updateInstalledModel();

	QDialog::showEvent(ev);
}

void DkPluginManagerDialog::deleteInstance(QSharedPointer<DkPluginContainer> plugin) {

	DkPluginManager::instance().removePlugin(plugin);
}

QMap<QString, QString> DkPluginManagerDialog::getPreviouslyInstalledPlugins() {

	return previouslyInstalledPlugins;
}

/**********************************************************************************
 * DkPluginTableWidget : Widget with table views containing plugin data
 **********************************************************************************/
DkPluginTableWidget::DkPluginTableWidget(QWidget* parent) : QWidget(parent) {

	createLayout();
	QMetaObject::connectSlotsByName(this);
}

DkPluginTableWidget::~DkPluginTableWidget() {
}

// create the main layout of the plugin manager
void DkPluginTableWidget::createLayout() {

	// search line edit and update button
	mFilterEdit = new QLineEdit(this);
	mFilterEdit->setPlaceholderText(tr("Search plugins"));
	connect(mFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(filterTextChanged()));

	QPushButton* updateButton = new QPushButton(tr("Add or Remove Plugins"), this);
	updateButton->setObjectName("updateButton");
	if (DkSettingsManager::param().isPortable())
		updateButton->hide();

	QWidget* searchWidget = new QWidget(this);
	QHBoxLayout* sLayout = new QHBoxLayout(searchWidget);
	sLayout->setContentsMargins(0, 0, 0, 0);
	sLayout->addWidget(mFilterEdit);
	//sLayout->addStretch();
	sLayout->addWidget(updateButton);

	// main table
    mTableView = new QTableView(this);
	mProxyModel = new QSortFilterProxyModel(this);
    mProxyModel->setDynamicSortFilter(true);

	mModel = new DkInstalledPluginsModel(this);
	mProxyModel->setSourceModel(mModel);
	mTableView->setModel(mProxyModel);
	mTableView->resizeColumnsToContents();
	mTableView->setColumnWidth(ip_column_name, qMax(mTableView->columnWidth(ip_column_name), 300));
	mTableView->setColumnWidth(ip_column_version, qMax(mTableView->columnWidth(ip_column_version), 80));

	mTableView->resizeRowsToContents();
    mTableView->horizontalHeader()->setStretchLastSection(true);
    mTableView->setSortingEnabled(true);
	mTableView->sortByColumn(ip_column_name, Qt::AscendingOrder);
    mTableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    mTableView->verticalHeader()->hide();
    mTableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
    mTableView->setSelectionMode(QAbstractItemView::SingleSelection);
	mTableView->setAlternatingRowColors(true);

	if (DkSettingsManager::instance().param().isPortable()) {
		DkPushButtonDelegate* buttonDelegate = new DkPushButtonDelegate(mTableView);
		mTableView->setItemDelegateForColumn(ip_column_uninstall, buttonDelegate);
		connect(buttonDelegate, SIGNAL(buttonClicked(QModelIndex)), this, SLOT(uninstallPlugin(QModelIndex)));
	}

	DkDescriptionEdit* descriptionEdit = new DkDescriptionEdit(mModel, mProxyModel, mTableView->selectionModel(), this);
	connect(mTableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), descriptionEdit, SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
	connect(mProxyModel, SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)), descriptionEdit, SLOT(dataChanged(const QModelIndex &, const QModelIndex &)));

	DkDescriptionImage* descriptionImg = new DkDescriptionImage(mModel, mProxyModel, mTableView->selectionModel(), this);
	connect(mTableView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), descriptionImg, SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
	connect(mProxyModel, SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)), descriptionImg, SLOT(dataChanged(const QModelIndex &, const QModelIndex &)));

	QWidget* descWidget = new QWidget(this);
	QHBoxLayout* dLayout = new QHBoxLayout(descWidget);
	dLayout->setContentsMargins(0, 0, 0, 0);
	dLayout->addWidget(descriptionEdit);
	dLayout->addWidget(descriptionImg);

	QVBoxLayout* layout = new QVBoxLayout(this);
	layout->addWidget(searchWidget);
	layout->addWidget(mTableView);
	layout->addWidget(descWidget);
}

void DkPluginTableWidget::on_updateButton_clicked() {

	DkInstallUpdater::updateNomacs("--manage-packages");
}

void DkPluginTableWidget::reloadPlugins() {

	DkPluginManager::instance().reload();
	updateInstalledModel();
}

void DkPluginTableWidget::filterTextChanged() {

	QRegExp regExp(mFilterEdit->text(), Qt::CaseInsensitive, QRegExp::FixedString);
	mProxyModel->setFilterRegExp(regExp);
	mTableView->resizeRowsToContents();
}

void DkPluginTableWidget::uninstallPlugin(const QModelIndex &index) {

	int selectedRow = mProxyModel->mapToSource(index).row();

	if (selectedRow < 0 || selectedRow > DkPluginManager::instance().getPlugins().size()) {
		qWarning() << "illegal row in uninstall plugin: " << selectedRow;
		return;
	}

	QSharedPointer<DkPluginContainer> plugin = DkPluginManager::instance().getPlugins().at(selectedRow);

	if (!plugin) {
		qWarning() << "plugin is NULL... aborting uninstall";
		return;
	}

	bool removed = DkPluginManager::instance().deletePlugin(plugin);

	if (!removed)
		return;

	//updateInstalledModel();	// !!! update model before deleting the interface

	mTableView->model()->removeRow(index.row());
	//emit dataChanged(index, index);

	mTableView->resizeRowsToContents();
}

void DkPluginTableWidget::clearTableFilters(){

	mFilterEdit->clear();
	mFilterEdit->setFocus();
}


//update models if new plugins are installed or copied into the folder
void DkPluginTableWidget::updateInstalledModel() {

	clearTableFilters();
	//mTableView->relo;
}

//**********************************************************************************
//DkInstalledPluginsModel : Model managing installed plugins data in the table
//**********************************************************************************
DkInstalledPluginsModel::DkInstalledPluginsModel(QObject *parent) : QAbstractTableModel(parent) {

	mParentTable = static_cast<DkPluginTableWidget*>(parent);
}

int DkInstalledPluginsModel::rowCount(const QModelIndex&) const {

	return DkPluginManager::instance().getPlugins().size();
}

int DkInstalledPluginsModel::columnCount(const QModelIndex&) const {

	return DkSettingsManager::param().isPortable() ? ip_column_size : ip_column_size-1;
}

QVariant DkInstalledPluginsModel::data(const QModelIndex &index, int role) const {

	if (!index.isValid()) {
		return QVariant();
	}

	const QVector<QSharedPointer<DkPluginContainer> >& plugins = DkPluginManager::instance().getPlugins();

	if (index.row() >= plugins.size() || index.row() < 0) {
		return QVariant();
	}

    if (role == Qt::DisplayRole) {

		QSharedPointer<DkPluginContainer> plugin = plugins.at(index.row());
        if (index.column() == ip_column_name) {
			return plugin->pluginName();
		}
        else if (index.column() == ip_column_version) {
			return plugin->version();
		}
		else if (index.column() == ip_column_uninstall)
			return QString(tr("Uninstall"));
    }

    return QVariant();
}

QVariant DkInstalledPluginsModel::headerData(int section, Qt::Orientation orientation, int role) const {

    if (role != Qt::DisplayRole) return QVariant();

    if (orientation == Qt::Horizontal) {
        switch (section) {
            case ip_column_name:
                return tr("Name");
            case ip_column_version:
                return tr("Version");
			case ip_column_uninstall:
				return tr("Uninstall plugin");
            default:
                return QVariant();
        }
    }
    return QVariant();
}

Qt::ItemFlags DkInstalledPluginsModel::flags(const QModelIndex &index) const {

    if (!index.isValid())
        return Qt::ItemIsEnabled;

	return QAbstractTableModel::flags(index);
}

bool DkInstalledPluginsModel::removeRows(int position, int rows, const QModelIndex & index) {

	beginRemoveRows(QModelIndex(), position, position+rows-1);
	endRemoveRows();

	emit dataChanged(index, index);

	return true;
}


void DkInstalledPluginsModel::setDataToInsert(QSharedPointer<DkPluginContainer> newData) {

	mPluginToInsert = newData;
}

//*********************************************************************************
//DkCheckBoxDelegate : delagete for checkbox only column in the model
//*********************************************************************************

static QRect CheckBoxRect(const QStyleOptionViewItem &viewItemStyleOptions) {

	QStyleOptionButton checkBoxStyleOption;
	QRect checkBoxRect = QApplication::style()->subElementRect(QStyle::SE_CheckBoxIndicator, &checkBoxStyleOption);
	QPoint checkBoxPoint(viewItemStyleOptions.rect.x() + viewItemStyleOptions.rect.width() / 2 - checkBoxRect.width() / 2,
                         viewItemStyleOptions.rect.y() + viewItemStyleOptions.rect.height() / 2 - checkBoxRect.height() / 2);
	return QRect(checkBoxPoint, checkBoxRect.size());
}

DkPluginCheckBoxDelegate::DkPluginCheckBoxDelegate(QObject *parent) : QStyledItemDelegate(parent) {

	mParentTable = static_cast<QTableView*>(parent);
}

void DkPluginCheckBoxDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {

	if (option.state & QStyle::State_Selected) {
		if (mParentTable->hasFocus()) painter->fillRect(option.rect, option.palette.highlight());
		else  painter->fillRect(option.rect, option.palette.background());
	}
	//else if (index.row() % 2 == 1) painter->fillRect(option.rect, option.palette.alternateBase());	// already done automatically

	bool checked = index.model()->data(index, Qt::DisplayRole).toBool();

	QStyleOptionButton checkBoxStyleOption;
	checkBoxStyleOption.state |= QStyle::State_Enabled;
	if (checked) {
		checkBoxStyleOption.state |= QStyle::State_On;
	} else {
		checkBoxStyleOption.state |= QStyle::State_Off;
	}
	checkBoxStyleOption.rect = CheckBoxRect(option);

	QApplication::style()->drawControl(QStyle::CE_CheckBox, &checkBoxStyleOption, painter);
}

bool DkPluginCheckBoxDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) {

	if ((event->type() == QEvent::MouseButtonRelease) || (event->type() == QEvent::MouseButtonDblClick)) {
		QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
		if (mouseEvent->button() != Qt::LeftButton || !CheckBoxRect(option).contains(mouseEvent->pos())) {
			return false;
		}
		if (event->type() == QEvent::MouseButtonDblClick) {
			return true;
		}
	}
	else if (event->type() == QEvent::KeyPress) {
		if (static_cast<QKeyEvent*>(event)->key() != Qt::Key_Space && static_cast<QKeyEvent*>(event)->key() != Qt::Key_Select) {
			return false;
		}
	}
	else return false;

	bool checked = index.model()->data(index, Qt::DisplayRole).toBool();
	return model->setData(index, !checked, Qt::EditRole);
}



//*********************************************************************************
//DkPushButtonDelegate : delagete for uninstall column in the model
//*********************************************************************************

static QRect PushButtonRect(const QStyleOptionViewItem &viewItemStyleOptions) {


	QRect pushButtonRect = viewItemStyleOptions.rect;
	//pushButtonRect.setHeight(pushButtonRect.height() - 2);
	//pushButtonRect.setWidth(pushButtonRect.width() - 2);
	QPoint pushButtonPoint(viewItemStyleOptions.rect.x() + viewItemStyleOptions.rect.width() / 2 - pushButtonRect.width() / 2,
                         viewItemStyleOptions.rect.y() + viewItemStyleOptions.rect.height() / 2 - pushButtonRect.height() / 2);
	return QRect(pushButtonPoint, pushButtonRect.size());
}

DkPushButtonDelegate::DkPushButtonDelegate(QObject *parent) : QStyledItemDelegate(parent) {

	mParentTable = static_cast<QTableView*>(parent);
}

void DkPushButtonDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {

	if (option.state & QStyle::State_Selected) {
		if (mParentTable->hasFocus()) painter->fillRect(option.rect, option.palette.highlight());
		else  painter->fillRect(option.rect, option.palette.background());
	}

	QStyleOptionButton pushButtonStyleOption;
	pushButtonStyleOption.text = index.model()->data(index, Qt::DisplayRole).toString();
	if (mCRow == index.row()) pushButtonStyleOption.state = mPushButonState | QStyle::State_Enabled;
	else pushButtonStyleOption.state = QStyle::State_Enabled;
	pushButtonStyleOption.rect = PushButtonRect(option);

	QApplication::style()->drawControl(QStyle::CE_PushButton, &pushButtonStyleOption, painter);
}

bool DkPushButtonDelegate::editorEvent(QEvent *event, QAbstractItemModel*, const QStyleOptionViewItem &option, const QModelIndex &index) {

	if ((event->type() == QEvent::MouseButtonRelease) || (event->type() == QEvent::MouseButtonPress)) {

		QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
		if (mouseEvent->button() != Qt::LeftButton || !PushButtonRect(option).contains(mouseEvent->pos())) {
			mPushButonState = QStyle::State_Raised;
			return false;
		}
	}
	else if (event->type() == QEvent::KeyPress) {
		if (static_cast<QKeyEvent*>(event)->key() != Qt::Key_Space && static_cast<QKeyEvent*>(event)->key() != Qt::Key_Select) {
			mPushButonState = QStyle::State_Raised;
			return false;
		}
	}
	else {
		mPushButonState = QStyle::State_Raised;
		return false;
	}

    if(event->type() == QEvent::MouseButtonPress) {
		mPushButonState = QStyle::State_Sunken;
		mCRow = index.row();
	}
	else if( event->type() == QEvent::MouseButtonRelease) {
		mPushButonState = QStyle::State_Raised;
        emit buttonClicked(index);
    }
    return true;

}


//**********************************************************************************
// DkDescriptionEdit : text edit connected to tableView selection and models
//**********************************************************************************
DkDescriptionEdit::DkDescriptionEdit(QAbstractTableModel* data, QSortFilterProxyModel* proxy, QItemSelectionModel* selection, QWidget *parent) : QTextEdit(parent) {

	mParentTable = static_cast<DkPluginTableWidget*>(parent);
	mDataModel = data;
	mProxyModel = proxy;
	mSelectionModel = selection;
	setReadOnly(true);
}

void DkDescriptionEdit::updateText() {

	if (mSelectionModel->selection().indexes().empty()) {
		setText("");
		return;
	}

	QString text;
	QModelIndex sourceIndex = mProxyModel->mapToSource(mSelectionModel->selection().indexes().first());

	const QVector<QSharedPointer<DkPluginContainer> >& plugins = DkPluginManager::instance().getPlugins();
	QSharedPointer<DkPluginContainer> plugin = plugins.at(sourceIndex.row());
	if (plugin)
		text = plugin->fullDescription();

	if (text.isNull())
		text = tr("No metadata available!");
	setText(text);
}

void DkDescriptionEdit::dataChanged(const QModelIndex&, const QModelIndex&) {

	updateText();
}

void DkDescriptionEdit::selectionChanged(const QItemSelection&, const QItemSelection&) {

	updateText();
}


//**********************************************************************************
//DkDescriptionImage : image label connected to tableView selection and models
//**********************************************************************************

DkDescriptionImage::DkDescriptionImage(QAbstractTableModel* data, QSortFilterProxyModel* proxy, QItemSelectionModel* selection, QWidget *parent) : QLabel(parent) {

	mParentTable = static_cast<DkPluginTableWidget*>(parent);
	mDataModel = data;
	mProxyModel = proxy;
	mSelectionModel = selection;
	mDefaultImage = QPixmap(":/nomacs/img/plugin-banner.svg");
	setPixmap(mDefaultImage);
}

void DkDescriptionImage::updateImage() {

	switch(mSelectionModel->selection().indexes().count())
	{
		case 0:
			setPixmap(mDefaultImage);
			break;
		default:
			QModelIndex sourceIndex = mProxyModel->mapToSource(mSelectionModel->selection().indexes().first());
			QImage img;
			const QVector<QSharedPointer<DkPluginContainer> >& plugins = DkPluginManager::instance().getPlugins();
			QSharedPointer<DkPluginContainer> plugin = plugins.at(sourceIndex.row());

			if (plugin && plugin->plugin())
				img = plugin->plugin()->image();
			if (!img.isNull())
				setPixmap(QPixmap::fromImage(img));
			else
				setPixmap(mDefaultImage);
			break;
	}
}

void DkDescriptionImage::dataChanged(const QModelIndex&, const QModelIndex&) {

	updateImage();
}

void DkDescriptionImage::selectionChanged(const QItemSelection&, const QItemSelection&) {

	updateImage();
}

// DkPluginManager --------------------------------------------------------------------
DkPluginManager & DkPluginManager::instance() {

	static DkPluginManager inst;
	return inst;
}

DkPluginManager::DkPluginManager() {

	//loadPlugins();
}

DkPluginManager::~DkPluginManager() {
}

//returns map with id and interface
QVector<QSharedPointer<DkPluginContainer> >  DkPluginManager::getPlugins() const {

	return mPlugins;
}

void DkPluginManager::reload() {
	clear();
	loadPlugins();
}

void DkPluginManager::removePlugin(QSharedPointer<DkPluginContainer> plugin) {

	if (plugin)
		mPlugins.remove(mPlugins.indexOf(plugin));
	else
		qWarning() << "Could not delete plugin - it is NULL";

}

bool DkPluginManager::deletePlugin(QSharedPointer<DkPluginContainer> plugin) {

	if (plugin) {

		mPlugins.remove(mPlugins.indexOf(plugin));

		if (!plugin->uninstall()) {
			qDebug() << "Failed to delete plugin file!";
			QMessageBox::critical(
				DkUtils::getMainWindow(), 
				QObject::tr("Plugin Manager"), 
				QObject::tr("The dll could not be deleted!\nPlease restart nomacs and try again."));
			return false;
		}
		else
			return true;
	}

	return false;
}

void DkPluginManager::clear() {
	mPlugins.clear();
}

//Loads enabled plugins (i.e. when the menu is first hit)
void DkPluginManager::loadPlugins() {

	//// DEBUG --------------------------------------------------------------------
	//nmc::DkDependencyWalker dw("C:/VSProjects/READ/nomacs/build2015-x64/Debug/plugins/writerIdentificationPlugin.dll");
	//if (!dw.findDependencies())
	//	qWarning() << "could not find dependencies for" << dw.filePath();

	//qDebug() << "all dependencies:" << dw.dependencies();
	//qDebug() << "filtered dependencies:" << dw.filteredDependencies();

	//return 0;
	//// DEBUG --------------------------------------------------------------------

	// do not load twice
	if (!mPlugins.empty())
		return;

	DkTimer dt;

	QStringList loadedPluginFileNames = QStringList();
	QStringList libPaths = QCoreApplication::libraryPaths();
	libPaths.append(QCoreApplication::applicationDirPath() + "/plugins");

	for (const QString& cPath : libPaths) {

		// skip the nomacs dir
		if (cPath == QApplication::applicationDirPath())
			continue;

		// skip image plugins
		if (cPath.contains("imageformats"))
			continue;

		QDir pluginsDir(cPath);

		for (const QString& fileName : pluginsDir.entryList(QDir::Files)) {
			DkTimer dtt;
#ifdef Q_OS_LINUX
			// needed because of symbolic links of sonames
			QFileInfo file(pluginsDir.absoluteFilePath(fileName));
			if(file.isSymLink())
				continue;
#elif defined(Q_OS_WIN)
			if (!fileName.contains(".dll"))
				continue;

#endif
			QString shortFileName = fileName.split("/").last();
			if (!loadedPluginFileNames.contains(shortFileName)) { // prevent double loading of the same plugin
				if (singlePluginLoad(pluginsDir.absoluteFilePath(fileName)))
					loadedPluginFileNames.append(shortFileName);
			}
			//else
			//	qDebug() << "rejected since it is twice: " << shortFileName;
		}
	}

	qSort(mPlugins.begin(), mPlugins.end());// , &DkPluginContainer::operator<);
	qInfo() << mPlugins.size() << "plugins loaded in" << dt;

	if (mPlugins.empty())
		qInfo() << "I was searching these paths" << libPaths;
}

/**
* Loads one plugin from file fileName
* @param fileName
**/
bool DkPluginManager::singlePluginLoad(const QString& filePath) {

	if (isBlackListed(filePath))
		return false;

	DkTimer dt;
	QSharedPointer<DkPluginContainer> plugin = QSharedPointer<DkPluginContainer>(new DkPluginContainer(filePath));
	if (plugin->load())
		mPlugins.append(plugin);

	return plugin->isLoaded();
}

QSharedPointer<DkPluginContainer> DkPluginManager::getPluginByName(const QString & pluginName) const {

	for (auto p : mPlugins) {

		if (p && pluginName == p->pluginName())
			return p;
	}

	return QSharedPointer<DkPluginContainer>();
}

QSharedPointer<DkPluginContainer> DkPluginManager::getPluginByPath(const QString & path) const {

	for (auto p : mPlugins) {

		if (p && path == p->pluginPath())
			return p;
	}

	return QSharedPointer<DkPluginContainer>();
}

QVector<QSharedPointer<DkPluginContainer> > DkPluginManager::getBasicPlugins() const {

	QVector<QSharedPointer<DkPluginContainer> > plugins;

	for (auto plugin : mPlugins) {

		DkPluginInterface* p = plugin->plugin();

		if (p && p->interfaceType() == DkPluginInterface::interface_basic) {
			plugins.append(plugin);
		}
	}

	return plugins;
}

QVector<QSharedPointer<DkPluginContainer> > DkPluginManager::getBatchPlugins() const {

	QVector<QSharedPointer<DkPluginContainer> > plugins;

	for (auto plugin : mPlugins) {

		DkPluginInterface* p = plugin->plugin();

		if (p &&
			(p->interfaceType() == DkPluginInterface::interface_basic ||
			 p->interfaceType() == DkPluginInterface::interface_batch)) {
			plugins.append(plugin);
		}
	}

	return plugins;
}

QSharedPointer<DkPluginContainer> DkPluginManager::getRunningPlugin() const {

	for (auto plugin : mPlugins) {
		if (plugin->isActive())
			return plugin;
	}

	return QSharedPointer<DkPluginContainer>();
}

void DkPluginManager::clearRunningPlugin() {

	for (auto plugin : mPlugins)
		plugin->setActive(false);
}

void DkPluginManager::runPlugin(QSharedPointer<DkPluginContainer> plugin) {

	if (getRunningPlugin()) {

		// the plugin is not closed in time
		QMessageBox infoDialog(DkUtils::getMainWindow());
		infoDialog.setWindowTitle(QObject::tr("Close plugin"));
		infoDialog.setIcon(QMessageBox::Information);
		infoDialog.setText(QObject::tr("Please close the currently opened plugin."));
		infoDialog.show();

		infoDialog.exec();

		//TODO: dialog with yes/no - unload plugin if user wants to
	}

	plugin->setActive();
}

bool DkPluginManager::isBlackListed(const QString & pluginPath) const {

	QString fileName = QFileInfo(pluginPath).fileName();

	for (const QString& filter : blackList())
		if (pluginPath.contains(filter))
			return true;

	return false;
}

QStringList DkPluginManager::blackList() {
	return QStringList() << "opencv";
}

void DkPluginManager::createPluginsPath() {

#ifdef WITH_PLUGINS
	// initialize plugin paths -----------------------------------------
#ifdef Q_OS_WIN
	QDir pluginsDir = QCoreApplication::applicationDirPath() + "/plugins";
#else
	QDir pluginsDir = QCoreApplication::applicationDirPath() +  "/../lib/nomacs-plugins/";
#endif // Q_OS_WIN

	if (!pluginsDir.exists())
		pluginsDir.mkpath(pluginsDir.absolutePath());

	nmc::DkSettingsManager::param().global().pluginsDir = pluginsDir.absolutePath();
	qInfo() << "plugins dir set to: " << nmc::DkSettingsManager::param().global().pluginsDir;

	QCoreApplication::addLibraryPath(nmc::DkSettingsManager::param().global().pluginsDir);
	QCoreApplication::addLibraryPath("./imageformats");

#endif // WITH_PLUGINS
}

// DkPluginActionManager --------------------------------------------------------------------
DkPluginActionManager::DkPluginActionManager(QObject* parent) : QObject(parent) {

	assignCustomPluginShortcuts();
}

void DkPluginActionManager::assignCustomPluginShortcuts() {

	DefaultSettings settings;
	settings.beginGroup("CustomPluginShortcuts");
	QStringList psKeys = settings.allKeys();
	settings.endGroup();

	if (psKeys.size() > 0) {

		settings.beginGroup("CustomShortcuts");

		mPluginDummyActions = QVector<QAction *>();

		for (int i = 0; i< psKeys.size(); i++) {

			QAction* action = new QAction(psKeys.at(i), this);
			QString val = settings.value(psKeys.at(i), "no-shortcut").toString();
			if (val != "no-shortcut")
				action->setShortcut(val);
			connect(action, SIGNAL(triggered()), this, SLOT(runPluginFromShortcut()));
			// assign widget shortcuts to all of them
			action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
			mPluginDummyActions.append(action);
			//qDebug() << "new plugin action: " << psKeys.at(i);
		}

		settings.endGroup();
	}
}

void DkPluginActionManager::setMenu(QMenu* menu) {
	mMenu = menu;
	connect(mMenu, SIGNAL(aboutToShow()), this, SLOT(updateMenu()));
}

QMenu* DkPluginActionManager::menu() const {
	return mMenu;
}

QVector<QAction*> DkPluginActionManager::pluginDummyActions() const {
	return mPluginDummyActions;
}

QVector<QAction*> DkPluginActionManager::pluginActions() const {
	return mPluginActions;
}

QVector<QMenu*> DkPluginActionManager::pluginSubMenus() const {
	return mPluginSubMenus;
}

void DkPluginActionManager::updateMenu() {
	qDebug() << "CREATING plugin menu";

	if (!mMenu) {
		qWarning() << "plugin menu is NULL where it should not be!";
	}

	DkPluginManager::instance().loadPlugins();
	QVector<QSharedPointer<DkPluginContainer> > plugins = DkPluginManager::instance().getPlugins();

	if (plugins.empty()) {
		//mPluginActions.resize(DkActionManager::menu_plugins_end);
		mPluginActions = DkActionManager::instance().pluginActions();
	}
	mMenu->clear();

	for (auto p : plugins) {
		connect(p.data(), SIGNAL(runPlugin(DkViewPortInterface*, bool)), this, SIGNAL(runPlugin(DkViewPortInterface*, bool)), Qt::UniqueConnection);
		connect(p.data(), SIGNAL(runPlugin(DkPluginContainer*, const QString&)), this, SIGNAL(runPlugin(DkPluginContainer*, const QString&)), Qt::UniqueConnection);
	}

	if (plugins.isEmpty()) { // no  plugins
		mMenu->addAction(mPluginActions[DkActionManager::menu_plugin_manager]);
		mPluginActions.resize(DkActionManager::menu_plugin_manager);		// reduce the size again
	}
	else {
		// delete old plugin actions
		for (int idx = mPluginActions.size(); idx > DkActionManager::menu_plugins_end; idx--) {
			mPluginActions.pop_back();
		}
		addPluginsToMenu();
	}

}

/**
* Creates the plugin menu when it is not empty
* called in DkNoMacs::createPluginsMenu()
**/
void DkPluginActionManager::addPluginsToMenu() {

 	QVector<QSharedPointer<DkPluginContainer> > loadedPlugins = DkPluginManager::instance().getPlugins();
	qSort(loadedPlugins);

	mPluginSubMenus.clear();

	QStringList pluginMenu = QStringList();

	for (auto plugin : loadedPlugins) {

		DkPluginInterface* pi = plugin->plugin();

		if (pi && plugin->pluginMenu()) {
			QList<QAction*> actions = pi->createActions(DkUtils::getMainWindow());
			mPluginSubMenus.append(plugin->pluginMenu());
			mMenu->addMenu(plugin->pluginMenu());
		}
		else if (pi) {
			QAction* a = new QAction(plugin->pluginName(), this);
			a->setData(plugin->id());
			mPluginActions.append(a);
			mMenu->addAction(a);
			connect(a, SIGNAL(triggered()), plugin.data(), SLOT(run()));
		}
	}

	mMenu->addSeparator();
	mMenu->addAction(DkActionManager::instance().action(DkActionManager::menu_plugin_manager));

	QVector<QAction*> allPluginActions = mPluginActions;

	for (const QMenu* m : mPluginSubMenus) {
		allPluginActions << m->actions().toVector();
	}

	DkActionManager::instance().assignCustomShortcuts(allPluginActions);
	savePluginActions(allPluginActions);
}

void DkPluginActionManager::runPluginFromShortcut() {

	qDebug() << "running plugin shortcut...";

	QAction* action = qobject_cast<QAction*>(sender());
	QString actionName = action->text();

	updateMenu();

	QVector<QAction*> allPluginActions = mPluginActions;

	for (const QMenu* m : mPluginSubMenus) {
		allPluginActions << m->actions().toVector();
	}

	// this method fails if two plugins have the same action name!!
	for (int i = 0; i < allPluginActions.size(); i++)
		if (allPluginActions.at(i)->text().compare(actionName) == 0) {
			allPluginActions.at(i)->trigger();
			break;
		}
}

void DkPluginActionManager::savePluginActions(QVector<QAction *> actions) const {

	DefaultSettings settings;
	settings.beginGroup("CustomPluginShortcuts");
	settings.remove("");
	for (int i = 0; i < actions.size(); i++)
		settings.setValue(actions.at(i)->text(), actions.at(i)->text());
	settings.endGroup();
}

}
