/*****************************************************************************
 * $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 "Application.h"
#include "HotPlugExtensionManager.h"
#include "PythonHotPlugActionExtension.h"
#include "PythonHotPlugAction.h"
#include "PythonManager.h"
#include "AbortException.h"

#include "Log.h"

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

//-- Qt Stuff
#include <QMessageBox>
namespace camitk {

// -------------------- constructor --------------------
PythonHotPlugActionExtension::PythonHotPlugActionExtension(const QString& camitkFilePath, bool verifyVirtualEnv) : HotPlugActionExtension(camitkFilePath) {
    verifyVirtualEnvRequested = (HotPlugExtensionManager::getVerifyOrRebuildOption() || verifyVirtualEnv);
}

// -------------------- destructor --------------------
PythonHotPlugActionExtension::~PythonHotPlugActionExtension() {
}

// -------------------- initActions --------------------
bool PythonHotPlugActionExtension::initActions(int progressMinimum, int progressMaximum) {
    extensionDir.setPath(QFileInfo(getLocation()).absolutePath());
    CamiTKExtensionModel camitkExtensionModel(getLocation());

    float progressValue = progressMinimum;
    // number of steps = 4, i.e., init python environment, create environment, install packages, initialize all actions
    float progressStep = (progressMaximum - progressMinimum) / 4.0;
    QString extensionShortName = QFileInfo(getLocation()).fileName();

    Application::setProgressBarValue(progressValue);

    //-- 1. Check python status
    Application::showStatusBarMessage("Checking python interpreter...");
    successfullyLoaded = !PythonManager::getPythonStatus().contains("Python disabled");
    if (!successfullyLoaded) {
        CAMITK_ERROR(QString("Python interpreter was not initialized.\nCannot load extension %1").arg(extensionShortName))
        return false;
    }

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

    //-- 2. Check if virtual env exists and create if needed
    bool virtualEnvMustBeCreated = !PythonManager::checkVirtualEnvPath(extensionDir.absolutePath());
    // if the verification is requested or if the virtual env does not exist yet
    if (verifyVirtualEnvRequested || virtualEnvMustBeCreated) {
        Application::showStatusBarMessage("Initializing python virtual environment for " + extensionShortName + "...");
        successfullyLoaded = PythonManager::createVirtualEnv(extensionDir.absolutePath());
        if (!successfullyLoaded) {
            // no need for a CAMITK_ERROR (already done by createVirtualEnv)
            return false;
        }
    }

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

    //-- 3. Check requirements and install packages if needed
    QStringList packageList = camitkExtensionModel.getModel()["requirements"].getValue().toStringList();
    int requirementCount = packageList.size();
    QString requirementMsg = "Verifying ";
    if (requirementCount == 0) {
        requirementMsg += "requirements";
    }
    else {
        if (requirementCount == 1) {
            requirementMsg += "1 requirement";
        }
        else {
            requirementMsg += QString::number(packageList.size()) + " requirements";
        }
    }
    requirementMsg += " for " + extensionShortName + "...";
    Application::showStatusBarMessage(requirementMsg);
    successfullyLoaded = PythonManager::installPackages(extensionDir.absoluteFilePath(".venv"), packageList, progressValue, progressValue + progressStep);
    if (!successfullyLoaded) {
        // no need for a CAMITK_ERROR (already done by installPackages)
        return false;
    }

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

    //-- 4. init all actions
    QStringList failedActionNames;
    const VariantDataModel& allActions = camitkExtensionModel.getModel()["actions"];
    progressStep = (progressMaximum - progressValue) / allActions.size();
    for (int i = 0; i < allActions.size(); i++) {
        Application::showStatusBarMessage("Initializing action '" + allActions[i]["name"].toString() + "' of " + extensionShortName + "...");
        PythonHotPlugAction* a = new PythonHotPlugAction(this, allActions[i]);
        // check if script exists
        QFileInfo fileInfo(extensionDir.absoluteFilePath(a->getPythonName() + ".py"));
        if (!fileInfo.exists()) {
            CAMITK_WARNING(QString("Cannot create python action '%1':\nScript '%2' not found").arg(a->getName()).arg(fileInfo.absoluteFilePath()))
            failedActionNames.append(a->getName());
        }
        else {
            bool initializationStatus = a->init();
            if (!initializationStatus) {
                CAMITK_WARNING(QString("Cannot create python action '%1':\nError during 'init()'").arg(a->getName()))
                failedActionNames.append(a->getName());
            }
            else {
                registerAction(a);
            }
        }
        progressValue += progressStep;
        Application::setProgressBarValue(progressValue);
    }

    successfullyLoaded = (failedActionNames.size() == 0);
    Application::showStatusBarMessage("Python action extension " + extensionShortName + " loaded " + (successfullyLoaded ? "successfully" : "with errors"));

    return successfullyLoaded;
}

// -------------------- callPython --------------------
PythonHotPlugActionExtension::PythonCallStatus PythonHotPlugActionExtension::callPython(PythonHotPlugAction* action, QString methodName, QString param) {
    // block concurrent calls
    static bool inCallPython = false;
    if (inCallPython) {
        return PythonCallStatus::ALREADY_IN_CALL;
    }
    inCallPython = true;

    PythonCallStatus status = PythonCallStatus::SUCCESS;
    QString actionScript = extensionDir.absoluteFilePath(action->getPythonName() + ".py");

    py::module_ pythonActionModule = PythonManager::lockContext(extensionDir.absoluteFilePath(".venv"), actionScript);

    // contains a specific message in case of a script problem arise (method() not found or not callable)
    QString userScriptProblem;

    // if pythonActionModule is an empty module, then skip the call
    if (!pythonActionModule.is(py::module_())) {
        PythonManager::setPythonPointer(action, py::cast(action, py::return_value_policy::reference));
        try {
            // restore previous self.__dict__
            PythonManager::restorePythonState(action);

            // CAMITK_INFO_ALT("callPython: " + action->getName() + "::" + methodName + "(" + param +")")
            // PythonManager::dump(pythonActionModule.attr("__dict__"));

            // check method is an object of the module
            if (py::hasattr(pythonActionModule, methodName.toStdString().c_str())) {
                py::object functor = pythonActionModule.attr(methodName.toStdString().c_str());
                // check method is callable
                if (py::isinstance<py::function>(functor)) {
                    py::object result;
                    // call the method
                    if (param.isNull()) {
                        CAMITK_TRACE_ALT("Executing python script '" + methodName + "(self)' of action '" + action->getName() + "'...")
                        result = pythonActionModule.attr(methodName.toStdString().c_str())(action);
                    }
                    else {
                        CAMITK_TRACE_ALT("Executing python script '" + methodName + "(self, " + param + ")' of action '" + action->getName() + "'...")
                        result = pythonActionModule.attr(methodName.toStdString().c_str())(action, param.toStdString().c_str());
                    }

                    // save any variable/member that were created or updated in self.
                    PythonManager::backupPythonState(action);

                    // now try to cast the return value
                    if (!result.is_none() && py::isinstance<py::bool_>(result)) {
                        status = result.cast<bool>() ? PythonCallStatus::SUCCESS : PythonCallStatus::RETURNED_FALSE;
                    }
                    else {
                        // if the method did not return a boolean, consider everything went well
                        status = PythonCallStatus::SUCCESS;
                    }
                }
                else {
                    CAMITK_TRACE_ALT("Python script '" + actionScript + "' of action '" + action->getName() + "': '" +  methodName + "' is not a function");
                    status = PythonCallStatus::NOT_A_FUNCTION;
                }
            }
            else {
                CAMITK_TRACE_ALT("Python script '" + actionScript + "' of action '" + action->getName() + "': missing function '" +  methodName + "(self)'");
                status = PythonCallStatus::MISSING_FUNCTION;
            }
        }
        catch (const std::exception& e) {
            CAMITK_WARNING_ALT("Python exception when calling '" + methodName + "()' of script '" + actionScript + "' of action '" + action->getName() + "':\n" + e.what());
            status = PythonCallStatus::EXCEPTION_RAISED;
        }
        catch (...) {
            CAMITK_WARNING_ALT("Unknown exception occurred when calling '" + methodName + "()' of script '" + actionScript + "' of action '" + action->getName() + "'...");
            status = PythonCallStatus::EXCEPTION_RAISED;
        }
    }
    else {
        // the python module was not loaded (e.g. due to syntax error)
        status = PythonCallStatus::MODULE_NOT_LOADED;
    }

    PythonManager::unlock();

    inCallPython = false;
    return status;
}


} // namespace camitk

#endif // PYTHON_BINDING