/* SPDX-License-Identifier: BSD-2-Clause */

/*
 * This file implements the enum support.
 *
 * Copyright (c) 2024 Phil Thompson <phil@riverbankcomputing.com>
 */


/* Remove when Python v3.12 is no longer supported. */
#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include <assert.h>

#include "sip_core.h"

#include "sip_enum.h"


#define IS_UNSIGNED_ENUM(etd)   ((etd)->etd_base_type == SIP_ENUM_UINT_ENUM || (etd)->etd_base_type == SIP_ENUM_INT_FLAG || (etd)->etd_base_type == SIP_ENUM_FLAG)


static PyObject *int_type = NULL;               /* The int type. */
static PyObject *object_type = NULL;            /* The object type. */

static PyObject *enum_type = NULL;              /* The enum.Enum type. */
static PyObject *int_enum_type = NULL;          /* The enum.IntEnum type. */
static PyObject *flag_type = NULL;              /* The enum.Flag type. */
static PyObject *int_flag_type = NULL;          /* The enum.IntFlag type. */

static PyObject *str_dunder_new = NULL;         /* '__new__' */
static PyObject *str_dunder_sip = NULL;         /* '__sip__' */
static PyObject *str_sunder_missing = NULL;     /* '_missing_' */
static PyObject *str_sunder_name = NULL;        /* '_name_' */
static PyObject *str_sunder_sip_missing = NULL; /* '_sip_missing_' */
static PyObject *str_sunder_value = NULL;       /* '_value_' */
static PyObject *str_module = NULL;             /* 'module' */
static PyObject *str_qualname = NULL;           /* 'qualname' */
static PyObject *str_value = NULL;              /* 'value' */


/* Forward references. */
static PyObject *create_enum_object(sipExportedModuleDef *client,
        sipEnumTypeDef *etd, sipIntInstanceDef **next_int_p, PyObject *name);
static void enum_expected(PyObject *obj, const sipTypeDef *td);
static PyObject *get_enum_type(const sipTypeDef *td);
static PyObject *missing(PyObject *cls, PyObject *value, int int_enum);
static PyObject *missing_enum(PyObject *cls, PyObject *value);
static PyObject *missing_int_enum(PyObject *cls, PyObject *value);


/*
 * Create a Python object for a member of a named enum.
 */
PyObject *sip_api_convert_from_enum(int member, const sipTypeDef *td)
{
    PyObject *et;

    assert(sipTypeIsEnum(td));

    et = get_enum_type(td);

    return PyObject_CallFunction(et,
            IS_UNSIGNED_ENUM((sipEnumTypeDef *)td) ? "(I)" : "(i)", member);
}


/*
 * Convert a Python object implementing an enum to an integer value.  An
 * exception is raised if there was an error.
 */
int sip_api_convert_to_enum(PyObject *obj, const sipTypeDef *td)
{
    PyObject *val_obj, *type_obj;
    int val;

    assert(sipTypeIsEnum(td));

    /* Make sure the enum object has been created. */
    type_obj = get_enum_type(td);

    /* Check the type of the Python object. */
    if (PyObject_IsInstance(obj, type_obj) <= 0)
    {
        enum_expected(obj, td);
        return -1;
    }

    /* Get the value from the object. */
    if ((val_obj = PyObject_GetAttr(obj, str_value)) == NULL)
        return -1;

    /* Flags are implicitly unsigned. */
    if (IS_UNSIGNED_ENUM((sipEnumTypeDef *)td))
        val = (int)sip_api_long_as_unsigned_int(val_obj);
    else
        val = sip_api_long_as_int(val_obj);

    Py_DECREF(val_obj);

    return val;
}


/*
 * Return a non-zero value if an object is a sub-class of enum.Flag.
 */
int sip_api_is_enum_flag(PyObject *obj)
{
    return (PyObject_IsSubclass(obj, flag_type) == 1);
}


/*
 * Create an enum object and add it to a dictionary.  A negative value is
 * returned (and an exception set) if there was an error.
 */
int sip_enum_create(sipExportedModuleDef *client, sipEnumTypeDef *etd,
        sipIntInstanceDef **next_int_p, PyObject *dict)
{
    int rc;
    PyObject *name, *enum_obj;

    /* Create an object corresponding to the type name. */
    if ((name = PyUnicode_FromString(sipPyNameOfEnum(etd))) == NULL)
        return -1;

    /* Create the enum object. */
    if ((enum_obj = create_enum_object(client, etd, next_int_p, name)) == NULL)
    {
        Py_DECREF(name);
        return -1;
    }

    /* Add the enum to the "parent" dictionary. */
    rc = PyDict_SetItem(dict, name, enum_obj);

    /* We can now release our remaining references. */
    Py_DECREF(name);
    Py_DECREF(enum_obj);

    return rc;
}


/*
 * Return the generated type structure for a Python enum object that wraps a
 * C/C++ enum or NULL (and no exception set) if the object is something else.
 */
const sipTypeDef *sip_enum_get_generated_type(PyObject *obj)
{
    if (sip_enum_is_enum(obj))
    {
        PyObject *etd_cap;

        if ((etd_cap = PyObject_GetAttr(obj, str_dunder_sip)) != NULL)
        {
            sipTypeDef *td = (sipTypeDef *)PyCapsule_GetPointer(etd_cap, NULL);

            Py_DECREF(etd_cap);

            return td;
        }

        PyErr_Clear();
    }

    return NULL;
}


/*
 * Initialise the enum support.  A negative value is returned (and an exception
 * set) if there was an error.
 */
int sip_enum_init(void)
{
    PyObject *builtins, *enum_module;

    /* Get the builtin types. */
    builtins = PyEval_GetBuiltins();

    if ((int_type = PyDict_GetItemString(builtins, "int")) == NULL)
        return -1;

    if ((object_type = PyDict_GetItemString(builtins, "object")) == NULL)
        return -1;

    /* Get the enum types. */
    if ((enum_module = PyImport_ImportModule("enum")) == NULL)
        return -1;

    enum_type = PyObject_GetAttrString(enum_module, "Enum");
    int_enum_type = PyObject_GetAttrString(enum_module, "IntEnum");
    flag_type = PyObject_GetAttrString(enum_module, "Flag");
    int_flag_type = PyObject_GetAttrString(enum_module, "IntFlag");

    Py_DECREF(enum_module);

    if (enum_type == NULL || int_enum_type == NULL || flag_type == NULL || int_flag_type == NULL)
    {
        Py_XDECREF(enum_type);
        Py_XDECREF(int_enum_type);
        Py_XDECREF(flag_type);
        Py_XDECREF(int_flag_type);

        return -1;
    }

    /* Objectify the strings. */
    if (sip_objectify("__new__", &str_dunder_new) < 0)
        return -1;

    if (sip_objectify("__sip__", &str_dunder_sip) < 0)
        return -1;

    if (sip_objectify("_missing_", &str_sunder_missing) < 0)
        return -1;

    if (sip_objectify("_name_", &str_sunder_name) < 0)
        return -1;

    if (sip_objectify("_sip_missing_", &str_sunder_sip_missing) < 0)
        return -1;

    if (sip_objectify("_value_", &str_sunder_value) < 0)
        return -1;

    if (sip_objectify("module", &str_module) < 0)
        return -1;

    if (sip_objectify("qualname", &str_qualname) < 0)
        return -1;

    if (sip_objectify("value", &str_value) < 0)
        return -1;

    return 0;
}


/*
 * Return a non-zero value if an object is a sub-class of enum.Enum.
 */
int sip_enum_is_enum(PyObject *obj)
{
    return (PyObject_IsSubclass(obj, enum_type) == 1);
}



/*
 * Create an enum object.
 */
static PyObject *create_enum_object(sipExportedModuleDef *client,
        sipEnumTypeDef *etd, sipIntInstanceDef **next_int_p, PyObject *name)
{
    int i;
    PyObject *members, *enum_factory, *enum_obj, *args, *kw_args, *etd_cap;
    PyMethodDef *missing_md;
    sipIntInstanceDef *next_int;

    /* Create a dict of the members. */
    if ((members = PyDict_New()) == NULL)
        goto ret_err;

    next_int = *next_int_p;
    assert(next_int != NULL);

    for (i = 0; i < etd->etd_nr_members; ++i)
    {
        PyObject *value_obj;

        assert(next_int->ii_name != NULL);

        /* Flags are implicitly unsigned. */
        if (IS_UNSIGNED_ENUM(etd))
            value_obj = PyLong_FromUnsignedLong((unsigned)next_int->ii_val);
        else
            value_obj = PyLong_FromLong(next_int->ii_val);

        if (sip_dict_set_and_discard(members, next_int->ii_name, value_obj) < 0)
            goto rel_members;

        ++next_int;
    }

    *next_int_p = next_int;

    if ((args = PyTuple_Pack(2, name, members)) == NULL)
        goto rel_members;

    if ((kw_args = PyDict_New()) == NULL)
        goto rel_args;

    if (PyDict_SetItem(kw_args, str_module, client->em_nameobj) < 0)
        goto rel_kw_args;

    /*
     * If the enum has a scope then the default __qualname__ will be incorrect.
     */
     if (etd->etd_scope >= 0)
     {
        int rc;
        PyObject *qualname;

        if ((qualname = sip_get_qualname(client->em_types[etd->etd_scope], name)) == NULL)
            goto rel_kw_args;

        rc = PyDict_SetItem(kw_args, str_qualname, qualname);

        Py_DECREF(qualname);

        if (rc < 0)
            goto rel_kw_args;
    }

    missing_md = NULL;

    if (etd->etd_base_type == SIP_ENUM_INT_FLAG)
    {
        enum_factory = int_flag_type;
    }
    else if (etd->etd_base_type == SIP_ENUM_FLAG)
    {
        enum_factory = flag_type;
    }
    else if (etd->etd_base_type == SIP_ENUM_INT_ENUM || etd->etd_base_type == SIP_ENUM_UINT_ENUM)
    {
        static PyMethodDef missing_int_enum_md = {
            "_missing_", missing_int_enum, METH_O|METH_CLASS, NULL
        };

        enum_factory = int_enum_type;
        missing_md = &missing_int_enum_md;
    }
    else
    {
        static PyMethodDef missing_enum_md = {
            "_missing_", missing_enum, METH_O|METH_CLASS, NULL
        };

        enum_factory = enum_type;
        missing_md = &missing_enum_md;
    }

    if ((enum_obj = PyObject_Call(enum_factory, args, kw_args)) == NULL)
        goto rel_kw_args;

    Py_DECREF(kw_args);
    Py_DECREF(args);
    Py_DECREF(members);

    etd->etd_base.td_py_type = (PyTypeObject *)enum_obj;

    /* Inject _missing_. */
    if (missing_md != NULL)
    {
        PyObject *missing_cfunc;

        if ((missing_cfunc = PyCFunction_New(missing_md, enum_obj)) == NULL)
        {
            Py_DECREF(enum_obj);
            return NULL;
        }

        if (PyObject_SetAttr(enum_obj, str_sunder_missing, missing_cfunc) < 0)
        {
            Py_DECREF(missing_cfunc);
            Py_DECREF(enum_obj);
            return NULL;
        }

        Py_DECREF(missing_cfunc);
    }

    /* Wrap the generated type definition in a capsule. */
    if ((etd_cap = PyCapsule_New(etd, NULL, NULL)) == NULL)
    {
        Py_DECREF(enum_obj);
        return NULL;
    }

    if (PyObject_SetAttr(enum_obj, str_dunder_sip, etd_cap) < 0)
    {
        Py_DECREF(etd_cap);
        Py_DECREF(enum_obj);
        return NULL;
    }

    Py_DECREF(etd_cap);

    if (etd->etd_pyslots != NULL)
        sip_add_type_slots((PyHeapTypeObject *)enum_obj, etd->etd_pyslots);

    return enum_obj;

    /* Unwind on errors. */

rel_kw_args:
    Py_DECREF(kw_args);

rel_args:
    Py_DECREF(args);

rel_members:
    Py_DECREF(members);

ret_err:
    return NULL;
}


/*
 * Raise an exception when failing to convert an enum because of its type.
 */
static void enum_expected(PyObject *obj, const sipTypeDef *td)
{
    PyErr_Format(PyExc_TypeError, "a member of enum '%s' is expected not '%s'",
            sipPyNameOfEnum((sipEnumTypeDef *)td), Py_TYPE(obj)->tp_name);
}


/*
 * Get the Python object for an enum type.
 */
static PyObject *get_enum_type(const sipTypeDef *td)
{
    PyObject *type_obj;

    /* Make sure the enum object has been created. */
    type_obj = (PyObject *)sipTypeAsPyTypeObject(td);

    if (type_obj == NULL)
    {
        if (sip_add_all_lazy_attrs(sip_api_type_scope(td)) < 0)
            return NULL;

        type_obj = (PyObject *)sipTypeAsPyTypeObject(td);
    }

    return type_obj;
}


/*
 * The bulk of the implementation of _missing_ that handles missing members.
 */
static PyObject *missing(PyObject *cls, PyObject *value, int int_enum)
{
    PyObject *sip_missing, *member, *value_str;

    /* Get the dict of previously missing members. */
    if ((sip_missing = PyObject_GetAttr(cls, str_sunder_sip_missing)) != NULL)
    {
        if ((member = PyDict_GetItemWithError(sip_missing, value)) != NULL)
        {
            /* Return the already missing member. */
            Py_INCREF(member);
            return member;
        }

        /* A missing key will not raise an exception. */
        if (PyErr_Occurred())
        {
            Py_DECREF(sip_missing);
            return NULL;
        }
    }
    else if (PyErr_ExceptionMatches(PyExc_AttributeError))
    {
        PyErr_Clear();

        /* Create the dict and save it in the class. */
        if ((sip_missing = PyDict_New()) == NULL)
            return NULL;

        if (PyObject_SetAttr(cls, str_sunder_sip_missing, sip_missing) < 0)
        {
            Py_DECREF(sip_missing);
            return NULL;
        }
    }
    else
    {
        /* The exception is unexpected. */
        return NULL;
    }

    /* Create a member for the missing value. */
    if (int_enum)
        member = PyObject_CallMethodObjArgs(int_type, str_dunder_new, cls,
                value, NULL);
    else
        member = PyObject_CallMethodObjArgs(object_type, str_dunder_new, cls,
                NULL);

    if (member == NULL)
    {
        Py_DECREF(sip_missing);
        return NULL;
    }

    /* Set the member's attributes. */
    if ((value_str = PyObject_Str(value)) == NULL)
    {
        Py_DECREF(member);
        Py_DECREF(sip_missing);
        return NULL;
    }

    if (PyObject_SetAttr(member, str_sunder_name, value_str) < 0)
    {
        Py_DECREF(value_str);
        Py_DECREF(member);
        Py_DECREF(sip_missing);
        return NULL;
    }

    Py_DECREF(value_str);

    if (PyObject_SetAttr(member, str_sunder_value, value) < 0)
    {
        Py_DECREF(member);
        Py_DECREF(sip_missing);
        return NULL;
    }

    /* Save the member so that it is a singleton. */
    if (PyDict_SetItem(sip_missing, value, member) < 0)
    {
        Py_DECREF(member);
        Py_DECREF(sip_missing);
        return NULL;
    }

    Py_DECREF(sip_missing);

    return member;
}


/*
 * The replacment implementation of _missing_ that handles missing members in
 * Enums.
 */
static PyObject *missing_enum(PyObject *cls, PyObject *value)
{
    return missing(cls, value, FALSE);
}


/*
 * The replacment implementation of _missing_ that handles missing members in
 * IntEnums.
 */
static PyObject *missing_int_enum(PyObject *cls, PyObject *value)
{
    return missing(cls, value, TRUE);
}
