/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2016 Univ. Grenoble Alpes, CNRS, TIMC-IMAG UMR 5525 (GMCAO)
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK 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 version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

// -- Core stuff
#include "ExtensionManager.h"
#include "Core.h"
#include "Log.h"
#include "Application.h"
#include "Action.h"

// -- QT stuff
#include <QDir>
#include <QtGui>
#include <QSettings>
#include <QApplication>
#include <QMessageBox>

namespace camitk {

// -------------------- autoload --------------------
void ExtensionManager::autoload() {
    autoload(COMPONENT);
    autoload(ACTION);
}

void ExtensionManager::autoload(ExtensionManager::ExtensionType type) {
    QStringList extensionDir;

    switch(type) {
    case ACTION:
        extensionDir = Core::getActionDirectories();
        break;
    case COMPONENT:
        extensionDir = Core::getComponentDirectories();
        break;
    default:
        CAMITK_DEBUG("ExtensionManager", "autoload of unknown type", "type should be either ACTION or COMPONENT, other extensions are not implemented yet");
        break;
    }

    //-- remove duplicates
    QStringList extensionFileNames;
    QStringList extensionAbsoluteFileNames;
    QMap<QString, QDir> extensionUniqueDir;
    foreach(QString dirName, extensionDir) {
        QDir dir(dirName);
        QStringList pluginFileNames = getPluginFileNames(dir);
        foreach(QString pluginFile, pluginFileNames) {
            QString pluginAbsoluteFileName = dir.absoluteFilePath(pluginFile);
            if (!extensionFileNames.contains(pluginFile)) {
                extensionFileNames.append(pluginFile);
                extensionUniqueDir.insert(pluginFile,dir);
                extensionAbsoluteFileNames.append(pluginAbsoluteFileName);
            } else {
                CAMITK_INFO("ExtensionManager", "autoload", "Warning: duplicate extension: " << pluginFile.toStdString() << " found in " << dir.absolutePath().toStdString() << ".\nUsing extension in " << extensionUniqueDir.value(pluginFile).absolutePath().toStdString() << " instead (higher priority)");
            }
        }
    }

    //-- load all extensions, try five times to avoid error for internal dependencies
    // if there is more than 5 dependency levels, then you have two choices:
    // - consider simplifying your component
    // - increase maxNumberOfTries
    int maxNumberOfTries = 5;
    int tryNr = 0;

    do {
        QMutableListIterator<QString> it(extensionAbsoluteFileNames);

        while (it.hasNext()) {
            QString extFileName = it.next();

            if (loadExtension(type,extFileName)) {
                // this one is loaded, remove it from the list to load
                it.remove();
            }
        }

        tryNr++;
    } while (tryNr < maxNumberOfTries && extensionAbsoluteFileNames.size() > 0);

    //-- manage loading errors
    if (extensionAbsoluteFileNames.size() > 0) {
        // get the messages from Qt
        QStringList errorStrings;
        foreach(QString fileName, extensionAbsoluteFileNames) {
            QPluginLoader pluginLoader(fileName);
            QObject *plugin = pluginLoader.instance();

            if (!plugin) {
                errorStrings << QString("Plugin " + fileName + ", error: " + pluginLoader.errorString());
            } else {
                errorStrings << QString("Plugin " + fileName + ", no error (you need to decrease your number of dependency level, or contact the CamiTK SDK developer team)");
            }
        }

        QMessageBox::warning(NULL, "ExtensionManager Opening Error...",
                             "AutoLoad plugin failed after " + QString::number(tryNr) + " tries for the following extension(s):<ul><li>"
                             + errorStrings.join("</li><li>")
                             + "</li></ul>"
                             + "List of library paths:<ul><li>"
                             + Application::instance()->libraryPaths().join("</li><li>")
                             + "</li></ul>"
                             + "List of path:<ul><li>"
#ifdef WIN32
                             + QString(qgetenv("PATH")).split(";").join("</li><li>")
#else
                             + QString(qgetenv("LD_LIBRARY_PATH")).split(":").join("</li><li>")
#endif
                             + "</li></ul>");

    }


    //-- load user extensions
    QSettings & settings = Application::getSettings();
    settings.beginGroup ( "UserExtensions" );
    QStringList userRegisteredExtensions;
    switch(type) {
    case ACTION:
        userRegisteredExtensions = settings.value("actions", QVariant(QStringList())).toStringList();
        break;
    case COMPONENT:
        userRegisteredExtensions = settings.value("components", QVariant(QStringList())).toStringList();
        break;
    default:
        break;
    }
    settings.endGroup();

    foreach(QString userRegisteredExtensionFile, userRegisteredExtensions) {
        loadExtension(type, userRegisteredExtensionFile);
    }

}

// -------------------- loadExtension --------------------
bool ExtensionManager::loadExtension(ExtensionManager::ExtensionType type, QString fileName) {

    // First of all, check we have the private library directories in our session PATH
    initPrivateLibDirs();

    bool returnValue = false;
    QPluginLoader pluginLoader(fileName);
    QObject *extension = pluginLoader.instance();

    if (extension) {
        switch(type) {
        case ACTION: {
            ActionExtension *ext = qobject_cast<ActionExtension *> (extension);

            if (ext) {
                ext->setLocation(fileName);
                ext->initResources();

                //-- register the filename
                getActionExtensionMap().insert(fileName, ext);
                // initialize all actions
                ext->init();
                //-- register all actions
                Application::registerAllActions(ext);
                returnValue = true;
            }
        }
        break;

        case COMPONENT: {
            ComponentExtension *cp = qobject_cast<ComponentExtension *> (extension);

            if (cp) {
                cp->setLocation(fileName);
                cp->initResources();

                //-- insert the ComponentExtension plugin in the application wide list
                if (cp->hasDataDirectory()) {
                    getDataDirectoryComponentExtensionMap().insert(cp->getName(), cp);
                } else {
                    // (cannot do that in the constructor because the virtual symbol table seems to be confused!)
                    foreach(QString ext, cp->getFileExtensions()) {
                        getComponentExtensionMap().insert(ext, cp);
                    }
                }

                returnValue = true;
            }
        }
        break;

        default:
            CAMITK_DEBUG("ExtensionManager", "loadExtension of unknown type", "type should be either ACTION or COMPONENT, other extensions are not implemented yet");
            break;
        }
    } else {
        pluginLoader.unload();  // to make sure we could try again later

        // try to load the missing shared object directly from private dir (give absolute path)
        QRegExp libname("\\((lib.*): cannot open shared object file");
        if (libname.indexIn(pluginLoader.errorString(), 0)!= -1) {
            QString privateLibToLoad=Core::getGlobalInstallDir() + "/lib/" + QString(Core::shortVersion) + "/" + libname.cap(1);
            QLibrary privateLib(privateLibToLoad);
            privateLib.load();
        }
    }

    return returnValue;

}

// -------------------- getInstallationString --------------------
QString ExtensionManager::getInstallationString(QString file) {
    QString whichInstallDir;
    QDir dir(QFileInfo(file).absolutePath());
    // go up three levels (one for the extension name, one for the camitk short version name, one for the lib name)
    dir.cdUp();
    dir.cdUp();
    dir.cdUp();

    QString absolutePath = dir.absolutePath();
    if (absolutePath == Core::getGlobalInstallDir()) {
        whichInstallDir = "[G]";
    } else if (absolutePath == Core::getUserInstallDir()) {
        whichInstallDir = "[L]";
    } else if (absolutePath == Core::getCurrentWorkingDir()) {
        whichInstallDir = "[W]";
    } else {
        whichInstallDir = "[U]";
    }
    return whichInstallDir;
}

// -------------------- getComponentExtensionMap --------------------
QMap< QString, ComponentExtension* > &  ExtensionManager::getComponentExtensionMap() {
    static QMap<QString, ComponentExtension*> componentExtensionMap;

    return componentExtensionMap;
}



// -------------------- getDataDirectoryComponentExtensionMap --------------------
QMap< QString, ComponentExtension* > & ExtensionManager::getDataDirectoryComponentExtensionMap() {
    static QMap<QString, ComponentExtension*> dataDirectoryComponentExtensionMap;

    return dataDirectoryComponentExtensionMap;
}

// -------------------- getComponentExtension --------------------
const ComponentExtension * ExtensionManager::getComponentExtension(QString extOrName) {
    ComponentExtension *cp = getComponentExtensionMap().value(extOrName);

    if (!cp) {
        cp = getDataDirectoryComponentExtensionMap().value(extOrName);

        if (!cp) {
            // look for the name in getComponentExtensionMap()
            QMapIterator<QString, ComponentExtension *> it(getComponentExtensionMap());

            while (it.hasNext() && !cp) {
                it.next();

                if (it.value()->getName() == extOrName)
                    cp = it.value();
            }
        }
    }

    return cp;
}

// -------------------- getComponentExtensions --------------------
const QMap<QString, ComponentExtension*> & ExtensionManager::getComponentExtensions() {
    return getComponentExtensionMap();
}

// -------------------- getComponentExtensionsList --------------------
const QList< ComponentExtension* > ExtensionManager::getComponentExtensionsList() {
    QList< ComponentExtension* > sortedList = getComponentExtensionMap().values().toSet().toList(); // to remove duplicates
    qSort(sortedList.begin(), sortedList.end(), ExtensionManager::componentExtensionLessThan);

    return sortedList;
}

// -------------------- getDataDirectoryComponents --------------------
const QMap<QString, ComponentExtension*> & ExtensionManager::getDataDirectoryComponents() {
    return getDataDirectoryComponentExtensionMap();
}

// -------------------- getDataDirectoryComponentsList --------------------
const QList< ComponentExtension* > ExtensionManager::getDataDirectoryComponentsList() {
    QList< ComponentExtension* > sortedList = getDataDirectoryComponentExtensionMap().values().toSet().toList(); // to remove duplicates
    qSort(sortedList.begin(), sortedList.end(), ExtensionManager::componentExtensionLessThan);

    return sortedList;
}

// -------------------- getFileExtensions --------------------
QStringList ExtensionManager::getFileExtensions() {
    return ExtensionManager::getComponentExtensions().keys();
}

// -------------------- getDataDirectoryExtNames --------------------
QStringList ExtensionManager::getDataDirectoryExtNames() {
    return ExtensionManager::getDataDirectoryComponents().keys();
}


// -------------------- getActionExtensionMap --------------------
QMap<QString, ActionExtension*>  &  ExtensionManager::getActionExtensionMap() {
    static QMap<QString, ActionExtension*> actionExtensionMap;

    return actionExtensionMap;
}

// -------------------- getActionExtensions --------------------
const QMap< QString, ActionExtension* >& ExtensionManager::getActionExtensions() {
    return getActionExtensionMap();
}

// -------------------- getActionExtensionsList --------------------
const QList< ActionExtension* > ExtensionManager::getActionExtensionsList() {
    QList< ActionExtension* > sortedList = getActionExtensionMap().values().toSet().toList(); // to remove duplicates
    qSort(sortedList.begin(), sortedList.end(), ExtensionManager::actionExtensionLessThan);

    return sortedList;
}

// -------------------- unloadAllActionExtensions --------------------
void ExtensionManager::unloadAllActionExtensions() {
    QList<QString> allExtensions = getActionExtensionMap().keys();

    while (!allExtensions.isEmpty())
        unloadActionExtension(allExtensions.takeFirst());
}

// -------------------- registerFileExtension --------------------
void ExtensionManager::registerFileExtension(QString fileExtension) {
    // we get the application name and its binary path for the association
    QString appName = QApplication::applicationName(); // used for windows registry
    QString appFilePath = QApplication::applicationFilePath(); // needed for associating file for opening command

#ifdef WIN32
    // WINDOWS ONLY
    appFilePath.replace("/", "\\");
    // Associate the file extension with the current application for opening
    // Store that information in the windows registery
    QSettings* registry = new QSettings("HKEY_CURRENT_USER\\Software\\Classes\\camitk-" + appName + "." + fileExtension, QSettings::NativeFormat);
    registry->setValue("Default", "CamiTK Image file");

    registry = new QSettings("HKEY_CURRENT_USER\\Software\\Classes\\camitk-" + appName + "." + fileExtension + "\\DefaultIcon", QSettings::NativeFormat);
    registry->setValue("Default", "\"" + appFilePath + "\", 0");

    registry = new QSettings("HKEY_CURRENT_USER\\Software\\Classes\\camitk-" + appName + "." + fileExtension + "\\shell\\Open\\command", QSettings::NativeFormat);
    registry->setValue("Default", "\"" + appFilePath + "\" %1");

    registry = new QSettings("HKEY_CURRENT_USER\\Software\\Classes\\." + fileExtension + "\\ShellNew", QSettings::NativeFormat);
    registry->setValue("Default", "");

    registry = new QSettings("HKEY_CURRENT_USER\\Software\\Classes\\." + fileExtension, QSettings::NativeFormat);
    registry->setValue("Default", "camitk-" + appName + "." + fileExtension);
#endif

    // TODO : associate file opening on Linux & MacOS
}

// -------------------- promptRegisterFileExtensions --------------------
bool ExtensionManager::promptRegisterFileExtensions(QStringList fileExtensions) {
    // Prompt the user if he wishes those file extension to be associated with this application for default file opening.
    QMessageBox msgBox;
    msgBox.setWindowTitle(tr("Associate new file extensions for opening."));
    msgBox.setText(tr("New component extension(s) allow(s) this application to handle new file type(s) : \n\n")
                   + fileExtensions.join(", ") + "\n\n"
                   + tr("Do you want this/these file type(s) to be opened by default with this application")
                   + " (" + QApplication::applicationName() + ") ?\n");
    msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
    msgBox.setDefaultButton(QMessageBox::Yes);

    return (msgBox.exec() == QMessageBox::Yes);
}

// -------------------- getExtensionFilter --------------------
QStringList ExtensionManager::getExtensionFilter() {
    // try to load all Components in the directory
    QStringList pluginFilter;
    // linux: .so, windows: .dll, macOs: .dylib
    pluginFilter << "*.so." + QString(Core::soVersion) << "*." + QString(Core::soVersion) + ".dylib";
    if (Core::isDebugBuild())
        pluginFilter << "*" + QString(Core::debugPostfix) +".dll";
    else
        pluginFilter << "*.dll";
    return pluginFilter;
}

// -------------------- getPluginFileNames --------------------
QStringList ExtensionManager::getPluginFileNames(QDir extensionsDir) {
    // loop to load component plugin, taking into account internal dependencies (i.e. dependency between
    // one component and another one.
    QStringList pluginFileNames = extensionsDir.entryList(getExtensionFilter(), QDir::Files, QDir::Name);

#ifdef WIN32
    // Get the MSVC debug dlls
    QStringList pluginFileNamesDebugMSVC = pluginFileNames.filter(QRegExp(".*"+QString(Core::debugPostfix)+".dll"));

    if (Core::isDebugBuild()) {
        return pluginFileNamesDebugMSVC;
    } else {
        // remove debug dll one by one
        foreach(QString debugDLL, pluginFileNamesDebugMSVC) {
            pluginFileNames.removeAll(debugDLL);
        }
        return pluginFileNames;
    }
#endif

    return pluginFileNames;

}

// -------------------- unloadActionExtension --------------------
bool ExtensionManager::unloadActionExtension(QString fileName) {
    if (getActionExtensionMap().contains(fileName)) {
        ActionExtension *ext = getActionExtensionMap().value(fileName);
        //-- unregister all actions
        foreach(Action *action, ext->getActions()) {
            getActionExtensionMap().remove(action->getName());
        }
        //-- unregister extension
        getActionExtensionMap().remove(fileName);
        // -- unregister actions from application
        Application::unregisterAllActions(ext);
        //-- delete extensions (and all its actions)
        delete ext;
        return true;
    } else
        return false;
}

// -------------------- unloadComponentExtension --------------------
bool ExtensionManager::unloadComponentExtension(QString extOrName) {
    bool unloaded = false;
    // remove from the application wide list
    ComponentExtension * cp = getComponentExtensionMap().take(extOrName);

    if (cp != NULL) {
        // remove all the other extensions
        QMutableMapIterator<QString, ComponentExtension *> it(getComponentExtensionMap());

        while (it.hasNext()) {
            it.next();

            if (it.value() == cp) {
                getComponentExtensionMap().take(it.key());
            }
        }

        // open the plugin
        QPluginLoader pluginLoader(cp->getLocation());
        // delete the pointer
        delete cp;
        // try to unload
        unloaded = pluginLoader.unload();
    } else {
        cp = getDataDirectoryComponentExtensionMap().take(extOrName);

        if (cp != NULL) {
            // open the plugin
            QPluginLoader pluginLoader(cp->getLocation());
            // delete the pointer
            delete cp;
            // try to unload
            unloaded = pluginLoader.unload();
        } else {
            // look for the name in getComponentExtensionMap()
            QMutableMapIterator<QString, ComponentExtension *> it(getComponentExtensionMap());

            while (it.hasNext()) {
                it.next();

                if (it.value()->getName() == extOrName)
                    return unloadComponentExtension(it.value()->getName());
            }
        }
    }

    return unloaded;
}

// -------------------- initPrivateLibDirs --------------------
void ExtensionManager::initPrivateLibDirs() {
    // Add the private lib dirs to the current application environment's PATH variable
    // This modifies the current PATH variable in order for the OS to find the private
    // libraries compiled as shared objects/dll/dylib needed by some extensions
    static bool alreadyInitialized = false;

    if(!alreadyInitialized) {
        // Build directory
        QDir privateBuildLibDir(Core::getCurrentWorkingDir());
        if (privateBuildLibDir.cd("lib/"+QString(Core::shortVersion))) {
            QByteArray privateBuildLibDirPath = privateBuildLibDir.canonicalPath().toUtf8();
#ifdef WIN32 // for Windows
            QByteArray path = qgetenv("PATH");
            path.append(";");
            path.append(privateBuildLibDirPath);
            path.append(";");
            path.append(privateBuildLibDirPath + "/actions");
            path.append(";");
            path.append(privateBuildLibDirPath + "/components");
            qputenv("PATH",path);
#endif
            // update the application / qt plugins library path
            Application::instance()->addLibraryPath(privateBuildLibDirPath);
            Application::instance()->addLibraryPath(privateBuildLibDirPath + "/actions");
            Application::instance()->addLibraryPath(privateBuildLibDirPath + "/components");
        }

        // Local install directory
        QDir privateLocalLibDir(Core::getUserInstallDir());
        if (privateLocalLibDir.cd("lib/"+QString(Core::shortVersion))) {
            QByteArray privateLocalLibDirPath = privateLocalLibDir.canonicalPath().toUtf8();
#ifdef WIN32 // for Windows
            QByteArray path = qgetenv("PATH");
            path.append(";");
            path.append(privateLocalLibDirPath);
            path.append(";");
            path.append(privateLocalLibDirPath + "/actions");
            path.append(";");
            path.append(privateLocalLibDirPath + "/components");
            qputenv("PATH",path);
#endif
            // update the application / qt plugins library path
            Application::instance()->addLibraryPath(privateLocalLibDirPath);
            Application::instance()->addLibraryPath(privateLocalLibDirPath + "/actions");
            Application::instance()->addLibraryPath(privateLocalLibDirPath + "/components");
        }

        // Global install directory
        QDir privateGlobalLibDir(Core::getGlobalInstallDir());
        if (privateGlobalLibDir.cd("lib/"+QString(Core::shortVersion))) {
            QByteArray privateGlobalLibDirPath = privateGlobalLibDir.canonicalPath().toUtf8();
#ifdef WIN32 // for Windows
            QByteArray path = qgetenv("PATH");
            path.append(";");
            path.append(privateGlobalLibDirPath);
            path.append(";");
            path.append(privateGlobalLibDirPath + "/actions");
            path.append(";");
            path.append(privateGlobalLibDirPath + "/components");
            qputenv("PATH",path);
#endif
            // update the application / qt plugins library path
            Application::instance()->addLibraryPath(privateGlobalLibDirPath);
            Application::instance()->addLibraryPath(privateGlobalLibDirPath + "/actions");
            Application::instance()->addLibraryPath(privateGlobalLibDirPath + "/components");
        }
        alreadyInitialized = true;
    }
}

// -------------------- componentExtensionLessThan --------------------
bool ExtensionManager::componentExtensionLessThan(const ComponentExtension* left, const ComponentExtension* right) {
    return left->getName() < right->getName();
}

// -------------------- actionExtensionLessThan --------------------
bool ExtensionManager::actionExtensionLessThan(ActionExtension* left, ActionExtension* right) {
    return left->getName() < right->getName();
}











// ----------------------------------------------------------------
//
//                     OBSOLETE PUBLIC METHODS
//
// TODO CAMITK_DEPRECATED. This section list all the methods marked as deprecated.
// They are to be removed in CamiTK 4.0
// ----------------------------------------------------------------

// -------------------- loadComponentExtension --------------------
bool ExtensionManager::loadComponentExtension(QString fileName) {
    return loadExtension(COMPONENT,fileName);
}

// -------------------- loadActionExtension --------------------
bool ExtensionManager::loadActionExtension(QString fileName) {
    return loadExtension(ACTION,fileName);
}

// -------------------- autoloadComponentExtensions --------------------
void ExtensionManager::autoloadComponentExtensions() {
    autoload(COMPONENT);
}

// -------------------- autoloadActionExtensions --------------------
void ExtensionManager::autoloadActionExtensions() {
    autoload(ACTION);
}

}


