/* Chrysalide - Outil d'analyse de fichiers binaires
 * workqueue.c - prototypes pour l'équivalent Python du fichier "glibext/workqueue.c"
 *
 * 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 "workqueue.h"


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


#include <glibext/workqueue-int.h>


#include "work.h"
#include "../access.h"
#include "../helpers.h"



/* ------------------------ GLUE POUR CREATION DEPUIS PYTHON ------------------------ */


CREATE_DYN_CONSTRUCTOR(work_queue, G_TYPE_WORK_QUEUE);

/* Initialise une instance sur la base du dérivé de GObject. */
static int py_work_queue_init(PyObject *, PyObject *, PyObject *);



/* ------------------------- CONNEXION AVEC L'API DE PYTHON ------------------------- */


/* Constitue un nouveau groupe de travail. */
static PyObject *py_work_queue_define_group(PyObject *, PyObject *);

/* Dissout un groupe de travail existant. */
static PyObject *py_work_queue_delete_group(PyObject *, PyObject *);

/* Place une nouvelle tâche en attente. */
static PyObject *py_work_queue_schedule(PyObject *, PyObject *);

/* Détermine si un groupe est vide de toute programmation. */
static PyObject *py_work_queue_is_empty(PyObject *, PyObject *);

/* Attend que toutes les tâches d'un groupe soient traitées. */
static PyObject *py_work_queue_wait_for_completion(PyObject *, PyObject *);

/* Force un réveil d'une attente en cours pour la confirmer. */
static PyObject *py_work_queue_wake_up_waiters(PyObject *, PyObject *);



/* ---------------------------------------------------------------------------------- */
/*                          GLUE POUR CREATION DEPUIS PYTHON                          */
/* ---------------------------------------------------------------------------------- */


/******************************************************************************
*                                                                             *
*  Paramètres  : self = objet à initialiser (théoriquement).                  *
*                args = arguments fournis à l'appel.                          *
*                kwds = arguments de type key=val fournis.                    *
*                                                                             *
*  Description : Initialise une instance sur la base du dérivé de GObject.    *
*                                                                             *
*  Retour      : 0.                                                           *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static int py_work_queue_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    int ret;                                /* Bilan de lecture des args.  */

#define WORK_QUEUE_DOC                                            \
    "WorkQueue defines a basic work aimed to get processed in a"  \
    " thread setup by a pychrysalide.glibext.WorkQueue instance.\n" \
    "\n"                                                            \
    "Instances can be created using the following constructor:\n"   \
    "\n"                                                            \
    "    WorkQueue()"

    /* Initialisation d'un objet GLib */

    ret = forward_pygobjet_init(self);
    if (ret == -1) return -1;

    return 0;

}



/* ---------------------------------------------------------------------------------- */
/*                           CONNEXION AVEC L'API DE PYTHON                           */
/* ---------------------------------------------------------------------------------- */


/******************************************************************************
*                                                                             *
*  Paramètres  : self = élément d'appel à consulter.                          *
*                args = arguments fournis pour l'opération.                   *
*                                                                             *
*  Description : Constitue un nouveau groupe de travail.                      *
*                                                                             *
*  Retour      : Nouvel identifiant unique d'un nouveau groupe de travail.    *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_work_queue_define_group(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Bilan à retourner           */
    unsigned int count;                     /* Nombre de threads à activer */
    int ret;                                /* Bilan de lecture des args.  */
    GWorkQueue *queue;                      /* Version native              */
    wgroup_id_t id;                         /* Identifiant de groupe créé  */

#define WORK_QUEUE_DEFINE_GROUP_METHOD PYTHON_METHOD_DEF            \
(                                                                   \
    define_group, "$self, /, count=0",                              \
    METH_VARARGS, py_work_queue,                                    \
    "Create a new work group for a work queue.\n"                   \
    "\n"                                                            \
    "The *count* argument states the number of threads which will"  \
    " be activated for the processing. If the provided value is"    \
    " zero, then a default value based on the current hardware is"  \
    " selected.\n"                                                  \
    "\n"                                                            \
    "The result is a work group identifier."                        \
)

    count = 0;

    ret = PyArg_ParseTuple(args, "|I", &count);
    if (!ret) return NULL;

    queue = G_WORK_QUEUE(pygobject_get(self));

    id = g_work_queue_define_group(queue, count);

    result = PyLong_FromUnsignedLong(id);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : self = élément d'appel à consulter.                          *
*                args = arguments fournis pour l'opération.                   *
*                                                                             *
*  Description : Dissout un groupe de travail existant.                       *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_work_queue_delete_group(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Bilan à retourner           */
    unsigned long long id;                  /* Identifiant de groupe ciblé */
    int ret;                                /* Bilan de lecture des args.  */
    GWorkQueue *queue;                      /* Version native              */

#define WORK_QUEUE_DELETE_GROUP_METHOD PYTHON_METHOD_DEF            \
(                                                                   \
    delete_group, "$self, id",                                      \
    METH_VARARGS, py_work_queue,                                    \
    "Destroy a work group previously registered inside a queue.\n"  \
    "\n"                                                            \
    "The *id* argument refers to a work group identifier. It"       \
    " should have been provided by a previous call to"              \
    " pychrysalide.glibext.WorkQueue.g_work_queue_define_group()."  \
)

    ret = PyArg_ParseTuple(args, "K", &id);
    if (!ret) return NULL;

    queue = G_WORK_QUEUE(pygobject_get(self));

    g_work_queue_delete_group(queue, id);

    result = Py_None;
    Py_INCREF(result);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : self = élément d'appel à consulter.                          *
*                args = arguments fournis pour l'opération.                   *
*                                                                             *
*  Description : Place une nouvelle tâche en attente.                         *
*                                                                             *
*  Retour      : Bilan, qui correspond à l'existence du groupe ciblé.         *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_work_queue_schedule(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Bilan à retourner           */
    GGenericWork *work;                     /* Tâche à programmer          */
    unsigned long long id;                  /* Identifiant de groupe ciblé */
    int ret;                                /* Bilan de lecture des args.  */
    GWorkQueue *queue;                      /* Version native              */
    bool status;                            /* Bilan de programmation      */

#define WORK_QUEUE_SCHEDULE_METHOD PYTHON_METHOD_DEF                \
(                                                                   \
    schedule, "$self, work, id",                                    \
    METH_VARARGS, py_work_queue,                                    \
    "Schedule a work into the queue processing.\n"                  \
    "\n"                                                            \
    "The *work* argument has to point to a"                         \
    " pychrysalide.glibext.GenericWork instance, and the *id*"      \
    " argument refers to a work group identifier which should have" \
    " been provided by a previous call to"                          \
    " pychrysalide.glibext.WorkQueue.g_work_queue_define_group().\n"\
    "\n"                                                            \
    "The result is a boolean value: *True* in case of succes (does" \
    " the provided identifier exist?), *False* otherwise."          \
)

    ret = PyArg_ParseTuple(args, "O&K", convert_to_generic_work, &work, &id);
    if (!ret) return NULL;

    queue = G_WORK_QUEUE(pygobject_get(self));

    status = g_work_queue_schedule(queue, work, id);

    result = status ? Py_True : Py_False;
    Py_INCREF(result);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : self = élément d'appel à consulter.                          *
*                args = arguments fournis pour l'opération.                   *
*                                                                             *
*  Description : Détermine si un groupe est vide de toute programmation.      *
*                                                                             *
*  Retour      : Etat du groupe de travail.                                   *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_work_queue_is_empty(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Bilan à retourner           */
    unsigned long long id;                  /* Identifiant de groupe ciblé */
    int ret;                                /* Bilan de lecture des args.  */
    GWorkQueue *queue;                      /* Version native              */
    bool status;                            /* Bilan de consultation       */

#define WORK_QUEUE_IS_EMPTY_METHOD PYTHON_METHOD_DEF                \
(                                                                   \
    is_empty, "$self, id",                                          \
    METH_VARARGS, py_work_queue,                                    \
    "Signal if there is some remaining work to get processed by"    \
    " the queue or not.\n"                                          \
    "\n"                                                            \
    "The *id* argument refers to a work group identifier. It"       \
    " should have been provided by a previous call to"              \
    " pychrysalide.glibext.WorkQueue.g_work_queue_define_group().\n"\
    "\n"                                                            \
    "The result is a boolean value: *True* if the group does not"   \
    " contain any works or does not exist, *False* otherwise."      \
)

    ret = PyArg_ParseTuple(args, "K", &id);
    if (!ret) return NULL;

    queue = G_WORK_QUEUE(pygobject_get(self));

    status = g_work_queue_is_empty(queue, id);

    result = status ? Py_True : Py_False;
    Py_INCREF(result);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : self = élément d'appel à consulter.                          *
*                args = arguments fournis pour l'opération.                   *
*                                                                             *
*  Description : Attend que toutes les tâches d'un groupe soient traitées.    *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_work_queue_wait_for_completion(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Bilan à retourner           */
    unsigned long long rel;                 /* Durée d'attente             */
    unsigned long long id;                  /* Identifiant de groupe ciblé */
    int ret;                                /* Bilan de lecture des args.  */
    GWorkQueue *queue;                      /* Version native              */
    bool status;                            /* Bilan de l'attente menée    */

    /**
     * Python appelle cette fonction, lorsqu'elle est sollicitée dans des scripts,
     * via la fonction PyObject_Vectorcall(), qui obtient la structure de suivi
     * du thread courant avec _PyThreadState_GET().
     *
     * Un commentaire associé à cette dernière fonction précise :
     *
     *    The caller must hold the GIL.
     *
     * Comme le verrou GIL est posé par Python au moment de l'exécution de
     * py_work_queue_wait_for_completion(), aucune attente bloquante ne peut
     * alors être engagée ici : elle bloquerait la tâche _run() d'un travail
     * réalisé en Python(), comme son déférencement via unref_ojbect() qui
     * implique une exécution de la fonction pyg_toggle_notify() appelant
     * PyGILState_Ensure() dans python3-gi.
     *
     * Aussi une attente bornée dans le temps est la seule option possible
     * ici.
     */

#define WORK_QUEUE_WAIT_FOR_COMPLETION_METHOD PYTHON_METHOD_DEF     \
(                                                                   \
    wait_for_completion, "$self, id, /, rel=100000",                \
    METH_VARARGS, py_work_queue,                                    \
    "Wait until all the works from a given group have been"         \
    " processed.\n"                                                 \
    "\n"                                                            \
    "The *id* argument refers to a work group identifier. It"       \
    " should have been provided by a previous call to"              \
    " pychrysalide.glibext.WorkQueue.g_work_queue_define_group()."  \
    " The *rel* range provide a relative time to wait (100ms by"    \
    " default).\n"                                                  \
    "\n"                                                            \
    "The returned value is a boolean status: *False* on a timeout," \
    " *True* otherwise."                                            \
    "\n"                                                            \
    "_Important note:_\n"                                           \
    "As this function is called by Python with its Global"          \
    " Interpreter Lock (GIL) held, an endless wait is impossible"   \
    " because it would keep the GIL unreleased whereas others"      \
    " threads (including those which may end the wait) may need to" \
    " acquire it.\n"                                                \
    "\n"                                                            \
    "So an unoptimized timed wait is performed here instead, and"   \
    " the returned status has to be checked by the caller in order" \
    " to schedule another wait if needed."                          \
)

    rel = 100 * G_TIME_SPAN_MILLISECOND;

    ret = PyArg_ParseTuple(args, "K|K", &id, &rel);
    if (!ret) return NULL;

    queue = G_WORK_QUEUE(pygobject_get(self));

    status = g_work_queue_wait_timed_for_completion(queue, id, rel);

    result = status ? Py_True : Py_False;
    Py_INCREF(result);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : self = élément d'appel à consulter.                          *
*                args = arguments fournis pour l'opération.                   *
*                                                                             *
*  Description : Force un réveil d'une attente en cours pour la confirmer.    *
*                                                                             *
*  Retour      : -                                                            *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

static PyObject *py_work_queue_wake_up_waiters(PyObject *self, PyObject *args)
{
    PyObject *result;                       /* Bilan à retourner           */
    unsigned long long id;                  /* Identifiant de groupe ciblé */
    int ret;                                /* Bilan de lecture des args.  */
    GWorkQueue *queue;                      /* Version native              */

#define WORK_QUEUE_WAKE_UP_WAITERS_METHOD PYTHON_METHOD_DEF         \
(                                                                   \
    wake_up_waiters, "$self, id",                                   \
    METH_VARARGS, py_work_queue,                                    \
    "Force a wake up of all threads waiting for processing a given" \
    " queue work group.\n"                                          \
    "\n"                                                            \
    "The *id* argument refers to a work group identifier. It"       \
    " should have been provided by a previous call to"              \
    " pychrysalide.glibext.WorkQueue.g_work_queue_define_group()."  \
)

    ret = PyArg_ParseTuple(args, "K", &id);
    if (!ret) return NULL;

    queue = G_WORK_QUEUE(pygobject_get(self));

    g_work_queue_wake_up_waiters(queue, id);

    result = Py_None;
    Py_INCREF(result);

    return result;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : -                                                            *
*                                                                             *
*  Description : Fournit un accès à une définition de type à diffuser.        *
*                                                                             *
*  Retour      : Définition d'objet pour Python.                              *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

PyTypeObject *get_python_work_queue_type(void)
{
    static PyMethodDef py_work_queue_methods[] = {
        WORK_QUEUE_DEFINE_GROUP_METHOD,
        WORK_QUEUE_DELETE_GROUP_METHOD,
        WORK_QUEUE_SCHEDULE_METHOD,
        WORK_QUEUE_IS_EMPTY_METHOD,
        WORK_QUEUE_WAIT_FOR_COMPLETION_METHOD,
        WORK_QUEUE_WAKE_UP_WAITERS_METHOD,
        { NULL }
    };

    static PyGetSetDef py_work_queue_getseters[] = {
        { NULL }
    };

    static PyTypeObject py_work_queue_type = {

        PyVarObject_HEAD_INIT(NULL, 0)

        .tp_name        = "pychrysalide.glibext.WorkQueue",
        .tp_basicsize   = sizeof(PyGObject),

        .tp_flags       = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

        .tp_doc         = WORK_QUEUE_DOC,

        .tp_methods     = py_work_queue_methods,
        .tp_getset      = py_work_queue_getseters,

        .tp_init        = py_work_queue_init,
        .tp_new         = py_work_queue_new,

    };

    return &py_work_queue_type;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : module = module dont la définition est à compléter.          *
*                                                                             *
*  Description : Prend en charge l'objet 'pychrysalide.glibext.WorkQueue'.    *
*                                                                             *
*  Retour      : Bilan de l'opération.                                        *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

bool ensure_python_work_queue_is_registered(void)
{
    PyTypeObject *type;                     /* Type Python 'WorkQueue'     */
    PyObject *module;                       /* Module à recompléter        */
    PyObject *dict;                         /* Dictionnaire du module      */

    type = get_python_work_queue_type();

    if (!PyType_HasFeature(type, Py_TPFLAGS_READY))
    {
        module = get_access_to_python_module("pychrysalide.glibext");

        dict = PyModule_GetDict(module);

        if (!register_class_for_pygobject(dict, G_TYPE_WORK_QUEUE, type))
            return false;

    }

    return true;

}


/******************************************************************************
*                                                                             *
*  Paramètres  : arg = argument quelconque à tenter de convertir.             *
*                dst = destination des valeurs récupérées en cas de succès.   *
*                                                                             *
*  Description : Tente de convertir en gestionnaire de travaux différés.      *
*                                                                             *
*  Retour      : Bilan de l'opération, voire indications supplémentaires.     *
*                                                                             *
*  Remarques   : -                                                            *
*                                                                             *
******************************************************************************/

int convert_to_work_queue(PyObject *arg, void *dst)
{
    int result;                             /* Bilan à retourner           */

    result = PyObject_IsInstance(arg, (PyObject *)get_python_work_queue_type());

    switch (result)
    {
        case -1:
            /* L'exception est déjà fixée par Python */
            result = 0;
            break;

        case 0:
            PyErr_SetString(PyExc_TypeError, "unable to convert the provided argument to work queue");
            break;

        case 1:
            *((GWorkQueue **)dst) = G_WORK_QUEUE(pygobject_get(arg));
            break;

        default:
            assert(false);
            break;

    }

    return result;

}