/*****************************************************************************
 * $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$
 ****************************************************************************/

#include "CppHotPlugActionExtension.h"
#include "CppHotPlugAction.h"

#include "HotPlugExtensionManager.h"

#include <TransformEngine.h>
#include <CMakeProjectManager.h>
#include <CamiTKExtensionModel.h>

#include <Log.h>

//-- Qt Stuff
#include <QMessageBox>
#include <QApplication>
#include <QTimer>

namespace camitk {

// -------------------- constructor --------------------
CppHotPlugActionExtension::CppHotPlugActionExtension(const QString& camitkFilePath, bool forceRebuild) : HotPlugActionExtension(camitkFilePath) {
    // setup watch
    connect(&fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, [ =, this](const QString & path) {
        QTimer::singleShot(1000, [ =, this ]() {
            this->extensionRebuilt(path);
        });
    });

    // check if rebuild is required
    CamiTKExtensionModel camitkExtensionModel(camitkFilePath);
    VariantDataModel& data = camitkExtensionModel.getModel();
    if (data["alwaysRebuild"].getValue().toBool() || HotPlugExtensionManager::getVerifyOrRebuildOption() || forceRebuild) {
        CppHotPlugActionExtension::rebuild();
    }
}

// -------------------- destructor --------------------
CppHotPlugActionExtension::~CppHotPlugActionExtension() {
    // make sure nothing is watched anymore
    fileSystemWatcher.removePaths(fileSystemWatcher.files());
}

// -------------------- initActions --------------------
bool CppHotPlugActionExtension::initActions(int progressMinimum, int progressMaximum) {
    CamiTKExtensionModel camitkExtensionModel(getLocation());
    QList<HotPlugAction*> failedActions;

    const VariantDataModel& allActions = camitkExtensionModel.getModel()["actions"];
    for (int i = 0; i < allActions.size(); i++) {
        HotPlugAction* a = new CppHotPlugAction(this, allActions[i]);
        a->init();

        // if the newly created action still needs update now, it means it failed to load its shared library
        if (a->needsUpdate()) {
            failedActions.append(a);
        }
        registerAction(a);
    }

    updateWatchedFiles();

    // warn the user when actions were not successfully loaded
    QStringList failedActionNames;
    for (HotPlugAction* a : failedActions) {
        failedActionNames.append(a->getName());
    }
    QString failedActionMessage;
    if (failedActions.size() == actions.size()) {
        failedActionMessage = QString((actions.size() == 1) ? "the defined action" : "all defined actions") + " (" + failedActionNames.join(", ") + ").";
    }
    else {
        failedActionMessage = "the following actions:<ul><li>" + failedActionNames.join("</li><li>") + "</li></ul>";
    }
    if (failedActions.size() > 0) {
        //-- ask for rebuild when extension dynamic library/shared object
        // Add a timer to auto close the dialog after 30000 to avoid blocking modal dialog during test
        QTimer::singleShot(30000, [ = ]()  {
            QWidget* widget = QApplication::activeModalWidget();
            if (widget) {
                widget->close();
            }
        });
        QMessageBox::StandardButton reply = QMessageBox::warning(nullptr, "Loading HotPlug Action Failed",
                                            tr("Action extension \"%1\" is registered, but an error occurred while loading %2<br/>HotPlug action implementation libraries could not be loaded.<br/>Do you want to rebuild the extension now (this message will automatically close after 30 seconds)?").arg(name).arg(failedActionMessage),
                                            QMessageBox::Yes | QMessageBox::No);
        if (reply == QMessageBox::Yes) {
            rebuild();
            // delete all registered
            while (!actions.empty()) {
                Action* toDelete = actions.takeFirst();
                delete toDelete;
            }
            // retry initialization
            init();
        }
        else {
            // User chose not to rebuild the extension
        }
    }
    else {
        successfullyLoaded = true;
    }

    return successfullyLoaded;
}

// -------------------- updateWatchedFiles --------------------
void CppHotPlugActionExtension::updateWatchedFiles() {
    // see QFileSystemWatcher::fileChanged documentation:
    // Note: As a safety measure, many applications save an open file by writing a new file and then deleting the old one.
    // In your slot function, you can check watcher.files().contains(path). If it returns false, check whether the file still
    // exists and then call addPath() to continue watching it.
    //
    // It seems that is exactly what is happening here. In our case, the application stated in the documentation above
    // is the cmake process (or more precisely nmake, make, msvc, g++, ld,...)
    //
    // If you watch a detailed log of what is happening when an action library is rebuilt while the QFileSystemWatcher is
    // watching the action .so/.dll/.dynlib file (lets call it libmyaction.so from now), you will see that
    // 1. extensionRebuilt is called a first time
    //    - the action is updated using the new libmyaction.so version, the action update the value of lastLoaded
    //    - updateWatchedFiles() is called a first time and nothing has to be done as the file is still in the paths
    //      watched by fileSystemWatcher
    // 2. extensionRebuilt is called a second time
    //    - the action considers that it is up to date (lastLoaded is greater than lastModified)
    //    - updateWatchedFiles() is called a second time, but this time the file seems not in the list of paths
    //      watched by fileSystemWatcher. It is therefore added _again_

    for (auto e : watchedUserActionLibraries.keys()) {
        if (!fileSystemWatcher.files().contains(e)) {
            if (QFileInfo(e).exists()) {
                CAMITK_TRACE(QString("%1 exists but not yet watched: added to watched.").arg(e));
                fileSystemWatcher.addPath(e);
            }
            else {
                CAMITK_TRACE(QString("Action extension \"%1\": file %2 is not available anymore. Not added to watched.").arg(getName()).arg(e));
            }
        }
    }
}

// -------------------- watchActionLibrary --------------------
void CppHotPlugActionExtension::watchActionLibrary(QString libraryPath, HotPlugAction* action) {
    watchedUserActionLibraries.insert(libraryPath, action);
}

// -------------------- extensionRebuilt --------------------
void CppHotPlugActionExtension::extensionRebuilt(const QString& path, int timerCallCount) {
    // this will be hit by the first library watch signal, locks for all, the other thread/signals will pass
    if (reloadMutex.tryLock()) {
        bool updateSucceeded = false;
        HotPlugAction* hotPlugAction = watchedUserActionLibraries.value(path);
        if (hotPlugAction) {
            if (hotPlugAction->needsUpdate()) {
                CAMITK_TRACE(QString("Updating HotPlug Action \"%1\"...").arg(hotPlugAction->getName()));
                updateSucceeded = hotPlugAction->update();
            }
            else {
                updateSucceeded = true;
                CAMITK_TRACE(QString("HotPlug Action \"%1\" does not need any update").arg(hotPlugAction->getName()));
            }
            updateWatchedFiles();
        }
        else {
            CAMITK_INFO(QString("HotPlug Action not found"));
        }

        if (!updateSucceeded && timerCallCount < 2) {
            CAMITK_TRACE(QString("HotPlug Action update failed, retrying in 5 sec..."));
            /// try again in 5 sec as the failure to load the shared library might be due to an incomplete file
            QTimer::singleShot(5000, [ =, this ]() {
                this->extensionRebuilt(path, timerCallCount + 1);
            });
        }

        // unlocking the mutex for watch to be active again
        reloadMutex.unlock();
    }
}

// -------------------- rebuild --------------------
void CppHotPlugActionExtension::rebuild() {
    // remove build
    QString sourcePath = QFileInfo(getLocation()).absolutePath();
    QDir buildExtensionDir;
    buildExtensionDir.setPath(sourcePath + "/build");
    if (buildExtensionDir.exists()) {
        buildExtensionDir.removeRecursively();
    }

    // rebuild now
    CMakeProjectManager manager(getLocation());
    CAMITK_INFO_ALT(QString("Building %1").arg(getLocation()));
    if (!manager.success()) {
        CAMITK_WARNING_ALT(QString("Failed to initialize build for \"%1\"").arg(getLocation()));
    }
    else {
        manager.setStages({CMakeProjectManager::Check_System, CMakeProjectManager::Generate_Source_Files, CMakeProjectManager::Configure_CMake, CMakeProjectManager::Build_Project});

        // connect signal to follow progress
        connect(&manager, &CMakeProjectManager::stageStarted, [ = ](const QString & stage) {
            CAMITK_INFO_ALT(QString("Starting %1...").arg(stage));
        });

        connect(&manager, &CMakeProjectManager::stageFinished, [ = ](const QString & stage, bool success, const QString & output) {
            CAMITK_WARNING_IF_ALT(!success, QString("%1 finished %2").arg(stage).arg((success) ? "[OK]" : "[FAILED]"));
            CAMITK_WARNING_IF_ALT(!success, QString("Output:\n%1\n").arg(output));
            CAMITK_INFO_IF_ALT(success, QString("%1 finished %2").arg(stage).arg((success) ? "[OK]" : "[FAILED]"));
        });

        connect(&manager, &CMakeProjectManager::allStagesFinished, [ this ](bool status) {
            if (!status) {
                CAMITK_WARNING(QString("Building %1 failed.").arg(getLocation()));
            }
        });

        manager.start();
    }
}

} // namespace camitk