/*
 * This is the qmlscene plugin that collects all the Python plugins it can
 * find.
 *
 * Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
 * 
 * This file is part of PyQt5.
 * 
 * This file may be used under the terms of the GNU General Public License
 * version 3.0 as published by the Free Software Foundation and appearing in
 * the file LICENSE included in the packaging of this file.  Please review the
 * following information to ensure the GNU General Public License version 3.0
 * requirements will be met: http://www.gnu.org/copyleft/gpl.html.
 * 
 * If you do not wish to use this file under the terms of the GPL version 3.0
 * then you may purchase a commercial license.  For more information contact
 * info@riverbankcomputing.com.
 * 
 * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
 * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */


#include <stdlib.h>

#include <Python.h>

#include <QCoreApplication>
#include <QDir>
#include <QLibrary>
#include <QLibraryInfo>
#include <QVector>
#include <QQmlEngine>

#include "pluginloader.h"


// Construct the C++ plugin.
PyQt5QmlPlugin::PyQt5QmlPlugin(QObject *parent) : QQmlExtensionPlugin(parent),
        py_plugin_obj(0), sip(0)
{
    // Make sure the interpreter is initialised.
    if (!Py_IsInitialized())
    {
        QLibrary library(PYTHON_LIB);

        library.setLoadHints(QLibrary::ExportExternalSymbolsHint);

        if (library.load())
        {
            Py_Initialize();

            getSipAPI();

#ifdef WITH_THREAD
            // Make sure we don't have the GIL.
#if PY_VERSION_HEX < 0x03070000
            PyEval_InitThreads();
#endif
            PyEval_SaveThread();
#endif
        }
    }
}


// Destroy the C++ plugin.
PyQt5QmlPlugin::~PyQt5QmlPlugin()
{
    if (Py_IsInitialized())
    {
#ifdef WITH_THREAD
        PyGILState_STATE gil = PyGILState_Ensure();
#endif

        Py_XDECREF(py_plugin_obj);

#ifdef WITH_THREAD
        PyGILState_Release(gil);
#endif
    }
}


// Initialize the QML engine.
void PyQt5QmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
{
    if (!Py_IsInitialized() || !py_plugin_obj || !sip)
        return;

#ifdef WITH_THREAD
    PyGILState_STATE gil = PyGILState_Ensure();
#endif

    const sipTypeDef *td = sip->api_find_type("QQmlEngine");

    if (!td)
    {
        PyErr_SetString(PyExc_AttributeError,
                "unable to find type for QQmlEngine");
    }
    else
    {
        PyObject *engine_obj = sip->api_convert_from_type(engine, td, 0);

        if (!engine_obj)
        {
            td = 0;
        }
        else
        {
            PyObject *res_obj = PyObject_CallMethod(py_plugin_obj,
                    const_cast<char *>("initializeEngine"),
                    const_cast<char *>("Os"), engine_obj, uri);

            Py_DECREF(engine_obj);

            if (res_obj != Py_None)
            {
                if (res_obj)
#if PY_MAJOR_VERSION >= 3
                    PyErr_Format(PyExc_TypeError,
                            "unexpected result from initializeEngine(): %S",
                            res_obj);
#else
                {
                    PyObject *res_s = PyObject_Str(res_obj);

                    if (res_s != NULL)
                    {
                        PyErr_Format(PyExc_TypeError,
                                "unexpected result from initializeEngine(): %s",
                                PyString_AsString(res_s));

                        Py_DECREF(res_s);
                    }
                }
#endif

                td = 0;
            }

            Py_XDECREF(res_obj);
        }
    }

    if (!td)
        PyErr_Print();

#ifdef WITH_THREAD
    PyGILState_Release(gil);
#endif
}


// Register the plugin's types.
void PyQt5QmlPlugin::registerTypes(const char *uri)
{
    // Construct the import path that the engine will have used by default.
    // Unfortunately we won't know about any directories added by qmlscene's -I
    // flag.
    QStringList import_path;

    import_path << QCoreApplication::applicationDirPath();

    const char *env_path = getenv("QML2_IMPORT_PATH");

    if (env_path)
    {
#if defined(Q_OS_WIN)
        QLatin1Char sep(';');
#else
        QLatin1Char sep(':');
#endif

        QStringList env_dirs = QString::fromLatin1(env_path).split(sep, QString::SkipEmptyParts);

        foreach (QString dir_name, env_dirs)
            import_path << QDir(dir_name).canonicalPath();
    }

    import_path << QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath);

    // Try and find the URI directory, check that it has a qmldir file and that
    // it has a file called *plugin.py.
    QString py_plugin;
    QString py_plugin_dir;

    foreach (QString dir_name, import_path)
    {
        dir_name.append('/');
        dir_name.append(uri);

        QDir dir(dir_name);

        if (!dir.exists() || !dir.exists("qmldir"))
            continue;

        QStringList candidates = dir.entryList(QDir::Files|QDir::Readable);

        foreach (QString py_plugin_file, candidates)
        {
            QStringList parts = py_plugin_file.split('.');

            if (parts.size() == 2 && parts.at(0).endsWith("plugin") && parts.at(1).startsWith("py"))
            {
                py_plugin = parts.at(0);
                break;
            }
        }

        if (!py_plugin.isEmpty())
        {
            py_plugin_dir = QDir::toNativeSeparators(dir.absolutePath());
            break;
        }
    }

    if (py_plugin.isEmpty())
        return;

#ifdef WITH_THREAD
    PyGILState_STATE gil = PyGILState_Ensure();
#endif

    if (!addToSysPath(py_plugin_dir) || !callRegisterTypes(py_plugin, uri))
        PyErr_Print();

#ifdef WITH_THREAD
    PyGILState_Release(gil);
#endif
}


// Call the registerTypes() function of a plugin.
bool PyQt5QmlPlugin::callRegisterTypes(const QString &py_plugin,
        const char *uri)
{
    // Import the plugin.
    PyObject *plugin_mod = PyImport_ImportModule(py_plugin.toLatin1().data());

    if (!plugin_mod)
        return false;

    // Get QQmlExtensionPlugin.  We do this after the import of the plugin to
    // allow it to fiddle with the context.
    PyObject *extension_plugin = getModuleAttr("PyQt5.QtQml",
            "QQmlExtensionPlugin");

    if (!extension_plugin)
    {
        Py_DECREF(plugin_mod);
        return false;
    }

    // Go through the module looking for a type that implements
    // QQmlExtensionPlugin.
    PyObject *mod_dict = PyModule_GetDict(plugin_mod);
    PyObject *key, *value, *plugin_type = 0;
    Py_ssize_t pos = 0;

    while (PyDict_Next(mod_dict, &pos, &key, &value))
        if (value != extension_plugin && PyType_Check(value) && PyType_IsSubtype((PyTypeObject *)value, (PyTypeObject *)extension_plugin))
        {
            plugin_type = value;
            break;
        }

    Py_DECREF(extension_plugin);

    if (!plugin_type)
    {
        PyErr_Format(PyExc_AttributeError,
                "%s does not contain an implementation of QQmlExtensionPlugin",
                py_plugin.toLatin1().constData());

        Py_DECREF(plugin_mod);
        return false;
    }

    // Create the plugin instance.
    PyObject *plugin_obj = PyObject_CallObject(plugin_type, NULL);

    Py_DECREF(plugin_mod);

    if (!plugin_obj)
        return false;

    // Call registerTypes().
    PyObject *res_obj = PyObject_CallMethod(plugin_obj,
            const_cast<char *>("registerTypes"), const_cast<char *>("s"), uri);

    if (res_obj != Py_None)
    {
        Py_DECREF(plugin_obj);

        if (res_obj)
        {
#if PY_MAJOR_VERSION >= 3
            PyErr_Format(PyExc_TypeError,
                    "unexpected result from registerTypes(): %S", res_obj);
#else
            PyObject *res_s = PyObject_Str(res_obj);

            if (res_s != NULL)
            {
                PyErr_Format(PyExc_TypeError,
                        "unexpected result from registerTypes(): %s",
                        PyString_AsString(res_s));

                Py_DECREF(res_s);
            }
#endif

            Py_DECREF(res_obj);
        }

        return false;
    }

    Py_DECREF(res_obj);

    py_plugin_obj = plugin_obj;

    return true;
}


// Add a directory to sys.path.
bool PyQt5QmlPlugin::addToSysPath(const QString &py_plugin_dir)
{
    PyObject *sys_path = getModuleAttr("sys", "path");

    if (!sys_path)
        return false;

#if PY_VERSION_HEX >= 0x03030000
    PyObject *plugin_dir_obj = PyUnicode_FromKindAndData(PyUnicode_2BYTE_KIND,
            py_plugin_dir.constData(), py_plugin_dir.length());

    if (!plugin_dir_obj)
    {
        Py_DECREF(sys_path);
        return false;
    }
#elif PY_VERSION_HEX >= 0x03000000
#if defined(Py_UNICODE_WIDE)
    QVector<uint> ucs4 = py_plugin_dir.toUcs4();

    PyObject *plugin_dir_obj = PyUnicode_FromUnicode(0, ucs4.size());

    if (!plugin_dir_obj)
    {
        Py_DECREF(sys_path);
        return false;
    }

    memcpy(PyUnicode_AS_UNICODE(plugin_dir_obj), ucs4.constData(),
            ucs4.size() * sizeof (Py_UNICODE));
#else
    PyObject *plugin_dir_obj = PyUnicode_FromUnicode(0,
            py_plugin_dir.length());

    if (!plugin_dir_obj)
    {
        Py_DECREF(sys_path);
        return false;
    }

    memcpy(PyUnicode_AS_UNICODE(plugin_dir_obj), py_plugin_dir.utf16(),
            py_plugin_dir.length() * sizeof (Py_UNICODE));
#endif
#else
    PyObject *plugin_dir_obj = PyString_FromString(
            py_plugin_dir.toLatin1().constData());

    if (!plugin_dir_obj)
    {
        Py_DECREF(sys_path);
        return false;
    }
#endif

    int rc = PyList_Append(sys_path, plugin_dir_obj);
    Py_DECREF(plugin_dir_obj);
    Py_DECREF(sys_path);

    if (rc < 0)
    {
        return false;
    }

    return true;
}


// Return the named attribute object from the named module.
PyObject *PyQt5QmlPlugin::getModuleAttr(const char *module, const char *attr)
{
    PyObject *mod = PyImport_ImportModule(module);

    if (!mod)
        return 0;

    PyObject *obj = PyObject_GetAttrString(mod, attr);

    Py_DECREF(mod);

    return obj;
}


// Get the SIP API.
void PyQt5QmlPlugin::getSipAPI()
{
#if defined(SIP_USE_PYCAPSULE)
    sip = (const sipAPIDef *)PyCapsule_Import("PyQt5.sip._C_API", 0);
#else
    PyObject *c_api = getModuleAttr("PyQt5.sip", "_C_API");

    if (c_api)
    {
        sip = (const sipAPIDef *)PyCObject_AsVoidPtr(c_api);
        Py_DECREF(c_api);
    }
#endif

    if (!sip)
        PyErr_Print();
}
