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

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


from .scoped_name import ScopedName
from .specification import ArgumentType, CachedName, IfaceFile, IfaceFileType


def append_iface_file(iface_file_list, iface_file):
    """ Append an IfaceFile object to a list of them. """

    # Make sure we don't try and add an interface file to its own list.
    if iface_file.used is iface_file_list:
        return

    # Don't bother if it is already there.
    if iface_file in iface_file_list:
        return

    iface_file_list.append(iface_file)


def argument_as_str(arg):
    """ Convert an Argument object to a string of valid C++. """

    if arg.original_typedef is None or arg.original_typedef.no_type_name:
        if arg.type is ArgumentType.TEMPLATE:
            s = str(arg.definition.cpp_name)
            s += '<'

            need_comma = False
            for sub_arg in arg.definition.types.args:
                if need_comma:
                    s += ','
                else:
                    need_comma = True

                s += argument_as_str(sub_arg)

            if s.endswith('>'):
                # For compilers earlier than C++11.
                s += ' '

            s += '>'

        elif arg.type in (ArgumentType.CLASS, ArgumentType.STRUCT, ArgumentType.UNION, ArgumentType.DEFINED):
            s = str(arg.definition)

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

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

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

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

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

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

        elif arg.type is ArgumentType.UINT:
            s = 'uint'

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

        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 in (ArgumentType.FLOAT, ArgumentType.CFLOAT):
            s = 'float'

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

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

        elif arg.type is ArgumentType.VOID:
            s = 'void'

        elif arg.type is ArgumentType.CAPSULE:
            s = 'void *'

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

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

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

    else:
        # Use the original typedef.
        s = str(arg.original_typedef.fq_cpp_name)

    # Remove any global scope specifier (simply to make generated less
    # cluttered).
    if s.startswith('::'):
        s = s[2:]

    for _ in arg.derefs:
        s += '*'

    if arg.is_reference:
        s += '&'

    return s


def cached_name(spec, name):
    """ Add a name to the cache if necessary and return the cached name. """

    # Get the line of the cache for the length of this name creating it if
    # necessary.
    line = spec.name_cache.setdefault(len(name), [])

    # See if the name has already been cached.
    for nd in line:
        if nd.name == name:
            return nd

    # Create a new entry.
    nd = CachedName(name)
    line.append(nd)

    return nd


def find_iface_file(spec, mod, fq_cpp_name, iface_file_type, error_logger,
        cpp_type=None, scope=None):
    """ Return an interface file for a fully qualified C/C++ name and type
    creating it if necessary.
    """

    # See if the name is already used.
    for iff in spec.iface_files:
        if not iff.fq_cpp_name.matches(fq_cpp_name, scope=scope):
            continue

        # They must be the same type except that we allow a class if we want an
        # exception.  This is because we allow classes to be used before they
        # are defined.
        if iff.type is not iface_file_type:
            if iface_file_type is not IfaceFileType.EXCEPTION or iff.type is not IfaceFileType.CLASS:
                error_logger(
                        "a class, exception, namespace or mapped type has already been defined with the same name")

        # Ignore an external class declared in another module.
        if iface_file_type is IfaceFileType.CLASS and iff.module is not None and iff.module is not mod:
            for cd in spec.classes:
                if cd.iface_file is iff:
                    external = cd.external
                    break
            else:
                external = False

            if external:
                continue

        # If this is a mapped type with the same name defined in a different
        # module, then check that this type isn't the same as any of the mapped
        # types defined in that module.
        if iface_file_type == IfaceFileType.MAPPED_TYPE and iff.module is not mod:
            for mtd in spec.mapped_types:
                if mtd.iface_file is not iff:
                    continue

                if cpp_type.type is not ArgumentType.TEMPLATE or \
                    mtd.type.type is not ArgumentType.TEMPLATE or \
                    same_base_type(cpp_type, mtd.type):
                    error_logger(
                            "the mapped type has already been defined in another module")

            # If we got here then we have a mapped type based on an existing
            # template, but with unique parameters.  We don't want to use
            # interface files from other modules, so skip this one.
            continue

        # Ignore a namespace defined in another module.
        if iface_file_type is IfaceFileType.NAMESPACE and iff.module is not mod:
            continue

        return iff

    # Create a new interface file.
    fq_cpp_name = normalised_scoped_name(fq_cpp_name, scope)

    iff = IfaceFile(iface_file_type,
                cpp_name=cached_name(spec, str(fq_cpp_name)),
                fq_cpp_name=fq_cpp_name)

    # Use the same ordering as the old parser.
    spec.iface_files.insert(0, iff)

    return iff


def find_method(klass, name):
    """ Return the Member object for a named member of a class or None if there
    was none.
    """

    for member in klass.members:
        if member.py_name.name == name:
            return member

    return None


def normalised_scoped_name(scoped_name, scope):
    """ Convert a scoped name to a fully qualified name. """

    # Clone the name.
    fq_scoped_name = ScopedName(scoped_name)

    if fq_scoped_name.is_absolute:
        pass
    elif scope is None:
        fq_scoped_name.make_absolute()
    elif fq_scoped_name.is_simple:
        fq_scoped_name.prepend(scope.iface_file.fq_cpp_name)
    else:
        # The relative name has a scope and appears within a scope so we need
        # lookup the name's scope within the current scope.
        names_scope = fq_scoped_name[0]
        scope_fq_cpp_name = scope.iface_file.fq_cpp_name

        while scope_fq_cpp_name is not None:
            if scope_fq_cpp_name.base_name == names_scope:
                del fq_scoped_name[0]
                fq_scoped_name.prepend(scope_fq_cpp_name)
                break

            scope_fq_cpp_name = scope_fq_cpp_name.scope
        else:
            # The lookup failed so just make the name absolute.
            fq_scoped_name.make_absolute()

    return fq_scoped_name


# Argument type qualifiers.
_PY_STRING = (ArgumentType.USTRING, ArgumentType.SSTRING, ArgumentType.STRING,
        ArgumentType.ASCII_STRING, ArgumentType.LATIN1_STRING,
        ArgumentType.UTF8_STRING)
_PY_FLOAT = (ArgumentType.CFLOAT, ArgumentType.FLOAT, ArgumentType.CDOUBLE,
        ArgumentType.DOUBLE)
_PY_INT = (ArgumentType.BOOL, ArgumentType.HASH, ArgumentType.SSIZE,
        ArgumentType.SIZE, ArgumentType.BYTE, ArgumentType.SBYTE,
        ArgumentType.UBYTE, ArgumentType.SHORT, ArgumentType.USHORT,
        ArgumentType.CINT, ArgumentType.INT, ArgumentType.UINT)
_PY_LONG = (ArgumentType.LONG, ArgumentType.LONGLONG)
_PY_ULONG = (ArgumentType.ULONG, ArgumentType.ULONGLONG)
_PY_AUTO = (ArgumentType.BOOL, ArgumentType.BYTE, ArgumentType.SBYTE,
        ArgumentType.UBYTE, ArgumentType.SHORT, ArgumentType.USHORT,
        ArgumentType.INT, ArgumentType.UINT, ArgumentType.FLOAT,
        ArgumentType.DOUBLE)
_PY_CONSTRAINED = (ArgumentType.CBOOL, ArgumentType.CINT, ArgumentType.CFLOAT,
        ArgumentType.CDOUBLE)


def py_as_int(type):
    """ Return True if Python will interpret a type as int. """

    return type.type in _PY_INT or type.type in _PY_LONG or type.type in _PY_ULONG


def same_argument_type(spec, arg1, arg2, strict=True):
    """ Compare two argument types and return True if they are the same.
    'strict' means as C++ would see it, rather than Python.
    """

    if arg1.is_reference != arg2.is_reference:
        return False

    if len(arg1.derefs) != len(arg2.derefs):
        return False

    if strict:
        # The const should be the same.
        if arg1.is_const != arg2.is_const:
            return False

        return same_base_type(arg1, arg2)

    # If both are constrained fundamental types then the types must match.
    if arg1.type in _PY_CONSTRAINED and arg2.type in _PY_CONSTRAINED:
        return arg1.type is arg2.type

    if spec.target_abi >= (13, 0):
        # Anonymous enums are ints.
        if arg1.type in _PY_INT and arg2.type is ArgumentType.ENUM and arg2.definition.fq_cpp_name is None:
            return True

        if arg1.type is ArgumentType.ENUM and arg1.definition.fq_cpp_name is None and arg2.type in _PY_INT:
            return True
    else:
        # An unconstrained enum also acts as a (very) constrained int.
        if arg1.type in _PY_INT and arg2.type is ArgumentType.ENUM and not arg2.is_constrained:
            return True

        if arg1.type is ArgumentType.ENUM and not arg1.is_constrained and arg2.type in _PY_INT:
            return True

    # Python will see all these as strings.
    if arg1.type in _PY_STRING and arg2.type in _PY_STRING:
        return True

    # Python will see all these as floats.
    if arg1.type in _PY_FLOAT and arg2.type in _PY_FLOAT:
        return True

    # Python will see all these as ints.
    if arg1.type in _PY_INT and arg2.type in _PY_INT:
        return True

    # Python will see all these as longs.
    if arg1.type in _PY_LONG and arg2.type in _PY_LONG:
        return True

    # Python will see all these as unsigned longs.
    if arg1.type in _PY_ULONG and arg2.type in _PY_ULONG:
        return True

    # Python will automatically convert between these.
    if arg1.type in _PY_AUTO and arg2.type in _PY_AUTO:
        return True

    # All the special cases have been handled.
    return same_base_type(arg1, arg2)


def same_base_type(type1, type2):
    """ Return True if two Argument objects refer to the same base type, ie.
    without taking into account const and pointers.
    """

    # The types must be the same.
    if type1.type is not type2.type:
        # Compare original typedef names if appropriate.
        if type1.original_typedef is not None and type2.type is ArgumentType.DEFINED:
            return type1.original_typedef.fq_cpp_name.matches(type2.definition)

        if type1.type is ArgumentType.DEFINED and type2.original_typedef is not None:
            return type2.original_typedef.fq_cpp_name.matches(type1.definition)

        # If we are comparing a template with those that have already been used
        # to instantiate a class or mapped type then we need to compare with
        # the class or mapped type name.

        if type1.type is ArgumentType.CLASS and type2.type is ArgumentType.DEFINED:
            return type1.definition.iface_file.fq_cpp_name.matches(type2.definition)

        if type1.type is ArgumentType.DEFINED and type2.type is ArgumentType.CLASS:
            return type2.definition.iface_file.fq_cpp_name.matches(type1.definition)

        if type1.type is ArgumentType.MAPPED and type2.type is ArgumentType.DEFINED:
            return type1.definition.iface_file.fq_cpp_name.matches(type2.definition)

        if type1.type is ArgumentType.DEFINED and type2.type is ArgumentType.MAPPED:
            return type2.definition.iface_file.fq_cpp_name.matches(type1.definition)

        if type1.type is ArgumentType.ENUM and type2.type is ArgumentType.DEFINED:
            return type1.definition.fq_cpp_name.matches(type2.definition)

        if type1.type is ArgumentType.DEFINED and type2.type is ArgumentType.ENUM:
            return type2.definition.fq_cpp_name.matches(type1.definition)

        return False

    if type1.type is ArgumentType.CLASS:
        return type1.definition is type2.definition

    if type1.type is ArgumentType.ENUM:
        return type1.definition is type2.definition

    if type1.type is ArgumentType.TEMPLATE:
        td1 = type1.definition
        td2 = type2.definition

        if td1.cpp_name.absolute != td2.cpp_name.absolute:
            return False

        if len(td1.types.args) != len(td2.types.args):
            return False

        for ad1, ad2 in zip(td1.types.args, td2.types.args):
            if len(ad1.derefs) != len(ad2.derefs):
                return False

            if not same_base_type(ad1, ad2):
                return False

        return True

    if type1.type in (ArgumentType.STRUCT, ArgumentType.UNION):
        return type1.definition == type2.definition

    if type1.type is ArgumentType.DEFINED:
        return type1.definition == type2.definition

    if type1.type is ArgumentType.MAPPED:
        return type1.definition is type2.definition

    # They must be the same if we've got this far.
    return True


def same_signature(spec, sig1, sig2, strict=True):
    """ Compare two signatures and return True if they are the same. """

    if strict:
        # Count all the arguments.
        na1 = len(sig1.args)
        na2 = len(sig2.args)
    else:
        # Count only the compulsory arguments.
        na1 = 0

        for arg in sig1.args:
            if arg.default_value is not None:
                break

            na1 += 1

        na2 = 0

        for arg in sig2.args:
            if arg.default_value is not None:
                break;

            na2 += 1

    # The number of arguments must be the same.
    if na1 != na2:
        return False

    # The arguments must be the same.
    for a in range(len(sig1.args)):
        if not strict and sig1.args[a].default_value is not None:
            break

        if not same_argument_type(spec, sig1.args[a], sig2.args[a], strict=strict):
            return False

    # Must be the same if we've got this far.
    return True


def search_typedefs(spec, cpp_name, type):
    """ Search the typedefs and update the given type from any definition. """

    # Look for the name.
    fq_cpp_name = ScopedName(cpp_name)
    fq_cpp_name.make_absolute()

    for typedef in spec.typedefs:
        if typedef.fq_cpp_name == fq_cpp_name:
            break
    else:
        return

    # Update the type.
    type.type = typedef.type.type
    type.allow_none = type.allow_none or typedef.type.allow_none
    type.definition = typedef.type.definition
    type.derefs.extend(typedef.type.derefs)
    type.disallow_none = type.disallow_none or typedef.type.disallow_none
    type.is_const = type.is_const or typedef.type.is_const
    type.is_reference = type.is_reference or typedef.type.is_reference
    type.type_hints = type.type_hints or typedef.type.type_hints

    # Remember the original typedef.
    if type.original_typedef is None:
        type.original_typedef = typedef
