/* Chrysalide - Outil d'analyse de fichiers binaires
 * bindings.c - éléments d'un socle commun aux fonctionnalités graphiques et non graphiques
 *
 * Copyright (C) 2024 Cyrille Bagard
 *
 *  This file is part of Chrysalide.
 *
 *  Chrysalide is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Chrysalide 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


#include "bindings.h"


#include <assert.h>
#include <dlfcn.h>
#include <pygobject.h>
#include <stdio.h>


#include <common/cpp.h>
#include <common/extstr.h>
#include <core/core.h>
#include <plugins/pglist.h>
#include <plugins/self.h>


#include "access.h"
#include "constants.h"
#include "helpers.h"
#include "star.h"
//#include "strenum.h"
#include "struct.h"
#include "analysis/module.h"
#include "arch/module.h"
#include "common/module.h"
#include "core/module.h"
#include "glibext/module.h"
/* #include "debug/module.h" */
#include "format/module.h"
/* #ifdef INCLUDE_GTK_SUPPORT */
/* #   include "gtkext/module.h" */
/* #   include "gui/module.h" */
/* #endif */
/* #include "mangling/module.h" */
#include "plugins/module.h"





/* ------------------------ FONCTIONNALITES POUR CODE PYTHON ------------------------ */


#define PYCHRYSALIDE_NAME "pychrysalide"

#define PYCHRYSALIDE_DOC                                                                                        \
    "PyChrysalide is a module containing Chrysalide's features and designed for Python users.\n"                \
    "\n"                                                                                                        \
    "The whole API is defined in a single library named 'pychrysalide.so' and can be used in two ways:\n"       \
    "* either from the Chrysalide's GUI, by registering hooks or GLib signals;\n"                               \
    "* or from a shell command line, by setting PYTHONPATH to point to the directory containing the library.\n" \
    "\n"                                                                                                        \
    "In both cases, this is a good start point to have a look at already existing plugins to quickly learn "    \
    "how the API works.\n"                                                                                      \
    "\n"                                                                                                        \
    "These plugins are located in the 'plugins/python' directory.\n"                                            \
    "\n"                                                                                                        \
    "The *pychrysalide* module imports the GLib module (version 2.0) from the GI repository at startup."


/* Fournit la révision du programme global. */
static PyObject *py_chrysalide_revision(PyObject *, PyObject *);

/* Fournit la version du programme global. */
static PyObject *py_chrysalide_version(PyObject *, PyObject *);

/* Fournit la version du greffon pour Python. */
static PyObject *py_chrysalide_mod_version(PyObject *, PyObject *);



/* ------------------------ FONCTIONNALITES DE MISE EN PLACE ------------------------ */


/* Détermine si l'interpréteur lancé est celui pris en compte. */
static bool is_current_abi_suitable(void);

/* Assure une pleine initialisation des objets de Python-GI. */
static bool install_metaclass_for_python_gobjects(void);

/* Met en place un environnement pour l'extension Python. */
static bool setup_python_context(void);

/* Assure la définition d'un type GObject pour Python adapté. */
static void ensure_native_pygobject_type(PyTypeObject **);

/* Fournit la référence à un éventuel module déjà en place. */
static PyObject *get_existing_modules(void);

/* Définit les différents modules du support Python. */
static PyObject *create_basic_modules(void);

/* Inscrit les défintions des objets Python de Chrysalide. */
static bool populate_python_modules(const pyinit_details_t *);

/* Restore une ancienne définition de type GObject au besoin. */
static void restore_original_pygobject_type(PyTypeObject *);


/* ------------------------ FONCTIONS GLOBALES DE CHRYSALIDE ------------------------ */


/* Assure le plein chargement dans un interpréteur Python. */
static bool init_python_interpreter_for_standalone_mode(const pyinit_details_t *);

/* Point de sortie pour l'initialisation de Python. */
static void PyExit_pychrysalide(void);





/* ---------------------------------------------------------------------------------- */
/*                          FONCTIONNALITES POUR CODE PYTHON                          */
/* ---------------------------------------------------------------------------------- */

/******************************************************************************
*                                                                             *
*  Paramètres  : self = NULL car méthode statique.                            *
*                args = non utilisé ici.                                      *
*                                                                             *
*  Description : Fournit la révision du programme global.                     *
*                                                                             *
*  Retour      : Numéro de révision.                                          *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_chrysalide_revision(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Valeur à retourner          */

#define PY_CHRYSALIDE_REVISION_METHOD PYTHON_METHOD_DEF                     \
(                                                                           \
     revision, "/",                                                         \
     METH_NOARGS, py_chrysalide,                                            \
     "Provide the revision number of Chrysalide.\n"                         \
     "\n"                                                                   \
     "The returned value is provided as a string, for instance: 'r1665'."   \
)

    result = PyUnicode_FromString("r" XSTR(REVISION));

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : self = NULL car méthode statique.                            *
*                args = non utilisé ici.                                      *
*                                                                             *
*  Description : Fournit la version du programme global.                      *
*                                                                             *
*  Retour      : Numéro de version.                                           *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_chrysalide_version(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Valeur à retourner          */
    int major;                              /* Numéro de version majeur    */
    int minor;                              /* Numéro de version mineur    */
    int revision;                           /* Numéro de révision          */
    char version[16];                       /* Conservation temporaire     */

#define PY_CHRYSALIDE_VERSION_METHOD PYTHON_METHOD_DEF                      \
(                                                                           \
     version, "/",                                                          \
     METH_NOARGS, py_chrysalide,                                            \
     "Provide the version number of Chrysalide.\n"                          \
     "\n"                                                                   \
     "The returned value is provided as a string, for instance: '1.6.65'."  \
)

    major = REVISION / 1000;
    minor = (REVISION - (major * 1000)) / 100;
    revision = REVISION % 100;

    snprintf(version, sizeof(version), "%d.%d.%d", major, minor, revision);

    result = PyUnicode_FromString(version);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : self = NULL car méthode statique.                            *
*                args = non utilisé ici.                                      *
*                                                                             *
*  Description : Fournit la version du greffon pour Python.                   *
*                                                                             *
*  Retour      : Numéro de version.                                           *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_chrysalide_mod_version(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Valeur à retourner          */
    char version[16];                       /* Conservation temporaire     */

#define PY_CHRYSALIDE_MOD_VERSION_METHOD PYTHON_METHOD_DEF                  \
(                                                                           \
     mod_version, "/",                                                      \
     METH_NOARGS, py_chrysalide,                                            \
     "Provide the version number of Chrysalide module for Python.\n"        \
     "\n"                                                                   \
     "The returned value is provided as a string, for instance: '0.1.0'."   \
)

    snprintf(version, sizeof(version), "%s", "x.x.x");// FIXME _chrysalide_plugin.version);

    result = PyUnicode_FromString(version);

    return result;

}



/* ---------------------------------------------------------------------------------- */
/*                          FONCTIONNALITES DE MISE EN PLACE                          */
/* ---------------------------------------------------------------------------------- */


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Détermine si l'interpréteur lancé est celui pris en compte.  *
*                                                                             *
*  Retour      : true si l'exécution peut se poursuivre, false sinon.         *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static bool is_current_abi_suitable(void)
{
    bool result;
    int fds[2];
    int ret;
    char cmds[128];
    char content[64];
    ssize_t got;

#define GRAB_ABI_FLAGS_IN_PYTHON                        \
    "import sys" "\n"                                   \
    "import os" "\n"                                    \
    "data = bytes(sys.abiflags, 'UTF-8') + b'\\0'" "\n" \
    "os.write(%d, data)" "\n"

    result = false;

    ret = pipe(fds);
    if (ret == -1)
    {
        perror("pipe()");
        goto exit;
    }

    snprintf(cmds, sizeof(cmds), GRAB_ABI_FLAGS_IN_PYTHON, fds[1]);

    ret = PyRun_SimpleString(cmds);
    if (ret != 0) goto exit_with_pipe;

    got = read(fds[0], content, sizeof(content));
    if (got < 0)
    {
        perror("read()");
        goto exit_with_pipe;
    }

    content[got] = '\0';

    result = (strcmp(content, LIBPYTHON_ABI_FLAGS) == 0);

 exit_with_pipe:

    close(fds[0]);
    close(fds[1]);

 exit:

    if (!result)
        PyErr_SetString(PyExc_SystemError, "the ABI flags of the current interpreter do not match " \
                        "the ones of the Python library used during the module compilation.");

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Assure une pleine initialisation des objets de Python-GI.    *
*                                                                             *
*  Retour      : Bilan de l'opération.                                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static bool install_metaclass_for_python_gobjects(void)
{
    bool result;                            /* Bilan à retourner           */
    PyObject *gi_types_mod;                 /* Module Python-GObject       */

    /**
     * Les extensions Python sont chargées à partir de la fonction load_python_plugins(),
     * qui fait appel à create_python_plugin(). Une instance y est construite via un
     * appel à PyObject_CallFunction() avec la classe spécifiée par l'alias AutoLoad
     * dans le fichier __init__.py présent dans chaque module d'extension.
     *
     * Le constructeur py_plugin_module_new() renvoie in fine à la fonction générique
     * python_abstract_constructor_with_dynamic_gtype(), laquelle conduit à la fonction
     * pygobject_register_class() définie dans <python3-gi>/gi/pygobject-object.c.
     * Le code de cette dernière comprend notamment la portion suivante :
     *
     *    [...]
     *    Py_SET_TYPE(type, PyGObject_MetaType);
     *    [...]
     *    if (PyType_Ready(type) < 0) {
     *        g_warning ("couldn't make the type `%s' ready", type->tp_name);
     *        return;
     *    }
     *    [...]
     *
     * La fonction PyType_Ready() est définie dans <python3>/Objects/typeobject.c
     * et commence par :
     *
     *    int PyType_Ready(PyTypeObject *type)
     *    {
     *        if (type->tp_flags & Py_TPFLAGS_READY) {
     *            assert(_PyType_CheckConsistency(type));
     *            return 0;
     *        }
     *        [...]
     *    }
     *
     * La vérification de cohérencce commence par analyser le type et son propre
     * type :
     *
     *  - cf. _PyType_CheckConsistency() dans <python3>/Objects/typeobject.c :
     *
     *    int _PyType_CheckConsistency(PyTypeObject *type)
     *    {
     *        [...]
     *        CHECK(!_PyObject_IsFreed((PyObject *)type));
     *        [...]
     *    }
     *
     *  - cf. _PyObject_IsFreed() dans <python3>/Objects/object.c :
     *
     *    int _PyObject_IsFreed(PyObject *op)
     *    {
     *        if (_PyMem_IsPtrFreed(op) || _PyMem_IsPtrFreed(Py_TYPE(op))) {
     *            return 1;
     *    }
     *
     * La fonction _PyMem_IsPtrFreed() recherche entre autres la valeur NULL.
     *
     * Or le type du type est écrasé dans la fonction pygobject_register_class()
     * avec la valeur de la variable PyGObject_MetaType. Cette variable n'est
     * définie qu'à un seul endroit, dans <python3-gi>/gi/gimodule.c :
     *
     *    static PyObject *
     *    pyg__install_metaclass(PyObject *dummy, PyTypeObject *metaclass)
     *    {
     *        Py_INCREF(metaclass);
     *        PyGObject_MetaType = metaclass;
     *        Py_INCREF(metaclass);
     *
     *        Py_SET_TYPE(&PyGObject_Type, metaclass);
     *
     *        Py_INCREF(Py_None);
     *        return Py_None;
     *    }
     *
     * Afin de valider la vérification de _PyType_CheckConsistency() pour les
     * modules externes qui entraînent un enregistrement tout en portant le drapeau
     * Py_TPFLAGS_READY (typiquement ceux du répertoire "plugins/python/", il faut
     * initialiser au besoin la variable PyGObject_MetaType.
     *
     * Une ligne suffit donc à enregistrer le type intermédiaire :
     *
     *    from _gi import types
     *
     * On simule ici une déclaration similaire si nécessaire, selon la valeur
     * portée par PyGObject_Type.ob_base.ob_base.ob_type.tp_name :
     *    - "type" (PyType_Type) : état initial ;
     *    - "_GObjectMetaBase" : état revu.
     */

    /**
     * PyGObject_Type.ob_base.ob_base.ob_type != &PyType_Type ?
     */
    result = (PyType_CheckExact(&PyGObject_Type) == 0);

    if (!result)
    {
        gi_types_mod = PyImport_ImportModule("gi.types");

        result = (PyErr_Occurred() == NULL);

        if (result)
            result = (PyType_CheckExact(&PyGObject_Type) == 0);

        Py_XDECREF(gi_types_mod);

    }

#ifndef NDEBUG
    if (result)
        assert(strcmp(PyGObject_Type.ob_base.ob_base.ob_type->tp_name, "_GObjectMetaBase") == 0);
#endif

    if (!result)
        PyErr_SetString(PyExc_SystemError, "unable to install metaclass for Python GObjects.");

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Met en place un environnement pour l'extension Python.       *
*                                                                             *
*  Retour      : Bilan de l'opération.                                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static bool setup_python_context(void)
{
    bool result;                            /* Bilan à retourner           */

    result = false;

    /**
     * Un message d'erreur doit être défini en cas d'échec de l'initialisation,
     * via un appel à PyErr_SetString().
     */

    if (!is_current_abi_suitable())
        goto exit;

    if (pygobject_init(-1, -1, -1) == NULL)
    {
        PyErr_SetString(PyExc_SystemError, "unable to init GObject in Python.");
        goto exit;
    }

    if (!install_metaclass_for_python_gobjects())
        goto exit;

    result = true;

 exit:

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : namespace = module particulier à charger à partir de gi.     *
*                version   = idenfiant de la version à stipuler.              *
*                                                                             *
*  Description : Charge un module GI dans Python avec une version attendue.   *
*                                                                             *
*  Retour      : Bilan de l'opération.                                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

bool import_namespace_from_gi_repository(const char *namespace, const char *version)
{
    bool result;                            /* Bilan à retourner           */
    PyObject *module;                       /* Module Python-GObject       */
    PyObject *args;                         /* Arguments à fournir         */
    int ret;                                /* Bilan d'une mise en place   */

    result = false;

    /* Sélection d'une version */

    module = PyImport_ImportModule("gi");

    if (module != NULL)
    {
        args = Py_BuildValue("ss", namespace, version);

        run_python_method(module, "require_version", args);

        result = (PyErr_Occurred() == NULL);

        Py_DECREF(args);
        Py_DECREF(module);

    }

    /* Importation du module visé */

    if (result)
    {
        args = PyTuple_New(1);

        ret = PyTuple_SetItem(args, 0, PyUnicode_FromString(namespace));
        if (ret != 0)
        {
            result = false;
            goto args_error;
        }

        module = PyImport_ImportModuleEx("gi.repository", NULL, NULL, args);

        result = (module != NULL);

        Py_XDECREF(module);

 args_error:

        Py_DECREF(args);

    }

    // TODO : message d'erreur ?

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : py_gobj_def = définition de type actuelle. [OUT]             *
*                                                                             *
*  Description : Assure la définition d'un type GObject pour Python adapté.   *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static void ensure_native_pygobject_type(PyTypeObject **py_gobj_def)
{
    GQuark pygobject_class_key;             /* Copie d'un accès GI interne */

    /**
     * Les appels suivants procèdent à l'enregistrement de différents éléments
     * dans l'espace de noms Python pour Chrysalide.
     *
     * Une majeure partie de ces éléments est constituée d'objets dérivés de
     * GObject. Ce type d'objet (G_TYPE_OBJECT) est représenté par deux types
     * en Python :
     *
     *    - gi._gi.GObject, mis en place lors de l'importation du module gi.
     *
     *      Ce dernier lance automatiquement l'importation du module natif gi._gi,
     *      lequel, via son initialisation dans la fonction PyInit__gi()
     *      (cf. ./gi/gimodule.c) lance un appel à pyi_object_register_types()
     *      qui procède à l'enregistrement du type "GObject" porté par la structure
     *      PyGObject_Type en correspondance au type GLib G_TYPE_OBJECT (cf. appel
     *      à pygobject_register_class() dans gi/pygobject-object.c).
     *
     *    - gi.repository.GObject.Object, qui vient surclasser le type précédent
     *      lors d'un appel d'initialisation : from gi.repository import GObject.
     *
     *      Cette seconde définition est destinée à apporter une représentation
     *      de l'introspection GObject de plus haut niveau pour l'utilisateur par
     *      rapport à celle de bas niveau gi._gi.
     *
     * Il demeure que la seconde définition est entièrement implémentée en Python
     * et porte ainsi le fanion Py_TPFLAGS_HEAPTYPE, imposant cette même dernière
     * propriétée à tous les objets qui en dérivent.
     *
     * Les définitions de Chrysalide sont cependant toutes statiques et donc
     * incompatibles avec une définition gi.repository.GObject.Object, comme le
     * pointent les validations opérées par PyType_Ready().
     *
     * Une solution consiste ici à restaurer au besoin la définition gi._gi.GObject
     * brute, effectuer les enregistrements de Chrysalide sur cette base, et
     * remettre en place la définition éventuellement remplacée ensuite.
     */

    *py_gobj_def = pygobject_lookup_class(G_TYPE_OBJECT);

    if (*py_gobj_def != &PyGObject_Type)
    {
        Py_INCREF((PyObject *)*py_gobj_def);

        /* Définition récupérée de pyi_object_register_types() */
        pygobject_class_key = g_quark_from_static_string("PyGObject::class");

        g_type_set_qdata(G_TYPE_OBJECT, pygobject_class_key, &PyGObject_Type);

    }

}


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Fournit la référence à un éventuel module déjà en place.     *
*                                                                             *
*  Retour      : Pointeur vers le module mis en place.                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *get_existing_modules(void)
{
    PyObject *result;                       /* Module Python à retourner   */

    /**
     * Vérification préalable : dans le cas où on est embarqué directement dans
     * un interpréteur Python, le module se charge et termine par charger à leur
     * tour les différentes extensions trouvées, via load_remaning_plugins() puis
     * chrysalide_plugin_on_native_loaded().
     *
     * Lesquelles vont très probablement charger le module pychrysalide.
     *
     * Comme le chargement de ce dernier n'est alors pas encore terminé,
     * Python va relancer cette procédure, et register_access_to_python_module()
     * va détecter un doublon.
     */

    result = get_access_to_python_module(PYCHRYSALIDE_NAME);

    Py_XINCREF(result);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Définit les différents modules du support Python.            *
*                                                                             *
*  Retour      : Pointeur vers le module mis en place.                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *create_basic_modules(void)
{
    PyObject *result;                       /* Module Python à retourner   */
    bool status;                            /* Bilan des inclusions        */

    static PyMethodDef py_chrysalide_methods[] = {
        PY_CHRYSALIDE_REVISION_METHOD,
        PY_CHRYSALIDE_VERSION_METHOD,
        PY_CHRYSALIDE_MOD_VERSION_METHOD,
        { NULL }
    };

    static PyModuleDef py_chrysalide_module = {

        .m_base = PyModuleDef_HEAD_INIT,

        .m_name = PYCHRYSALIDE_NAME,
        .m_doc = PYCHRYSALIDE_DOC,

        .m_size = -1,

        .m_methods = py_chrysalide_methods

    };

    result = PyModule_Create(&py_chrysalide_module);

    register_access_to_python_module(py_chrysalide_module.m_name, result);

    status = true;

    /**
     * Réceptacle pour objets et constantes : à laisser en premier ajout donc.
     */
    if (status) status = add_features_module(result);

    if (status) status = define_data_types_constants(result);

    if (status) status = add_analysis_module(result);
    if (status) status = add_arch_module(result);
    if (status) status = add_common_module(result);
    if (status) status = add_glibext_module(result);
    if (status) status = add_core_module(result);
    /*
    if (status) status = add_debug_module(result);
    */
    if (status) status = add_format_module(result);
    /*
#ifdef INCLUDE_GTK_SUPPORT
    if (status) status = add_gtkext_module(result);
    if (status) status = add_gui_module(result);
#endif
    if (status) status = add_mangling_module(result);
    */
    if (status) status = add_plugins_module(result);

    if (!status)
    {
        Py_DECREF(result);
        result = NULL;

    }

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : details = précisions de chargement complémentaires.          *
*                                                                             *
*  Description : Inscrit les défintions des objets Python de Chrysalide.      *
*                                                                             *
*  Retour      : Bilan de l'opération.                                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static bool populate_python_modules(const pyinit_details_t *details)
{
    bool result;                            /* Bilan à retourner           */

    /**
     * Les chargements de types supplémentaires, apportés par la version UI, ne
     * peuvent être forcés depuis les mises en places des versions non-UI via
     * les classiques appels ensure_xxx().
     *
     * L'assurance d'un chargement préalable est ainsi réalisée ici, via
     * un chargement préliminaire, si besoin est.
     */

    if (details->populate_extra)
        result = details->populate_extra();
    else
        result = true;

    /*
    if (result) result = ensure_python_string_enum_is_registered();
    */
    if (result) result = ensure_python_py_struct_is_registered();

    if (result) result = populate_analysis_module();
    if (result) result = populate_arch_module();
    if (result) result = populate_glibext_module();
    if (result) result = populate_common_module();
    if (result) result = populate_core_module();
    /*
    if (result) result = populate_debug_module();
    */
    if (result) result = populate_format_module();
    /*
#ifdef INCLUDE_GTK_SUPPORT
    if (result) result = populate_gtkext_module();
    if (result) result = populate_gui_module();
#endif
    if (result) result = populate_mangling_module();
    */
    if (result) result = populate_plugins_module();

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : py_gobj_def = définition de type actuelle. [OUT]             *
*                                                                             *
*  Description : Restore une ancienne définition de type GObject au besoin.   *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

void restore_original_pygobject_type(PyTypeObject *py_gobj_def)
{
    GQuark pygobject_class_key;             /* Copie d'un accès GI interne */

    if (py_gobj_def != &PyGObject_Type)
    {
        /* Définition récupérée de pyi_object_register_types() */
        pygobject_class_key = g_quark_from_static_string("PyGObject::class");

        g_type_set_qdata(G_TYPE_OBJECT, pygobject_class_key, py_gobj_def);

        Py_DECREF((PyObject *)py_gobj_def);

    }

}


/******************************************************************************
*                                                                             *
*  Paramètres  : details = précisions de chargement complémentaires.          *
*                                                                             *
*  Description : Implémente le point d'entrée pour l'initialisation de Python.*
*                                                                             *
*  Retour      : Module mis en place ou NULL en cas d'échec.                  *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

PyObject *init_python_pychrysalide_module(const pyinit_details_t *details)
{
    PyObject *result;                       /* Module Python à retourner   */
    PyTypeObject *py_gobj_def;              /* Définition GObject courante */
    bool status;                            /* Bilan des inclusions        */

    result = get_existing_modules();

    if (result != NULL)
        return result;

    if (!setup_python_context())
        goto exit;

    /**
     * Le chargement forcé de l'espace GLib pour Python permet d'éviter un écueil,
     * à savoir des types convertis de façon incomplète. Par exemple, pour une
     * structure GChecksum, le type à l'exécution est :
     *
     *    - sans module GLib : [<class 'gobject.GBoxed'>, <class 'object'>]
     *
     *    - avec module GLib : [<class 'gi.repository.GLib.Checksum'>, <class 'gi.Boxed'>, <class 'gobject.GBoxed'>, <class 'object'>]
     *
     * Par ailleurs, il est à noter que le message suivant n'apparaît qu'avec
     * la version debug de Python3 (version de python3-gi : 3.42.2-3) :
     *
     *    <frozen importlib._bootstrap>:673: ImportWarning: DynamicImporter.exec_module() not found; falling back to load_module()
     *
     * Code de reproduction dans un interpréteur classique :
     *
     *    import gi
     *    gi.require_version('GLib', '2.0')
     *    from gi.repository import GLib
     *
     */

    if (!import_namespace_from_gi_repository("GLib", "2.0"))
        goto exit;

    /* Mise en place des fonctionnalités offertes */

    ensure_native_pygobject_type(&py_gobj_def);

    result = create_basic_modules();

    if (result == NULL)
        PyErr_SetString(PyExc_SystemError, "failed to create all PyChrysalide modules.");

    else
    {
        status = populate_python_modules(details);

        if (!status)
            PyErr_SetString(PyExc_SystemError, "failed to load all PyChrysalide components.");

        else if (details->standalone)
            status = init_python_interpreter_for_standalone_mode(details);

        if (!status)
        {
            Py_DECREF(result);
            result = NULL;
        }

    }

    restore_original_pygobject_type(py_gobj_def);

 exit:

    if (result == NULL && !details->standalone)
        log_pychrysalide_exception("Python bindings loading failed");

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : prefix = message d'introduction à faire apparaître à l'écran.*
*                                                                             *
*  Description : Présente dans le journal une exception survenue.             *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

void log_pychrysalide_exception(const char *prefix, ...)
{
    va_list ap;                             /* Compléments argumentaires   */
    char *msg;                              /* Message complet à imprimer  */
    PyObject *err_type;                     /* Type d'erreur Python        */
    PyObject *err_value;                    /* Instance Python d'erreur    */
    PyObject *err_traceback;                /* Trace Python associée       */
    PyObject *err_string;                   /* Description Python d'erreur */
    const char *err_msg;                    /* Représentation humaine      */

    assert(PyGILState_Check() == 1);

    if (PyErr_Occurred())
    {
        /* Base de la communication */

        va_start(ap, prefix);

        vasprintf(&msg, prefix, ap);

        va_end(ap);

        /* Détails complémentaires */

        PyErr_Fetch(&err_type, &err_value, &err_traceback);

        PyErr_NormalizeException(&err_type, &err_value, &err_traceback);

        if (err_traceback == NULL)
        {
            err_traceback = Py_None;
            Py_INCREF(err_traceback);
        }

        PyException_SetTraceback(err_value, err_traceback);

        if (err_value == NULL)
            msg = stradd(msg, _(": no extra information is provided..."));

        else
        {
            err_string = PyObject_Str(err_value);
            err_msg = PyUnicode_AsUTF8(err_string);

            msg = stradd(msg, ": ");
            msg = stradd(msg, err_msg);

            Py_DECREF(err_string);

        }

        /**
         * Bien que la documentation précise que la fonction PyErr_Fetch()
         * transfère la propritété des éléments retournés, la pratique
         * montre que le programme plante à la terminaison en cas d'exception.
         *
         * C'est par exemple le cas quand un greffon Python ne peut se lancer
         * correctement ; l'exception est alors levée à partir de la fonction
         * create_python_plugin() et le plantage intervient en sortie d'exécution,
         * au moment de la libération de l'extension Python :
         *
         *    ==14939== Jump to the invalid address stated on the next line
         *    ==14939==    at 0x1A8FCBC9: ???
         *    ==14939==    by 0x53DCDB2: g_object_unref (in /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0.5800.3)
         *    ==14939==    by 0x610F834: on_plugin_ref_toggle (pglist.c:370)
         *    ==14939==    by 0x610F31A: exit_all_plugins (pglist.c:153)
         *    ==14939==    by 0x10AD19: main (main.c:440)
         *    ==14939==  Address 0x1a8fcbc9 is not stack'd, malloc'd or (recently) free'd
         *
         * Curieusement, un appel à PyErr_PrintEx(1) corrige l'effet, alors qu'un
         * appel à PyErr_PrintEx(0) ne change rien.
         *
         * La seule différence de l'instruction set_sys_last_vars réside en quelques
         * lignes dans le code de l'interpréteur Python :
         *
         *    if (set_sys_last_vars) {
         *        _PySys_SetObjectId(&PyId_last_type, exception);
         *        _PySys_SetObjectId(&PyId_last_value, v);
         *        _PySys_SetObjectId(&PyId_last_traceback, tb);
         *    }
         *
         * L'explication n'est pas encore déterminé : bogue dans Chrysalide ou dans Python ?
         * L'ajout des éléments dans le dictionnaire du module sys ajoute une référence
         * à ces éléments.
         *
         * On reproduit ici le comportement du code correcteur avec PySys_SetObject().
         */

        PySys_SetObject("last_type", err_type);
        PySys_SetObject("last_value", err_value);
        PySys_SetObject("last_traceback", err_traceback);

        Py_XDECREF(err_traceback);
        Py_XDECREF(err_value);
        Py_XDECREF(err_type);

        log_plugin_simple_message(LMT_EXT_ERROR, msg);

        free(msg);

    }

}



/* ---------------------------------------------------------------------------------- */
/*                          FONCTIONS GLOBALES DE CHRYSALIDE                          */
/* ---------------------------------------------------------------------------------- */


/******************************************************************************
*                                                                             *
*  Paramètres  : details = précisions de chargement complémentaires.          *
*                                                                             *
*  Description : Assure le plein chargement dans un interpréteur Python.      *
*                                                                             *
*  Retour      : Bilan de l'opération.                                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static bool init_python_interpreter_for_standalone_mode(const pyinit_details_t *details)
{
    bool result;                            /* Bilan à retourner           */
    int ret;                                /* Bilan de préparatifs        */
    Dl_info info;                           /* Informations dynamiques     */
    GModule *module;                        /* Structure de chargement GLib*/
    GPluginModule *self;                    /* Représentation interne      */

    result = false;

    ret = Py_AtExit(PyExit_pychrysalide);
    if (ret == -1)
    {
        PyErr_SetString(PyExc_SystemError, "failed to register a cleanup function.");
        goto exit;
    }

    if (!load_core_components(ACC_ALL_COMPONENTS))
    {
        PyErr_SetString(PyExc_SystemError, "unable to load core components.");
        goto exit;
    }

    /**
     * Le module chargé par Python n'apparaît pas dans la liste des greffons de
     * Chrysalide et ne peut donc pas être référencé comme dépendance par d'autres
     * extensions.
     *
     * Par ailleurs, lors de la recherche d'autres greffons via l'appel à la
     * fonction init_all_plugins() ci-après, il faut que le nom du greffon soit
     * déjà réservé pour faire échouer le second chargement du greffon courant
     * lors du parcours des répertoires conservant les fichiers d'extensions.
     */

    ret = dladdr(__FUNCTION__, &info);
    if (ret == 0)
    {
        LOG_ERROR_DL_N("dladdr");

        PyErr_SetString(PyExc_SystemError, "failed to force bindings registration.");
        goto exit;

    }

    module = g_module_open(info.dli_fname, G_MODULE_BIND_LAZY);
    assert(module != NULL);

    self = details->create_self(module);

    /* A ce stade, le greffon a été chargé correctement */
    g_plugin_module_override_flags(self, PSF_LOADED);

    register_plugin(self);

    unref_object(self);

    /**
     * Intégration des fonctionnalités portées par d'autres greffons.
     */

    result = true;

    init_all_plugins(true);

 exit:

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Point de sortie pour l'initialisation de Python.             *
*                                                                             *
*  Retour      : ?                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static void PyExit_pychrysalide(void)
{
    exit_all_plugins();

    unload_core_components(ACC_ALL_COMPONENTS);

}