//! @file PythonExtensionManager.cpp

// This file is part of Cantera. See License.txt in the top-level directory or
// at https://cantera.org/license.txt for license and copyright information.

#include "cantera/extensions/PythonExtensionManager.h"
#include "cantera/extensions/PythonHandle.h"
#include "cantera/base/ExtensionManagerFactory.h"

#include "cantera/kinetics/ReactionRateFactory.h"
#include "cantera/kinetics/ReactionRateDelegator.h"
#include "delegator.h" // generated by Cython

#include "cantera/base/Solution.h"

using namespace std;

namespace {

std::string getPythonExceptionInfo()
{
    if (!PyErr_Occurred()) {
        return "no Python exception raised";
    }

    PyObject* ex_type;
    PyObject* ex_value;
    PyObject* traceback;
    PyErr_Fetch(&ex_type, &ex_value, &traceback);
    PyErr_NormalizeException(&ex_type, &ex_value, &traceback);
    if (traceback == nullptr) {
        traceback = Py_None;
    }
    char* c_exstr = ct_getExceptionString(ex_type, ex_value, traceback);
    string message;
    if (c_exstr != nullptr) {
        message = c_exstr;
        free(c_exstr);
    } else {
        message = "Couldn't get exception message";
    }
    Py_XDECREF(ex_type);
    Py_XDECREF(ex_value);
    Py_XDECREF(traceback);
    return message;
}

} // end anonymous namespace

namespace Cantera
{

void PythonExtensionManager::registerSelf()
{
    if (!Py_IsInitialized()) {
        throw CanteraError("PythonExtensionManager::registerSelf",
            "Must be called only after initializing Python");
    }

    ExtensionManagerFactory::factory().reg("python",
        []() { return new PythonExtensionManager(); });
}

void PythonExtensionManager::registerRateBuilders(const string& extensionName)
{
    // Each rate builder class is decorated with @extension, which calls the
    // registerRateBuilder method to register that class. So all we have
    // to do here is load the module.
    PyObject* module_name = PyUnicode_FromString(extensionName.c_str());
    PyObject* py_module = PyImport_Import(module_name);
    Py_DECREF(module_name);
    if (py_module == nullptr) {
        throw CanteraError("PythonExtensionManager::registerRateBuilders",
                           "Problem loading module:\n{}", getPythonExceptionInfo());
    }
    ct_registerReactionDelegators();
}

void PythonExtensionManager::registerRateBuilder(
    const std::string& moduleName, const std::string& className,
    const std::string& rateName)
{
    // Make sure the helper module has been loaded
    PythonExtensionManager mgr;

    // Create a function that constructs and links a C++ ReactionRateDelegator
    // object and a Python ExtensibleRate object of a particular type, and register
    // this as the builder for reactions of this type
    auto builder = [moduleName, className](const AnyMap& params, const UnitStack& units) {
        auto delegator = make_unique<ReactionRateDelegator>();
        PyObject* extRate = ct_newPythonExtensibleRate(delegator.get(),
                moduleName, className);
        if (extRate == nullptr) {
            throw CanteraError("PythonExtensionManager::registerRateBuilders",
                                "Problem in ct_newPythonExtensibleRate:\n{}",
                                getPythonExceptionInfo());
        }
        //! Call setParameters after the delegated functions have been connected
        delegator->setParameters(params, units);

        // The delegator needs to hold a reference to the Python object to prevent
        // garbage collection for as long as the delegator exists. Breaking the
        // reference cycle is handled on the Python side.
        delegator->holdExternalHandle("python",
                                      make_shared<PythonHandle>(extRate, false));
        Py_XDECREF(extRate);
        return delegator.release();
    };
    ReactionRateFactory::factory()->reg(rateName, builder);
}

void PythonExtensionManager::registerRateDataBuilder(
    const string& moduleName, const string& className, const string& rateName)
{
    // Make sure the helper module has been loaded
    PythonExtensionManager mgr;
    // Create a function that links a C++ ReactionDataDelegator
    // object and a Python ExtensibleRateData object of a particular type, and register
    // this function for making that link
    auto builder = [moduleName, className](ReactionDataDelegator& delegator) {
        PyObject* extData = ct_newPythonExtensibleRateData(&delegator,
                moduleName, className);
        if (extData == nullptr) {
            throw CanteraError("PythonExtensionManager::registerPythonRateDataBuilder",
                               "Problem in ct_newPythonExtensibleRateData:\n{}",
                               getPythonExceptionInfo());
        }
        auto handle = make_shared<PythonHandle>(extData, false);
        Py_XDECREF(extData);
        delegator.setWrapper(handle);
    };
    mgr.registerReactionDataLinker(rateName, "python", builder);

    // Create a function that will link a Python Solution object to the C++ Solution
    // object that gets passed to the Reaction
    auto solnLinker = [](shared_ptr<Solution> soln) {
        PyObject* pySoln = ct_wrapSolution(soln);
        if (pySoln == nullptr) {
            throw CanteraError("PythonExtensionManager::registerPythonRateDataBuilder",
                               "Problem in ct_wrapSolution:\n{}",
                               getPythonExceptionInfo());
        }
        auto handle = make_shared<PythonHandle>(pySoln, false);
        Py_XDECREF(pySoln);
        return handle;
    };
    mgr.registerSolutionLinker("python", solnLinker);
}

};
