# SPDX-License-Identifier: BSD-2-Clause

# Copyright (c) 2024 Phil Thompson <phil@riverbankcomputing.com>


from ...scoped_name import STRIP_NONE
from ...specification import ArgumentType, ArrayArgument, ClassKey, ValueType

from ..type_hints import format_voidptr, TypeHintManager

from .misc import fmt_scoped_py_name


def fmt_argument_as_name(spec, arg, arg_nr):
    """ Return an argument as a name. """

    if spec.module.use_arg_names and arg.name is not None and arg.type is not ArgumentType.ELLIPSIS:
        return arg.name.name

    return f'a{arg_nr}'


def fmt_argument_as_cpp_type(spec, arg, name=None, scope=None,
        strip=STRIP_NONE, make_public=False, use_typename=True, plain=False,
        no_derefs=False, as_xml=False):
    """ Return an argument as a C++ type. """

    original_typedef = arg.original_typedef
    nr_derefs = 0 if no_derefs else len(arg.derefs)
    is_const = arg.is_const and not plain
    is_reference = arg.is_reference and not plain

    s = ''

    if use_typename and original_typedef is not None and not original_typedef.no_type_name and arg.array is not ArrayArgument.ARRAY_SIZE:
        # Reverse the previous expansion of the typedef.
        if is_const and not original_typedef.type.is_const:
            s += 'const '

        nr_derefs -= len(original_typedef.type.derefs)

        if original_typedef.type.is_reference:
            is_reference = False

        s += original_typedef.fq_cpp_name.cpp_stripped(strip)

    else:
        # A function type is handled differently because of the position of the
        # name.
        if arg.type is ArgumentType.FUNCTION:
            from .signature import fmt_signature_as_cpp_declaration

            s += fmt_argument_as_cpp_type(spec, arg.definition.result,
                    scope=scope, strip=strip, as_xml=as_xml)

            s += ' (' + '*' * nr_derefs + name + ')('

            s += fmt_signature_as_cpp_declaration(spec, arg.definition,
                    scope=scope, as_xml=as_xml)

            s += ')'

            return s

        if is_const:
            s += 'const '

        if arg.type in (ArgumentType.SBYTE, ArgumentType.SSTRING):
            s += 'signed char'

        elif arg.type in (ArgumentType.UBYTE, ArgumentType.USTRING):
            s += 'unsigned char'

        elif arg.type is ArgumentType.WSTRING:
            s += 'wchar_t'

        elif arg.type in (ArgumentType.BYTE, ArgumentType.ASCII_STRING, ArgumentType.LATIN1_STRING, ArgumentType.UTF8_STRING, ArgumentType.STRING):
            s += 'char'

        elif arg.type is ArgumentType.USHORT:
            s += 'unsigned short'

        elif arg.type is ArgumentType.SHORT:
            s += 'short'

        elif arg.type is ArgumentType.UINT:
            # Qt4 moc uses "uint" in signal signatures.  We do all the time and
            # hope it is always defined.
            s += 'uint'

        elif arg.type in (ArgumentType.INT, ArgumentType.CINT):
            s += 'int'

        elif arg.type is ArgumentType.HASH:
            s += 'Py_hash_t'

        elif arg.type is ArgumentType.SSIZE:
            s += 'Py_ssize_t'

        elif arg.type is ArgumentType.SIZE:
            s += 'size_t'

        elif arg.type is ArgumentType.ULONG:
            s += 'unsigned long'

        elif arg.type is ArgumentType.LONG:
            s += 'long'

        elif arg.type is ArgumentType.ULONGLONG:
            s += 'unsigned long long'

        elif arg.type is ArgumentType.LONGLONG:
            s += 'long long'

        elif arg.type is ArgumentType.STRUCT:
            s += 'struct ' + arg.definition.as_cpp

        elif arg.type is ArgumentType.UNION:
            s += 'union ' + arg.definition.as_cpp

        elif arg.type is ArgumentType.CAPSULE:
            nr_derefs = 1
            s += 'void'

        elif arg.type in (ArgumentType.FAKE_VOID, ArgumentType.VOID):
            s += 'void'

        elif arg.type in (ArgumentType.BOOL, ArgumentType.CBOOL):
            s += 'bool'

        elif arg.type in (ArgumentType.FLOAT, ArgumentType.CFLOAT):
            s += 'float'

        elif arg.type in (ArgumentType.DOUBLE, ArgumentType.CDOUBLE):
            s += 'double'

        elif arg.type is ArgumentType.DEFINED:
            # The only defined types still remaining are arguments to templates
            # and default values.
            if as_xml:
                s += arg.definition.as_py
            else:
                if spec.c_bindings:
                    s += 'struct '

                s += arg.definition.cpp_stripped(strip)

        elif arg.type is ArgumentType.MAPPED:
            s += fmt_argument_as_cpp_type(spec, arg.definition.type,
                    scope=scope, strip=strip, as_xml=as_xml)

        elif arg.type is ArgumentType.CLASS:
            from .klass import fmt_class_as_scoped_name

            if spec.c_bindings:
                s += 'union ' if arg.definition.class_key is ClassKey.UNION else 'struct '

            s += fmt_class_as_scoped_name(spec, arg.definition, scope=scope,
                    strip=strip, make_public=make_public, as_xml=as_xml)

        elif arg.type is ArgumentType.TEMPLATE:
            from .template import fmt_template_as_cpp_type

            s += fmt_template_as_cpp_type(spec, arg.definition, strip=strip,
                    as_xml=as_xml)

        elif arg.type is ArgumentType.ENUM:
            from .enum import fmt_enum_as_cpp_type

            s += fmt_enum_as_cpp_type(arg.definition, make_public=make_public,
                    strip=strip)

        elif arg.type in (ArgumentType.PYOBJECT, ArgumentType.PYTUPLE, ArgumentType.PYLIST, ArgumentType.PYDICT, ArgumentType.PYCALLABLE, ArgumentType.PYSLICE, ArgumentType.PYTYPE, ArgumentType.PYBUFFER, ArgumentType.PYENUM, ArgumentType.ELLIPSIS):
            s += 'PyObject *'

    space_before_name = True

    for i in range(nr_derefs):
        # Note that we don't put a space before the '*' so that Qt normalised
        # signal signatures are correct.
        s += '*'
        space_before_name = False

        if arg.derefs[i]:
            s += ' const'
            space_before_name = True

    if is_reference:
        s += '&'

    if name:
        if space_before_name:
            s += ' '

        s += name

    return s


# The types that are implicitly pointers.
_IMPLICIT_POINTERS = (ArgumentType.PYOBJECT, ArgumentType.PYTUPLE,
        ArgumentType.PYLIST, ArgumentType.PYDICT, ArgumentType.PYCALLABLE,
        ArgumentType.PYSLICE, ArgumentType.PYTYPE, ArgumentType.CAPSULE,
        ArgumentType.PYBUFFER, ArgumentType.PYENUM
)

def fmt_argument_as_py_default_value(spec, arg, type_name, embedded=False,
        as_xml=False):
    """ Return the Python representation of an argument's default value. """

    from .value_list import fmt_value_list_as_py_expression

    # Use any explicitly provided documentation.
    if arg.type_hints is not None and arg.type_hints.default_value is not None:
        return arg.type_hints.default_value

    # Translate some special cases.
    if len(arg.default_value) == 1 and arg.default_value[0].value_type is ValueType.NUMERIC:
        value = arg.default_value[0].value

        if value == 0 and ('voidptr' in type_name or len(arg.derefs) != 0 or arg.type in _IMPLICIT_POINTERS):
            return 'None'

        if arg.type in (ArgumentType.BOOL, ArgumentType.CBOOL):
            return 'True' if value else 'False'

    return fmt_value_list_as_py_expression(spec, arg.default_value,
            embedded=embedded, as_xml=as_xml)


def fmt_argument_as_py_type(spec, arg, pep484=False, default_value=False,
        as_xml=False):
    """ Return an argument as a Python type. """

    scope, name = _py_arg(spec, arg, pep484, as_xml)

    s = fmt_scoped_py_name(scope, name)

    if default_value and arg.default_value is not None:
        if arg.name is not None:
            s += ' ' + arg.name.name

        s += '=' + fmt_argument_as_py_default_value(spec, arg, name)

    return s


def fmt_argument_as_rest_ref(spec, arg, out, as_xml=False):
    """ Return an argument as a reST reference. """

    s = ''

    hint = _get_hint(arg, out)

    if hint is None:
        if arg.type is ArgumentType.CLASS:
            from .klass import fmt_class_as_rest_ref

            s += fmt_class_as_rest_ref(arg.definition)
        elif arg.type is ArgumentType.ENUM:
            if arg.definition.py_name is not None:
                from .enum import fmt_enum_as_rest_ref

                s += fmt_enum_as_rest_ref(arg.definition)
            else:
                s += 'int'
        elif arg.type is ArgumentType.MAPPED:
            # There would normally be a type hint.
            s += "unknown-type"
        else:
            s += fmt_argument_as_py_type(spec, arg, as_xml=as_xml)
    else:
        context = arg.definition if arg.type is ArgumentType.CLASS else None

        s += TypeHintManager(spec).as_rest_ref(hint, out, context,
                as_xml=as_xml)

    return s


def fmt_argument_as_type_hint(spec, arg, defined, arg_nr=-1):
    """ Return an argument as a type hint. """

    if arg.array is ArrayArgument.ARRAY_SIZE:
        return ''

    pep484 = defined is not None

    s = ''

    if arg_nr is None:
        # It's a variable.
        out = False
    elif arg_nr >= 0:
        # It's a callable input argument.
        out = False

        if arg.name is None:
            s += f'a{arg_nr}: '
        else:
            name = _fix_py_keyword(arg.name.name)
            s += f'{name}: '
    else:
        # It's a callable result or output argument.
        out = True

    hint = _get_hint(arg, out)

    # Assume pointers can be None unless specified otherwise.
    allow_none = arg.allow_none or (arg.type in (ArgumentType.CLASS, ArgumentType.MAPPED) and arg.definition.handles_none)

    if hint is None and allow_none:
        use_optional = True
    else:
        use_optional = (not arg.disallow_none and len(arg.derefs) != 0)

    if use_optional:
        s += 'typing.Optional[' if pep484 else 'Optional['

    if arg.array is ArrayArgument.ARRAY:
        s += _sip_module_name(spec) + 'array['

    if hint is None:
        if arg.type is ArgumentType.CLASS:
            from .klass import fmt_class_as_type_hint

            type_name = fmt_class_as_type_hint(spec, arg.definition, defined)
        elif arg.type is ArgumentType.ENUM:
            if arg.definition.py_name is not None:
                from .enum import fmt_enum_as_type_hint

                type_name = fmt_enum_as_type_hint(spec, arg.definition,
                        defined)
            else:
                type_name = 'int'
        elif arg.type is ArgumentType.MAPPED:
            # There would normally be a type hint.
            type_name = 'typing.Any' if pep484 else 'Any'
        else:
            type_name = fmt_argument_as_py_type(spec, arg, pep484=pep484)
    else:
        type_hint_manager = TypeHintManager(spec)

        context = arg.definition if arg.type is ArgumentType.CLASS else None

        if pep484:
            type_name = type_hint_manager.as_type_hint(hint, out, context,
                    defined)
        else:
            type_name = type_hint_manager.as_docstring(hint, out, context)

    s += type_name

    if arg.array is ArrayArgument.ARRAY:
        s += ']'

    if use_optional:
        s += ']'

    # See if the argument is optional.
    if arg_nr is not None and arg_nr >= 0 and arg.default_value is not None:
        # Historically .pyi files don't include specific default values.
        s += ' = '
        s += '...' if pep484 else fmt_argument_as_py_default_value(spec, arg, type_name, embedded=True)

    return s


_PYTHON_KEYWORDS = (
    'False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class',
    'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for',
    'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not',
    'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield', 'exec',
    'print',
)

def _fix_py_keyword(word):
    """ Return a fixed word if it is a Python keyword (or has been at any
    time).
    """

    if word in _PYTHON_KEYWORDS:
        word += '_'

    return word


def _get_hint(arg, out):
    """ Return a raw type hint. """

    # Use any explicit type hint unless the argument is constrained.
    if arg.type_hints is None:
        hint = None
    elif out:
        hint = arg.type_hints.hint_out
    elif arg.is_constrained:
        hint = None
    else:
        hint = arg.type_hints.hint_in

    return hint


def _py_arg(spec, arg, pep484, as_xml):
    """ Return an argument as a 2-tuple of scope and name. """

    type = arg.type
    definition = arg.definition

    sip_module_name = _sip_module_name(spec)

    scope = None
    name = "unknown-type"

    if type is ArgumentType.CLASS:
        scope = definition.scope
        name = definition.py_name.name

    elif type is ArgumentType.MAPPED:
        if definition.py_name is not None:
            name = definition.py_name.name

    elif type is ArgumentType.ENUM:
        if definition.py_name is None:
            name = 'int'
        else:
            scope = definition.scope
            name = definition.py_name.name

    elif type is ArgumentType.DEFINED:
        name = definition.as_py

    elif type is ArgumentType.CAPSULE:
        name = definition.base_name

    elif type in (ArgumentType.STRUCT, ArgumentType.UNION, ArgumentType.VOID):
        name = format_voidptr(spec, as_xml)

    elif type in (ArgumentType.STRING, ArgumentType.SSTRING, ArgumentType.USTRING):
        name = 'bytes'

    elif type in (ArgumentType.WSTRING, ArgumentType.ASCII_STRING, ArgumentType.LATIN1_STRING, ArgumentType.UTF8_STRING):
        name = 'bytes' if arg.array is ArrayArgument.ARRAY else 'str'

    elif type in (ArgumentType.BYTE, ArgumentType.SBYTE, ArgumentType.UBYTE, ArgumentType.USHORT, ArgumentType.UINT, ArgumentType.LONG, ArgumentType.LONGLONG, ArgumentType.ULONG, ArgumentType.ULONGLONG, ArgumentType.SHORT, ArgumentType.INT, ArgumentType.CINT, ArgumentType.SSIZE, ArgumentType.SIZE, ArgumentType.HASH):
        name = 'int'

    elif type in (ArgumentType.FLOAT, ArgumentType.CFLOAT, ArgumentType.DOUBLE, ArgumentType.CDOUBLE):
        name = 'float'

    elif type in (ArgumentType.BOOL, ArgumentType.CBOOL):
        name = 'bool'

    elif type in (ArgumentType.PYOBJECT, ArgumentType.ELLIPSIS):
        name = 'typing.Any' if pep484 else 'Any'

    elif type is ArgumentType.PYTUPLE:
        name = 'typing.Tuple' if pep484 else 'Tuple'

    elif type is ArgumentType.PYLIST:
        name = 'typing.List' if pep484 else 'List'

    elif type is ArgumentType.PYDICT:
        name = 'typing.Dict' if pep484 else 'Dict'

    elif type is ArgumentType.PYCALLABLE:
        name = 'typing.Callable[..., None]' if pep484 else 'Callable[..., None]'

    elif type is ArgumentType.PYSLICE:
        name = 'slice'

    elif type is ArgumentType.PYTYPE:
        name = 'type'

    elif type is ArgumentType.PYBUFFER:
        if pep484:
            name = sip_module_name + 'Buffer'
        else:
            # This replicates sip.pyi.
            name = f'Union[bytes, bytearray, memoryview, {sip_module_name}array, {sip_module_name}voidptr]'

    elif type is ArgumentType.PYENUM:
        name = 'enum.Enum'

    return scope, name


def _sip_module_name(spec):
    """ Return the name of the sip module to be used as a prefix to an object
    in the module.
    """

    sip_module = spec.sip_module

    return sip_module + '.' if sip_module else ''
