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

#ifndef __PYTHON_MANAGER__
#define __PYTHON_MANAGER__

#include "CamiTKAPI.h"

#include <QDir>
#include <QProcess>

#pragma push_macro("slots")
#undef slots
#include <pybind11/embed.h>
#pragma pop_macro("slots")

namespace py = pybind11;

namespace camitk {

/**
 * @brief PythonManager manages the interaction with the python language from CamiTK using pybind11.
 *
 * This manager is in charge of
 * - starting and interacting with the python interpreter
 * - switching between python context (virtual environment and script).
 *
 * Note that in CamiTK, using python virtual environment is mandatory.
 *
 * A python context is made of:
 * - a virtual environment (the directory that contains the venv created by "python -m venv")
 * - a script to load as a module in the python interpreter
 *
 * Only one python context can be loaded at the same time. A locking mechanism
 * is used to avoid discrepancy: when a python context is initialized using lockContext(...)
 * the python manager will not allow another call to lockContext(..) until unlock() is called.
 * Note that if lockContext(..) can be called many times with the same context, only one final
 * call to unlock() is required.
 *
 * Most usage involved:
 * - initPython() initialize the python interpreter (does nothing if it was already initialized
 *   during the application life time)
 * - lockContext(..) load the given script and set the python interpreter in the given virtual env.
 *   This also locks the python manager until unlock() is called.
 * - unlock() release the lock on the current context
 *
 * For instance:
 * \code
 *
 * PythonManager::initPython(); // typically done in the client class constructor
 * ...
 *
 * // load the script as a pybind11 python module
 * // (typically done just before python function must be called)
 * mod = Python::lockContext(myVenvPath, myScript);
 *
 * // test that method foo exists and is a callable function
 * if (py::hasattr(mod, "foo") && py::isinstance<py::function>(mod.attr("foo"))) {
 *     // call the python function "foo(param)" in the python script defined in file myScript
 *     // with the python interpreter configured inside the virtual env defined in myVenvPath
 *     mod.attr("foo")(param);
 *     ...
 * }
 *
 * \endcode
 *
 * \note Python manager is not aware of who is using the python interpreter (that is, it is not
 * aware of PythonHotPlug classes).
 *
 * \note If you need to get more debug information from python itself, use PYTHONVERBOSE=1 bin/camitk-imp
 */
class CAMITK_API PythonManager {

public:
    /// @brief Initialize the python interpreter if possible.
    ///
    /// The interpreter can only be started if
    /// - CamiTK core was build using PYTHON_BINDING
    /// - python is installed on the machine
    /// - the camitk python module is available in python system paths or camitk install
    ///   library paths, see findPythonModule()
    /// This will determine:
    /// - the python version (and load python symbols on Linux as the python executable is
    ///   not linked to the python binary)
    /// - the path to the camitk python module
    /// And will then initialize the global interpreter.
    ///
    /// see pythonEnabled()
    ///
    /// @return true if the interpreter was initialized
    static bool initPython();

    /// @brief Returns the python status
    /// Python status specifies if python interpreter has been initialized and ready to run scripts
    /// and if it is the python version and camitk python module being used
    /// \note: PythonManager::getPythonStatus().contains("Python disabled") returns true if
    /// Python support is currently disabled
    static QString getPythonStatus();

    /// load the python user-script from the given venv config and return the pybind11 module object.
    /// If the switch to the virtual env went well and the module was loaded without any problem,
    /// it "locks" the PythonManager until unlock() is called.
    ///
    /// If called more than once for the same context, it will return the valid current module.
    ///
    /// @see PythonHotPlugActionExtension::callPython() for an usage example
    ///
    /// @return the pybind11 module object (might be uninitialized if something went wrong or if
    /// a different context is already locked)
    static py::module_ lockContext(QString virtualEnvPath, QString scriptPath);

    /// Run the given script in the given virtualEnvPath and returns a map of symbols created during the script execution.
    /// The returned QMap contains the variables of the local dictionary.
    /// You can then check the dictionary for created symbols.
    /// @see fromPython(..) for the list of supported types
    ///
    /// @see TestPythonScript::scriptVariable() for a usage example
    ///
    /// @param virtualEnvPath the virtual environment to use to run the script string
    /// @param pythonScript a QString that contains python instruction to run (beware this is NOT a path to a python script file)
    /// @param pythonError a QString that contains the python exception if any was generated
    /// @return the local dictionary created during the script execution as a QMap
    static QMap<QString, QVariant> runScript(QString virtualEnvPath, const QString& pythonScript, QString& pythonError);

    /// release the lock on the current context. After that, lockContext will be possible again
    static void unlock();

    /// get the detected python version (major.minor)
    static QString getPythonVersion();

    /// return a multiline string showing python environment debug information, including information
    /// about the current virtual environment.
    static QString pythonEnvironmentDebugInfo();

    /// Print the content of the given python dictionary
    static void dump(py::dict dict);

    /// Convert a py::handle to QVariant.
    /// Supported types are:
    /// - None → invalid QVariant (i.e. QVariant())
    /// - boolean py::bool_ → QVariant(bool)
    /// - int py::int_ → QVariant(int)
    /// - py::bytes_ → QByteArray
    /// - py::float_ → QVariant(double)
    /// - py::str → QVariant(QString)
    /// - list/tuple → QVariantList
    /// - dict → QVariantMap
    ///
    /// If the handle is of unsupported type, the method returns a QVariant(QString) with
    /// the value "<unsupported Python type 'type name'>" (if it can deduce the type name)
    static QVariant fromPython(const py::handle& value);

    /// Check that a .venv virtual venv path exists in the given path.
    /// Checks that:
    /// - .venv, .venv/bin, .venv/lib/pythonx.y/site-packages path exists
    /// - .venv/bin has a pip executable
    /// @param silent if true no warning are emitted
    /// @return true if everything is OK
    static bool checkVirtualEnvPath(QString virtualEnvRootPath, bool silent = true);

    /// create a virtual environment ".venv" as a subdirectory of the given path if does not
    /// have a valid virtual env yet (this calls checkVirtualEnvPath(..) first)
    /// @return true if everything went OK
    static bool createVirtualEnv(QString virtualEnvRootPath);

    /// install the given list of packages inside the given virtual env
    /// (i.e. using the virtual env pip command)
    /// @param progressMinimum is the current initial progress value
    /// @param progressMaximum is the maximum progress bar value to use when all packages are installed
    static bool installPackages(QString virtualEnvPath, QStringList packages, int progressMinimum = 0, int progressMaximum = 100);

    /// Associate the given QObject to the __dict__ of the given python object.
    ///
    /// @note only the first call associate the qObject to the pointer
    ///
    /// @see PythonHotPlugActionExtension for a usage example, where the pythonPointer is
    /// the pointer to a PythonHotPlugAction in the python world. setPythonPointer() used
    /// in conjunction with backupPythonState() and backupPythonState() to store
    /// the python __dict__ of the action between two calls (therefore storing any
    /// python symbols created/updated with "self.something = ...")
    ///
    /// Once called, backupPythonState() and restorePythonState() can be called any number of times
    static void setPythonPointer(QObject* qObject, py::object pythonPointer);

    /// backup the current state of the __dict__ of the python object associated with the given qObject.
    /// The __dict__ can then be restore any time using restorePythonState()
    static void backupPythonState(QObject* qObject);

    /// restore the __dict__ of the python object associated with the given qObject.
    /// @note backupPythonState() must be called at least once before calling restorePythonState()
    static void restorePythonState(QObject* qObject);

private:
    /// check which shared lib need to be preloaded and determine version
    static void resolvePythonSharedLibPathAndVersion();

    /// "manually" load python shared object
    static bool loadPythonSharedLibrary();

    /// find the filename (path) to the given python module searching in the python system paths
    /// and installed CamiTK library directories
    /// This method checks for a file that starts with "moduleName"
    /// followed by ".cpython" (or ".cp" on windows) and ending with .so (or .pyd on windows)
    static QString findPythonModule(const QString& moduleName);

    /// check if value is already in given list and if not insert it at the beginning of the list.
    /// This is used to modify sys.path and avoid multiple setup of the same path.
    /// @return true if value was inserted, false if it was already there
    static bool insertIfNotAlreadyInList(py::list* list, const QString& value);

    /// check if there are more than one package accessible
    /// from the python interpreter that has the same name but
    /// a different version
    static void checkPythonPackageConflicts();

    /// check that the python command is found in the system path
    /// @return the path to the command or null QString if not found
    static QString findPythonExecutable();

    /// import or load the given module name in the current python context.
    /// \warning you have to ensure that the interpreter is already configured in a valid
    /// current context or virtual env before calling this method
    ///
    /// @param clearAll if true the method will remove all previous symbols from the
    /// given module and call the garbage collector to effectively remove the symbols.
    ///
    /// Removing all symbols from the module is required for instance when a method
    /// is removed from the python script between to calls.
    static py::module_ importOrReload(const QString& moduleName, bool clearAll = false);

    /// clear dict, setup path to camitk module, enter virtual env and set up the console redirection.
    /// Setup the current python context to the given virtual env, then importOrReload the python user-script (if given)
    /// @return true if everything went well
    static bool setupPython(QString virtualEnvPath, QString scriptPath = QString());

    /// The current python status (is interpreter initialized and ready, which python version, camitk python module location)
    static QString pythonStatusString;

    /// path to where the camitk.cpython-312-x86_64-linux-gnu.so / camitk.dll resides
    static QDir pythonCamitkModulePath;

    /// filename of system python.so to load (contains the version number)
    static QString pythonSoLib;

    /// python major.minor version
    static QString pythonVersion;

    /// path to the system python executable (or null QString if not found)
    static QString systemPythonExecutable;

    /// lock status
    static bool isLocked;

    /// Store the given py:object (can then be use to backup/restore __dict__ value)
    static QMap<QObject*, QPair<py::object, py::dict>> pythonStateMap;

    /// current python context and loaded module
    static QString currentVirtualEnvPath;
    static QString currentScriptPath;
    static py::module_ currentModule;

};

} // namespace camitk

#endif // __PYTHON_MANAGER__
#endif // PYTHON_BINDING