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

#ifdef PYTHON_BINDING

#include "PythonManager.h"
#include "PythonHotPlugAction.h"
#include "Core.h"
#include "Application.h"
#include "AbortException.h"

#include <QRegularExpression>
#include <QProcess>
#include <QStandardPaths>
#include <QFile>

// manage ld_preload
#ifdef Q_OS_LINUX
#include <dlfcn.h>
#endif

#include "Log.h"

namespace camitk {

QString PythonManager::pythonStatusString;
QDir PythonManager::pythonCamitkModulePath;
QString PythonManager::pythonSoLib;
QString PythonManager::pythonVersion;
QString PythonManager::systemPythonExecutable;
bool PythonManager::isLocked;
QMap<QObject*, QPair<py::object, py::dict>> PythonManager::pythonStateMap;
QString PythonManager::currentVirtualEnvPath;
QString PythonManager::currentScriptPath;
py::module_ PythonManager::currentModule;

// -------------------- initPython --------------------
bool PythonManager::initPython() {
    if (Py_IsInitialized() != 0) {
        return true;
    }

    // check that python is installed
    systemPythonExecutable = findPythonExecutable();
    if (systemPythonExecutable.isNull()) {
        pythonStatusString = "Python disabled: cannot find python executable anywhere on the system paths.";
        CAMITK_TRACE_ALT(pythonStatusString)
        return false;
    }

    // 1. load libpython at startup
    //
    // FIX for import c-module (such as numpy) from interpreter embedded in dlopen shared object
    // raises undefined symbol (unix-only) / ImportError
    //
    // Analysis:
    // When the interpreter is asked to import a Python script that contains "import numpy"
    // (but that can happen with other python module), numpy is using cpython symbols.
    // Unfortunately, and and least on Debian/Ubuntu Linux, these symbols are not available
    // as the python executable is statically linked to the python library.
    //
    // Workaround:
    // Use explicit dlopen to load the shared python library.
    // Note: of course the shared library should be the same version as the one use
    // to statically link the system python executable.
    // We therefore need to:
    // - 1. determine which version and where is the corresponding shared library
    // - 2. manually load the python shared library
    //
    // When do we need this workaround?
    // To check if python executable was linked statically to the python library,
    // you need to run "python3 -m sysconfig | grep CONFIG_ARGS" and check if
    // the flag "--disable-shared" is there.
    // Alternatively (as this "disable-shared" is not present on Debian/Linux python),
    // running "ldd /usr/bin/python" shows that that /usr/bin/python is not linked to
    // any libpython.so shared object.
    //
    // Will this work?
    // To be able to load the python shared library manually (so for this workaround to be
    // efficient), run "python3 -m sysconfig | grep LINKFORSHARED" and check that
    // LINKFORSHARED contains  "-Xlinker -export-dynamic"
    //
    // References:
    // [1] https://github.com/pybind/pybind11/issues/3555#issuecomment-996560050
    // [2] https://github.com/pybind/pybind11/issues/3555#issuecomment-1736351976
    // [3] https://github.com/NOAA-OWP/ngen/issues/655#issuecomment-1736273405
    // [4] https://stackoverflow.com/a/60746446

    // use python to determine the correct libpython shared object to load via sysconfig
    resolvePythonSharedLibPathAndVersion();
    if (pythonVersion.isNull() || pythonVersion.isEmpty()) {
        pythonStatusString = "Python disabled: cannot determine python version.";
        CAMITK_TRACE_ALT(pythonStatusString)
        return false;
    }

    // use dl_open to load it first thing (simulate LD_PRELOAD)
    if (!loadPythonSharedLibrary()) {
        pythonStatusString = "Python disabled: could not load Python shared library. Check your installation.";
        CAMITK_TRACE_ALT(pythonStatusString);
        return false;
    }

    // 2. Determine the CamiTK directory that contains CamiTK python module
    QString pythonCamitkModuleFilePath = findPythonModule("camitk");
    if (pythonCamitkModuleFilePath.isNull()) {
        pythonStatusString = "Python disabled: cannot find the CamiTK python module anywhere in the python system paths nor CamiTK install paths.";
        CAMITK_TRACE_ALT(pythonStatusString)
        return false;
    }
    else {
        // use the directory
        pythonCamitkModulePath = QFileInfo(pythonCamitkModuleFilePath).absoluteDir();
    }

    // 3. launch python interpreter inside CamiTK
    // alternative to py::scoped_interpreter guard;
    py::initialize_interpreter();

    pythonStatusString = QString("Python %1 enabled.\nUsing camitk python module %2").arg(pythonVersion).arg(pythonCamitkModuleFilePath);

    CAMITK_TRACE_ALT(pythonStatusString)
    isLocked = false;
    return true;
}

// -------------------- getPythonStatus --------------------
QString PythonManager::getPythonStatus() {
    return pythonStatusString;
}

// -------------------- checkPythonPackageConflicts --------------------
void PythonManager::checkPythonPackageConflicts() {
    py::module_ importlib_metadata = py::module_::import("importlib.metadata");

    using PackageInfo = std::tuple<std::string, std::string>; // (version, location)
    std::unordered_map<std::string, PackageInfo> seen;

    for (auto dist : importlib_metadata.attr("distributions")()) {
        std::string name     = py::str(dist.attr("metadata")["Name"]);
        std::string version  = py::str(dist.attr("version"));
        std::string location = py::str(dist.attr("locate_file")("").attr("__str__")());

        auto it = seen.find(name);
        if (it != seen.end()) {
            const auto& [prev_version, prev_location] = it->second;
            if ((prev_version != version || prev_location != location) && (name != "packaging")) {
                // Already seen → conflict
                QStringList errorMsg;
                errorMsg << "[Conflict] " + QString::fromStdString(name) + ":";
                errorMsg << "  - " + QString::fromStdString(prev_version) + " at " + QString::fromStdString(prev_location);
                errorMsg << "  - " + QString::fromStdString(version) +  " at " + QString::fromStdString(location);
                QString errorString = errorMsg.join("\n");
                CAMITK_ERROR_ALT(errorString)
            }
        }
        else {
            seen[name] = {version, location};
        }
    }
}

// -------------------- getPythonVersion --------------------
QString PythonManager::getPythonVersion() {
    return pythonVersion;
}

// -------------------- resolvePythonSharedLibPath --------------------
void PythonManager::resolvePythonSharedLibPathAndVersion() {
    py::scoped_interpreter guard{};  // start interpreter just for this

    py::module_ sys = py::module_::import("sys");
    if (!sys.is(py::module_())) {
        pythonVersion = QString::fromStdString(py::str(sys.attr("version_info").attr("major"))) + "." + QString::fromStdString(py::str(sys.attr("version_info").attr("minor")));
    }

#ifdef Q_OS_LINUX
    py::module_ sysconfig = py::module_::import("sysconfig");

    if (!sysconfig.is_none()) {
        //PythonManager::dump(sysconfig.attr("__dict__"));
        std::string libdir = py::str(sysconfig.attr("get_config_var")("LIBDIR"));
        std::string ldlib = py::str(sysconfig.attr("get_config_var")("LDLIBRARY"));
        pythonSoLib = QString::fromStdString(libdir + "/" + ldlib);
    }
#endif
}

// -------------------- loadPythonSharedLibrary --------------------
bool PythonManager::loadPythonSharedLibrary() {
#ifdef Q_OS_LINUX
    void* handle = dlopen(pythonSoLib.toUtf8().data(), RTLD_NOW | RTLD_GLOBAL);
    if (!handle) {
        CAMITK_WARNING_ALT(QString("dlopen failed: %1").arg(dlerror()));
        return false;
    }
#endif
    return true;
}

//-------------------- importOrReload --------------------
py::module_ PythonManager::importOrReload(const QString& moduleName, bool clearAll) {
    QString operation = "import/reload";
    try {
        py::module_ sys = py::module_::import("sys");

        py::dict sys_modules = sys.attr("modules");

        if (sys_modules.contains(moduleName.toStdString().c_str())) {
            operation = "reload";
            py::object existingModule = sys_modules[moduleName.toStdString().c_str()];

            if (!py::isinstance<py::module_>(existingModule)) {
                // not a module, probably a python error already happened previously
                return py::module_();
            }

            if (clearAll) {
                //--1. before reloading purge the __dict__ before reloading from
                //     all variables and functions, but do not delete dunders nor modules
                QStringList keysToRemove;
                py::dict existingModuledict = existingModule.attr("__dict__");
                for (auto item : existingModuledict) {
                    QString key = QString::fromStdString(py::str(item.first));
                    // skip dunder and modules
                    if (!key.startsWith("__") && !py::isinstance<py::module_>(item.second)) {
                        // only remove variables and function, not import
                        keysToRemove.append(key);
                    }
                }
                for (auto key : keysToRemove) {
                    existingModuledict.attr("pop")(key.toStdString());
                }

                //--2. Force GC collection
                try {
                    py::module_ gc = py::module_::import("gc");
                    gc.attr("collect")();
                }
                catch (...) {
                    CAMITK_WARNING_ALT("GC module not available or failed to run.")
                }
            }

            py::module_ importlib = py::module_::import("importlib");
            return importlib.attr("reload")(existingModule);
        }
        else {
            operation = "import";
            return py::module_::import(moduleName.toStdString().c_str());
        }
    }
    catch (const std::bad_alloc& e) {
        CAMITK_WARNING_ALT(QString("Caught a bad alloc exception, the application has run out of memory during %1 of module '%2':\n%3").arg(operation).arg(moduleName).arg(e.what()))
    }
    catch (const std::exception& e) {
        py::module_ sys = py::module_::import("sys");
        QStringList sysPath;
        for (const auto& path : sys.attr("path")) {
            sysPath << QString::fromStdString(py::str(path));
        }
        CAMITK_WARNING_ALT(QString("Error during %1 of module '%2':\n%3\nsys.path:\n- %4\n").arg(operation).arg(moduleName).arg(e.what()).arg(sysPath.join("\n- ")))
    }

    // something wrong happen
    CAMITK_TRACE_ALT(QString("Error loading camitk module using the following python setup:\n %1").arg(pythonEnvironmentDebugInfo()))
    return py::module_();
}

// -------------------- findPythonModuleDir --------------------
QString PythonManager::findPythonModule(const QString& moduleName) {
    QSet<QString> searchPathSet;

    // 1. add current build dir
    // useful for non-conventional python installation (when testing directly from CamiTK SDK build or
    // if CamiTK was manually locally/globally installed using the provided targets or package zip archive on windows)
    QStringList camitkExtensionDir = Core::getExtensionDirectories("");
    for (QString path : camitkExtensionDir) {
        QDir installationLibPath;
        installationLibPath.setPath(path);
        installationLibPath.cdUp();
        searchPathSet.insert(installationLibPath.absolutePath());
#ifdef Q_OS_LINUX
        // check also python path inside installed path
        installationLibPath.cd("python" + getPythonVersion().left(getPythonVersion().indexOf('.')) + "/dist-packages");
        if (installationLibPath.exists()) {
            searchPathSet.insert(installationLibPath.absolutePath());
        }
#endif
    }

    // 2. Add system paths from Python itself
    py::scoped_interpreter guard{};  // temporarily start python
    py::module_ sys = py::module_::import("sys");
    py::list sysPaths = sys.attr("path");

    for (const auto& path : sysPaths) {
        QDir pathDir;
        pathDir.setPath(QString::fromStdString(py::str(path)));
        if (pathDir.exists()) {
            searchPathSet.insert(pathDir.absolutePath());
        }
    }

    // 3. look for the module in all the path
    QString modulePattern;
#ifdef Q_OS_WIN
    modulePattern = "^" + moduleName + R"regex(\.cp.*\.pyd)regex";
#else
    // on Q_OS_LINUX
    modulePattern = "^" + moduleName + R"regex(\.cpython.*\.so.*)regex";
#endif

    QRegularExpression moduleNameRegExp(modulePattern);
    QStringList searchPaths = searchPathSet.values();
    // Check for a file in searchPaths.at(i) that match the pattern
    int i = 0;
    QString foundModulePath;
    while (i < searchPaths.size() && foundModulePath.isNull()) {
        QStringList filesInPath = QDir(searchPaths.at(i)).entryList();
        QStringList matchingFiles = filesInPath.filter(moduleNameRegExp);
        if (matchingFiles.size() > 0) {
            foundModulePath = QDir(searchPaths.at(i)).absoluteFilePath(matchingFiles.first());
        }
        i++;
    }
    return foundModulePath;
}

// -------------------- insertIfNotAlreadyInList --------------------
bool PythonManager::insertIfNotAlreadyInList(py::list* list, const QString& value) {

    auto it = std::find_if(list->begin(), list->end(), [&](const py::handle & entry) {
        return QString::fromStdString(py::str(entry).cast<std::string>().c_str()) == value;
    });

    if (it == list->end()) {
        list->insert(0, value.toStdString());
        return true;
    }
    else {
        return false;
    }
}

// -------------------- findPythonExecutable --------------------
QString PythonManager::findPythonExecutable() {
    QString pythonPath;
#ifdef Q_OS_WIN
    // Try finding the 'py' launcher first
    pythonPath = QStandardPaths::findExecutable("py");
    if (pythonPath.isEmpty()) {
        // Fall back to python
        pythonPath = QStandardPaths::findExecutable("python");
    }
#else
    // On Linux/macOS try python3 first
    pythonPath = QStandardPaths::findExecutable("python3");
    if (pythonPath.isEmpty()) {
        // Fallback to python (for older systems)
        pythonPath = QStandardPaths::findExecutable("python");
    }
#endif
    return pythonPath;
}

// -------------------- pythonEnvironmentDebugInfo --------------------
QString PythonManager::pythonEnvironmentDebugInfo() {
    QStringList debugInfo;

    try {
        py::module_ sys = py::module_::import("sys");
        py::module_ site = py::module_::import("site");
        py::module_ metadata = py::module_::import("importlib.metadata");
        py::module_ platform = py::module_::import("platform");

        debugInfo << "# Python Environment Report\n";
        debugInfo << "Python executable: " + QString::fromStdString(py::str(sys.attr("executable"))) + ", system python executable: " + systemPythonExecutable;
        debugInfo << "Platform: " + QString::fromStdString(py::str(platform.attr("platform")()));
        debugInfo << "Python version: " + QString::fromStdString(py::str(platform.attr("python_version")()));

        debugInfo << "\n## sys.prefix info\n";
        debugInfo << "sys.prefix: " + QString::fromStdString(py::str(sys.attr("prefix")));
        debugInfo << "sys.base_prefix: " + QString::fromStdString(py::str(sys.attr("base_prefix")));
        debugInfo << "sys.exec_prefix: " + QString::fromStdString(py::str(sys.attr("exec_prefix")));
        debugInfo << "sys.base_exec_prefix: " + QString::fromStdString(py::str(sys.attr("base_exec_prefix")));

        debugInfo << "\n## sys.path\n";
        for (const auto& path : sys.attr("path")) {
            debugInfo << " - " + QString::fromStdString(py::str(path));
        }

        debugInfo << "\n## Environment Variables\n";
        QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
        for (const QString& key : env.keys()) {
            if (key.contains("PYTHON", Qt::CaseInsensitive) || key.contains("VIRTUAL", Qt::CaseInsensitive)) {
                debugInfo << " - " + key + " = " + env.value(key);
            }
        }

        debugInfo << "\n## Site info\n";
        debugInfo << "site.getsitepackages():";
        if (py::hasattr(site, "getsitepackages")) {
            for (const auto& path : site.attr("getsitepackages")()) {
                debugInfo << " - " + QString::fromStdString(py::str(path));
            }
        }
        debugInfo << "site.getusersitepackages():" + QString::fromStdString(py::str(site.attr("getusersitepackages")()));
        if (py::hasattr(site, "ENABLE_USER_SITE")) {
            bool userSite = py::cast<bool>(site.attr("ENABLE_USER_SITE"));
            debugInfo << "site.ENABLE_USER_SITE: " + QString(userSite ? "True" : "False");
        }

        debugInfo << "\n## sys.flags\n";
        QStringList sysFlags;
        py::object flags = sys.attr("flags");
        const std::vector<std::string> flag_names = {
            "debug", "inspect", "interactive", "optimize", "dont_write_bytecode", "no_user_site", "no_site", "ignore_environment", "verbose", "bytes_warning", "quiet", "hash_randomization", "isolated", "dev_mode", "utf8_mode", "warn_default_encoding", "safe_path", "int_max_str_digits"
        };
        for (const auto& flag : flag_names) {
            sysFlags << QString::fromStdString(flag) + "=" + QString::fromStdString(py::str(flags.attr(flag.c_str())));
        }
        debugInfo << "sys.flags(" + sysFlags.join(", ") + ")";

        debugInfo << "\n## Installed packages:";
        py::list distributions = metadata.attr("distributions")();
        for (const auto& dist : distributions) {
            std::string name = py::str(dist.attr("metadata")["Name"]);
            std::string version = py::str(dist.attr("metadata")["Version"]);
            debugInfo << "- " + QString::fromStdString(name + "==" + version);
        }
    }
    catch (const py::error_already_set& e) {
        debugInfo << "Python error while attempting to get environment debug info: " + QString(e.what());
        CAMITK_INFO_ALT(QString("Python error: %1").arg(e.what()))
    }

    return debugInfo.join("\n");
}

// -------------------- setupPython --------------------
bool PythonManager::setupPython(QString virtualEnvPath, QString scriptPath) {
    try {
        // load the sys module
        py::module_ sys = py::module_::import("sys");

        // create sys.path from scratch
        py::list sys_path = py::list();

        //-- 1. path to python CamiTK module
        insertIfNotAlreadyInList(&sys_path, pythonCamitkModulePath.absolutePath());

        //-- 2. path to the extension venv installed packages
#ifdef Q_OS_WIN
        QString sitePackage = virtualEnvPath + "/Lib/site-packages";
#else
        QString sitePackage = virtualEnvPath + "/lib/python" + getPythonVersion() + "/site-packages";
#endif
        insertIfNotAlreadyInList(&sys_path, sitePackage);

        //-- 3. common path (for global dll, but not for packages)
#ifdef Q_OS_WIN
        QString sysBasePrefix = QString::fromStdString(py::str(sys.attr("base_prefix"))); // something like C:\Python312 (default install dir)
        insertIfNotAlreadyInList(&sys_path, sysBasePrefix + "\\python" + getPythonVersion().remove('.') + ".zip");
        insertIfNotAlreadyInList(&sys_path, sysBasePrefix + "\\DLLs");
        insertIfNotAlreadyInList(&sys_path, sysBasePrefix + "\\Lib");
        insertIfNotAlreadyInList(&sys_path, sysBasePrefix);
#else
        insertIfNotAlreadyInList(&sys_path, "/usr/lib/python" + getPythonVersion() + "/lib-dynload");
        insertIfNotAlreadyInList(&sys_path, "/usr/lib/python" + getPythonVersion());
        insertIfNotAlreadyInList(&sys_path, "/usr/lib/python" + getPythonVersion().remove('.') + ".zip");
#endif

        //-- 4. path to the extension directory to load the script as modules if given
        if (!scriptPath.isNull()) {
            insertIfNotAlreadyInList(&sys_path, QFileInfo(scriptPath).absoluteDir().path());
        }

        //-- 5. force the interpreter inside the action venv
        sys.attr("path") = sys_path;
#ifdef Q_OS_WIN
        sys.attr("executable") = virtualEnvPath.toStdString() + "/Scripts/python";
#else
        sys.attr("executable") = virtualEnvPath.toStdString() + "/bin/python";
#endif
        sys.attr("prefix") = virtualEnvPath.toStdString();
        sys.attr("exec_prefix") = virtualEnvPath.toStdString();

        //-- 6. set the ENABLE_USER_SITE to true as it is the case when inside a venv
        // Note: this might have no real effect, but we want to mimic the venv as well as possible
        py::module_ site = py::module_::import("site");
        site.attr("ENABLE_USER_SITE") = py::bool_(false);

        //-- 7. set VIRTUAL_ENV value
        // Note: this might have no real effect, but we want to mimic the venv as well as possible
        qputenv("VIRTUAL_ENV", virtualEnvPath.toUtf8());

        //-- 8. monkey patch sitepackages
        // this is using a lambda in Python that returns a fixed list of paths
        py::list custom_paths;
        custom_paths.append(sitePackage.toStdString());
#ifdef Q_OS_WIN
        custom_paths.append(virtualEnvPath.toStdString());
#else
        custom_paths.append(virtualEnvPath.toStdString() + "/local/lib/python" + getPythonVersion().toStdString() + "/dist-packages");
        custom_paths.append(virtualEnvPath.toStdString() + "/lib/python" + getPythonVersion().mid(0, 1).toStdString() + "/dist-packages");
        custom_paths.append(virtualEnvPath.toStdString() + "/lib/python" + getPythonVersion().toStdString() + "/dist-packages");
#endif

        //-- 9. Replace site.getsitepackages with a lambda returning our list
        // equivalent of site.getsitepackages = lambda: ['/path/to/dir/1', '/path/to/dir/2'])"
        site.attr("getsitepackages") = py::cpp_function([custom_paths]() {
            return custom_paths;
        });

        // Finally, load python camitk module
        py::module_ pythonCamitkModule = importOrReload("camitk");
        if (pythonCamitkModule.is(py::module_())) {
            CAMITK_WARNING_ALT(QString("Cannot load camitk module required by virtual environment '%1'").arg(virtualEnvPath))
            return false;
        }
        else {
            // redirect python stdout and stderr to CamiTK
            pythonCamitkModule.attr("redirectStandardStreams")();
        }
    }
    catch (const std::exception& e) {
        CAMITK_WARNING_ALT(QString("Error during Python setup to virtual environment '%1':\n%2").arg(virtualEnvPath).arg(e.what()))
        return false;
    }

    // FIXME : should we do this only once per venv?
    checkPythonPackageConflicts();

    return true;
}

// -------------------- loadScript --------------------
py::module_ PythonManager::lockContext(QString virtualEnvPath, QString scriptPath) {
    // check the venv and script exists first
    if (!checkVirtualEnvPath(QFileInfo(virtualEnvPath).absoluteDir().absolutePath(), false)) {
        // no need to print some warning, this is already done by calling checkVirtualEnvPath in non silent mode
        return py::module_(); // uninitialized module
    }
    if (!QFileInfo(scriptPath).exists()) {
        CAMITK_WARNING_ALT(QString("Python script '%1' not found").arg(scriptPath))
        return py::module_(); // uninitialized module
    }

    if (isLocked) {
        if (virtualEnvPath == currentVirtualEnvPath && scriptPath == currentScriptPath) {
            return currentModule;
        }
        else {
            CAMITK_TRACE_ALT("Python context already locked. Please call unlock() first.")
            return py::module_(); // uninitialized module
        }
    }


    // reset module
    currentModule = py::module_();

    if (setupPython(virtualEnvPath, scriptPath)) {
        try {
            QString scriptModuleName = QFileInfo(scriptPath).baseName();
            // load user script as module
            currentModule = importOrReload(scriptModuleName, true);
            currentVirtualEnvPath = virtualEnvPath;
            currentScriptPath = scriptPath;
            isLocked = true;
        }
        catch (const std::exception& e) {
            py::module_ sys = py::module_::import("sys");
            py::list sysPaths = sys.attr("path");
            QStringList sysPathList;
            for (const auto& path : sysPaths) {
                sysPathList.append(QString::fromStdString(py::str(path)));
            }
            QString executablePath = QString::fromStdString(py::str(sys.attr("executable")));
            QString prefixPath = QString::fromStdString(py::str(sys.attr("prefix")));

            CAMITK_WARNING_ALT(QString("Error importing Python script '%1':\n%2\nSystem path:\n- %3\nExecutable: %4\nPrefix: %5").arg(scriptPath).arg(e.what()).arg(sysPathList.join("\n- ")).arg(executablePath).arg(prefixPath))

        }
    }

    return currentModule; // might be null/non initialized
}

// -------------------- unlock --------------------
void PythonManager::unlock() {
    isLocked = false;
}

// -------------------- runScript --------------------
QMap<QString, QVariant> PythonManager::runScript(QString virtualEnvPath, const QString& pythonScript, QString& pythonError) {
    QMap<QString, QVariant> scriptVariables;
    // check the venv and script exists first
    if (!checkVirtualEnvPath(QFileInfo(virtualEnvPath).absoluteDir().absolutePath(), false)) {
        // no need to print some warning, this is already done by calling checkVirtualEnvPath in non silent mode
        return scriptVariables;
    }

    // prevent mixing virtual env
    if (isLocked) {
        CAMITK_TRACE_ALT("Python context already locked. Please call unlock() first.")
        return scriptVariables;
    }

    // Force clearing all references to modules and variables from `__main__`
    // see https://github.com/pybind/pybind11/issues/5173#issuecomment-2185000795
    // see https://github.com/pybind/pybind11/discussions/5171#discussioncomment-9857969
    // Beware that this is a trick over pybind11, and might not work in the future
    // if pybind11 team changes this behaviour
    // PyDict_Clear(PyModule_GetDict(PyImport_AddModule("__main__")));

    if (setupPython(virtualEnvPath)) {
        try {
            std::string script = pythonScript.toStdString();

            // Use a dedicated namespace (dictionary) for script execution
            py::dict locals;
            py::exec(script, py::globals(), locals);

            for (auto& item : locals) {
                QString key = QString::fromStdString(py::str(item.first));
                scriptVariables.insert(key, fromPython(item.second));
            }

            return scriptVariables;
        }
        catch (const py::error_already_set& e) {
            CAMITK_WARNING_ALT(QString("Error when running script '%1':\n%2").arg(pythonScript).arg(e.what()))
            pythonError = e.what();
            return scriptVariables;
        }
    }

    return scriptVariables;
}

// -------------------- fromPython --------------------
QVariant PythonManager::fromPython(const py::handle& value) {
    // convert classic variable types
    if (value.is_none()) {
        return QVariant();
    }

    if (py::isinstance<py::bool_>(value)) {
        return QVariant(static_cast<bool>(value.cast<py::bool_>()));
    }

    if (py::isinstance<py::int_>(value)) {
        return QVariant(static_cast<qint64>(value.cast<py::int_>()));
    }

    if (py::isinstance<py::float_>(value)) {
        return QVariant(static_cast<double>(value.cast<py::float_>()));
    }

    if (py::isinstance<py::str>(value)) {
        return QVariant(QString::fromStdString(value.cast<std::string>()));
    }

    if (py::isinstance<py::bytes>(value)) {
        std::string s = value.cast<std::string>();
        return QVariant(QByteArray::fromRawData(s.data(), static_cast<int>(s.size())));
    }

    if (py::isinstance<py::list>(value) || py::isinstance<py::tuple>(value)) {
        QVariantList list;
        for (auto item : value) {
            // beware: recurse
            list.append(fromPython(item));
        }
        return list;
    }

    if (py::isinstance<py::dict>(value)) {
        QVariantMap map;
        py::dict dictValue = py::reinterpret_borrow<py::dict>(value);
        for (auto item : dictValue) {
            QString key = QString::fromUtf8(py::str(item.first).cast<std::string>().c_str());
            // beware: recurse
            map.insert(key, fromPython(item.second));
        }
        return map;
    }

    try {
        py::object pythonObject = py::reinterpret_borrow<py::object>(value);
        py::str pythonTypeName = py::str(py::type::of(pythonObject));
        return QVariant(QString("<unsupported Python type '%1'>").arg(QString::fromStdString(pythonTypeName.cast<std::string>())));
    }
    catch (...) {
        return QVariant(QString("<unsupported Python type>"));
    }
}


// -------------------- checkVirtualEnvPath --------------------
bool PythonManager::checkVirtualEnvPath(QString virtualEnvRootPath, bool silent) {
    QFileInfo venvRootPath(virtualEnvRootPath);
    if (!venvRootPath.exists() || !venvRootPath.isDir() || !venvRootPath.isWritable()) {
        CAMITK_WARNING_IF_ALT(!silent, "Invalid python virtual environment root directory '" + venvRootPath.absolutePath() + "': directory does not exist or is not writable.");
        return false;
    }

    QDir checkDir;
    checkDir.setPath(virtualEnvRootPath);

    QDir venvDir = checkDir;
    if (!venvDir.cd(".venv")) {
        CAMITK_WARNING_IF_ALT(!silent, "Invalid python virtual environment root directory '" + checkDir.absolutePath() + "': missing or invalid '.venv' directory.");
        return false;
    }

    QDir binDir = venvDir;
    bool binDirExists;
#ifdef Q_OS_WIN
    binDirExists = binDir.cd("Scripts");
#else
    binDirExists = binDir.cd("bin");
#endif
    if (!binDirExists) {
        CAMITK_WARNING_IF_ALT(!silent, "Invalid python virtual environment directory '" + venvDir.absolutePath() + "': subdirectory 'bin' does not exist or is invalid.");
        return false;
    }

    QDir sitePackageDir = venvDir;
#ifdef Q_OS_WIN
    QString sitePackage = "Lib/site-packages";
#else
    QString sitePackage = "lib/python" + PythonManager::getPythonVersion() + "/site-packages";
#endif
    if (!sitePackageDir.cd(sitePackage)) {
        CAMITK_WARNING_IF_ALT(!silent, "Invalid python virtual environment directory '" + venvDir.absolutePath() + "': missing '" + sitePackage + "' subdirectory.");
        return false;
    }

    QString pipCommandPath;
#ifdef Q_OS_WIN
    pipCommandPath = binDir.absoluteFilePath("pip.exe");
#else
    pipCommandPath = binDir.absoluteFilePath("pip");
#endif
    if (!QFile::exists(pipCommandPath)) {
        CAMITK_WARNING_IF_ALT(!silent, "Invalid python virtual environment directory '" + venvDir.absolutePath() + "': missing 'pip' command.");
        return false;
    }

    return true;
}

// -------------------- createVirtualEnv --------------------
bool PythonManager::createVirtualEnv(QString virtualEnvRootPath) {
    if (!checkVirtualEnvPath(virtualEnvRootPath)) {
        QStringList args = {"-m", "venv", virtualEnvRootPath + "/.venv"};
        QProcess pythonCommand;
        pythonCommand.setProgram(systemPythonExecutable);
        pythonCommand.setWorkingDirectory(virtualEnvRootPath);
        pythonCommand.setProcessChannelMode(QProcess::MergedChannels);
        pythonCommand.setArguments(args);
        pythonCommand.start();
        if (!pythonCommand.waitForStarted()) {
            CAMITK_WARNING_ALT("Command '" + systemPythonExecutable + " " + args.join(" ") + "' failed to start:\n" + QString(pythonCommand.errorString()))
        }
        if (!pythonCommand.waitForFinished(300000)) { // 5 min (default is 30s, but some Windows machine may be very slow)
            CAMITK_WARNING_ALT("Command '" + systemPythonExecutable + " " + args.join(" ") + "' timed out (5 min):\n" + QString(pythonCommand.readAll()))
        }
        if (pythonCommand.exitCode() != 0) {
            CAMITK_WARNING_ALT("Failed to create virtual environment using system python " + systemPythonExecutable + "\nExit code: " + QString::number(pythonCommand.exitCode()) + "\nOutput:\n" + QString(pythonCommand.readAll()))
        }
        return checkVirtualEnvPath(virtualEnvRootPath, false);
    }
    else {
        return true;
    }
}

// -------------------- installPackages --------------------
bool PythonManager::installPackages(QString virtualEnvPath, QStringList packages, int progressMinimum, int progressMaximum) {
    // PyPI is case insensitive, ensure requirements are lowercase to simplify comparison
    // see https://packaging.python.org/en/latest/specifications/
    for (QString &req : packages) {
        req = req.toLower();
    }

    // always add numpy (needed by CamiTK python module) and setuptools (needed by PythonManager itself)
    if (packages.filter("setuptools").size() == 0) { // Note using filter(..) to take into account requirements with version number (e.g. "setuptools==68.1.2")
        packages.append("setuptools");
    }
    if (packages.filter("numpy").size() == 0) {
        packages.append("numpy");
    }

    //-- 0. prepare the pip command
    QDir venvDir;
    venvDir.setPath(virtualEnvPath);

#ifdef Q_OS_WIN
    venvDir.cd("Scripts");
#else
    venvDir.cd("bin");
#endif

    QString pipCommandPath;
#ifdef Q_OS_WIN
    pipCommandPath = venvDir.absoluteFilePath("pip.exe");
#else
    pipCommandPath = venvDir.absoluteFilePath("pip");
#endif

    QStringList output;
    QProcess pipCommand;
    pipCommand.setProgram(pipCommandPath);
    pipCommand.setWorkingDirectory(virtualEnvPath);
    pipCommand.setProcessChannelMode(QProcess::MergedChannels);

    QString virtualEnvPackagePath = virtualEnvPath + "/lib/python" + getPythonVersion() + "/site-packages/";
    QString systemPackagePath = "/usr/lib/python" + getPythonVersion().mid(0, 1) + "/dist-packages/";

    //-- 1. Verify if all requirements are already installed
    pipCommand.setArguments({ "list", "--format=freeze"});
    pipCommand.start();
    if (!pipCommand.waitForStarted()) {
        CAMITK_WARNING_ALT("Command '" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "' failed to start:\n" + QString(pipCommand.errorString()))
        return false;
    }
    if (!pipCommand.waitForFinished()) {
        CAMITK_WARNING_ALT("Command '" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "' timed out (30 sec):\n" + QString(pipCommand.readAll()))
        return false;
    }
    if (pipCommand.exitCode() != 0) {
        CAMITK_WARNING_ALT("Failed to list already installed packages in " + virtualEnvPath + " (needed to verify the virtual env):\n" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "\nExit code: " + QString::number(pipCommand.exitCode()) + "\nOutput:\n" + QString(pipCommand.readAll()))
        return false;
    }

    QByteArray rawData = pipCommand.readAll();
    QString outputString = QString::fromUtf8(rawData);

    QStringList installedPackages;
    QStringList lines = outputString.split('\n', Qt::SkipEmptyParts);
    for (const QString& line : lines) {
        installedPackages.append(line.section("==", 0, 0).trimmed().toLower());
    }

    QStringList missingPackages;
    for (const QString& req : packages) {
        // Check lowercase version against our set
        if (!installedPackages.contains(req.section("==", 0, 0).trimmed())) {
            missingPackages.append(req);
        }
    }

    if (missingPackages.isEmpty()) {
        Application::showStatusBarMessage(QString("All requirements already installed."));
    }

    // Only install the missing packages
    packages = missingPackages;

    float progressValue = progressMinimum;
    float progressStep = (progressMaximum - progressMinimum) / float(packages.size());

#ifdef Q_OS_LINUX
    //-- 2. manage special case of VTK and Qt
    // Note on virtual environment requiring VTK
    // if vtk is needed, installing the package using pip will generate an application crash in case the
    // installed version of the package differs from the version used to build camitkcore.
    //
    // Analysis:
    // - camitkcore is linked with a specific version of vtk
    // - on supported Linux OS, the vtk version is determined by the system packages available in the distribution
    //   (for instance on Ubuntu 24.04 LTS the system the packages provide vtk9.1)
    // - on supported Linux OS, the system package automatically install the python vtk package (e.g. python-vtk9-9.1.0...)
    // - the vtkCommon shared object is loaded when the application starts (e.g. libvtkCommonCore-9.1)
    // - if vtk is installed using "pip" in the virtual environment, "import vtk" will load the package installed in the virtual env by pip
    // - the available python vtk package might have a different version than the package version (system package and python
    //   packages are not synchronized/built by the same team).
    //   For instance on Ubuntu 24.04 LTS, "pip install vtk==9.1.0" will generate:
    //   ERROR: Could not find a version that satisfies the requirement vtk==9.1.0 (from versions: 9.3.0rc2, 9.3.0, 9.3.1, 9.4.0rc3, 9.4.0, 9.4.1, 9.4.2)
    // - A std::bad exception will be generated by "import vtk", as it seems that the vtk python package will call some symbol in the
    //   system installed vtkCommon while it thinks it is calling symbol in the venv installed vtk (symptoms: huge memory allocation resulting in
    //   a crash).
    //   Although it can be potentially/theoretically caught, it seems quite fishy.
    //
    // Workaround:
    // - Check that the workaround (see below) is not already in place
    // - if not already applied:
    //   - uninstall vtk from the venv in case it was installed manually
    //   - add a symlink in the venv to thse system site package:
    //     ln -s /usr/lib/python3/dist-packages/vtk.py venv/lib/python3.12/site-packages
    //     ln -s /usr/lib/python3/dist-packages/vtkmodules venv/lib/python3.12/site-packages
    //   - trace it to let the user know
    //
    // The problem is the same with Qt python bindings with a supplementary degree of complexity as there are two sets of bindings, PyQt and PySide
    // As CamiTK is distributed mostly through Debian and Ubuntu packages, the PySide packages are provided by the same team as QT packages
    // PyQt being a different team, the releases of Qt and PyQt may not be synchronised.
    // The other option that was explored was to force pip install PyQt==CAMITK_QT_VERSION but their is no certainty that the pip repo has all
    // the versions of PyQt/PySide, including the one we will need.
    // The solution here is to use the same workaround as VTK, using the system python3-vtk instead of the pip version,
    // we will force here the use of the system PySide bindings.

    QStringList vtkRequirements = packages.filter("vtk");
    if (vtkRequirements.size() > 0) {
        Application::showStatusBarMessage(QString("Verifying requirement 1/%1 (%2)...").arg(packages.size()).arg("vtk"));

        //-- VTK workaround
        bool vtkWorkaroundInPlace = false;
        QFileInfo vtkSymLink(virtualEnvPackagePath + "vtk.py");

        if (vtkSymLink.exists()) {
            if (vtkSymLink.isSymLink()) { // note isSymbolicLink() has a different behaviour on Win and MacOS
                QString existingTarget = vtkSymLink.symLinkTarget();
                if (existingTarget == systemPackagePath + "vtk.py") {
                    vtkWorkaroundInPlace = true; // everything is ok, nothing to do
                }
                else {
                    CAMITK_WARNING_ALT("VTK workaround symlink exists but points to the wrong target. Removing it.")
                }
            }
            else {
                CAMITK_WARNING_ALT("VTK workaround path exists but is not a symlink. Removing it.")
            }
        }

        if (!vtkWorkaroundInPlace) {
            // remove the vtk package in case it was installed manually in the venv
            pipCommand.setArguments({"uninstall", "--yes", "vtk"});
            pipCommand.start();
            if (!pipCommand.waitForStarted()) {
                CAMITK_WARNING_ALT("Command '" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "' failed to start:\n" + QString(pipCommand.errorString()))
                return false;
            }
            if (!pipCommand.waitForFinished(300000)) { // 5 min (default is 30s, but some Windows machine may be very slow)
                CAMITK_WARNING_ALT("Command '" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "' timed out (5 min):\n" + QString(pipCommand.readAll()))
                return false;
            }
            if (pipCommand.exitCode() != 0) {
                CAMITK_WARNING_ALT("Failed to uninstall package 'vtk' (needed to install the Linux VTK symlink work around):\n" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "\nExit code: " + QString::number(pipCommand.exitCode()) + "\nOutput:\n" + QString(pipCommand.readAll()))
                return false;
            }

            // start from scratch
            QFile::remove(vtkSymLink.absoluteFilePath());
            QFile::remove(virtualEnvPackagePath + "vtkmodules");

            // add the symlink workaround
            if (!QFile::link(systemPackagePath + "vtk.py", virtualEnvPackagePath + "vtk.py")) {
                CAMITK_WARNING_ALT("Failed to create VTK workaround symlink from" + systemPackagePath + "to" + virtualEnvPackagePath + ": system-wide Vtk python module must be installed to use VTK from Python")
                return false;
            }
            else {
                if (!QFile::link(systemPackagePath + "vtkmodules", virtualEnvPackagePath + "vtkmodules")) {
                    CAMITK_WARNING_ALT("Failed to create VTK modules workaround symlink from" + systemPackagePath + "to" + virtualEnvPackagePath + ": system-wide Vtk python module must be installed to use VTK from Python")
                    return false;
                }
                else {
                    CAMITK_TRACE_ALT("VTK python workaround symlink created successfully.");
                }
            }

            progressValue += progressStep;
            Application::setProgressBarValue(progressValue);
        }

        // remove vtk requirements from the list of packages to install
        for (QString req : vtkRequirements) {
            packages.removeAll(req);
        }
    }
#endif

    // Check Qt bindings : reject PyQt, check Qt version for PySide2/PySide6
    if (packages.filter("pyqt").size() > 0) {
        CAMITK_WARNING_ALT("Requirements contains PyQt, which is not supported. If you need Qt python bindings, you must use PySide.\nRationale: as CamiTK is linked to a specific version of Qt C++ library, Python bindings must use the same Qt version. This can only be guaranteed by using PySide instead of PyQt.\nPlease replace PyQt requirements by PySide.")
        return false;
    }

#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
    if (packages.filter("pyside6").size()>0) {
        CAMITK_WARNING_ALT("Requirements contains PySide6 but this CamiTK version was built with Qt5. Please replace PySide6 by PySide2.")
        return false;
    }
#else
    if (packages.filter("pyside2").size()>0) {
        CAMITK_WARNING_ALT("Requirements contains PySide2 but this CamiTK version was built with Qt6. Please replace PySide2 by PySide6.")
        return false;
    }
#endif

#ifdef Q_OS_LINUX
    // Like for vtk, use the system's PySide
    QStringList qtRequirements = packages.filter("pyside");
    if (qtRequirements.size() > 0) {
        QString qtPackageName = qtRequirements.contains("pyside2") ? "PySide2" : "PySide6";
        QString qtShibokenName = qtPackageName == "PySide2" ? "shiboken2" : "shiboken6";
        Application::showStatusBarMessage(QString("Verifying requirement %1/%2 (%3)...").arg(vtkRequirements.size() > 0 ? "2" : "1").arg(packages.size()).arg(qtPackageName));


        // The problem is the same as VTK with Qt python bindings
        // As CamiTK is distributed mostly through Debian and Ubuntu packages, the PySide packages are provided by the same team as QT packages
        // PyQt being a different team, the releases of Qt and PyQt may not be synchronised.
        // The other option that was explored was to force pip install PyQt==CAMITK_QT_VERSION but their is no certainty that the pip repo has all
        // the versions of PyQt/PySide, including the one we will need.
        // The solution here is to use the same workaround as VTK, using the system bindings instead of the pip version,
        // we will force here the use of the system PySide bindings.

        //-- Qt/PySide workaround
        bool qtWorkaroundInPlace = false;
        QFileInfo qtSymLink(virtualEnvPackagePath + qtPackageName);

        if (qtSymLink.exists()) {
            if (qtSymLink.isSymLink()) { // note isSymbolicLink() has a different behaviour on Win and MacOS
                QString existingTarget = qtSymLink.symLinkTarget();
                if (existingTarget == systemPackagePath + qtPackageName) {
                    qtWorkaroundInPlace = true; // everything is ok, nothing to do
                }
                else {
                    CAMITK_WARNING_ALT("PySide workaround symlink exists but points to the wrong target. Removing it.")
                }
            }
            else {
                CAMITK_WARNING_ALT("PySide workaround path exists but is not a symlink. Removing it.")
            }
        }

        if (!qtWorkaroundInPlace) {
            // remove the qt package in case it was installed manually in the venv
            pipCommand.setArguments({"uninstall", "--yes", qtPackageName});
            pipCommand.start();
            if (!pipCommand.waitForStarted()) {
                CAMITK_WARNING_ALT("Command '" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "' failed to start:\n" + QString(pipCommand.errorString()))
                return false;
            }
            if (!pipCommand.waitForFinished(300000)) { // 5 min (default is 30s, but some Windows machine may be very slow)
                CAMITK_WARNING_ALT("Command '" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "' timed out (5 min):\n" + QString(pipCommand.readAll()))
                return false;
            }
            if (pipCommand.exitCode() != 0) {
                CAMITK_WARNING_ALT("Failed to uninstall package '" + qtPackageName + "' (needed to install the Linux Qt/PySide symlink work around):\n" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "\nExit code: " + QString::number(pipCommand.exitCode()) + "\nOutput:\n" + QString(pipCommand.readAll()))
                return false;
            }

            // start from scratch
            QFile::remove(qtSymLink.absoluteFilePath());
            QFile::remove(virtualEnvPackagePath + qtShibokenName);

            // check that proper pyside version is installed in the system
            if (!QFile::exists(systemPackagePath + qtPackageName) || !QFile::exists(systemPackagePath + qtShibokenName) ) {
                CAMITK_WARNING_ALT(qtPackageName + " not found in system path: " + systemPackagePath + ": system-wide PySide python module must be installed to use Qt/PySide from Python")
                return false;
            }

            // add the symlink workaround
            if (!QFile::link(systemPackagePath + qtPackageName, virtualEnvPackagePath + qtPackageName) || !QFile::link(systemPackagePath + qtShibokenName, virtualEnvPackagePath + qtShibokenName)) {
                CAMITK_WARNING_ALT("Failed to create Qt/PySide workaround symlink from" + systemPackagePath + "to" + virtualEnvPackagePath + ": system-wide PySide python module must be installed to use Qt/PySide from Python")
                return false;
            }
            else {
                CAMITK_TRACE_ALT("Qt/PySide workaround symlink created successfully.");
            }

            progressValue += progressStep;
            Application::setProgressBarValue(progressValue);
        }

        // remove PySide from the list of packages to install
        for (QString req : qtRequirements) {
            packages.removeAll(req);
        }
    }
#endif

    //-- 3. install all remaining requirements
    int i = 0;
    bool exitCode = 0;
    while (i < packages.size() && exitCode == 0) {
        Application::showStatusBarMessage(QString("Verifying requirement %1/%2 (%3)...").arg(i + 1).arg(packages.size()).arg(packages.at(i)));
        output << "Verifying " + packages.at(i) + " in " + virtualEnvPath + "...";
        pipCommand.setArguments({"install", packages.at(i)});
        pipCommand.start();
        if (!pipCommand.waitForStarted()) {
            CAMITK_WARNING_ALT("Command '" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "' failed to start:\n" + QString(pipCommand.errorString()))
            return false;
        }
        if (!pipCommand.waitForFinished(300000)) { // 5 min (default is 30s, but some Windows machine may be very slow)
            CAMITK_WARNING_ALT("Command '" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "' timed out (5 min):\n" + QString(pipCommand.readAll()))
            return false;
        }
        exitCode = pipCommand.exitCode();
        if (exitCode != 0) {
            CAMITK_WARNING_ALT("Failed to install package:\n" + pipCommand.program() + " " + pipCommand.arguments().join(" ") + "\nExit code: " + QString::number(pipCommand.exitCode()) + "\nOutput:\n" + QString(pipCommand.readAll()))
        }
        else {
            output << QString(pipCommand.readAll());
            i++;
        }
        progressValue += progressStep;
        Application::setProgressBarValue(progressValue);
    }

    CAMITK_TRACE_ALT(output.join("\n"))

    // if all packages were not installed, return false
    return !(i < packages.size());
}

// -------------------- dump --------------------
void PythonManager::dump(py::dict dict) {
    py::module_ inspect = py::module::import("inspect");
    for (auto item : dict) {
        QString key = QString::fromStdString(py::str(item.first));
        py::int_ address = py::int_(py::eval("id")(item.second));
        if (key.startsWith("__")) {
            CAMITK_INFO_ALT("- Dunder: " + key + ", " + CAMITK_PRINT_POINTER(address));
        }
        else {
            if (py::isinstance<py::function>(item.second)) {
                py::object functor = py::reinterpret_borrow<py::object>(item.second);
                if (py::isinstance<py::function>(functor)) {
                    CAMITK_INFO_ALT("- Function/Method: " + key + ", pointer: " + CAMITK_PRINT_POINTER(functor.ptr()))
                    try {
                        py::str source = inspect.attr("getsource")(functor);
                        CAMITK_INFO_ALT("  Source code:\n" + QString::fromStdString(source) + "\n")
                    }
                    catch (const py::error_already_set& e) {
                        CAMITK_INFO_ALT("  Could not retrieve source code:\n" + QString::fromStdString(e.what()))
                    }
                }
            }
            else {
                QString valueType = QString::fromStdString(py::str(py::type::of(item.second)));
                QString value = QString::fromStdString(py::str(item.second));
                CAMITK_INFO_ALT("- Variable: " + key + ", type: " + valueType + ", value: '" + value + "'")
            }
        }
    }
}

// ------------------- setPythonPointer -------------------
void PythonManager::setPythonPointer(QObject* qObject, py::object pythonPointer) {
    if (!pythonStateMap.contains(qObject)) {
        pythonStateMap.insert(qObject, qMakePair(pythonPointer, pythonPointer.attr("__dict__")));
    }
}

// ------------------- restorePythonState -------------------
void PythonManager::restorePythonState(QObject* qObject) {
    if (pythonStateMap.contains(qObject)) {
        py::object pythonPointer = pythonStateMap.value(qObject).first;
        py::dict pythonStateDict = pythonPointer.attr("__dict__");
        for (auto item : pythonStateMap.value(qObject).second) {
            pythonStateDict[item.first] = item.second;
        }
    }
}

// ------------------- backupPythonState -------------------
void PythonManager::backupPythonState(QObject* qObject) {
    if (pythonStateMap.contains(qObject)) {
        py::object pythonPointer = pythonStateMap.value(qObject).first;
        py::dict pythonStateDict = pythonPointer.attr("__dict__");
        for (auto item : pythonStateDict) {
            std::string key = py::str(item.first);
            if (key != "__builtins__" && key.rfind("__", 0) != 0) {
                pythonStateMap.value(qObject).second[key.c_str()] = item.second;
            }
        }
    }
}

} // namespace camitk

#endif // PYTHON_BINDING