/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * 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 "Application.h"
#include "Core.h"
#include "MainWindow.h"
#include "ExtensionManager.h"
#include "Action.h"
#include "Viewer.h"
#include "HistoryComponent.h"
#include "ImageComponent.h"
#include "Property.h"
#include "PropertyObject.h"
#include "TransformationManager.h"
#include "PersistenceManager.h"
#include "SplashScreen.h"
#include "AbortException.h"
#ifdef PYTHON_BINDING
#include "PythonManager.h"
#endif

#include "Log.h"

// -- QT stuff
#include <QSettings>
#include <QMessageBox>
#include <QInputDialog>
#include <QFileDialog>
#include <QDomDocument>
#include <QTextStream>
#include <QDateTime>
#include <QProcess>

// -- VTK stuff (only for updating progress bar via vtk filters)
#include <vtkAlgorithm.h>

#include<array>

// see http://doc.qt.nokia.com/latest/resources.html (bottom)
inline void initIcons() {
    Q_INIT_RESOURCE(CamiTKIcons);
}

namespace camitk {

// the main window (static, unique instance, verifies singleton)
MainWindow* Application::mainWindow = nullptr;
SplashScreen* Application::splashScreen = nullptr;
QSettings Application::settings(QSettings::IniFormat, QSettings::UserScope, "CamiTK", QString(Core::version()).remove(QChar(' ')));
QList<QFileInfo> Application::recentDocuments;
QDir Application::lastUsedDirectory;
int Application::maxRecentDocuments = 0;
QString Application::name = Core::version();
int Application::argc = 0;
char** Application::argv = nullptr;
QTranslator* Application::translator = nullptr;
PropertyObject* Application::propertyObject = nullptr;
Action* Application::currentAction = nullptr;

// ----------------- constructor --------------------
Application::Application(QString name, int& theArgc, char** theArgv, bool autoloadExtensions, bool registerFileExtension, bool useSplashScreen) : QApplication(theArgc, theArgv) {

    //-- generic init
    this->name = name;
    QApplication::setApplicationName(name);    // associate the QProperty to the Qt meta-object

    argc = theArgc;
    argv = theArgv;
    mainWindow = nullptr;
    translator = nullptr;

    //-- sometimes needed to get the icon when compiled in static mode
    initIcons();

    //-- Load the resources of the application (mainly its translation file)
    initResources();

    //-- Add application properties
    createProperties();

    //-- load all property values from the settings and apply them a first time
    propertyObject->loadFromSettings(Application::getName() + ".Application");
    applyPropertyValues();

    CAMITK_INFO(tr("Starting application..."))

    // recommended (see exec() comment in QApplication)
    connect(this, SIGNAL(aboutToQuit()), SLOT(quitting()));
    connect(this, SIGNAL(lastWindowClosed()), SLOT(quitting()));

    //-- create splash screen if required
    if (useSplashScreen) {
        splashScreen = new SplashScreen();
        splashScreen->show();
        showStatusBarMessage(QString("Initializing %1...").arg(name));
        setProgressBarValue(0);
    }

#ifdef PYTHON_BINDING
    // Init the python interpreter.
    //
    // This is done automatically in the Application constructor to take
    // the following scenarios into account (see also PythonManager::python):
    // - CamiTK CE was built without PYTHON_BINDING: no effect, python extensions are not available
    // - CamiTK CE was built with PYTHON_BINDING but python is not installed
    //   or the camitk python module is not installed: python extensions are not available
    // - CamiTK CE was built with PYTHON_BINDING and python AND camitk module are installed:
    //   python extensions are available.
    //
    // The second scenario requires that PythonManager::initPython() must be called
    // systematically when an application starts and should not produce an error, just disable
    // python if not available.
    //
    // Use PythonManager::pythonEnabled() to check the python interpreter status at any time
    PythonManager::initPython();
#endif

    //-- Load extensions
    // once the log is setup, only now one can try to load the extensions
    if (autoloadExtensions) {
        ExtensionManager::autoload();
    }

    //-- initialize recent/lastUsedDirectory documents from the settings
    settings.beginGroup(name + ".Application");

    // max memorized recent documents
    maxRecentDocuments = settings.value("maxRecentDocuments", 10).toInt();

    // the recent documents
    QStringList recentDoc = settings.value("recentDocuments").toStringList();
    recentDocuments.clear();

    for (QString fileName : recentDoc) {
        recentDocuments.append(QFileInfo(fileName));
    }

    // the last used directory
    QDir defaultDir(Core::getTestDataDir());
    lastUsedDirectory.setPath(settings.value("lastUsedDirectory", defaultDir.absolutePath()).toString());
    settings.endGroup();

    //-- register file association with this application for opening
    if (registerFileExtension) {
        // TODO : remove ifdef WIN32 as soon as we handle file association for other platform than Windows
        // see ExtensionManager::registerFileExtension method.
#ifdef WIN32
        // TODO : uses a better way to ask the user so that each component can be selected individually
        // File types association with the application for opening
        QStringList newFileExtensions;
        QStringList fileExtensionsAlreadyRegistered = settings.value("fileExtensionsRegistered").toStringList();
        // Forbidden list, to avoid user to have common image file type associated with the application
        QStringList fileExtensionForbidden;
        fileExtensionForbidden.append("jpg");
        fileExtensionForbidden.append("png");
        fileExtensionForbidden.append("bmp");
        fileExtensionForbidden.append("tif");
        fileExtensionForbidden.append("tiff");

        for (QString extensionFile : ExtensionManager::getFileExtensions()) {
            // check the application can handle new file type
            if (!fileExtensionsAlreadyRegistered.contains(extensionFile) && !fileExtensionForbidden.contains(extensionFile)) {
                newFileExtensions.append(extensionFile);
            }
        }

        // check if the application handles new file types
        if (!newFileExtensions.isEmpty()) {
            // new component extension(s) allow(s) the application to handle new file types
            // Prompt the user if he wishes those file extension to be associated with this application for default file opening.
            // CCC Exception: Use a QMessageBox as the user interaction is required
            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")
                           + newFileExtensions.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);
            msgBox.setWindowFlags(Qt::WindowStaysOnTopHint); // to stay on top of the splash screen

            if (msgBox.exec() == QMessageBox::Yes) {
                // user agrees : register each new file type
                for (QString fileExtensionToRegister : newFileExtensions) {
                    ExtensionManager::registerFileExtension(fileExtensionToRegister);
                }
            }

            // save the file types in the application's settings in order not to be prompt again
            fileExtensionsAlreadyRegistered.append(newFileExtensions);
            settings.setValue("fileExtensionsRegistered", fileExtensionsAlreadyRegistered);
        }

#endif
    }

    //-- Get notified (in evenFilter method) every time one of the property is modified
    // All event filter on the property object are delegated to the Application class
    // @see eventFilter()
    propertyObject->installEventFilter(this);

    // trigger change for all the values
    Application::eventFilter(this, new QEvent(QEvent::DynamicPropertyChange));
}

// ----------------- destructor --------------------
Application::~Application() {
    // do not use the destructor to clean or free resources, but quitting()

    // delete property object and all its properties !
    if (propertyObject) {
        delete propertyObject;
    }

    // finish all logging properly
    CAMITK_INFO(tr("Exiting application..."))
}

// ----------------- destructor --------------------
QString Application::getName() {
    return name;
}

// ----------------- quitting --------------------
void Application::quitting() {
    // this is connected to the aboutToQuit signal from QApplication
    // it should contain all the code that frees the resources

    // delete all actions (they are instantiated when the extension is loaded)
    ExtensionManager::unloadAllActionExtensions();
    ExtensionManager::unloadAllViewerExtensions();

    if (translator != nullptr) {
        // delete instance of internationalization support
        delete translator;
    }
}

// ----------------- notify --------------------
bool Application::notify(QObject* receiver, QEvent* event) {
    bool done = true;
    std::exception_ptr otherException;

    try {
        done = QApplication::notify(receiver, event);
    }
    catch (const std::bad_alloc& e) {
        CAMITK_WARNING_ALT(QString("Caught a bad alloc exception, the application has run out of memory:\n%1").arg(e.what()))
    }
    catch (const std::exception& e) {
        CAMITK_ERROR(tr("Caught a std exception:\n%1").arg(e.what()))
    }
    catch (...) {
        CAMITK_ERROR(tr("Caught an unknown exception"))
        otherException = std::current_exception();

        try {
            if (otherException) {
                std::rethrow_exception(otherException);
            }
        }
        catch (const std::exception& e) {
            CAMITK_ERROR(tr("Exception: %1").arg(e.what()))
        }
    }

    return done;
}

// ----------------- setMainWindow --------------------
void Application::setMainWindow(MainWindow* mw, bool redirect) {
    if (mw == nullptr) {
        mainWindow = new MainWindow(name);
    }
    else {
        mainWindow = mw;
    }

    // by default redirect to console
    mainWindow->redirectToConsole(redirect);

    // apply properties that modify the main window (e.g. stylesheet for splitter/separator)
    applyMainWindowPropertyValues();

    // Set the locale to C for using dot as decimal point despite locale
    // Set utf8 for output to enforce using utf8 strings.
    //
    // see http://doc.qt.io/qt-5/qcoreapplication.html#locale-settings
    // and various threads or forum discussions such as http://stackoverflow.com/questions/25661295/why-does-qcoreapplication-call-setlocalelc-all-by-default-on-unix-linux
    char* statusOk = setlocale(LC_CTYPE, "C.UTF-8");

    if (statusOk != nullptr) {
        statusOk = setlocale(LC_NUMERIC, "C.UTF-8");
    }

    if (statusOk != nullptr) {
        statusOk = setlocale(LC_TIME, "C.UTF-8");
    }

    // try without UTF-8
    if (!statusOk) {
        statusOk = setlocale(LC_CTYPE, "C");

        if (statusOk != nullptr) {
            statusOk = setlocale(LC_NUMERIC, "C");
        }

        if (statusOk != nullptr) {
            statusOk = setlocale(LC_TIME, "C");
        }

        if (statusOk == nullptr) {
            CAMITK_ERROR(tr("Could not set the locale to C. This is mandatory to enforce dot as the decimal separator and ensure platform independency of numerics.\nThis can cause a lot of trouble for numerics I/O... Beware of decimal dots...\n"))
        }
    }

    // hand over everything to main window
    if (splashScreen) {
        splashScreen->close();
    }
}


// ----------------- getMainWindow --------------------
MainWindow* Application::getMainWindow() {
    if (mainWindow == nullptr && dynamic_cast<Application*>(qApp) != nullptr) {
        // this is the first time getMainWindow() is called and no MainWindow were created
        // In this case, force the creation of a default MainWindow by sending nullptr
        dynamic_cast<Application*>(qApp)->setMainWindow(nullptr);
        // and then returns it
    }

    return mainWindow;
}

// ----------------- getSettings --------------------
QSettings& Application::getSettings() {
    return settings;
}

// ----------------- exec --------------------
int Application::exec() {
    if (mainWindow == nullptr && dynamic_cast<Application*>(qApp) != nullptr) {
        dynamic_cast<Application*>(qApp)->setMainWindow(nullptr);
    }

    // if mainWindow was nullptr when entering this method, it should have
    // been created by the call to setMainWindow(nullptr) above
    if (mainWindow != nullptr) {
        mainWindow->aboutToShow();
        mainWindow->show();
    }
    else {
        CAMITK_ERROR_ALT(tr("Cannot create MainWindow instance"));
    }

    return QApplication::exec();
}

// ------------- restart -----------------
void Application::restart(QString reason) {
    // ask for reload when extension dynamic library/shared object changes on the disk
    QStringList options;
    options << "Restart & Reload" << "Restart Only";
    bool ok;
    QString restartOption = QInputDialog::getItem(nullptr, "Restart", reason, options, 0, false, &ok);

    if (ok && !restartOption.isEmpty()) {
        QStringList arguments;
        // save workspace
        if (restartOption == options[0]) {
            // Create "unique" temporary camitk file
            QString temporaryFile = QDir::temp().absoluteFilePath(name + "-" + QString::number(QDateTime::currentMSecsSinceEpoch()) + ".camitk");
            bool saveStatus = Application::saveWorkspace(temporaryFile);
            if (!saveStatus) {
                QMessageBox::StandardButton reply = QMessageBox::question(nullptr, "Error Saving Workspace",
                                                    "An error occurred when attempting to save the workspace: it will not be possible to reload the currently opened components.<br/>Do you still want to restart?",
                                                    QMessageBox::Yes | QMessageBox::No);
                if (reply == QMessageBox::No) {
                    mainWindow->statusBar()->showMessage(tr("Restart cancelled..."));
                    return;
                }
            }
            else {
                arguments.append(temporaryFile);
            }
        }
        QString executable = QCoreApplication::applicationFilePath();

        QProcess::startDetached(executable, arguments);
        QCoreApplication::quit();
    }
    else {
        // User chose not to reload the extension
        if (mainWindow) {
            mainWindow->statusBar()->showMessage(tr("Restart cancelled..."));
        }
    }
}

// ----------------- refresh --------------------
void Application::refresh() {
    if (mainWindow != nullptr) {
        mainWindow->refresh();
    }

    // now that every viewer was refresh, clear the interface node modification flag
    for (Component* c : Application::getAllComponents()) {
        c->setNodeModified(false);
    }
}

// ----------------- showStatusBarMessage --------------------
void Application::showStatusBarMessage(QString msg, int timeout) {
    // if this application has no main window (no GUI)
    // there is no status bar, check if the splashscreen is available
    if (mainWindow && mainWindow->statusBar()) {
        mainWindow->statusBar()->showMessage(msg, timeout);
        QApplication::processEvents();
    }
    else {
        if (splashScreen && splashScreen->isVisible()) {
            splashScreen->showMessage(msg);
        }
        else {
            CAMITK_INFO_ALT(msg);
        }
    }
}

// ----------------- resetProgressBar --------------------
void Application::resetProgressBar() {
    // if this application has no main window (no GUI)
    // there is no status bar, therefore nothing to do
    if (mainWindow) {
        QProgressBar* progress = mainWindow->getProgressBar();

        if (progress) {
            progress->setValue(0);
        }
    }
}

// ----------------- setProgressBarValue --------------------
void Application::setProgressBarValue(int value) {
    // if this application has no main window (no GUI)
    // there is no status bar, check for a splash screen
    if (mainWindow && mainWindow->getProgressBar()) {
        mainWindow->getProgressBar()->setValue(value);
    }
    else {
        if (splashScreen && splashScreen->isVisible()) {
            splashScreen->setProgress(value);
        }
    }
}

// ----------------- vtkProgressFunction --------------------
void Application::vtkProgressFunction(vtkObject* caller, long unsigned int, void*, void*) {
    // if this application has no main window (no GUI)
    // there is no status bar, therefore nothing to do
    if (mainWindow) {
        QProgressBar* progress = mainWindow->getProgressBar();
        auto* filter = static_cast<vtkAlgorithm*>(caller);
        int progressVal = filter->GetProgress() * 100;

        if (progress) {
            progress->setValue(progressVal);
        }
    }
}

// ----------------- addRecentDocument --------------------
void Application::addRecentDocument(QString filename) {
    QFileInfo fileInfo(filename);
    recentDocuments.removeOne(fileInfo);
    recentDocuments.append(fileInfo);

    // update the last used dir
    lastUsedDirectory = recentDocuments.last().absoluteDir();

    // save settings (the last 10 recent files by default)
    settings.beginGroup(name + ".Application");

    // max memorized recent documents
    settings.setValue("maxRecentDocuments", maxRecentDocuments);

    // save all up to maxRecentDocuments
    int firstOpened = recentDocuments.size() - maxRecentDocuments;

    if (firstOpened < 0) {
        firstOpened = 0;
    }

    QStringList recentDoc;

    for (int i = firstOpened; i < recentDocuments.size(); i++) {
        recentDoc.append(recentDocuments[i].absoluteFilePath());
    }

    settings.setValue("recentDocuments", recentDoc);

    // last used directory
    settings.setValue("lastUsedDirectory", lastUsedDirectory.absolutePath());
    settings.endGroup();
}

// ----------------- getLastUsedDirectory --------------------
const QDir Application::getLastUsedDirectory() {
    return lastUsedDirectory;
}

// ----------------- setLastUsedDirectory --------------------
void Application::setLastUsedDirectory(QDir last) {
    lastUsedDirectory = last;
}

// ----------------- getRecentDocuments --------------------
const QList< QFileInfo > Application::getRecentDocuments() {
    return recentDocuments;
}

// ----------------- getMaxRecentDocuments --------------------
const int Application::getMaxRecentDocuments() {
    return maxRecentDocuments;
}

// -------------------- open --------------------
Component* Application::open(const QString& fileName, bool blockRefresh) {
    // set waiting cursor
    setOverrideCursor(QCursor(Qt::WaitCursor));

    Component* comp = nullptr;

    // -- Get the corresponding extension... (compatible format)
    QFileInfo fileInfo(fileName);

    // -- ask the plugin instance to create the Component instance using the
    // suffix (i.e. = check "bar" extension for fileName equals to "/path/to/file.foo.bar")
    ComponentExtension* componentExtension = ExtensionManager::getComponentExtension(fileInfo.suffix());

    // -- try harder: file may be compressed (as in ".foo.gz") so we check the complete extension
    if (componentExtension == nullptr) {
        // here check check "foo.bar" extension for fileName equals to "/path/to/file.foo.bar"
        componentExtension = ExtensionManager::getComponentExtension(fileInfo.completeSuffix());
    }

    // -- check the validity of the plugin
    if (!componentExtension) {
        // restore the normal cursor/progress bar
        restoreOverrideCursor();
        resetProgressBar();

        CAMITK_ERROR_ALT(tr("ComponentExtension Opening Error: cannot find the appropriate component plugin for opening:\n\"%1\" (extension \"%2\" or \"%3\")\nTo solve this problem, make sure that:\n - A corresponding valid plugin is present in one of the following directories: \"%4\"\n - Your application loaded the the appropriate extension before trying to open a file")
                         .arg(fileName,
                              fileInfo.suffix(),
                              fileInfo.completeSuffix(),
                              Core::getComponentDirectories().join(", ")))
    }
    else {
        std::exception_ptr otherException;

        // -- ask the plugin to create the top level component
        try {
            // open using transformed path so that anything looking like C:\\Dir1\\Dir2\\foo.zzz is unified to a C:/Dir1/Dir2/foo.zz
            comp = componentExtension->open(QFileInfo(fileName).absoluteFilePath());

            if (comp != nullptr) {
                // if a component is created, add the document to the recent list
                addRecentDocument(fileName);
                showStatusBarMessage(tr(QString("File " + fileName + " successfully loaded").toStdString().c_str()));

                CAMITK_WARNING_IF_ALT((!comp->isTopLevel()), tr("Instantiating a NON top level component."))

                if (!blockRefresh) {
                    // Cleanup unused frames and transformations, after opening component some may be unused now
                    TransformationManager::cleanupFramesAndTransformations();

                    // now refresh
                    refresh();
                }
            }
        }
        catch (AbortException& e) {
            // restore the normal cursor/progress bar
            restoreOverrideCursor();
            resetProgressBar();
            CAMITK_ERROR_ALT(tr("Opening aborted: extension: \"%1\"\nError: cannot open file \"%2\"\nReason:\n%3").arg(componentExtension->getName(), fileName, e.what()))
            comp = nullptr;
        }
        catch (std::exception& e) {
            // restore the normal cursor/progress bar
            restoreOverrideCursor();
            resetProgressBar();
            CAMITK_ERROR_ALT(tr("Opening aborted: extension: \"%1\"\nError: cannot open file \"%2\"\nExternal exception:\nThis exception was not generated directly by the extension,\nbut by one of its dependency.\nReason:\n%3").arg(componentExtension->getName(), fileName, e.what()))
            comp = nullptr;
        }
        catch (...) {
            // restore the normal cursor/progress bar
            restoreOverrideCursor();
            resetProgressBar();
            comp = nullptr;

            CAMITK_ERROR_ALT(tr("Opening aborted: extension: \"%1\"\nError: cannot open file \"%2\"\nUnknown Reason:\nThis unknown exception was not generated directly by the extension,\nbut by one of its dependency.").arg(componentExtension->getName(), fileName))

            // try harder
            otherException = std::current_exception();

            try {
                if (otherException) {
                    std::rethrow_exception(otherException);
                }
            }
            catch (const std::exception& e) {
                CAMITK_ERROR_ALT(tr("Reason:\n%1").arg(e.what()))
            }
        }
    }

    // restore the normal cursor/progress bar
    restoreOverrideCursor();
    resetProgressBar();

    return comp;
}

// -------------------- openDirectory --------------------
Component* Application::openDirectory(const QString& dirName, const QString& pluginName) {
    // set waiting cursor
    setOverrideCursor(QCursor(Qt::WaitCursor));

    Component* comp = nullptr;

    ComponentExtension* cp = ExtensionManager::getDataDirectoryComponentExtension(pluginName);

    if (cp != nullptr) {
        std::exception_ptr otherException;

        // Ask the plugin instance to create the Component instance
        try {
            comp = cp->open(QDir(dirName).absolutePath());

            // -- notify the application
            if (comp == nullptr) {
                CAMITK_ERROR_ALT(tr("Error loading directory: extension: \"%1\" error: cannot open directory: \"%2\"").arg(cp->getName(), dirName))
                showStatusBarMessage(tr("Error loading directory:") + dirName);
            }
            else {
                showStatusBarMessage(tr("Directory %1 successfully loaded").arg(dirName).toStdString().c_str());

                // In the process of loading Components and Viewers, Frames and Transformations might have
                // been created and replaced : we should remove the unused ones
                TransformationManager::cleanupFramesAndTransformations();

                // refresh all viewers
                refresh();
            }
        }
        catch (AbortException& e) {
            // restore the normal cursor/progress bar
            restoreOverrideCursor();
            resetProgressBar();
            CAMITK_ERROR_ALT(tr("Opening aborted: extension: \"%1\" error: cannot open directory: \"%2\"\nReason:\n%3").arg(cp->getName(), dirName, e.what()))
            comp = nullptr;
        }
        catch (...) {
            // restore the normal cursor/progress bar
            restoreOverrideCursor();
            resetProgressBar();
            comp = nullptr;

            CAMITK_ERROR_ALT(tr("Opening aborted: extension \"%1\" error: cannot open directory: \"%2\"\nThis exception was not generated directly by the extension,\nbut by one of its dependency.").arg(cp->getName(), dirName))

            // try harder
            otherException = std::current_exception();

            try {
                if (otherException) {
                    std::rethrow_exception(otherException);
                }
            }
            catch (const std::exception& e) {
                CAMITK_ERROR_ALT(tr("Reason:\n%1").arg(e.what()))
            }
        }
    }
    else {
        // restore the normal cursor/progress bar
        restoreOverrideCursor();
        resetProgressBar();
        CAMITK_ERROR_ALT(tr("Opening Error: Cannot find the appropriate component plugin for opening directory:\n%1\n"
                            "To solve this problem, make sure that:\n"
                            " - A corresponding valid plugin is present in one of the component directories: \"%2\"\n"
                            " - And either your application is initialized with the autoloadExtensions option\n"
                            " - Or your correctly registered your component in the CamiTK settings").arg(dirName, Core::getComponentDirectories().join(", ")))
    }

    // restore the normal cursor/progress bar
    restoreOverrideCursor();
    resetProgressBar();


    return comp;
}

// -------------------- loadWorkspace --------------------
bool Application::loadWorkspace(const QString& filepath) {
    if (PersistenceManager::loadWorkspace(filepath)) {
        // Update MainWindow settings
        MainWindow* window = getMainWindow();
        if (window) {
            window->initSettings();
        }

        // add the document to the recent list
        addRecentDocument(filepath);
        showStatusBarMessage(tr("Workspace %1 successfully loaded").arg(filepath));

        // In the process of loading Components and Viewers, Frames and Transformations might have
        // been created and replaced : We should remove the unused ones
        TransformationManager::cleanupFramesAndTransformations();

        // now refresh
        refresh();

        return true;
    }
    else {
        return false;
    }
}

// -------------------------- close ------------------------------
bool Application::close(Component* comp, bool blockRefresh) {
    int keyPressed = QMessageBox::Discard;
    bool saveOk = true;
    QString compName = comp->getName();

    // check if the top-level component needs to be saved
    if (comp->getModified()) {
        // Component was modified, propose to save it
        // CCC Exception: Use a QMessageBox::warning instead of CAMITK_WARNING as the user interaction is required
        keyPressed = QMessageBox::warning(nullptr, "Closing...", tr("Component \"") + compName + tr("\" has been modified.\nDo you want to save your change before closing?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save);

        // Do we have to save or not?
        if (keyPressed == QMessageBox::Save) {
            saveOk = save(comp);
        }
    }

    // Do we have to close or not?
    if (saveOk && keyPressed != QMessageBox::Cancel) {
        // delete the data
        // The Component destructor will unregister it from Application
        // And remove its children with it
        delete comp;
        comp = nullptr;

        if (!blockRefresh) {
            // refresh all viewers (that may change their Frame)
            refresh();

            // Cleanup unused frames and transformations
            // Closing components and refreshing viewers, some may now be useless
            TransformationManager::cleanupFramesAndTransformations();

            // Refresh again so that the TransformationExplorer displays up-to-date data
            refresh();
        }

        showStatusBarMessage(compName + tr(" successfully closed..."));
        return true;
    }
    else {
        // return that the close was cancelled
        showStatusBarMessage(tr("Close cancelled..."));
        return false;
    }

}

// -------------------- save --------------------
bool Application::save(Component* component) {

    // no name -> save as
    if (component->getFileName().isEmpty()) {
        getAction("Save As")->setInputComponent(component);
        return (getAction("Save As")->apply() == Action::SUCCESS);
    }

    // -- Get the corresponding extension... (compatible format)
    QFileInfo fileInfo(component->getFileName());

    // -- ask the plugin instance to create the Component instance using the
    // suffix (i.e. = check "bar" extension for fileName equals to "/path/to/file.foo.bar")
    ComponentExtension* componentExtension = ExtensionManager::getComponentExtension(fileInfo.suffix());

    // -- try harder: file may be compressed (as in ".foo.gz") so we check the complete extension
    if (componentExtension == nullptr) {
        // here check check "foo.bar" extension for fileName equals to "/path/to/file.foo.bar"
        componentExtension = ExtensionManager::getComponentExtension(fileInfo.completeSuffix());
    }

    // -- check the validity of the plugin
    if (!componentExtension) {
        QString extensionFileSuffix = "\"" + fileInfo.suffix() + "\"";
        if (fileInfo.completeSuffix() != fileInfo.suffix()) {
            extensionFileSuffix += "(or \"" + fileInfo.completeSuffix() + "\")";
        }
        CAMITK_ERROR_ALT(tr("Saving Error: cannot find the appropriate component extension for saving component: \"%1\"\n"
                            "Of type: \"%2\"\n"
                            "In file: \"%3\"\n"
                            "Using suffix: %4\n"
                            "To solve this problem, make sure that:\n"
                            " - A component extension that manages %4 suffix is present in one of the component extension directories: [%5]\n"
                            " - And either your application is initialized with the autoloadExtensions option\n"
                            "   or you correctly registered your component in the CamiTK settings")
                         .arg(component->getName(),
                              component->metaObject()->className(),
                              fileInfo.filePath(),
                              extensionFileSuffix,
                              Core::getComponentDirectories().join(", ")))
        return false;
    }
    else {
        // ready to save
        QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

        if (componentExtension->save(component)) {

            // update the last used dir
            setLastUsedDirectory(QFileInfo(component->getFileName()).absoluteDir());
            showStatusBarMessage(tr("%1 successfully saved...").arg(component->getName()));
            component->setModified(false);

            QApplication::restoreOverrideCursor();
            return true;
        }
        else {
            QApplication::restoreOverrideCursor();
            return false;
        }
    }
}

bool Application::saveWorkspace(const QString& filepath) {
    // Check whether all components are saved
    QStringList unsavedComps;
    for (auto* c : Application::getTopLevelComponents()) {
        if (c->getModified()) {
            unsavedComps.push_back(c->getName());
        }
    }
    if (!unsavedComps.isEmpty()) {
        CAMITK_WARNING_ALT(tr("Some components are not saved, saving the workspace will not save them!\nList of unsaved components:\n- %1").arg(unsavedComps.join("\n- ")));
    }
    // Make sure the mainwindow has saved its settings
    MainWindow* window = getMainWindow();
    if (window) {
        window->saveSettings();
    }
    return PersistenceManager::saveWorkspace(filepath);
}

// -------------------- getActionMap --------------------
QMap<QString, Action*>& Application::getActionMap() {
    static QMap<QString, Action*> actionMap;

    return actionMap;
}

// -------------------- getActions --------------------
const ActionList Application::getActions() {
    return getActionMap().values();
}

// -------------------- registerAllActions --------------------
int Application::registerAllActions(ActionExtension* ext) {
    int registered = 0;

    for (Action* action : ext->getActions()) {
        // check if an action with same name was not already registered
        if (getActionMap().contains(action->getName())) {
            CAMITK_ERROR_ALT(tr("Cannot register action: %1 (extension: %2, family: %3, description: \"%4\")\n"
                                "extension of same name already registered by extension \"%5\"")
                             .arg(action->getName(),
                                  action->getExtensionName(),
                                  action->getFamily(),
                                  action->getDescription(),
                                  getAction(action->getName())->getExtensionName()))
        }
        else {
            getActionMap().insert(action->getName(), action);
            registered++;
        }
    }

    return registered;
}

// -------------------- unregisterAllActions --------------------
int Application::unregisterAllActions(ActionExtension* ext) {
    int registered = 0;

    for (Action* action : ext->getActions()) {
        getActionMap().remove(action->getName());
        registered++;
    }

    return registered;
}

// ---------------- actionLessThan ----------------
bool actionLessThan(const camitk::Action* a1, const camitk::Action* a2) {
    // This method is needed by qsort in the sort method to sort action by name
    return a1->getName() < a2->getName();
}

// ---------------- viewerLessThan ----------------
bool viewerLessThan(const camitk::Viewer* v1, const camitk::Viewer* v2) {
    // This method is needed by qsort in the sort method to sort viewer by name
    return v1->getName() < v2->getName();
}


// ---------------- sort ----------------
ActionList Application::sort(ActionSet actionSet) {
    // sort actions by name
    ActionList actionList = actionSet.values();
    std::sort(actionList.begin(), actionList.end(), actionLessThan);

    return actionList;
}

ViewerList Application::sort(ViewerSet viewerSet) {
    // sort actions by name
    ViewerList viewerList = viewerSet.values();
    std::sort(viewerList.begin(), viewerList.end(), viewerLessThan);

    return viewerList;
}


// ---------------- getAction ----------------
Action* Application::getAction(QString name) {
    return getActionMap().value(name);
}

// ---------------- getActions ----------------
ActionList Application::getActions(Component* component) {
    ActionSet actions;

    if (component) {
        QStringList componentHierarchy = component->getHierarchy();

        for (Action* currentAct : Application::getActions()) {
            if (componentHierarchy.contains(currentAct->getComponentClassName())) {
                actions.insert(currentAct);
            }
        }
    }
    else {
        for (Action* currentAct : Application::getActions()) {
            if (currentAct->getComponentClassName().isEmpty()) {
                actions.insert(currentAct);
            }
        }
    }

    return sort(actions);
}

// ---------------- getActions ----------------
ActionList Application::getActions(ComponentList cList) {
    // if this is an empty list, return all actions not using any component
    if (cList.size() == 0) {
        return getActions(nullptr);
    }
    else {
        ActionSet actions;

        for (Component* currentComp : cList) {
            ActionList listOfCompatibleActions = getActions(currentComp);
            actions += ActionSet(listOfCompatibleActions.begin(), listOfCompatibleActions.end());
        }

        return sort(actions);
    }
}

// ---------------- getActions ----------------
ActionList Application::getActions(ComponentList selComp, QString tag) {
    // first build the list of possible actions for cList
    ActionList possibleActions = getActions(selComp);

    // now check possibleActions considering the tag value
    ActionList actions;

    for (Action* action : possibleActions) {
        if (action->getTag().contains(tag)) {
            actions.append(action);
        }
    }

    // sort and return
    std::sort(actions.begin(), actions.end(), actionLessThan);
    return actions;

}

// -------------------- getViewerMap --------------------
QMap<QString, Viewer*>& Application::getViewerMap() {
    static QMap<QString, Viewer*> viewerMap;

    return viewerMap;
}

// -------------------- getViewer --------------------
Viewer* Application::getViewer(QString name) {
    return getViewerMap().value(name);
}

// -------------------- registerViewer --------------------
bool Application::registerViewer(Viewer* viewer) {
    // first check if a viewer with the same name is not already registered
    if (getViewerMap().contains(viewer->getName())) {
        // if it is already registered, find out more information about the viewer extension
        // that manages the already registered viewer
        QString extensionName = "";
        ViewerExtension* extension = getViewerExtension(viewer);

        if (extension != nullptr) {
            extensionName = extension->getName();
            CAMITK_ERROR_ALT(tr("Cannot register viewer: %1 (description: \"%2\")\n"
                                "viewer of same name already registered in extension %3")
                             .arg(viewer->getName(), viewer->getDescription(), extensionName))
        }
        else {
            // should never happen
            CAMITK_ERROR_ALT(tr("Cannot register viewer: %1 (description: \"%2\")\n"
                                "viewer of same name already registered in an unknown extension...")
                             .arg(viewer->getName(), viewer->getDescription()))
        }

        return false;
    }
    else {
        // everything is alright register the viewer
        getViewerMap().insert(viewer->getName(), viewer);
        return true;
    }
}

// -------------------- registerAllViewers --------------------
int Application::registerAllViewers(ViewerExtension* ext) {
    int registered = 0;

    // register all the viewer instances in this extension
    for (Viewer* viewer : ext->getViewers()) {
        if (registerViewer(viewer)) {
            registered++;
        }
    }

    return registered;
}

// -------------------- getViewers --------------------
const ViewerList Application::getViewers(Component* component) {
    ViewerSet viewers;

    if (component) {
        QStringList componentHierarchy = component->getHierarchy();

        for (Viewer* currentViewer : Application::getViewers()) {
            for (QString compType : currentViewer->getComponentClassNames()) {
                if (componentHierarchy.contains(compType)) {
                    viewers.insert(currentViewer);
                }
            }
        }
    }
    else {
        for (Viewer* currentViewer : Application::getViewers()) {
            for (QString compType : currentViewer->getComponentClassNames()) {
                if (compType.isEmpty()) {
                    viewers.insert(currentViewer);
                }
            }
        }
    }

    return sort(viewers);
}

// -------------------- getNewViewer --------------------
Viewer* Application::getNewViewer(QString name, QString className) {

    int i = 0;
    QList<ViewerExtension*> vl = ExtensionManager::getViewerExtensionsList();

    while (i < vl.size() && vl.at(i)->getViewerClassName() != className) {
        i++;
    }

    if (i < vl.size()) {
        ViewerExtension* ve = vl.at(i);
        return ve->getNewInstance(name);
    }
    else {
        CAMITK_ERROR_ALT(tr("Cannot find a viewer extension that manage viewers of type \"%1\"\n").arg(className))
        return nullptr;
    }
}

// -------------------- getViewers --------------------
const ViewerList Application::getViewers() {
    return getViewerMap().values();
}

// -------------------- unregisterAllViewers --------------------
int Application::unregisterAllViewers(ViewerExtension* ext) {
    int unregistered = 0;

    for (Viewer* viewer : ext->getViewers()) {
        getViewerMap().remove(viewer->getName());
        unregistered++;
    }

    return unregistered;
}

// -------------------- getViewerExtension --------------------
ViewerExtension* Application::getViewerExtension(Viewer* viewer) {

    int i = 0;
    bool found = false;
    ViewerExtension* extension = nullptr;

    while (i < ExtensionManager::getViewerExtensionsList().size() && !found) {
        int j = 0;

        while (j < ExtensionManager::getViewerExtensionsList().at(i)->getViewers().size() && !found) {
            found = (ExtensionManager::getViewerExtensionsList().at(i)->getViewers().at(j)->getName() == viewer->getName());
            j++;
        }

        i++;
    }

    if (found) {
        extension = ExtensionManager::getViewerExtensionsList().at(i - 1);
    }

    return extension;
}

// ---------- isAlive ----------
bool Application::isAlive(Component* comp) {
    return getAllComponents().contains(comp);
}

bool Application::isAlive(Action* action) {
    return getActions().contains(action);
}

// -------------------- hasModified --------------------
bool Application::hasModified() {
    // look for a top level component that has been modified
    int i = 0;

    while (i < getTopLevelComponents().size() && !getTopLevelComponents() [i]->getModified()) {
        i++;
    }

    return (i < getTopLevelComponents().size());
}

// -------------------- getUniqueComponentName --------------------
QString Application::getUniqueComponentName(QString name, const ComponentList& components) {
    auto nameEquals = [&name](const Component * existingComp) {
        return existingComp->getName() == name;
    };

    // look for (number) in name
    QRegularExpression numSearch(R"regex((^.*)\((\d+)\)$)regex");

    // If the same name is already present, add "(index)" to it (same for the label)
    while (std::ranges::any_of(components, nameEquals)) {
        // Does name have "(number)
        QRegularExpressionMatch match = numSearch.match(name);
        if (match.hasMatch()) {
            int nextIndex = match.capturedTexts()[2].toInt() + 1;
            name = match.capturedTexts()[1] + QString("(%1)").arg(nextIndex);
        }
        else {
            name = name + " (2)";
        }
    }
    return name;
}

// -------------------- addComponent --------------------
void Application::addComponent(Component* comp) {
    getAllComponentList().append(comp);

    if (comp->getParentComponent() == nullptr) {
        // this a top level component, make sure it has a unique name
        QString newName = getUniqueComponentName(comp->getName(), getTopLevelComponentList());
        if (newName != comp->getName()) {
            comp->setName(newName);
        }
        getTopLevelComponentList().append(comp);
    }
}

// -------------------- removeComponent --------------------
void Application::removeComponent(Component* comp) {
    getAllComponentList().removeAll(comp);
    getTopLevelComponentList().removeAll(comp);
    getSelectedComponentList().removeAll(comp);
}

// -------------------- getTopLevelComponentList --------------------
ComponentList& Application::getTopLevelComponentList() {
    static ComponentList topLevelComponents;

    return topLevelComponents;
}

// -------------------- getAllComponentList --------------------
ComponentList& Application::getAllComponentList() {
    static ComponentList allComponents;

    return allComponents;
}

// -------------------- getSelectedComponentList --------------------
ComponentList& Application::getSelectedComponentList() {
    static ComponentList selectedComponents;

    return selectedComponents;
}

// -------------------- getTopLevelComponents --------------------
const ComponentList& Application::getTopLevelComponents() {
    return getTopLevelComponentList();
}

// -------------------- getAllComponents --------------------
const ComponentList& Application::getAllComponents() {
    return getAllComponentList();
}

// -------------------- getSelectedComponents --------------------
const ComponentList& Application::getSelectedComponents() {
    return getSelectedComponentList();
}

// -------------------- setSelected --------------------
void Application::setSelected(Component* component, bool isSelected) {
    // in case the component is selected again, put it in the end
    // therefore the correct processing is :
    // 1. remove component from the list
    // 2. if isSelected is true, append
    getSelectedComponentList().removeAll(component);

    if (isSelected) {
        getSelectedComponentList().append(component);
    }
}

// -------------------- clearSelectedComponents --------------------
void Application::clearSelectedComponents() {
    for (Component* comp : getSelectedComponentList()) {
        comp->setSelected(false);
    }

    getSelectedComponentList().clear();
}

// -------------------- setSelectedAction --------------------
void Application::setTriggeredAction(Action* action) {
    currentAction = action;
}

// -------------------- getSelectedAction --------------------
Action* Application::getTriggeredAction() {
    return currentAction;
}

// -------------------- getHistory --------------------
QStack<HistoryItem>& Application::getHistory() {
    // static singleton declaration
    static QStack<HistoryItem> history;
    return history;
}

// -------------------- addHistory ------------------------------
void Application::addHistoryItem(HistoryItem item) {
    getHistory().push(item);
}

// -------------------- removeLastHistoryItem --------------------
HistoryItem Application::removeLastHistoryItem() {
    return getHistory().pop();
}

// -------------------- saveHistoryAsSCXML --------------------
void Application::saveHistoryAsSCXML() {
    // Empty history => do nothing
    if (Application::getHistory().isEmpty()) {
        CAMITK_WARNING_ALT(tr("History is empty: there is no history to save."))
        return;
    }

    // Create the XML document
    QDomDocument doc;
    QDomNode xmlProlog = doc.createProcessingInstruction("xml", R"(version="1.0" encoding="UTF-8")");
    doc.appendChild(xmlProlog);

    // root element
    QDomElement root = doc.createElement("scxml");
    root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
    root.setAttribute("xmlns", "http://camitk.imag.fr/3/smallScxml");
    root.setAttribute("xmlns:camitk", "http://camitk.imag.fr/3/asm");
    root.setAttribute("xsi:schemaLocation", "http://camitk.imag.fr/3/smallScxml/../resources/smallScxml.xsd");
    root.setAttribute("initial", "Initialize");
    doc.appendChild(root);

    // SXML always contains an Initialize element which describe the pipeline
    QDomElement initializeElt = doc.createElement("state");
    root.appendChild(initializeElt);
    initializeElt.setAttribute("id", "Initialize");
    QDomElement initializeElt_onEntry = doc.createElement("onentry");
    initializeElt.appendChild(initializeElt_onEntry);
    QDomElement initializeElt_onState = doc.createElement("camitk:onState");
    initializeElt_onEntry.appendChild(initializeElt_onState);
    // Description of the pipeline
    QDomElement initializeElt_desc = doc.createElement("camitk:description");
    initializeElt_onState.appendChild(initializeElt_desc);
    // create the pipeline description that will be filled while processing the history items.
    QString pipelineDescription = "This pipeline will process several actions on the input component(s): <br /> <ul>";
    // Transition
    QDomElement initializeElt_transition = doc.createElement("transition");
    initializeElt_transition.setAttribute("event", "Next");
    initializeElt_transition.setAttribute("target", "Action 1");
    initializeElt.appendChild(initializeElt_transition);
    QDomElement initializeElt_transition2 = doc.createElement("transition");

    // Create a list of all components created during the pipeline, to know which ones to delete once pipeline is reset
    // by the action state machine
    QList<HistoryComponent> allCreatedComponents;

    // Main loop across the different actions in the history
    for (int i = 0; i < Application::getHistory().size(); i++) {
        // Get the current history element, i.e. the action of the pipeline with its parameters
        HistoryItem historyItem = Application::getHistory().at(i);
        Action* action = Application::getAction(historyItem.getName());
        // state
        QDomElement stateElement = doc.createElement("state");
        root.appendChild(stateElement);
        stateElement.setAttribute("id", "Action " + QString::number(i + 1));
        // onEntry
        QDomElement onentryElement = doc.createElement("onentry");
        stateElement.appendChild(onentryElement);
        QDomElement onStateElement = doc.createElement("camitk:onState");
        onentryElement.appendChild(onStateElement);

        // action description
        QDomElement descriptionElement = doc.createElement("camitk:description");
        QDomText descriptionText = doc.createTextNode(action->getDescription());
        descriptionElement.appendChild(descriptionText);
        onStateElement.appendChild(descriptionElement);

        // action name & parameters
        QDomElement actionElement = doc.createElement("camitk:action");
        onStateElement.appendChild(actionElement);
        // action name
        QDomElement actionElementName = doc.createElement("camitk:name");
        actionElement.appendChild(actionElementName);
        pipelineDescription.append("<li>" + action->getName() + "</li>");
        QDomText actionElementNameText = doc.createTextNode(action->getName());
        actionElementName.appendChild(actionElementNameText);

        // action parameters
        QDomElement parametersElement = doc.createElement("camitk:parameters");
        actionElement.appendChild(parametersElement);

        if (!action->dynamicPropertyNames().isEmpty()) {
            for (QByteArray actionParameter : action->dynamicPropertyNames()) {
                QDomElement parameterElement = doc.createElement("camitk:parameter");
                parameterElement.setAttribute("name", QString(actionParameter));
                parameterElement.setAttribute("type", QVariant::typeToName(action->property(actionParameter).type()));
                parameterElement.setAttribute("value", action->property(actionParameter.data()).toString());
                parametersElement.appendChild(parameterElement);
            }
        }

        // action input components
        if (!historyItem.getInputHistoryComponents().isEmpty()) {
            QDomElement actionElementInputComp = doc.createElement("camitk:inputs");
            actionElement.appendChild(actionElementInputComp);

            for (int j = 0; j < historyItem.getInputHistoryComponents().size(); j++) {
                QDomElement componentElement = doc.createElement("camitk:component");
                // determine the type of each input component (image, meshes or other)
                HistoryComponent inputHistoryComponents = historyItem.getInputHistoryComponents().at(j);

                switch (inputHistoryComponents.getType()) {
                    case HistoryComponent::IMAGE_COMPONENT:
                        componentElement.setAttribute("type", "ImageComponent");
                        break;

                    case HistoryComponent::MESH_COMPONENT:
                        componentElement.setAttribute("type", "MeshComponent");
                        break;

                    case HistoryComponent::OTHER:
                    default:
                        componentElement.setAttribute("type", "Other");
                        break;
                }

                // save its name
                componentElement.setAttribute("name", inputHistoryComponents.getName());

                // save the current DOM element in the XML structure
                actionElementInputComp.appendChild(componentElement);
            }
        }

        // action output components
        if (!historyItem.getOutputHistoryComponents().isEmpty()) {
            QDomElement outputsElement = doc.createElement("camitk:outputs");
            actionElement.appendChild(outputsElement);

            for (int j = 0; j < historyItem.getOutputHistoryComponents().size(); j++) {
                QDomElement componentElement = doc.createElement("camitk:component");
                // determine the type of each input component (image, meshes or other)
                HistoryComponent outputHistoryComponents = historyItem.getOutputHistoryComponents().at(j);
                allCreatedComponents.append(outputHistoryComponents);    // note the component created

                switch (outputHistoryComponents.getType()) {
                    case HistoryComponent::IMAGE_COMPONENT:
                        componentElement.setAttribute("type", "ImageComponent");
                        break;

                    case HistoryComponent::MESH_COMPONENT:
                        componentElement.setAttribute("type", "MeshComponent");
                        break;

                    case HistoryComponent::OTHER:
                    default:
                        componentElement.setAttribute("type", "Other");
                        break;
                }

                // save its name
                componentElement.setAttribute("name", outputHistoryComponents.getName());

                // save the current DOM element in the XML structure
                outputsElement.appendChild(componentElement);
            }
        }

        // Transitions
        if (Application::getHistory().size() >= 1) {   // else, no transition at all
            // Next transition
            QDomElement nextTransitionElement = doc.createElement("transition");

            if (i == Application::getHistory().size() - 1) {   // Last action element => next element = Bye element.
                nextTransitionElement.setAttribute("target", "Bye");
            }
            else {   // Next generic action state
                nextTransitionElement.setAttribute("target", "Action " + QString::number(i + 2));
            }

            nextTransitionElement.setAttribute("event", "Next");

            // Back transition
            QDomElement backTransitionElement = doc.createElement("transition");
            backTransitionElement.setAttribute("event", "Back");

            if (i == 0) {   // Back to Initialize state
                backTransitionElement.setAttribute("target", "Initialize");
            }
            else {   // Back to previous state and delete optional created component in memory
                backTransitionElement.setAttribute("target", "Action " + QString::number(i));
                // Verify that previous element has not created components in memory
                HistoryItem previousItem = Application::getHistory().at(i - 1);

                if (!previousItem.getOutputHistoryComponents().isEmpty()) {
                    // Ask for deletion of each component created at the previous element
                    QDomElement onTransitionElement =  doc.createElement("onTransition");
                    backTransitionElement.appendChild(onTransitionElement);
                    QDomElement closeElement = doc.createElement("camitk:close");
                    onTransitionElement.appendChild(closeElement);

                    for (HistoryComponent outputHistoryComponent : previousItem.getOutputHistoryComponents()) {
                        QDomElement backTransitionComponentElement = doc.createElement("camitk:component");
                        closeElement.appendChild(backTransitionComponentElement);

                        switch (outputHistoryComponent.getType()) {
                            case HistoryComponent::IMAGE_COMPONENT:
                                backTransitionComponentElement.setAttribute("type", "ImageComponent");
                                break;

                            case HistoryComponent::MESH_COMPONENT:
                                backTransitionComponentElement.setAttribute("type", "MeshComponent");
                                break;

                            case HistoryComponent::OTHER:
                            default:
                                backTransitionComponentElement.setAttribute("type", "Other");
                                break;
                        }

                        backTransitionComponentElement.setAttribute("name", outputHistoryComponent.getName());
                    }
                }
            }

            // add back first so that the "Back" button appears on the left
            stateElement.appendChild(backTransitionElement);
            // and "Next" appears on the right
            stateElement.appendChild(nextTransitionElement);

        } // End transitions
    }

    // Update description of the pipeline, now that we have filled the it!
    initializeElt_desc.appendChild(doc.createCDATASection(pipelineDescription));

    // Last element : Bye
    QDomElement finalElt = doc.createElement("state");
    root.appendChild(finalElt);
    finalElt.setAttribute("id", "Bye");
    finalElt.setAttribute("final", "true");
    QDomElement finalElt_onEntry = doc.createElement("onentry");
    finalElt.appendChild(finalElt_onEntry);
    QDomElement finalElt_onState = doc.createElement("camitk:onState");
    finalElt_onEntry.appendChild(finalElt_onState);
    // Thanks !
    QDomElement finalElt_desc = doc.createElement("camitk:description");
    finalElt_onState.appendChild(finalElt_desc);
    QDomCDATASection finalEltDesc;
    finalElt_desc.appendChild(doc.createCDATASection("Thanks you for using the CamiTK Action State Machine !"));
    // Transition
    QDomElement finalElt_transition = doc.createElement("transition");
    finalElt_transition.setAttribute("event", "Back to the beginning");
    finalElt_transition.setAttribute("target", "Initialize");
    finalElt.appendChild(finalElt_transition);

    // Ask for deletion of all components created in the pipeline
    if (!allCreatedComponents.isEmpty()) {
        QDomElement finalEltTransition_onTransition =  doc.createElement("onTransition");
        finalElt_transition.appendChild(finalEltTransition_onTransition);
        QDomElement finalEltTransition_close = doc.createElement("camitk:close");
        finalEltTransition_onTransition.appendChild(finalEltTransition_close);

        for (HistoryComponent createdComponent : allCreatedComponents) {
            QDomElement finalEltTransition_comp = doc.createElement("camitk:component");
            finalEltTransition_close.appendChild(finalEltTransition_comp);

            switch (createdComponent.getType()) {
                case HistoryComponent::IMAGE_COMPONENT:
                    finalEltTransition_comp.setAttribute("type", "ImageComponent");
                    break;

                case HistoryComponent::MESH_COMPONENT:
                    finalEltTransition_comp.setAttribute("type", "MeshComponent");
                    break;

                case HistoryComponent::OTHER:
                default:
                    finalEltTransition_comp.setAttribute("type", "Other");
                    break;
            }

            finalEltTransition_comp.setAttribute("name", createdComponent.getName());
        }
    }

    // note: final element will automatically have a "Quit" transition, added by the action state machine.

    // Write the xml to a file
    QString outputFileName = QFileDialog::getSaveFileName(nullptr, tr("Save history of actions ..."), Core::getCurrentWorkingDir() + "/camitk-history-" + QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss") + ".scxml", tr("SCXML Files (*.scxml)"));

    QFile outputFile(outputFileName);

    if (outputFile.open(QFile::WriteOnly | QFile::Text)) {
        QTextStream out(&outputFile);
        // automatically indent XML output
        doc.save(out, 4);
    }
    else {
        CAMITK_ERROR_ALT(tr("Cannot open file \"%1\" for writing. No history saved.").arg(outputFileName))
    }
}


// -------------------- getSelectedLanguage --------------------
QString Application::getSelectedLanguage() {
    // Get the path of the Language File in the setting file
    QSettings& settings(Application::getSettings());
    settings.beginGroup(Application::getName() + ".Application");
    // Get the language an d flag for the current Application instance
    //QString language = settings.value("language").toString();
    QString languageAbreviation = settings.value("languageAbreviation").toString();
    //QString flagFile = settings.value("flagFilename").toString();
    settings.endGroup();

    return languageAbreviation;
}

// -------------------- initResources --------------------
void Application::initResources() {

    // Get the selected language
    QString selectedLanguage = Application::getSelectedLanguage();

    // if a language is defined, then try to load the translation file
    if (!selectedLanguage.isEmpty()) {
        // Load the application translation
        translator = new QTranslator();
        QString languageFile = ":/translate_" + getName().remove("camitk-") + "/translate/translate_" + selectedLanguage + ".qm";

        if (translator->load(languageFile)) {
            QCoreApplication::installTranslator(translator);
        }
        else {
            CAMITK_WARNING_ALT(tr("Cannot load file: %1").arg(languageFile))
        }
    }
}

// -------------------- createProperties --------------------
void Application::createProperties() {
    // instantiate the application property object
    propertyObject = new PropertyObject(getName());

    Property* property;

    //-- Auto Load Last Opened Component
    property = new Property("Auto Load Last Opened Component", false, "If true the last opened components from the previous run is opened when the application starts", "");
    propertyObject->addProperty(property);

    //-- Logger Level
    property = new Property("Logger Level", Log::getLogger()->getLogLevel(), "Logger level. Set the verbosity level of the application logger.", "");
    // Set the enum typeLogger Level"
    property->setEnumTypeName("LogLevel");
    // Set a custom list of GUI names
    QStringList enumGuiText;

    for (const InterfaceLogger::LogLevel l : {
                InterfaceLogger::NONE, InterfaceLogger::ERROR, InterfaceLogger::WARNING, InterfaceLogger::INFO, InterfaceLogger::TRACE
            }) {
        enumGuiText << Log::getLevelAsString(l);
    }
    property->setAttribute("enumNames", enumGuiText);
    propertyObject->addProperty(property);

    //-- Message Box Level
    property = new Property("Message Box Level", Log::getLogger()->getMessageBoxLevel(), "Message box level. Set the lowest log level that will open modal message box for messages instead of unintrusive log.", "");
    // Set the enum type
    property->setEnumTypeName("LogLevel");
    property->setAttribute("enumNames", enumGuiText);
    propertyObject->addProperty(property);

    //-- Log messages to standard output
    property = new Property("Log to Standard Output", Log::getLogger()->getLogToStandardOutput(), "If true all log messages above the current logger level are printed on the standard output.", "");
    propertyObject->addProperty(property);

    //-- Log messages to file
    property = new Property("Log to File", Log::getLogger()->getLogToFile(), "If true all the log messages above the current logger level are printed in the log file. To change the log file use the application preference dialog or do it programmatically (see InterfaceLogger API), as the \"Logger Parameters\" action will only change the value for the current session.", "");
    propertyObject->addProperty(property);

    //-- Display Debug Information
    property = new Property("Display Debug Information to Log Message", Log::getLogger()->getDebugInformation(), "If true display some debug information in the log message header (generally includes method name, file and line number from where the log message was issued. This is very useful for debugging extension projects.", "");
    propertyObject->addProperty(property);

    //-- Display Time stamp Information
    property = new Property("Display Time Stamp Information to Log Message", Log::getLogger()->getTimeStampInformation(), "If true display the time stamp for every log message in the \"yyyy-MM-dd HH:mm:ss.zzz\" format.", "");
    propertyObject->addProperty(property);

    // -- Window Separator Size
    property = new Property("Window Separator Size", 2, "The size in pixels of the separator between dock widgets and the central widget. Change the value if you prefer a larger splitter. If value is 1, no highlight is visible.", "pixels");
    property->setAttribute("minimum", 1);
    propertyObject->addProperty(property);
}

// ---------------------- getPropertyObject ------------------
PropertyObject* Application::getPropertyObject() {
    return propertyObject;
}

// ---------------- eventFilter ----------------
bool Application::eventFilter(QObject* object, QEvent* event) {
    // watch propertyObject instance for dynamic property changes
    if (event->type() == QEvent::DynamicPropertyChange) {

        //-- first perform the proper action
        applyPropertyValues();

        //-- then save the new values in the settings
        propertyObject->saveToSettings(Application::getName() + ".Application");

        return true;
    }
    else {
        // otherwise pass the event on to the parent class
        return QApplication::eventFilter(object, event);
    }

}

// ---------------------- applyPropertyValues ------------------
void Application::applyPropertyValues() {
    //-- List of settings that does not require any actions when the value is changed
    // - "Auto Load Last Opened Component";

    //-- All other property values to apply
    std::array<InterfaceLogger::LogLevel, 5> logLevelNames { {InterfaceLogger::NONE, InterfaceLogger::ERROR, InterfaceLogger::WARNING, InterfaceLogger::INFO, InterfaceLogger::TRACE} };

    Log::getLogger()->setLogLevel(logLevelNames[propertyObject->property("Logger Level").toInt()]);
    Log::getLogger()->setMessageBoxLevel(logLevelNames[propertyObject->property("Message Box Level").toInt()]);
    Log::getLogger()->setDebugInformation(propertyObject->property("Display Debug Information to Log Message").toBool());
    Log::getLogger()->setTimeStampInformation(propertyObject->property("Display Time Stamp Information to Log Message").toBool());
    Log::getLogger()->setLogToStandardOutput(propertyObject->property("Log to Standard Output").toBool());
    Log::getLogger()->setLogToFile(propertyObject->property("Log to File").toBool());

    applyMainWindowPropertyValues();
}


// ---------------------- applyMainWindowPropertyValues ------------------
void Application::applyMainWindowPropertyValues() {
    // increase the size and highlight the separator between dock widgets and central widget
    // by setting the stylesheet
    if (mainWindow != nullptr) {
        mainWindow->setStyleSheet(QString("QMainWindow::separator { \
          width: %1px; \
          height: %1px; \
        } \
        QMainWindow::separator:hover { \
          background: palette(highlight); \
        }").arg(propertyObject->property("Window Separator Size").toInt()));
    }
}
}

