from __future__ import annotations
from typing import TYPE_CHECKING, Final

import libclinic
from libclinic import fail, warn
from libclinic.function import (
    Function, Parameter,
    GETTER, SETTER, METHOD_NEW)
from libclinic.converter import CConverter
from libclinic.converters import (
    defining_class_converter, object_converter, self_converter)
if TYPE_CHECKING:
    from libclinic.clanguage import CLanguage
    from libclinic.codegen import CodeGen


def declare_parser(
    f: Function,
    *,
    hasformat: bool = False,
    codegen: CodeGen,
) -> str:
    """
    Generates the code template for a static local PyArg_Parser variable,
    with an initializer.  For core code (incl. builtin modules) the
    kwtuple field is also statically initialized.  Otherwise
    it is initialized at runtime.
    """
    limited_capi = codegen.limited_capi
    if hasformat:
        fname = ''
        format_ = '.format = "{format_units}:{name}",'
    else:
        fname = '.fname = "{name}",'
        format_ = ''

    num_keywords = len([
        p for p in f.parameters.values()
        if p.is_positional_or_keyword() or p.is_keyword_only()
    ])

    condition = '#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)'
    if limited_capi:
        declarations = """
            #define KWTUPLE NULL
        """
    elif num_keywords == 0:
        declarations = """
            #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
            #  define KWTUPLE (PyObject *)&_Py_SINGLETON(tuple_empty)
            #else
            #  define KWTUPLE NULL
            #endif
        """

        codegen.add_include('pycore_runtime.h', '_Py_SINGLETON()',
                            condition=condition)
    else:
        # XXX Why do we not statically allocate the tuple
        # for non-builtin modules?
        declarations = """
            #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)

            #define NUM_KEYWORDS %d
            static struct {{
                PyGC_Head _this_is_not_used;
                PyObject_VAR_HEAD
                Py_hash_t ob_hash;
                PyObject *ob_item[NUM_KEYWORDS];
            }} _kwtuple = {{
                .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
                .ob_hash = -1,
                .ob_item = {{ {keywords_py} }},
            }};
            #undef NUM_KEYWORDS
            #define KWTUPLE (&_kwtuple.ob_base.ob_base)

            #else  // !Py_BUILD_CORE
            #  define KWTUPLE NULL
            #endif  // !Py_BUILD_CORE
        """ % num_keywords

        codegen.add_include('pycore_gc.h', 'PyGC_Head',
                            condition=condition)
        codegen.add_include('pycore_runtime.h', '_Py_ID()',
                            condition=condition)

    declarations += """
            static const char * const _keywords[] = {{{keywords_c} NULL}};
            static _PyArg_Parser _parser = {{
                .keywords = _keywords,
                %s
                .kwtuple = KWTUPLE,
            }};
            #undef KWTUPLE
    """ % (format_ or fname)
    return libclinic.normalize_snippet(declarations)


NO_VARARG: Final[str] = "PY_SSIZE_T_MAX"
PARSER_PROTOTYPE_KEYWORD: Final[str] = libclinic.normalize_snippet("""
    static PyObject *
    {c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs)
""")
PARSER_PROTOTYPE_KEYWORD___INIT__: Final[str] = libclinic.normalize_snippet("""
    static int
    {c_basename}({self_type}{self_name}, PyObject *args, PyObject *kwargs)
""")
PARSER_PROTOTYPE_VARARGS: Final[str] = libclinic.normalize_snippet("""
    static PyObject *
    {c_basename}({self_type}{self_name}, PyObject *args)
""")
PARSER_PROTOTYPE_FASTCALL: Final[str] = libclinic.normalize_snippet("""
    static PyObject *
    {c_basename}({self_type}{self_name}, PyObject *const *args, Py_ssize_t nargs)
""")
PARSER_PROTOTYPE_FASTCALL_KEYWORDS: Final[str] = libclinic.normalize_snippet("""
    static PyObject *
    {c_basename}({self_type}{self_name}, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
""")
PARSER_PROTOTYPE_DEF_CLASS: Final[str] = libclinic.normalize_snippet("""
    static PyObject *
    {c_basename}({self_type}{self_name}, PyTypeObject *{defining_class_name}, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
""")
PARSER_PROTOTYPE_NOARGS: Final[str] = libclinic.normalize_snippet("""
    static PyObject *
    {c_basename}({self_type}{self_name}, PyObject *Py_UNUSED(ignored))
""")
PARSER_PROTOTYPE_GETTER: Final[str] = libclinic.normalize_snippet("""
    static PyObject *
    {c_basename}({self_type}{self_name}, void *Py_UNUSED(context))
""")
PARSER_PROTOTYPE_SETTER: Final[str] = libclinic.normalize_snippet("""
    static int
    {c_basename}({self_type}{self_name}, PyObject *value, void *Py_UNUSED(context))
""")
METH_O_PROTOTYPE: Final[str] = libclinic.normalize_snippet("""
    static PyObject *
    {c_basename}({self_type}{self_name}, {parser_parameters})
""")
DOCSTRING_PROTOTYPE_VAR: Final[str] = libclinic.normalize_snippet("""
    PyDoc_VAR({c_basename}__doc__);
""")
DOCSTRING_PROTOTYPE_STRVAR: Final[str] = libclinic.normalize_snippet("""
    PyDoc_STRVAR({c_basename}__doc__,
    {docstring});
""")
GETSET_DOCSTRING_PROTOTYPE_STRVAR: Final[str] = libclinic.normalize_snippet("""
    PyDoc_STRVAR({getset_basename}__doc__,
    {docstring});
    #if defined({getset_basename}_DOCSTR)
    #   undef {getset_basename}_DOCSTR
    #endif
    #define {getset_basename}_DOCSTR {getset_basename}__doc__
""")
IMPL_DEFINITION_PROTOTYPE: Final[str] = libclinic.normalize_snippet("""
    static {impl_return_type}
    {c_basename}_impl({impl_parameters})
""")
METHODDEF_PROTOTYPE_DEFINE: Final[str] = libclinic.normalize_snippet(r"""
    #define {methoddef_name}    \
        {{"{name}", {methoddef_cast}{c_basename}{methoddef_cast_end}, {methoddef_flags}, {c_basename}__doc__}},
""")
GETTERDEF_PROTOTYPE_DEFINE: Final[str] = libclinic.normalize_snippet(r"""
    #if !defined({getset_basename}_DOCSTR)
    #  define {getset_basename}_DOCSTR NULL
    #endif
    #if defined({getset_name}_GETSETDEF)
    #  undef {getset_name}_GETSETDEF
    #  define {getset_name}_GETSETDEF {{"{name}", (getter){getset_basename}_get, (setter){getset_basename}_set, {getset_basename}_DOCSTR}},
    #else
    #  define {getset_name}_GETSETDEF {{"{name}", (getter){getset_basename}_get, NULL, {getset_basename}_DOCSTR}},
    #endif
""")
SETTERDEF_PROTOTYPE_DEFINE: Final[str] = libclinic.normalize_snippet(r"""
    #if !defined({getset_basename}_DOCSTR)
    #  define {getset_basename}_DOCSTR NULL
    #endif
    #if defined({getset_name}_GETSETDEF)
    #  undef {getset_name}_GETSETDEF
    #  define {getset_name}_GETSETDEF {{"{name}", (getter){getset_basename}_get, (setter){getset_basename}_set, {getset_basename}_DOCSTR}},
    #else
    #  define {getset_name}_GETSETDEF {{"{name}", NULL, (setter){getset_basename}_set, NULL}},
    #endif
""")
METHODDEF_PROTOTYPE_IFNDEF: Final[str] = libclinic.normalize_snippet("""
    #ifndef {methoddef_name}
        #define {methoddef_name}
    #endif /* !defined({methoddef_name}) */
""")


class ParseArgsCodeGen:
    func: Function
    codegen: CodeGen
    limited_capi: bool = False

    # Function parameters
    parameters: list[Parameter]
    self_parameter_converter: self_converter
    converters: list[CConverter]

    # Is 'defining_class' used for the first parameter?
    requires_defining_class: bool

    # Use METH_FASTCALL calling convention?
    fastcall: bool

    # Declaration of the return variable (ex: "int return_value;")
    return_value_declaration: str

    # Calling convention (ex: "METH_NOARGS")
    flags: str

    # Variables declarations
    declarations: str

    pos_only: int = 0
    min_pos: int = 0
    max_pos: int = 0
    min_kw_only: int = 0
    varpos: Parameter | None = None
    var_keyword: Parameter | None = None

    docstring_prototype: str
    docstring_definition: str
    impl_prototype: str | None
    impl_definition: str
    methoddef_define: str
    parser_prototype: str
    parser_definition: str
    cpp_if: str
    cpp_endif: str
    methoddef_ifndef: str

    parser_body_fields: tuple[str, ...]

    def __init__(self, func: Function, codegen: CodeGen) -> None:
        self.func = func
        self.codegen = codegen

        self.parameters = list(self.func.parameters.values())
        self_parameter = self.parameters.pop(0)
        if not isinstance(self_parameter.converter, self_converter):
            raise ValueError("the first parameter must use self_converter")
        self.self_parameter_converter = self_parameter.converter

        self.requires_defining_class = False
        if self.parameters and isinstance(self.parameters[0].converter, defining_class_converter):
            self.requires_defining_class = True
            del self.parameters[0]

        for i, p in enumerate(self.parameters):
            if p.is_vararg():
                self.varpos = p
                del self.parameters[i]
                break

        for i, p in enumerate(self.parameters):
            if p.is_var_keyword():
                self.var_keyword = p
                del self.parameters[i]
                break

        self.converters = [p.converter for p in self.parameters]

        if self.func.critical_section:
            self.codegen.add_include('pycore_critical_section.h',
                                     'Py_BEGIN_CRITICAL_SECTION()')

        # Use fastcall if not disabled, except if in a __new__ or
        # __init__ method, or if there is a **kwargs parameter.
        if self.func.disable_fastcall:
            self.fastcall = False
        elif self.var_keyword is not None:
            self.fastcall = False
        else:
            self.fastcall = not self.is_new_or_init()

        self.pos_only = 0
        self.min_pos = 0
        self.max_pos = 0
        self.min_kw_only = 0
        for i, p in enumerate(self.parameters, 1):
            if p.is_keyword_only():
                assert not p.is_positional_only()
                if not p.is_optional():
                    self.min_kw_only = i - self.max_pos
            else:
                self.max_pos = i
                if p.is_positional_only():
                    self.pos_only = i
                if not p.is_optional():
                    self.min_pos = i

    def is_new_or_init(self) -> bool:
        return self.func.kind.new_or_init

    def has_option_groups(self) -> bool:
        return (bool(self.parameters
                and (self.parameters[0].group or self.parameters[-1].group)))

    def use_meth_o(self) -> bool:
        return (len(self.parameters) == 1
                and self.parameters[0].is_positional_only()
                and not self.has_option_groups()
                and not self.converters[0].is_optional()
                and not self.varpos
                and not self.requires_defining_class
                and not self.is_new_or_init())

    def use_simple_return(self) -> bool:
        return (self.func.return_converter.type == 'PyObject *'
                and not self.func.critical_section)

    def use_pyobject_self(self) -> bool:
        return self.self_parameter_converter.use_pyobject_self(self.func)

    def select_prototypes(self) -> None:
        self.docstring_prototype = ''
        self.docstring_definition = ''
        self.methoddef_define = METHODDEF_PROTOTYPE_DEFINE
        self.return_value_declaration = "PyObject *return_value = NULL;"

        if self.is_new_or_init() and not self.func.docstring:
            pass
        elif self.func.kind is GETTER:
            self.methoddef_define = GETTERDEF_PROTOTYPE_DEFINE
            if self.func.docstring:
                self.docstring_definition = GETSET_DOCSTRING_PROTOTYPE_STRVAR
        elif self.func.kind is SETTER:
            if self.func.docstring:
                fail("docstrings are only supported for @getter, not @setter")
            self.return_value_declaration = "int {return_value};"
            self.methoddef_define = SETTERDEF_PROTOTYPE_DEFINE
        else:
            self.docstring_prototype = DOCSTRING_PROTOTYPE_VAR
            self.docstring_definition = DOCSTRING_PROTOTYPE_STRVAR

    def init_limited_capi(self) -> None:
        self.limited_capi = self.codegen.limited_capi
        if self.limited_capi and (
                (self.varpos and self.pos_only < len(self.parameters)) or
                (any(p.is_optional() for p in self.parameters) and
                 any(p.is_keyword_only() and not p.is_optional() for p in self.parameters)) or
                any(c.broken_limited_capi for c in self.converters)):
            warn(f"Function {self.func.full_name} cannot use limited C API")
            self.limited_capi = False

    def parser_body(
        self,
        *fields: str,
        declarations: str = ''
    ) -> None:
        lines = [self.parser_prototype]
        self.parser_body_fields = fields

        preamble = libclinic.normalize_snippet("""
            {{
                {return_value_declaration}
                {parser_declarations}
                {declarations}
                {initializers}
        """) + "\n"
        finale = libclinic.normalize_snippet("""
                {modifications}
                {lock}
                {return_value} = {c_basename}_impl({impl_arguments});
                {unlock}
                {return_conversion}
                {post_parsing}

            {exit_label}
                {cleanup}
                return return_value;
            }}
        """)
        for field in preamble, *fields, finale:
            lines.append(field)
        code = libclinic.linear_format("\n".join(lines),
                                       parser_declarations=self.declarations)
        self.parser_definition = code

    def parse_no_args(self) -> None:
        parser_code: list[str] | None
        simple_return = self.use_simple_return()
        if self.func.kind is GETTER:
            self.parser_prototype = PARSER_PROTOTYPE_GETTER
            parser_code = []
        elif self.func.kind is SETTER:
            self.parser_prototype = PARSER_PROTOTYPE_SETTER
            parser_code = []
        elif not self.requires_defining_class:
            # no self.parameters, METH_NOARGS
            self.flags = "METH_NOARGS"
            self.parser_prototype = PARSER_PROTOTYPE_NOARGS
            parser_code = []
        else:
            assert self.fastcall

            self.flags = "METH_METHOD|METH_FASTCALL|METH_KEYWORDS"
            self.parser_prototype = PARSER_PROTOTYPE_DEF_CLASS
            return_error = ('return NULL;' if simple_return
                            else 'goto exit;')
            parser_code = [libclinic.normalize_snippet("""
                if (nargs || (kwnames && PyTuple_GET_SIZE(kwnames))) {{
                    PyErr_SetString(PyExc_TypeError, "{name}() takes no arguments");
                    %s
                }}
                """ % return_error, indent=4)]

        if simple_return:
            self.parser_definition = '\n'.join([
                self.parser_prototype,
                '{{',
                *parser_code,
                '    return {c_basename}_impl({impl_arguments});',
                '}}'])
        else:
            self.parser_body(*parser_code)

    def parse_one_arg(self) -> None:
        self.flags = "METH_O"

        if (isinstance(self.converters[0], object_converter) and
            self.converters[0].format_unit == 'O'):
            meth_o_prototype = METH_O_PROTOTYPE

            if self.use_simple_return() and self.use_pyobject_self():
                # maps perfectly to METH_O, doesn't need a return converter.
                # so we skip making a parse function
                # and call directly into the impl function.
                self.impl_prototype = ''
                self.impl_definition = meth_o_prototype
            else:
                # SLIGHT HACK
                # use impl_parameters for the parser here!
                self.parser_prototype = meth_o_prototype
                self.parser_body()

        else:
            argname = 'arg'
            if self.parameters[0].name == argname:
                argname += '_'
            self.parser_prototype = libclinic.normalize_snippet("""
                static PyObject *
                {c_basename}({self_type}{self_name}, PyObject *%s)
                """ % argname)

            displayname = self.parameters[0].get_displayname(0)
            parsearg: str | None
            parsearg = self.converters[0].parse_arg(argname, displayname,
                                                    limited_capi=self.limited_capi)
            if parsearg is None:
                self.converters[0].use_converter()
                parsearg = """
                    if (!PyArg_Parse(%s, "{format_units}:{name}", {parse_arguments})) {{
                        goto exit;
                    }}
                    """ % argname

            parser_code = libclinic.normalize_snippet(parsearg, indent=4)
            self.parser_body(parser_code)

    def parse_option_groups(self) -> None:
        # positional parameters with option groups
        # (we have to generate lots of PyArg_ParseTuple calls
        #  in a big switch statement)

        self.flags = "METH_VARARGS"
        self.parser_prototype = PARSER_PROTOTYPE_VARARGS
        parser_code = '    {option_group_parsing}'
        self.parser_body(parser_code)

    def _parse_vararg(self) -> str:
        assert self.varpos is not None
        c = self.varpos.converter
        assert isinstance(c, libclinic.converters.VarPosCConverter)
        return c.parse_vararg(pos_only=self.pos_only,
                              min_pos=self.min_pos,
                              max_pos=self.max_pos,
                              fastcall=self.fastcall,
                              limited_capi=self.limited_capi)

    def _parse_kwarg(self) -> str:
        assert self.var_keyword is not None
        c = self.var_keyword.converter
        assert isinstance(c, libclinic.converters.VarKeywordCConverter)
        return c.parse_var_keyword()

    def parse_pos_only(self) -> None:
        if self.fastcall:
            # positional-only, but no option groups
            # we only need one call to _PyArg_ParseStack

            self.flags = "METH_FASTCALL"
            self.parser_prototype = PARSER_PROTOTYPE_FASTCALL
            nargs = 'nargs'
            argname_fmt = 'args[%d]'
        else:
            # positional-only, but no option groups
            # we only need one call to PyArg_ParseTuple

            self.flags = "METH_VARARGS"
            self.parser_prototype = PARSER_PROTOTYPE_VARARGS
            if self.limited_capi:
                nargs = 'PyTuple_Size(args)'
                argname_fmt = 'PyTuple_GetItem(args, %d)'
            else:
                nargs = 'PyTuple_GET_SIZE(args)'
                argname_fmt = 'PyTuple_GET_ITEM(args, %d)'

        parser_code = []
        max_args = NO_VARARG if self.varpos else self.max_pos
        if self.limited_capi:
            if nargs != 'nargs':
                nargs_def = f'Py_ssize_t nargs = {nargs};'
                parser_code.append(libclinic.normalize_snippet(nargs_def, indent=4))
                nargs = 'nargs'
            if self.min_pos == max_args:
                pl = '' if self.min_pos == 1 else 's'
                parser_code.append(libclinic.normalize_snippet(f"""
                    if ({nargs} != {self.min_pos}) {{{{
                        PyErr_Format(PyExc_TypeError, "{{name}} expected {self.min_pos} argument{pl}, got %zd", {nargs});
                        goto exit;
                    }}}}
                    """,
                indent=4))
            else:
                if self.min_pos:
                    pl = '' if self.min_pos == 1 else 's'
                    parser_code.append(libclinic.normalize_snippet(f"""
                        if ({nargs} < {self.min_pos}) {{{{
                            PyErr_Format(PyExc_TypeError, "{{name}} expected at least {self.min_pos} argument{pl}, got %zd", {nargs});
                            goto exit;
                        }}}}
                        """,
                        indent=4))
                if max_args != NO_VARARG:
                    pl = '' if max_args == 1 else 's'
                    parser_code.append(libclinic.normalize_snippet(f"""
                        if ({nargs} > {max_args}) {{{{
                            PyErr_Format(PyExc_TypeError, "{{name}} expected at most {max_args} argument{pl}, got %zd", {nargs});
                            goto exit;
                        }}}}
                        """,
                    indent=4))
        elif self.min_pos or max_args != NO_VARARG:
            self.codegen.add_include('pycore_modsupport.h',
                                     '_PyArg_CheckPositional()')
            parser_code.append(libclinic.normalize_snippet(f"""
                if (!_PyArg_CheckPositional("{{name}}", {nargs}, {self.min_pos}, {max_args})) {{{{
                    goto exit;
                }}}}
                """, indent=4))

        has_optional = False
        use_parser_code = True
        for i, p in enumerate(self.parameters):
            displayname = p.get_displayname(i+1)
            argname = argname_fmt % i
            parsearg: str | None
            parsearg = p.converter.parse_arg(argname, displayname, limited_capi=self.limited_capi)
            if parsearg is None:
                if self.varpos:
                    raise ValueError(
                        f"Using converter {p.converter} is not supported "
                        f"in function with var-positional parameter")
                use_parser_code = False
                parser_code = []
                break
            if has_optional or p.is_optional():
                has_optional = True
                parser_code.append(libclinic.normalize_snippet("""
                    if (%s < %d) {{
                        goto skip_optional;
                    }}
                    """, indent=4) % (nargs, i + 1))
            parser_code.append(libclinic.normalize_snippet(parsearg, indent=4))

        if use_parser_code:
            if has_optional:
                parser_code.append("skip_optional:")
            if self.varpos:
                parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
            elif self.var_keyword:
                parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4))
        else:
            for parameter in self.parameters:
                parameter.converter.use_converter()

            if self.limited_capi:
                self.fastcall = False
            if self.fastcall:
                self.codegen.add_include('pycore_modsupport.h',
                                         '_PyArg_ParseStack()')
                parser_code = [libclinic.normalize_snippet("""
                    if (!_PyArg_ParseStack(args, nargs, "{format_units}:{name}",
                        {parse_arguments})) {{
                        goto exit;
                    }}
                    """, indent=4)]
            else:
                self.flags = "METH_VARARGS"
                self.parser_prototype = PARSER_PROTOTYPE_VARARGS
                parser_code = [libclinic.normalize_snippet("""
                    if (!PyArg_ParseTuple(args, "{format_units}:{name}",
                        {parse_arguments})) {{
                        goto exit;
                    }}
                    """, indent=4)]
        self.parser_body(*parser_code)

    def parse_var_keyword(self) -> None:
        self.flags = "METH_VARARGS|METH_KEYWORDS"
        self.parser_prototype = PARSER_PROTOTYPE_KEYWORD
        nargs = 'PyTuple_GET_SIZE(args)'

        parser_code = []
        max_args = NO_VARARG if self.varpos else self.max_pos
        if self.varpos is None and self.min_pos == self.max_pos == 0:
            self.codegen.add_include('pycore_modsupport.h',
                                     '_PyArg_NoPositional()')
            parser_code.append(libclinic.normalize_snippet("""
                if (!_PyArg_NoPositional("{name}", args)) {{
                    goto exit;
                }}
                """, indent=4))
        elif self.min_pos or max_args != NO_VARARG:
            self.codegen.add_include('pycore_modsupport.h',
                                     '_PyArg_CheckPositional()')
            parser_code.append(libclinic.normalize_snippet(f"""
                if (!_PyArg_CheckPositional("{{name}}", {nargs}, {self.min_pos}, {max_args})) {{{{
                    goto exit;
                }}}}
                """, indent=4))

        for i, p in enumerate(self.parameters):
            parse_arg = p.converter.parse_arg(
                f'PyTuple_GET_ITEM(args, {i})',
                p.get_displayname(i+1),
                limited_capi=self.limited_capi,
            )
            assert parse_arg is not None
            parser_code.append(libclinic.normalize_snippet(parse_arg, indent=4))

        if self.varpos:
            parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
        if self.var_keyword:
            parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4))
        self.parser_body(*parser_code)

    def parse_general(self, clang: CLanguage) -> None:
        parsearg: str | None
        deprecated_positionals: dict[int, Parameter] = {}
        deprecated_keywords: dict[int, Parameter] = {}
        for i, p in enumerate(self.parameters):
            if p.deprecated_positional:
                deprecated_positionals[i] = p
            if p.deprecated_keyword:
                deprecated_keywords[i] = p

        has_optional_kw = (
            max(self.pos_only, self.min_pos) + self.min_kw_only
            < len(self.converters)
        )

        use_parser_code = True
        if self.limited_capi:
            parser_code = []
            use_parser_code = False
            self.fastcall = False
        else:
            self.codegen.add_include('pycore_modsupport.h',
                                     '_PyArg_UnpackKeywords()')
            if not self.varpos:
                nargs = "nargs"
            else:
                nargs = f"Py_MIN(nargs, {self.max_pos})" if self.max_pos else "0"

            if self.fastcall:
                self.flags = "METH_FASTCALL|METH_KEYWORDS"
                self.parser_prototype = PARSER_PROTOTYPE_FASTCALL_KEYWORDS
                self.declarations = declare_parser(self.func, codegen=self.codegen)
                self.declarations += "\nPyObject *argsbuf[%s];" % (len(self.converters) or 1)
                if self.varpos:
                    self.declarations += "\nPyObject * const *fastargs;"
                    argsname = 'fastargs'
                    argname_fmt = 'fastargs[%d]'
                else:
                    argsname = 'args'
                    argname_fmt = 'args[%d]'
                if has_optional_kw:
                    self.declarations += "\nPy_ssize_t noptargs = %s + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - %d;" % (nargs, self.min_pos + self.min_kw_only)
                unpack_args = 'args, nargs, NULL, kwnames'
            else:
                # positional-or-keyword arguments
                self.flags = "METH_VARARGS|METH_KEYWORDS"
                self.parser_prototype = PARSER_PROTOTYPE_KEYWORD
                argsname = 'fastargs'
                argname_fmt = 'fastargs[%d]'
                self.declarations = declare_parser(self.func, codegen=self.codegen)
                self.declarations += "\nPyObject *argsbuf[%s];" % (len(self.converters) or 1)
                self.declarations += "\nPyObject * const *fastargs;"
                self.declarations += "\nPy_ssize_t nargs = PyTuple_GET_SIZE(args);"
                if has_optional_kw:
                    self.declarations += "\nPy_ssize_t noptargs = %s + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - %d;" % (nargs, self.min_pos + self.min_kw_only)
                unpack_args = '_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL'
            parser_code = [libclinic.normalize_snippet(f"""
                {argsname} = _PyArg_UnpackKeywords({unpack_args}, &_parser,
                        /*minpos*/ {self.min_pos}, /*maxpos*/ {self.max_pos}, /*minkw*/ {self.min_kw_only}, /*varpos*/ {1 if self.varpos else 0}, argsbuf);
                if (!{argsname}) {{{{
                    goto exit;
                }}}}
                """, indent=4)]

        if self.requires_defining_class:
            self.flags = 'METH_METHOD|' + self.flags
            self.parser_prototype = PARSER_PROTOTYPE_DEF_CLASS

        if use_parser_code:
            if deprecated_keywords:
                code = clang.deprecate_keyword_use(self.func, deprecated_keywords,
                                                   argname_fmt,
                                                   codegen=self.codegen,
                                                   fastcall=self.fastcall)
                parser_code.append(code)

            add_label: str | None = None
            for i, p in enumerate(self.parameters):
                if isinstance(p.converter, defining_class_converter):
                    raise ValueError("defining_class should be the first "
                                    "parameter (after clang)")
                displayname = p.get_displayname(i+1)
                parsearg = p.converter.parse_arg(argname_fmt % i, displayname, limited_capi=self.limited_capi)
                if parsearg is None:
                    parser_code = []
                    use_parser_code = False
                    break
                if add_label and (i == self.pos_only or i == self.max_pos):
                    parser_code.append("%s:" % add_label)
                    add_label = None
                if not p.is_optional():
                    parser_code.append(libclinic.normalize_snippet(parsearg, indent=4))
                elif i < self.pos_only:
                    add_label = 'skip_optional_posonly'
                    parser_code.append(libclinic.normalize_snippet("""
                        if (nargs < %d) {{
                            goto %s;
                        }}
                        """ % (i + 1, add_label), indent=4))
                    if has_optional_kw:
                        parser_code.append(libclinic.normalize_snippet("""
                            noptargs--;
                            """, indent=4))
                    parser_code.append(libclinic.normalize_snippet(parsearg, indent=4))
                else:
                    if i < self.max_pos:
                        label = 'skip_optional_pos'
                        first_opt = max(self.min_pos, self.pos_only)
                    else:
                        label = 'skip_optional_kwonly'
                        first_opt = self.max_pos + self.min_kw_only
                    if i == first_opt:
                        add_label = label
                        parser_code.append(libclinic.normalize_snippet("""
                            if (!noptargs) {{
                                goto %s;
                            }}
                            """ % add_label, indent=4))
                    if i + 1 == len(self.parameters):
                        parser_code.append(libclinic.normalize_snippet(parsearg, indent=4))
                    else:
                        add_label = label
                        parser_code.append(libclinic.normalize_snippet("""
                            if (%s) {{
                            """ % (argname_fmt % i), indent=4))
                        parser_code.append(libclinic.normalize_snippet(parsearg, indent=8))
                        parser_code.append(libclinic.normalize_snippet("""
                                if (!--noptargs) {{
                                    goto %s;
                                }}
                            }}
                            """ % add_label, indent=4))

        if use_parser_code:
            if add_label:
                parser_code.append("%s:" % add_label)
            if self.varpos:
                parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
        else:
            for parameter in self.parameters:
                parameter.converter.use_converter()

            self.declarations = declare_parser(self.func, codegen=self.codegen,
                                               hasformat=True)
            if self.limited_capi:
                # positional-or-keyword arguments
                assert not self.fastcall
                self.flags = "METH_VARARGS|METH_KEYWORDS"
                self.parser_prototype = PARSER_PROTOTYPE_KEYWORD
                parser_code = [libclinic.normalize_snippet("""
                    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "{format_units}:{name}", _keywords,
                        {parse_arguments}))
                        goto exit;
                """, indent=4)]
                self.declarations = "static char *_keywords[] = {{{keywords_c} NULL}};"
                if deprecated_positionals or deprecated_keywords:
                    self.declarations += "\nPy_ssize_t nargs = PyTuple_Size(args);"

            elif self.fastcall:
                self.codegen.add_include('pycore_modsupport.h',
                                         '_PyArg_ParseStackAndKeywords()')
                parser_code = [libclinic.normalize_snippet("""
                    if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser{parse_arguments_comma}
                        {parse_arguments})) {{
                        goto exit;
                    }}
                    """, indent=4)]
            else:
                self.codegen.add_include('pycore_modsupport.h',
                                         '_PyArg_ParseTupleAndKeywordsFast()')
                parser_code = [libclinic.normalize_snippet("""
                    if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &_parser,
                        {parse_arguments})) {{
                        goto exit;
                    }}
                    """, indent=4)]
                if deprecated_positionals or deprecated_keywords:
                    self.declarations += "\nPy_ssize_t nargs = PyTuple_GET_SIZE(args);"
            if deprecated_keywords:
                code = clang.deprecate_keyword_use(self.func, deprecated_keywords,
                                                   codegen=self.codegen,
                                                   fastcall=self.fastcall)
                parser_code.append(code)

        if deprecated_positionals:
            code = clang.deprecate_positional_use(self.func, deprecated_positionals)
            # Insert the deprecation code before parameter parsing.
            parser_code.insert(0, code)

        assert self.parser_prototype is not None
        self.parser_body(*parser_code, declarations=self.declarations)

    def copy_includes(self) -> None:
        # Copy includes from parameters to Clinic after parse_arg()
        # has been called above.
        converters = self.converters
        if self.varpos:
            converters = converters + [self.varpos.converter]
        for converter in converters:
            for include in converter.get_includes():
                self.codegen.add_include(
                    include.filename,
                    include.reason,
                    condition=include.condition)

    def handle_new_or_init(self) -> None:
        self.methoddef_define = ''

        if self.func.kind is METHOD_NEW:
            self.parser_prototype = PARSER_PROTOTYPE_KEYWORD
        else:
            self.return_value_declaration = "int return_value = -1;"
            self.parser_prototype = PARSER_PROTOTYPE_KEYWORD___INIT__

        fields: list[str] = list(self.parser_body_fields)
        parses_positional = 'METH_NOARGS' not in self.flags
        parses_keywords = 'METH_KEYWORDS' in self.flags
        if parses_keywords:
            assert parses_positional

        if self.requires_defining_class:
            raise ValueError("Slot methods cannot access their defining class.")

        if not parses_keywords:
            self.declarations = '{base_type_ptr}'
            self.codegen.add_include('pycore_modsupport.h',
                                     '_PyArg_NoKeywords()')
            fields.insert(0, libclinic.normalize_snippet("""
                if ({self_type_check}!_PyArg_NoKeywords("{name}", kwargs)) {{
                    goto exit;
                }}
                """, indent=4))
            if not parses_positional:
                self.codegen.add_include('pycore_modsupport.h',
                                         '_PyArg_NoPositional()')
                fields.insert(0, libclinic.normalize_snippet("""
                    if ({self_type_check}!_PyArg_NoPositional("{name}", args)) {{
                        goto exit;
                    }}
                    """, indent=4))

        self.parser_body(*fields, declarations=self.declarations)

    def process_methoddef(self, clang: CLanguage) -> None:
        methoddef_cast_end = ""
        if self.flags in ('METH_NOARGS', 'METH_O', 'METH_VARARGS'):
            methoddef_cast = "(PyCFunction)"
        elif self.func.kind is GETTER:
            methoddef_cast = "" # This should end up unused
        elif self.limited_capi:
            methoddef_cast = "(PyCFunction)(void(*)(void))"
        else:
            methoddef_cast = "_PyCFunction_CAST("
            methoddef_cast_end = ")"

        if self.func.methoddef_flags:
            self.flags += '|' + self.func.methoddef_flags

        self.methoddef_define = self.methoddef_define.replace('{methoddef_flags}', self.flags)
        self.methoddef_define = self.methoddef_define.replace('{methoddef_cast}', methoddef_cast)
        self.methoddef_define = self.methoddef_define.replace('{methoddef_cast_end}', methoddef_cast_end)

        self.methoddef_ifndef = ''
        conditional = clang.cpp.condition()
        if not conditional:
            self.cpp_if = self.cpp_endif = ''
        else:
            self.cpp_if = "#if " + conditional
            self.cpp_endif = "#endif /* " + conditional + " */"

            if self.methoddef_define and self.codegen.add_ifndef_symbol(self.func.full_name):
                self.methoddef_ifndef = METHODDEF_PROTOTYPE_IFNDEF

    def finalize(self, clang: CLanguage) -> None:
        # add ';' to the end of self.parser_prototype and self.impl_prototype
        # (they mustn't be None, but they could be an empty string.)
        assert self.parser_prototype is not None
        if self.parser_prototype:
            assert not self.parser_prototype.endswith(';')
            self.parser_prototype += ';'

        if self.impl_prototype is None:
            self.impl_prototype = self.impl_definition
        if self.impl_prototype:
            self.impl_prototype += ";"

        self.parser_definition = self.parser_definition.replace("{return_value_declaration}", self.return_value_declaration)

        compiler_warning = clang.compiler_deprecated_warning(self.func, self.parameters)
        if compiler_warning:
            self.parser_definition = compiler_warning + "\n\n" + self.parser_definition

    def create_template_dict(self) -> dict[str, str]:
        d = {
            "docstring_prototype" : self.docstring_prototype,
            "docstring_definition" : self.docstring_definition,
            "impl_prototype" : self.impl_prototype,
            "methoddef_define" : self.methoddef_define,
            "parser_prototype" : self.parser_prototype,
            "parser_definition" : self.parser_definition,
            "impl_definition" : self.impl_definition,
            "cpp_if" : self.cpp_if,
            "cpp_endif" : self.cpp_endif,
            "methoddef_ifndef" : self.methoddef_ifndef,
        }

        # make sure we didn't forget to assign something,
        # and wrap each non-empty value in \n's
        d2 = {}
        for name, value in d.items():
            assert value is not None, "got a None value for template " + repr(name)
            if value:
                value = '\n' + value + '\n'
            d2[name] = value
        return d2

    def parse_args(self, clang: CLanguage) -> dict[str, str]:
        self.select_prototypes()
        self.init_limited_capi()

        self.flags = ""
        self.declarations = ""
        self.parser_prototype = ""
        self.parser_definition = ""
        self.impl_prototype = None
        self.impl_definition = IMPL_DEFINITION_PROTOTYPE

        # parser_body_fields remembers the fields passed in to the
        # previous call to parser_body. this is used for an awful hack.
        self.parser_body_fields: tuple[str, ...] = ()

        if not self.parameters and not self.varpos and not self.var_keyword:
            self.parse_no_args()
        elif self.use_meth_o():
            self.parse_one_arg()
        elif self.has_option_groups():
            self.parse_option_groups()
        elif self.var_keyword is not None:
            self.parse_var_keyword()
        elif (not self.requires_defining_class
              and self.pos_only == len(self.parameters)):
            self.parse_pos_only()
        else:
            self.parse_general(clang)

        self.copy_includes()
        if self.is_new_or_init():
            self.handle_new_or_init()
        self.process_methoddef(clang)
        self.finalize(clang)

        return self.create_template_dict()
