#define NPY_NO_DEPRECATED_API NPY_API_VERSION
#define _MULTIARRAYMODULE

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include "numpy/ndarraytypes.h"
#include "numpy/npy_2_compat.h"
#include "npy_argparse.h"
#include "npy_atomic.h"
#include "npy_import.h"

#include "arrayfunction_override.h"

#if PY_VERSION_HEX < 0x30d00b3
static PyThread_type_lock argparse_mutex;
#define LOCK_ARGPARSE_MUTEX                             \
    PyThread_acquire_lock(argparse_mutex, WAIT_LOCK)
#define UNLOCK_ARGPARSE_MUTEX                            \
    PyThread_release_lock(argparse_mutex)
#else
static PyMutex argparse_mutex = {0};
#define LOCK_ARGPARSE_MUTEX PyMutex_Lock(&argparse_mutex)
#define UNLOCK_ARGPARSE_MUTEX PyMutex_Unlock(&argparse_mutex)
#endif

NPY_NO_EXPORT int
init_argparse_mutex(void) {
#if PY_VERSION_HEX < 0x30d00b3
    argparse_mutex = PyThread_allocate_lock();
    if (argparse_mutex == NULL) {
        PyErr_NoMemory();
        return -1;
    }
#endif
    return 0;
}

/**
 * Small wrapper converting to array just like CPython does.
 *
 * We could use our own PyArray_PyIntAsInt function, but it handles floats
 * differently.
 * A disadvantage of this function compared to ``PyArg_*("i")`` code is that
 * it will not say which parameter is wrong.
 *
 * @param obj The python object to convert
 * @param value The output value
 *
 * @returns 0 on failure and 1 on success (`NPY_FAIL`, `NPY_SUCCEED`)
 */
NPY_NO_EXPORT int
PyArray_PythonPyIntFromInt(PyObject *obj, int *value)
{
    /* Pythons behaviour is to check only for float explicitly... */
    if (NPY_UNLIKELY(PyFloat_Check(obj))) {
        PyErr_SetString(PyExc_TypeError,
                        "integer argument expected, got float");
        return NPY_FAIL;
    }

    long result = PyLong_AsLong(obj);
    if (NPY_UNLIKELY((result == -1) && PyErr_Occurred())) {
        return NPY_FAIL;
    }
    if (NPY_UNLIKELY((result > INT_MAX) || (result < INT_MIN))) {
        PyErr_SetString(PyExc_OverflowError,
                        "Python int too large to convert to C int");
        return NPY_FAIL;
    }
    else {
        *value = (int)result;
        return NPY_SUCCEED;
    }
}


typedef int convert(PyObject *, void *);

/**
 * Internal function to initialize keyword argument parsing.
 *
 * This does a few simple jobs:
 *
 * * Check the input for consistency to find coding errors, for example
 *   a parameter not marked with | after one marked with | (optional).
 * 2. Find the number of positional-only arguments, the number of
 *    total, required, and keyword arguments.
 * 3. Intern all keyword arguments strings to allow fast, identity based
 *    parsing and avoid string creation overhead on each call.
 *
 * @param funcname Name of the function, mainly used for errors.
 * @param cache A cache object stored statically in the parsing function
 * @param va_orig Argument list to npy_parse_arguments
 * @return 0 on success, -1 on failure
 */
static int
initialize_keywords(const char *funcname,
        _NpyArgParserCache *cache, va_list va_orig) {
    va_list va;
    int nargs = 0;
    int nkwargs = 0;
    int npositional_only = 0;
    int nrequired = 0;
    int npositional = 0;
    char state = '\0';

    va_copy(va, va_orig);
    while (1) {
        /* Count length first: */
        char *name = va_arg(va, char *);
        convert *converter = va_arg(va, convert *);
        void *data = va_arg(va, void *);

        /* Check if this is the sentinel, only converter may be NULL */
        if ((name == NULL) && (converter == NULL) && (data == NULL)) {
            break;
        }

        if (name == NULL) {
            PyErr_Format(PyExc_SystemError,
                    "NumPy internal error: name is NULL in %s() at "
                    "argument %d.", funcname, nargs);
            va_end(va);
            return -1;
        }
        if (data == NULL) {
            PyErr_Format(PyExc_SystemError,
                    "NumPy internal error: data is NULL in %s() at "
                    "argument %d.", funcname, nargs);
            va_end(va);
            return -1;
        }

        nargs += 1;
        if (*name == '|') {
            if (state == '$') {
                PyErr_Format(PyExc_SystemError,
                        "NumPy internal error: positional argument `|` "
                        "after keyword only `$` one to %s() at argument %d.",
                        funcname, nargs);
                va_end(va);
                return -1;
            }
            state = '|';
            name++;  /* advance to actual name. */
            npositional += 1;
        }
        else if (*name == '$') {
            state = '$';
            name++;  /* advance to actual name. */
        }
        else {
            if (state != '\0') {
                PyErr_Format(PyExc_SystemError,
                        "NumPy internal error: non-required argument after "
                        "required | or $ one to %s() at argument %d.",
                        funcname, nargs);
                va_end(va);
                return -1;
            }

            nrequired += 1;
            npositional += 1;
        }

        if (*name == '\0') {
            npositional_only += 1;
            /* Empty string signals positional only */
            if (state == '$' || npositional_only != npositional) {
                PyErr_Format(PyExc_SystemError,
                        "NumPy internal error: non-kwarg marked with $ "
                        "to %s() at argument %d or positional only following "
                        "kwarg.", funcname, nargs);
                va_end(va);
                return -1;
            }
        }
        else {
            nkwargs += 1;
        }
    }
    va_end(va);

    if (npositional == -1) {
        npositional = nargs;
    }

    if (nargs > _NPY_MAX_KWARGS) {
        PyErr_Format(PyExc_SystemError,
                "NumPy internal error: function %s() has %d arguments, but "
                "the maximum is currently limited to %d for easier parsing; "
                "it can be increased by modifying `_NPY_MAX_KWARGS`.",
                funcname, nargs, _NPY_MAX_KWARGS);
        return -1;
    }

    /*
     * Do any necessary string allocation and interning,
     * creating a caching object.
     */
    cache->nargs = nargs;
    cache->npositional_only = npositional_only;
    cache->npositional = npositional;
    cache->nrequired = nrequired;

    /* NULL kw_strings for easier cleanup (and NULL termination) */
    memset(cache->kw_strings, 0, sizeof(PyObject *) * (nkwargs + 1));

    va_copy(va, va_orig);
    for (int i = 0; i < nargs; i++) {
        /* Advance through non-kwargs, which do not require setup. */
        char *name = va_arg(va, char *);
        va_arg(va, convert *);
        va_arg(va, void *);

        if (*name == '|' || *name == '$') {
            name++;  /* ignore | and $ */
        }
        if (i >= npositional_only) {
            int i_kwarg = i - npositional_only;
            cache->kw_strings[i_kwarg] = PyUnicode_InternFromString(name);
            if (cache->kw_strings[i_kwarg] == NULL) {
                va_end(va);
                goto error;
            }
        }
    }

    va_end(va);
    return 0;

error:
    for (int i = 0; i < nkwargs; i++) {
        Py_XDECREF(cache->kw_strings[i]);
    }
    cache->npositional = -1;  /* not initialized */
    return -1;
}


static int
raise_incorrect_number_of_positional_args(const char *funcname,
        const _NpyArgParserCache *cache, Py_ssize_t len_args)
{
    if (cache->npositional == cache->nrequired) {
        PyErr_Format(PyExc_TypeError,
                "%s() takes %d positional arguments but %zd were given",
                funcname, cache->npositional, len_args);
    }
    else {
        PyErr_Format(PyExc_TypeError,
                "%s() takes from %d to %d positional arguments but "
                "%zd were given",
                funcname, cache->nrequired, cache->npositional, len_args);
    }
    return -1;
}

static void
raise_missing_argument(const char *funcname,
        const _NpyArgParserCache *cache, int i)
{
    if (i < cache->npositional_only) {
        PyErr_Format(PyExc_TypeError,
                "%s() missing required positional argument %d",
                funcname, i);
    }
    else {
        PyObject *kw = cache->kw_strings[i - cache->npositional_only];
        PyErr_Format(PyExc_TypeError,
                "%s() missing required argument '%S' (pos %d)",
                funcname, kw, i);
    }
}


/**
 * Generic helper for argument parsing
 *
 * See macro version for an example pattern of how to use this function.
 *
 * @param funcname Function name
 * @param cache a NULL initialized persistent storage for data
 * @param args Python passed args (METH_FASTCALL)
 * @param len_args Number of arguments (not flagged)
 * @param kwnames Tuple as passed by METH_FASTCALL or NULL.
 * @param ... List of arguments (see macro version).
 *
 * @return Returns 0 on success and -1 on failure.
 */
NPY_NO_EXPORT int
_npy_parse_arguments(const char *funcname,
         /* cache_ptr is a NULL initialized persistent storage for data */
        _NpyArgParserCache *cache,
        PyObject *const *args, Py_ssize_t len_args, PyObject *kwnames,
        /* ... is NULL, NULL, NULL terminated: name, converter, value */
        ...)
{
    if (!npy_atomic_load_uint8(&cache->initialized)) {
        LOCK_ARGPARSE_MUTEX;
        if (!npy_atomic_load_uint8(&cache->initialized)) {
            va_list va;
            va_start(va, kwnames);
            int res = initialize_keywords(funcname, cache, va);
            va_end(va);
            if (res < 0) {
                UNLOCK_ARGPARSE_MUTEX;
                return -1;
            }
            npy_atomic_store_uint8(&cache->initialized, 1);
        }
        UNLOCK_ARGPARSE_MUTEX;
    }

    if (NPY_UNLIKELY(len_args > cache->npositional)) {
        return raise_incorrect_number_of_positional_args(
                funcname, cache, len_args);
    }

    /* NOTE: Could remove the limit but too many kwargs are slow anyway. */
    PyObject *all_arguments[NPY_MAXARGS];

    for (Py_ssize_t i = 0; i < len_args; i++) {
        all_arguments[i] = args[i];
    }

    /* Without kwargs, do not iterate all converters. */
    int max_nargs = (int)len_args;
    Py_ssize_t len_kwargs = 0;

    /* If there are any kwargs, first handle them */
    if (NPY_LIKELY(kwnames != NULL)) {
        len_kwargs = PyTuple_GET_SIZE(kwnames);
        max_nargs = cache->nargs;

        for (int i = len_args; i < cache->nargs; i++) {
            all_arguments[i] = NULL;
        }

        for (Py_ssize_t i = 0; i < len_kwargs; i++) {
            PyObject *key = PyTuple_GET_ITEM(kwnames, i);
            PyObject *value = args[i + len_args];
            PyObject *const *name;

            /* Super-fast path, check identity: */
            for (name = cache->kw_strings; *name != NULL; name++) {
                if (*name == key) {
                    break;
                }
            }
            if (NPY_UNLIKELY(*name == NULL)) {
                /* Slow fallback, if identity checks failed for some reason */
                for (name = cache->kw_strings; *name != NULL; name++) {
                    int eq = PyObject_RichCompareBool(*name, key, Py_EQ);
                    if (eq == -1) {
                        return -1;
                    }
                    else if (eq) {
                        break;
                    }
                }
                if (NPY_UNLIKELY(*name == NULL)) {
                    /* Invalid keyword argument. */
                    PyErr_Format(PyExc_TypeError,
                            "%s() got an unexpected keyword argument '%S'",
                            funcname, key);
                    return -1;
                }
            }

             Py_ssize_t param_pos = (
                    (name - cache->kw_strings) + cache->npositional_only);

            /* There could be an identical positional argument */
            if (NPY_UNLIKELY(all_arguments[param_pos] != NULL)) {
                PyErr_Format(PyExc_TypeError,
                        "argument for %s() given by name ('%S') and position "
                        "(position %zd)", funcname, key, param_pos);
                return -1;
            }

            all_arguments[param_pos] = value;
        }
    }

    /*
     * There cannot be too many args, too many kwargs would find an
     * incorrect one above.
     */
    assert(len_args + len_kwargs <= cache->nargs);

    /* At this time `all_arguments` holds either NULLs or the objects */
    va_list va;
    va_start(va, kwnames);

    for (int i = 0; i < max_nargs; i++) {
        va_arg(va, char *);
        convert *converter = va_arg(va, convert *);
        void *data = va_arg(va, void *);

        if (all_arguments[i] == NULL) {
            continue;
        }

        int res;
        if (converter == NULL) {
            *((PyObject **) data) = all_arguments[i];
            continue;
        }
        res = converter(all_arguments[i], data);

        if (NPY_UNLIKELY(res == NPY_SUCCEED)) {
            continue;
        }
        else if (NPY_UNLIKELY(res == NPY_FAIL)) {
            /* It is usually the users responsibility to clean up. */
            goto converting_failed;
        }
        else if (NPY_UNLIKELY(res == Py_CLEANUP_SUPPORTED)) {
            /* TODO: Implementing cleanup if/when needed should not be hard */
            PyErr_Format(PyExc_SystemError,
                    "converter cleanup of parameter %d to %s() not supported.",
                    i, funcname);
            goto converting_failed;
        }
        assert(0);
    }

    /* Required arguments are typically not passed as keyword arguments */
    if (NPY_UNLIKELY(len_args < cache->nrequired)) {
        /* (PyArg_* also does this after the actual parsing is finished) */
        if (NPY_UNLIKELY(max_nargs < cache->nrequired)) {
            raise_missing_argument(funcname, cache, max_nargs);
            goto converting_failed;
        }
        for (int i = 0; i < cache->nrequired; i++) {
            if (NPY_UNLIKELY(all_arguments[i] == NULL)) {
                raise_missing_argument(funcname, cache, i);
                goto converting_failed;
            }
        }
    }

    va_end(va);
    return 0;

converting_failed:
    va_end(va);
    return -1;

}
