/* getargskeywordsfast implementation copied from Python 3.9 and stripped down to
 * only include the functionality we need.
 *
 * We also add support for required kwonly args and accepting *args / **kwargs.
 *
 * DOCUMENTATION OF THE EXTENSIONS:
 *  - Arguments given after a @ format specify 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/or **kwargs. In this case 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.
 */

#include <Python.h>
#include "CPy.h"

/* None of this is supported on Python 3.6 or earlier */
#if PY_VERSION_HEX >= 0x03070000

/* Forward */
static int
vgetargskeywordsfast_impl(PyObject *const *args, Py_ssize_t nargs,
                          PyObject *kwargs, PyObject *kwnames,
                          CPyArg_Parser *parser,
                          va_list *p_va);
static void skipitem_fast(const char **, va_list *);

/* Parse args for an arbitrary signature */
int
CPyArg_ParseStackAndKeywords(PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames,
                             CPyArg_Parser *parser, ...)
{
    int retval;
    va_list va;

    va_start(va, parser);
    retval = vgetargskeywordsfast_impl(args, nargs, NULL, kwnames, parser, &va);
    va_end(va);
    return retval;
}

/* Parse args for a function that takes no args */
int
CPyArg_ParseStackAndKeywordsNoArgs(PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames,
                                   CPyArg_Parser *parser, ...)
{
    int retval;
    va_list va;

    va_start(va, parser);
    if (nargs == 0 && kwnames == NULL) {
        // Fast path: no arguments
        retval = 1;
    } else {
        retval = vgetargskeywordsfast_impl(args, nargs, NULL, kwnames, parser, &va);
    }
    va_end(va);
    return retval;
}

/* Parse args for a function that takes one arg */
int
CPyArg_ParseStackAndKeywordsOneArg(PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames,
                                   CPyArg_Parser *parser, ...)
{
    int retval;
    va_list va;

    va_start(va, parser);
    if (kwnames == NULL && nargs == 1) {
        // Fast path: one positional argument
        PyObject **p;
        p = va_arg(va, PyObject **);
        *p = args[0];
        retval = 1;
    } else {
        retval = vgetargskeywordsfast_impl(args, nargs, NULL, kwnames, parser, &va);
    }
    va_end(va);
    return retval;
}

/* Parse args for a function that takes no keyword-only args, *args or **kwargs */
int
CPyArg_ParseStackAndKeywordsSimple(PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames,
                                   CPyArg_Parser *parser, ...)
{
    int retval;
    va_list va;

    va_start(va, parser);
    if (kwnames == NULL && nargs >= parser->min && nargs <= parser->max) {
        // Fast path: correct number of positional arguments only
        PyObject **p;
        Py_ssize_t i;
        for (i = 0; i < nargs; i++) {
            p = va_arg(va, PyObject **);
            *p = args[i];
        }
        retval = 1;
    } else {
        retval = vgetargskeywordsfast_impl(args, nargs, NULL, kwnames, parser, &va);
    }
    va_end(va);
    return retval;
}

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


/* List of static parsers. */
static struct CPyArg_Parser *static_arg_parsers = NULL;

static int
parser_init(CPyArg_Parser *parser)
{
    const char * const *keywords;
    const char *format, *msg;
    int i, len, min, max, nkw;
    PyObject *kwtuple;

    assert(parser->keywords != NULL);
    if (parser->kwtuple != NULL) {
        return 1;
    }

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

    parser->required_kwonly_start = INT_MAX;
    if (*parser->format == '%') {
        parser->format++;
        parser->varargs = 1;
    }

    format = parser->format;
    if (format) {
        /* grab the function name or custom error msg first (mutually exclusive) */
        parser->fname = strchr(parser->format, ':');
        if (parser->fname) {
            parser->fname++;
            parser->custom_msg = NULL;
        }
        else {
            parser->custom_msg = strchr(parser->format,';');
            if (parser->custom_msg)
                parser->custom_msg++;
        }

        min = max = INT_MAX;
        for (i = 0; i < len; i++) {
            if (*format == '|') {
                if (min != INT_MAX) {
                    PyErr_SetString(PyExc_SystemError,
                                    "Invalid format string (| specified twice)");
                    return 0;
                }
                if (max != INT_MAX) {
                    PyErr_SetString(PyExc_SystemError,
                                    "Invalid format string ($ before |)");
                    return 0;
                }
                min = i;
                format++;
            }
            if (*format == '$') {
                if (max != INT_MAX) {
                    PyErr_SetString(PyExc_SystemError,
                                    "Invalid format string ($ specified twice)");
                    return 0;
                }
                if (i < parser->pos) {
                    PyErr_SetString(PyExc_SystemError,
                                    "Empty parameter name after $");
                    return 0;
                }
                max = i;
                format++;
            }
            if (*format == '@') {
                if (parser->required_kwonly_start != INT_MAX) {
                    PyErr_SetString(PyExc_SystemError,
                                    "Invalid format string (@ specified twice)");
                    return 0;
                }
                if (min == INT_MAX && max == INT_MAX) {
                    PyErr_SetString(PyExc_SystemError,
                                    "Invalid format string "
                                    "(@ without preceding | and $)");
                    return 0;
                }
                format++;
                parser->has_required_kws = 1;
                parser->required_kwonly_start = i;
            }
            if (IS_END_OF_FORMAT(*format)) {
                PyErr_Format(PyExc_SystemError,
                            "More keyword list entries (%d) than "
                            "format specifiers (%d)", len, i);
                return 0;
            }

            skipitem_fast(&format, NULL);
        }
        parser->min = Py_MIN(min, len);
        parser->max = Py_MIN(max, len);

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

    nkw = len - parser->pos;
    kwtuple = PyTuple_New(nkw);
    if (kwtuple == NULL) {
        return 0;
    }
    keywords = parser->keywords + parser->pos;
    for (i = 0; i < nkw; i++) {
        PyObject *str = PyUnicode_FromString(keywords[i]);
        if (str == NULL) {
            Py_DECREF(kwtuple);
            return 0;
        }
        PyUnicode_InternInPlace(&str);
        PyTuple_SET_ITEM(kwtuple, i, str);
    }
    parser->kwtuple = kwtuple;

    assert(parser->next == NULL);
    parser->next = static_arg_parsers;
    static_arg_parsers = parser;
    return 1;
}

static PyObject*
find_keyword(PyObject *kwnames, PyObject *const *kwstack, PyObject *key)
{
    Py_ssize_t i, nkwargs;

    nkwargs = PyTuple_GET_SIZE(kwnames);
    for (i = 0; i < nkwargs; i++) {
        PyObject *kwname = PyTuple_GET_ITEM(kwnames, i);

        /* kwname == key will normally find a match in since keyword keys
           should be interned strings; if not retry below in a new loop. */
        if (kwname == key) {
            return kwstack[i];
        }
    }

    for (i = 0; i < nkwargs; i++) {
        PyObject *kwname = PyTuple_GET_ITEM(kwnames, i);
        assert(PyUnicode_Check(kwname));
        if (_PyUnicode_EQ(kwname, key)) {
            return kwstack[i];
        }
    }
    return NULL;
}

static int
vgetargskeywordsfast_impl(PyObject *const *args, Py_ssize_t nargs,
                          PyObject *kwargs, PyObject *kwnames,
                          CPyArg_Parser *parser,
                          va_list *p_va)
{
    PyObject *kwtuple;
    const char *format;
    PyObject *keyword;
    int i, pos, len;
    Py_ssize_t nkwargs;
    PyObject *current_arg;
    PyObject *const *kwstack = NULL;
    int bound_pos_args;
    PyObject **p_args = NULL, **p_kwargs = NULL;

    assert(kwargs == NULL || PyDict_Check(kwargs));
    assert(kwargs == NULL || kwnames == NULL);
    assert(p_va != NULL);

    if (!parser_init(parser)) {
        return 0;
    }

    kwtuple = parser->kwtuple;
    pos = parser->pos;
    len = pos + (int)PyTuple_GET_SIZE(kwtuple);

    if (parser->varargs) {
        p_args = va_arg(*p_va, PyObject **);
        p_kwargs = va_arg(*p_va, PyObject **);
    }

    if (kwargs != NULL) {
        nkwargs = PyDict_GET_SIZE(kwargs);
    }
    else if (kwnames != NULL) {
        nkwargs = PyTuple_GET_SIZE(kwnames);
        kwstack = args + nargs;
    }
    else {
        nkwargs = 0;
    }
    if (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)",
                     (parser->fname == NULL) ? "function" : parser->fname,
                     (parser->fname == NULL) ? "" : "()",
                     len,
                     (nargs == 0) ? "keyword " : "",
                     (len == 1) ? "" : "s",
                     nargs + nkwargs);
        return 0;
    }
    if (parser->max < nargs && !p_args) {
        if (parser->max == 0) {
            PyErr_Format(PyExc_TypeError,
                         "%.200s%s takes no positional arguments",
                         (parser->fname == NULL) ? "function" : parser->fname,
                         (parser->fname == NULL) ? "" : "()");
        }
        else {
            PyErr_Format(PyExc_TypeError,
                         "%.200s%s takes %s %d positional argument%s (%zd given)",
                         (parser->fname == NULL) ? "function" : parser->fname,
                         (parser->fname == NULL) ? "" : "()",
                         (parser->min < parser->max) ? "at most" : "exactly",
                         parser->max,
                         parser->max == 1 ? "" : "s",
                         nargs);
        }
        return 0;
    }

    format = parser->format;

    /* convert tuple args and keyword args in same loop, using kwtuple to drive process */
    for (i = 0; i < len; i++) {
        if (*format == '|') {
            format++;
        }
        if (*format == '$') {
            format++;
        }
        if (*format == '@') {
            format++;
        }
        assert(!IS_END_OF_FORMAT(*format));

        if (i < nargs && i < parser->max) {
            current_arg = args[i];
        }
        else if (nkwargs && i >= pos) {
            keyword = PyTuple_GET_ITEM(kwtuple, i - pos);
            if (kwargs != NULL) {
                current_arg = PyDict_GetItemWithError(kwargs, keyword);
                if (!current_arg && PyErr_Occurred()) {
                    return 0;
                }
            }
            else {
                current_arg = find_keyword(kwnames, kwstack, keyword);
            }
            if (current_arg) {
                --nkwargs;
            }
        }
        else {
            current_arg = NULL;
        }

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

        if (i < parser->min || i >= parser->required_kwonly_start) {
            /* Less arguments than required */
            if (i < pos) {
                Py_ssize_t min = Py_MIN(pos, parser->min);
                PyErr_Format(PyExc_TypeError,
                             "%.200s%s takes %s %d positional argument%s"
                             " (%zd given)",
                             (parser->fname == NULL) ? "function" : parser->fname,
                             (parser->fname == NULL) ? "" : "()",
                             min < parser->max ? "at least" : "exactly",
                             min,
                             min == 1 ? "" : "s",
                             nargs);
            }
            else {
                keyword = PyTuple_GET_ITEM(kwtuple, i - pos);
                if (i >= parser->max) {
                    PyErr_Format(PyExc_TypeError,  "%.200s%s missing required "
                                 "keyword-only argument '%U'",
                                 (parser->fname == NULL) ? "function" : parser->fname,
                                 (parser->fname == NULL) ? "" : "()",
                                 keyword);
                }
                else {
                    PyErr_Format(PyExc_TypeError,  "%.200s%s missing required "
                                 "argument '%U' (pos %d)",
                                 (parser->fname == NULL) ? "function" : parser->fname,
                                 (parser->fname == NULL) ? "" : "()",
                                 keyword, 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 && !parser->has_required_kws && !p_args && !p_kwargs) {
            return 1;
        }

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

    assert(IS_END_OF_FORMAT(*format) || (*format == '|') || (*format == '$'));

    bound_pos_args = Py_MIN(nargs, Py_MIN(parser->max, len));
    if (p_args) {
        *p_args = PyTuple_New(nargs - bound_pos_args);
        if (!*p_args) {
            return 0;
        }
        for (i = bound_pos_args; i < nargs; i++) {
            PyObject *arg = args[i];
            Py_INCREF(arg);
            PyTuple_SET_ITEM(*p_args, i - bound_pos_args, arg);
        }
    }

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

            return 0;
        }

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

    if (nkwargs > 0) {
        Py_ssize_t j;
        PyObject *value;
        /* make sure there are no arguments given by name and position */
        for (i = pos; i < bound_pos_args; i++) {
            keyword = PyTuple_GET_ITEM(kwtuple, i - pos);
            if (kwargs != NULL) {
                current_arg = PyDict_GetItemWithError(kwargs, keyword);
                if (!current_arg && PyErr_Occurred()) {
                    goto latefail;
                }
            }
            else {
                current_arg = find_keyword(kwnames, kwstack, keyword);
            }
            if (current_arg) {
                /* arg present in tuple and in dict */
                PyErr_Format(PyExc_TypeError,
                             "argument for %.200s%s given by name ('%U') "
                             "and position (%d)",
                             (parser->fname == NULL) ? "function" : parser->fname,
                             (parser->fname == NULL) ? "" : "()",
                             keyword, i+1);
                goto latefail;
            }
        }
        /* make sure there are no extraneous keyword arguments */
        j = 0;
        while (1) {
            int match;
            if (kwargs != NULL) {
                if (!PyDict_Next(kwargs, &j, &keyword, &value))
                    break;
            }
            else {
                if (j >= PyTuple_GET_SIZE(kwnames))
                    break;
                keyword = PyTuple_GET_ITEM(kwnames, j);
                value = kwstack[j];
                j++;
            }

            match = PySequence_Contains(kwtuple, keyword);
            if (match <= 0) {
                if (!match) {
                    if (!p_kwargs) {
                        PyErr_Format(PyExc_TypeError,
                                     "'%S' is an invalid keyword "
                                     "argument for %.200s%s",
                                     keyword,
                                     (parser->fname == NULL) ? "this function" : parser->fname,
                                     (parser->fname == NULL) ? "" : "()");
                        goto latefail;
                    } else {
                        if (PyDict_SetItem(*p_kwargs, keyword, value) < 0) {
                            goto latefail;
                        }
                    }
                } else {
                    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_fast(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;
}

#endif
