#define PY_SSIZE_T_CLEAN
#include <stdexcept>
#include "PyWrap.h"
#include <string>
#include <map>
#include <sstream>
#include <boost/python/numpy.hpp>

namespace pywrap {
    namespace np = boost::python::numpy;
    PyWrap* PyWrap::wrapper = nullptr;
    std::mutex PyWrap::mutex;
    CPyInstance* PyWrap::pyInstance = nullptr;
    auto moduleClassMap = std::map<std::pair<std::string, std::string>, std::tuple<PyObject*, PyObject*, PyObject*>>();

    PyWrap* PyWrap::GetInstance()
    {
        std::lock_guard<std::mutex> lock(mutex);
        if (wrapper == nullptr) {
            wrapper = new PyWrap();
            pyInstance = new CPyInstance();
        }
        return wrapper;
    }
    void PyWrap::RemoveInstance()
    {
        if (wrapper != nullptr) {
            if (pyInstance != nullptr) {
                delete pyInstance;
            }
            pyInstance = nullptr;
            if (wrapper != nullptr) {
                delete wrapper;
            }
            wrapper = nullptr;
        }
    }
    void PyWrap::importClass(const clfId_t id, const std::string& moduleName, const std::string& className)
    {
        std::lock_guard<std::mutex> lock(mutex);
        auto result = moduleClassMap.find(id);
        if (result != moduleClassMap.end()) {
            return;
        }
        std::cout << "1a" << std::endl;
        PyObject* module = PyImport_ImportModule(moduleName.c_str());
        if (PyErr_Occurred()) {
            errorAbort("Couldn't import module " + moduleName);
        }
        PyObject* classObject = PyObject_GetAttrString(module, className.c_str());
        if (PyErr_Occurred()) {
            errorAbort("Couldn't find class " + className);
        }
        PyObject* instance = PyObject_CallObject(classObject, NULL);
        if (PyErr_Occurred()) {
            errorAbort("Couldn't create instance of class " + className);
        }
        moduleClassMap.insert({ id, { module, classObject, instance } });
    }
    void PyWrap::clean(const clfId_t id)
    {
        // Remove Python interpreter if no more modules imported left
        std::lock_guard<std::mutex> lock(mutex);
        auto result = moduleClassMap.find(id);
        if (result == moduleClassMap.end()) {
            return;
        }
        Py_DECREF(std::get<0>(result->second));
        Py_DECREF(std::get<1>(result->second));
        Py_DECREF(std::get<2>(result->second));
        moduleClassMap.erase(result);
        if (PyErr_Occurred()) {
            PyErr_Print();
            errorAbort("Error cleaning module ");
        }
        if (moduleClassMap.empty()) {
            RemoveInstance();
        }
    }
    void PyWrap::errorAbort(const std::string& message)
    {
        std::cerr << message << std::endl;
        PyErr_Print();
        RemoveInstance();
        exit(1);
    }
    PyObject* PyWrap::getClass(const clfId_t id)
    {
        auto item = moduleClassMap.find(id);
        if (item == moduleClassMap.end()) {
            errorAbort("Module not found");
        }
        return std::get<2>(item->second);
    }
    std::string PyWrap::callMethodString(const clfId_t id, const std::string& method)
    {
        PyObject* instance = getClass(id);
        PyObject* result;
        try {
            if (!(result = PyObject_CallMethod(instance, method.c_str(), NULL)))
                errorAbort("Couldn't call method " + method);
        }
        catch (const std::exception& e) {
            errorAbort(e.what());
        }
        std::string value = PyUnicode_AsUTF8(result);
        Py_XDECREF(result);
        return value;
    }
    std::string PyWrap::version(const clfId_t id)
    {
        return callMethodString(id, "version");
    }
    void PyWrap::setHyperparameters(const clfId_t id, const json& hyperparameters)
    {
        // Set hyperparameters as attributes of the class
        PyObject* pValue;
        PyObject* instance = getClass(id);
        for (const auto& [key, value] : hyperparameters.items()) {
            std::stringstream oss;
            oss << value.type_name();
            if (oss.str() == "string") {
                pValue = Py_BuildValue("s", value.get<std::string>().c_str());
            } else {
                if (value.is_number_integer()) {
                    pValue = Py_BuildValue("i", value.get<int>());
                } else {
                    pValue = Py_BuildValue("f", value.get<double>());
                }
            }
            int res = PyObject_SetAttrString(instance, key.c_str(), pValue);
            if (res == -1 && PyErr_Occurred()) {
                Py_XDECREF(pValue);
                errorAbort("Couldn't set attribute " + key + "=" + value.dump());
            }
            Py_XDECREF(pValue);
        }
    }
    void PyWrap::fit(const clfId_t id, CPyObject& X, CPyObject& y)
    {
        PyObject* instance = getClass(id);
        CPyObject result;
        CPyObject method = PyUnicode_FromString("fit");
        try {
            if (!(result = PyObject_CallMethodObjArgs(instance, method.getObject(), X.getObject(), y.getObject(), NULL)))
                errorAbort("Couldn't call method fit");
        }
        catch (const std::exception& e) {
            errorAbort(e.what());
        }
    }
    PyObject* PyWrap::predict(const clfId_t id, CPyObject& X)
    {
        PyObject* instance = getClass(id);
        PyObject* result;
        CPyObject method = PyUnicode_FromString("predict");
        try {
            if (!(result = PyObject_CallMethodObjArgs(instance, method.getObject(), X.getObject(), NULL)))
                errorAbort("Couldn't call method predict");
        }
        catch (const std::exception& e) {
            errorAbort(e.what());
        }
        Py_INCREF(result);
        return result; // Caller must free this object
    }
    double PyWrap::score(const clfId_t id, CPyObject& X, CPyObject& y)
    {
        PyObject* instance = getClass(id);
        CPyObject result;
        CPyObject method = PyUnicode_FromString("score");
        try {
            if (!(result = PyObject_CallMethodObjArgs(instance, method.getObject(), X.getObject(), y.getObject(), NULL)))
                errorAbort("Couldn't call method score");
        }
        catch (const std::exception& e) {
            errorAbort(e.what());
        }
        double resultValue = PyFloat_AsDouble(result);
        return resultValue;
    }
}