/* getargs implementation copied from Python 3.8 and stripped down to only include
 * the functions we need.
 * We also add support for required kwonly args and accepting *args / **kwargs.
 * A good idea would be to also vendor in the Fast versions and get our stuff
 * working with *that*.
 * Another probably good idea is to strip out all the formatting stuff we don't need
 * and then add in custom stuff that we do need.
 *
 * DOCUMENTATION OF THE EXTENSIONS:
 *  - Arguments given after a @ format specify are required keyword-only arguments.
 *    The | and $ specifiers must both appear before @.
 *  - If the first character of a format string is %, then the function can support
 *    *args and **kwargs. On seeing a %, the parser will consume two arguments,
 *    which should be pointers to variables to store the *args and **kwargs, respectively.
 *    Either pointer can be NULL, in which case the function doesn't take that
 *    variety of vararg.
 *    Unlike most format specifiers, the caller takes ownership of these objects
 *    and is responsible for decrefing them.
 *  - All arguments must use the 'O' format.
 *  - There's minimal error checking of format strings. They are generated
 *    programmatically and can be assumed valid.
 */

// These macro definitions are copied from pyport.h in Python 3.9 and later
// https://bugs.python.org/issue19569
#if defined(__clang__)
#define _Py_COMP_DIAG_PUSH _Pragma("clang diagnostic push")
#define _Py_COMP_DIAG_IGNORE_DEPR_DECLS \
    _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")
#define _Py_COMP_DIAG_POP _Pragma("clang diagnostic pop")
#elif defined(__GNUC__) \
    && ((__GNUC__ >= 5) || (__GNUC__ == 4) && (__GNUC_MINOR__ >= 6))
#define _Py_COMP_DIAG_PUSH _Pragma("GCC diagnostic push")
#define _Py_COMP_DIAG_IGNORE_DEPR_DECLS \
    _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
#define _Py_COMP_DIAG_POP _Pragma("GCC diagnostic pop")
#elif defined(_MSC_VER)
#define _Py_COMP_DIAG_PUSH __pragma(warning(push))
#define _Py_COMP_DIAG_IGNORE_DEPR_DECLS __pragma(warning(disable: 4996))
#define _Py_COMP_DIAG_POP __pragma(warning(pop))
#else
#define _Py_COMP_DIAG_PUSH
#define _Py_COMP_DIAG_IGNORE_DEPR_DECLS
#define _Py_COMP_DIAG_POP
#endif

#include "Python.h"
#include "pythonsupport.h"

#include <ctype.h>
#include <float.h>

#ifndef PyDict_GET_SIZE
#define PyDict_GET_SIZE(d) PyDict_Size(d)
#endif


#ifdef __cplusplus
extern "C" {
#endif
int CPyArg_ParseTupleAndKeywords(PyObject *, PyObject *,
                                 const char *, const char *, const char * const *, ...);

/* Forward */
static int vgetargskeywords(PyObject *, PyObject *,
                            const char *, const char *, const char * const *, va_list *);
static void skipitem(const char **, va_list *);

/* Support for keyword arguments donated by
   Geoff Philbrick <philbric@delphi.hks.com> */

/* Return false (0) for error, else true. */
int
CPyArg_ParseTupleAndKeywords(PyObject *args,
                             PyObject *keywords,
                             const char *format,
                             const char *fname,
                             const char * const *kwlist, ...)
{
    int retval;
    va_list va;

    va_start(va, kwlist);
    retval = vgetargskeywords(args, keywords, format, fname, kwlist, &va);
    va_end(va);
    return retval;
}

#define IS_END_OF_FORMAT(c) (c == '\0' || c == ';' || c == ':')

static int
vgetargskeywords(PyObject *args, PyObject *kwargs, const char *format,
                 const char *fname, const char * const *kwlist, va_list *p_va)
{
    int min = INT_MAX;
    int max = INT_MAX;
    int required_kwonly_start = INT_MAX;
    int has_required_kws = 0;
    int i, pos, len;
    int skip = 0;
    Py_ssize_t nargs, nkwargs;
    PyObject *current_arg;
    int bound_pos_args;

    PyObject **p_args = NULL, **p_kwargs = NULL;

    assert(args != NULL && PyTuple_Check(args));
    assert(kwargs == NULL || PyDict_Check(kwargs));
    assert(format != NULL);
    assert(kwlist != NULL);
    assert(p_va != NULL);

    /* scan kwlist and count the number of positional-only parameters */
    for (pos = 0; kwlist[pos] && !*kwlist[pos]; pos++) {
    }
    /* scan kwlist and get greatest possible nbr of args */
    for (len = pos; kwlist[len]; len++) {
#ifdef DEBUG
        if (!*kwlist[len]) {
            PyErr_SetString(PyExc_SystemError,
                            "Empty keyword parameter name");
            return 0;
        }
#endif
    }

    if (*format == '%') {
        p_args = va_arg(*p_va, PyObject **);
        p_kwargs = va_arg(*p_va, PyObject **);
        format++;
    }

    nargs = PyTuple_GET_SIZE(args);
    nkwargs = (kwargs == NULL) ? 0 : PyDict_GET_SIZE(kwargs);
    if (unlikely(nargs + nkwargs > len && !p_args && !p_kwargs)) {
        /* Adding "keyword" (when nargs == 0) prevents producing wrong error
           messages in some special cases (see bpo-31229). */
        PyErr_Format(PyExc_TypeError,
                     "%.200s%s takes at most %d %sargument%s (%zd given)",
                     (fname == NULL) ? "function" : fname,
                     (fname == NULL) ? "" : "()",
                     len,
                     (nargs == 0) ? "keyword " : "",
                     (len == 1) ? "" : "s",
                     nargs + nkwargs);
        return 0;
    }

    /* convert tuple args and keyword args in same loop, using kwlist to drive process */
    for (i = 0; i < len; i++) {
        if (*format == '|') {
#ifdef DEBUG
            if (min != INT_MAX) {
                PyErr_SetString(PyExc_SystemError,
                                "Invalid format string (| specified twice)");
                return 0;
            }
#endif

            min = i;
            format++;

#ifdef DEBUG
            if (max != INT_MAX) {
                PyErr_SetString(PyExc_SystemError,
                                "Invalid format string ($ before |)");
                return 0;
            }
#endif

            /* If there are optional args, figure out whether we have
             * required keyword arguments so that we don't bail without
             * enforcing them. */
            has_required_kws = strchr(format, '@') != NULL;
        }
        if (*format == '$') {
#ifdef DEBUG
            if (max != INT_MAX) {
                PyErr_SetString(PyExc_SystemError,
                                "Invalid format string ($ specified twice)");
                return 0;
            }
#endif

            max = i;
            format++;

#ifdef DEBUG
            if (max < pos) {
                PyErr_SetString(PyExc_SystemError,
                                "Empty parameter name after $");
                return 0;
            }
#endif
            if (skip) {
                /* Now we know the minimal and the maximal numbers of
                 * positional arguments and can raise an exception with
                 * informative message (see below). */
                break;
            }
            if (unlikely(max < nargs && !p_args)) {
                if (max == 0) {
                    PyErr_Format(PyExc_TypeError,
                                 "%.200s%s takes no positional arguments",
                                 (fname == NULL) ? "function" : fname,
                                 (fname == NULL) ? "" : "()");
                }
                else {
                    PyErr_Format(PyExc_TypeError,
                                 "%.200s%s takes %s %d positional argument%s"
                                 " (%zd given)",
                                 (fname == NULL) ? "function" : fname,
                                 (fname == NULL) ? "" : "()",
                                 (min < max) ? "at most" : "exactly",
                                 max,
                                 max == 1 ? "" : "s",
                                 nargs);
                }
                return 0;
            }
        }
        if (*format == '@') {
#ifdef DEBUG
            if (min == INT_MAX && max == INT_MAX) {
                PyErr_SetString(PyExc_SystemError,
                                "Invalid format string "
                                "(@ without preceding | and $)");
                return 0;
            }
            if (required_kwonly_start != INT_MAX) {
                PyErr_SetString(PyExc_SystemError,
                                "Invalid format string (@ specified twice)");
                return 0;
            }
#endif

            required_kwonly_start = i;
            format++;
        }
#ifdef DEBUG
        if (IS_END_OF_FORMAT(*format)) {
            PyErr_Format(PyExc_SystemError,
                         "More keyword list entries (%d) than "
                         "format specifiers (%d)", len, i);
            return 0;
        }
#endif
        if (!skip) {
            if (i < nargs && i < max) {
                current_arg = Py_NewRef(PyTuple_GET_ITEM(args, i));
            }
            else if (nkwargs && i >= pos) {
                if (unlikely(PyDict_GetItemStringRef(kwargs, kwlist[i], &current_arg) < 0)) {
                    return 0;
                }
                if (current_arg) {
                    --nkwargs;
                }
            }
            else {
                current_arg = NULL;
            }

            if (current_arg) {
                PyObject **p = va_arg(*p_va, PyObject **);
                *p = current_arg;
                Py_DECREF(current_arg);
                format++;
                continue;
            }

            if (i < min || i >= required_kwonly_start) {
                if (likely(i < pos)) {
                    assert (min == INT_MAX);
                    assert (max == INT_MAX);
                    skip = 1;
                    /* At that moment we still don't know the minimal and
                     * the maximal numbers of positional arguments.  Raising
                     * an exception is deferred until we encounter | and $
                     * or the end of the format. */
                }
                else {
                    if (i >= max) {
                        PyErr_Format(PyExc_TypeError,
                                     "%.200s%s missing required "
                                     "keyword-only argument '%s'",
                                     (fname == NULL) ? "function" : fname,
                                     (fname == NULL) ? "" : "()",
                                     kwlist[i]);
                    }
                    else {
                        PyErr_Format(PyExc_TypeError,
                                     "%.200s%s missing required "
                                     "argument '%s' (pos %d)",
                                     (fname == NULL) ? "function" : fname,
                                     (fname == NULL) ? "" : "()",
                                     kwlist[i], i+1);
                    }
                    return 0;
                }
            }
            /* current code reports success when all required args
             * fulfilled and no keyword args left, with no further
             * validation. XXX Maybe skip this in debug build ?
             */
            if (!nkwargs && !skip && !has_required_kws &&
                !p_args && !p_kwargs)
            {
                return 1;
            }
        }

        /* We are into optional args, skip through to any remaining
         * keyword args */
        skipitem(&format, p_va);
    }

    if (unlikely(skip)) {
        PyErr_Format(PyExc_TypeError,
                     "%.200s%s takes %s %d positional argument%s"
                     " (%zd given)",
                     (fname == NULL) ? "function" : fname,
                     (fname == NULL) ? "" : "()",
                     (Py_MIN(pos, min) < i) ? "at least" : "exactly",
                     Py_MIN(pos, min),
                     Py_MIN(pos, min) == 1 ? "" : "s",
                     nargs);
        return 0;
    }

#ifdef DEBUG
    if (!IS_END_OF_FORMAT(*format) &&
        (*format != '|') && (*format != '$') && (*format != '@'))
    {
        PyErr_Format(PyExc_SystemError,
            "more argument specifiers than keyword list entries "
            "(remaining format:'%s')", format);
        return 0;
    }
#endif

    bound_pos_args = Py_MIN(nargs, Py_MIN(max, len));
    if (p_args) {
        *p_args = PyTuple_GetSlice(args, bound_pos_args, nargs);
        if (!*p_args) {
            return 0;
        }
    }

    if (p_kwargs) {
        /* This unfortunately needs to be special cased because if len is 0 then we
         * never go through the main loop. */
        if (unlikely(nargs > 0 && len == 0 && !p_args)) {
            PyErr_Format(PyExc_TypeError,
                         "%.200s%s takes no positional arguments",
                         (fname == NULL) ? "function" : fname,
                         (fname == NULL) ? "" : "()");

            return 0;
        }

        *p_kwargs = PyDict_New();
        if (!*p_kwargs) {
            goto latefail;
        }
    }

    if (nkwargs > 0) {
        PyObject *key, *value;
        Py_ssize_t j;
        /* make sure there are no arguments given by name and position */
        for (i = pos; i < bound_pos_args && i < len; i++) {
            PyObject *current_arg;
            if (unlikely(PyDict_GetItemStringRef(kwargs, kwlist[i], &current_arg) < 0)) {
                goto latefail;
            }
            if (unlikely(current_arg != NULL)) {
                Py_DECREF(current_arg);
                /* arg present in tuple and in dict */
                PyErr_Format(PyExc_TypeError,
                             "argument for %.200s%s given by name ('%s') "
                             "and position (%d)",
                             (fname == NULL) ? "function" : fname,
                             (fname == NULL) ? "" : "()",
                             kwlist[i], i+1);
                goto latefail;
            }
        }
        /* make sure there are no extraneous keyword arguments */
        j = 0;
        while (PyDict_Next(kwargs, &j, &key, &value)) {
            int match = 0;
            if (unlikely(!PyUnicode_Check(key))) {
                PyErr_SetString(PyExc_TypeError,
                                "keywords must be strings");
                goto latefail;
            }
            for (i = pos; i < len; i++) {
                if (PyUnicode_EqualToUTF8(key, kwlist[i])) {
                    match = 1;
                    break;
                }
            }
            if (!match) {
                if (unlikely(!p_kwargs)) {
                    PyErr_Format(PyExc_TypeError,
                                 "'%U' is an invalid keyword "
                                 "argument for %.200s%s",
                                 key,
                                 (fname == NULL) ? "this function" : fname,
                                 (fname == NULL) ? "" : "()");
                    goto latefail;
                } else {
                    if (PyDict_SetItem(*p_kwargs, key, value) < 0) {
                        goto latefail;
                    }
                }
            }
        }
    }

    return 1;
    /* Handle failures that have happened after we have tried to
     * create *args and **kwargs, if they exist. */
latefail:
    if (p_args) {
        Py_XDECREF(*p_args);
    }
    if (p_kwargs) {
        Py_XDECREF(*p_kwargs);
    }
    return 0;
}


static void
skipitem(const char **p_format, va_list *p_va)
{
    const char *format = *p_format;
    char c = *format++;

    if (p_va != NULL) {
        (void) va_arg(*p_va, PyObject **);
    }

    *p_format = format;
}

#ifdef __cplusplus
};
#endif
