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

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



from copy import copy

from ...sip_module_configuration import (apply_module_defaults,
        SipModuleConfiguration)

from ..error_log import ErrorLog
from ..instantiations import instantiate_type_hints
from ..python_slots import (is_hash_return_slot, is_int_return_slot,
        is_inplace_number_slot, is_rich_compare_slot, is_ssize_return_slot,
        is_void_return_slot, is_zero_arg_slot, reflected_slot)
from ..scoped_name import ScopedName
from ..specification import (AccessSpecifier, Argument, ArgumentType,
        ArrayArgument, CachedName, ClassKey, Constructor, EnumBaseType,
        IfaceFileType, IndexedClassList, MappedType, Member,
        PyQtMethodSpecifier, PySlot, Signature, Transfer, ValueType,
        VirtualHandler, VirtualOverload, VisibleMember, WrappedClass)
from ..templates import (encoded_template_name, same_template_signature,
        template_code, template_code_blocks, template_expansions)
from ..utils import (append_iface_file, argument_as_str, cached_name,
        fast_contains, find_iface_file, find_method, is_namespace_extender,
        same_argument_type, same_base_type, same_signature, search_typedefs)


def resolve(spec, modules):
    """ Resolve all types of a parsed specification and create additional views
    so that code can be generated.
    """

    error_log = ErrorLog()
    final_checks = []

    # Build the list of all imports for each module.
    for mod in modules:
        _set_all_imports(mod, error_log)

        # Set the base name of the module.  This is done for efficiency.
        mod.py_name = mod.fq_py_name.name.split('.')[-1]

    # There is nothing else that needs doing for composite modules.
    if spec.is_composite:
        return

    # Set the default meta-type for the main module if it doesn't have one
    # explicitly set.
    if spec.module.default_metatype is None:
        for mod in spec.module.all_imports:
            if mod.default_metatype is None:
                continue

            if spec.module.default_metatype is None:
                spec.module.default_metatype = mod.default_metatype
            elif spec.module.default_metatype is not mod.default_metatype:
                error_log.log(
                        "'{0}' module has imported different default meta-types '{1}' and '{2}'".format(
                                spec.module.fq_py_name,
                                spec.module.default_metatype,
                                mod.default_metatype))

    # Check each class has been defined.
    for klass in spec.classes:
        if klass.iface_file.module is None:
            error_log.log(
                    "class '{0}' has not been defined".format(
                            klass.iface_file.fq_cpp_name))

        # Mark any QObject class.  This flag will ripple through all derived
        # classes when we set the hierarchy.
        if klass.iface_file.fq_cpp_name.base_name == 'QObject':
            klass.is_qobject = True

    # The class list has the main module's classes at the front and the ones
    # from the module at the most nested %Import at the end.  Set the MRO for
    # each class and re-order the list of classes so that no class appears
    # before a super class or an enclosing scope class.
    reversed_classes = reversed(spec.classes)
    spec.classes = IndexedClassList()

    for klass in reversed_classes:
        # Ignore undefined classes.
        if klass.iface_file.module is None:
            continue

        _set_mro(spec, klass, error_log)

    # Resolve the various types in the modules.
    _resolve_module(spec, spec.module, error_log, final_checks)

    # Handle default ctors now that the argument types are resolved.
    for klass in spec.classes:
        if klass.no_default_ctors or klass.is_opaque or klass.iface_file.type is IfaceFileType.NAMESPACE:
            continue

        _add_default_copy_ctor(klass)

    # Add any automatically generated methods.
    for klass in spec.classes:
        for overload in klass.overloads:
            if overload.is_auto_generated:
                _add_auto_overload(spec, klass, overload)

    # Move casts and slots around to their correct classes (if in the same
    # module) or create extenders for them (if cross-module).
    _move_main_module_casts_slots(spec, error_log)

    # Automatically generate missing complementary slots.
    for klass in spec.classes:
        _add_complementary_slots(spec, klass)

    for klass in spec.module.extenders:
        _add_complementary_slots(spec, klass)

    # Generate the different class views.
    for klass in spec.classes:
        if klass.iface_file.type is IfaceFileType.CLASS:
            if klass.needs_shadow and not klass.is_incomplete and klass.dtor is not AccessSpecifier.PRIVATE and klass.can_create:
                klass.has_shadow = True

            # Get the list of visible Python member functions.
            _get_visible_py_members(spec, klass)

            # Get the virtual members.
            if klass.needs_shadow:
                _get_virtuals(spec, klass, error_log)

        elif klass.iface_file.type is IfaceFileType.NAMESPACE:
            for overload in klass.overloads:
                _iface_files_are_used_by_overload(spec, klass.iface_file.used,
                        overload)

    for mod in modules:
        # Create the list of numbered types sorted by type name.
        _create_sorted_numbered_types(spec, mod, error_log)

        for overload in mod.overloads:
            _iface_files_are_used_by_overload(spec, mod.used, overload)

        # Update class extenders with some information from the real classes.
        for klass in mod.extenders:
            klass.iface_file.type_nr = klass.real_class.iface_file.type_nr

    # Additional class specific checks.
    for klass in spec.classes:
        # Ignore undefined classes.
        if klass.iface_file.module is None:
            continue

        _check_helpers(spec, klass)
        _check_properties(klass, error_log)

    # Number the exceptions as they will be seen by the main module.
    for exception in spec.exceptions:
        exception_mod = exception.iface_file.module

        # Include the %TypeHeaderCode for exceptions defined in the main
        # module.
        if spec.target_abi >= (13, 1) or (spec.target_abi >= (12, 9) and spec.target_abi < (13, 0)):
            if exception_mod is spec.module:
                append_iface_file(spec.module.used, exception.iface_file)

        if _exception_needed(spec, exception):
            exception.exception_nr = exception_mod.nr_exceptions
            exception_mod.nr_exceptions += 1

    # For PyQt6 mark all enum interface files as being used.
    if 'PyQt6' in spec.plugins:
        for enum in spec.enums:
            if enum.module is spec.module:
                _enum_iface_file_is_used(enum, spec.module)

    # Finalise the sip module configuration.
    spec.sip_module_configuration = apply_module_defaults(
            spec.sip_module_configuration)

    # Perform any final checks.
    for check in final_checks:
        check()

    # Raise an exception for any errors.
    error_log.as_exception()


def _resolve_module(spec, mod, error_log, final_checks, seen=None):
    """ Resolve a module and the modules it imports. """

    if seen is None:
        seen = []
    elif mod in seen:
        # Nothing left to do.
        return

    # The modules on which this one depends must be done first because they
    # might generate new template-based types and they must be defined in the
    # right module.
    for imported_mod in mod.imports:
        _resolve_module(spec, imported_mod, error_log, final_checks, seen=seen)

    # Resolve typedefs, enums, variables and global functions.
    _resolve_typedefs(spec, mod, error_log)
    _resolve_enums(spec, error_log)
    _resolve_variables(spec, mod, error_log)
    _resolve_scope_overloads(spec, mod.overloads, error_log, final_checks)

    # Resolve class ctors, functions and casts.
    for klass in spec.classes:
        if klass.iface_file.module is mod:
            _resolve_ctors(spec, klass, error_log)

            # Handle any dtor exceptions.
            _set_needed_exceptions(spec, mod, klass.dtor_throw_args)

            _resolve_scope_overloads(spec, klass.overloads, error_log,
                    final_checks, scope=klass)
            _transform_casts(spec, klass, error_log)

    # Resolve mapped types based on templates.
    _resolve_mapped_types(spec, mod, error_log, final_checks)

    seen.append(mod)


# The map of slots and their compliments.
_SLOT_MAP = {
    PySlot.LT:  (PySlot.GE, '__ge__'),
    PySlot.LE:  (PySlot.GT, '__gt__'),
    PySlot.GT:  (PySlot.LE, '__le__'),
    PySlot.GE:  (PySlot.LT, '__lt__'),
    PySlot.EQ:  (PySlot.NE, '__ne__'),
    PySlot.NE:  (PySlot.EQ, '__eq__'),
}

def _add_complementary_slots(spec, klass):
    """ Add any missing complementary slots to a class.  This emulates the C++
    behaviour of automatically interpreting (for example) >= as !<.
    """

    for member in list(klass.members):
        try:
            compl, compl_name = _SLOT_MAP[member.py_slot]
        except KeyError:
            continue

        _add_complementary_slot(spec, klass, member, compl, compl_name)


def _add_complementary_slot(spec, klass, member, compl, compl_name):
    """ Add a complementary slot if it is missing. """

    member2 = None

    for overload in klass.overloads:
        if overload.common is not member or overload.is_complementary or overload.method_code is not None:
            continue

        # Try and find an existing complementary slot.
        for overload2 in klass.overloads:
            if overload2.common.py_slot is compl and same_signature(spec, overload.py_signature, overload2.py_signature):
                break
        else:
            # There is no explicit complementary slot so create a new member if
            # needed.
            if member2 is None:
                for member2 in klass.members:
                    if member2.py_slot is compl:
                        break
                else:
                    member2 = copy(member)

                    member2.py_name = cached_name(spec, compl_name)
                    member2.py_slot = compl

                    if member.py_name.used:
                        member2.py_name.used = True

                    klass.members.insert(0, member2)

            # Create the complementary slot.
            overload2 = copy(overload)

            overload2.is_virtual = False
            overload2.is_complementary = True
            overload2.common = member2
            overload2.cpp_name = compl_name

            klass.overloads.insert(0, overload2)
            break


def _check_helpers(spec, klass):
    """ See if a class supports array and copy helpers. """

    # We disregard classes that are abstract, or have a private assignment
    # operator or don't have a public dtor.
    if klass.is_abstract:
        return

    if klass.cannot_assign:
        return

    if klass.dtor is not AccessSpecifier.PUBLIC:
        return

    # See if the class has a default ctor and a public copy ctor.
    public_default_ctor = public_copy_ctor = False

    for ctor in klass.ctors:
        if ctor.cpp_signature is None or ctor.access_specifier is not AccessSpecifier.PUBLIC:
            continue

        if len(ctor.cpp_signature.args) == 0 or ctor.cpp_signature.args[0].default_value is not None:
            # The ctor either has no arguments or all arguments have defaults.
            public_default_ctor = True
        elif len(ctor.cpp_signature.args) == 1:
            arg = ctor.cpp_signature.args[0]

            if arg.type is not ArgumentType.CLASS:
                continue

            if arg.definition is klass and arg.is_reference and arg.is_const and len(arg.derefs) == 0 and arg.default_value is None:
                public_copy_ctor = True

    if public_default_ctor:
        klass.needs_array_helper = True
        append_iface_file(klass.iface_file.module.used, klass.iface_file)

    if public_copy_ctor:
        klass.needs_copy_helper = True
        append_iface_file(klass.iface_file.module.used, klass.iface_file)


def _set_all_imports(mod, error_log, seen=None):
    """ Set the list of all imports for a module.  The list is ordered so that
    a module appears before any module that imports it.
    """

    # Check for recursive imports.
    if seen is None:
        seen = []
    elif mod in seen:
        error_log.log(
                "module '{0}' is imported recursively".format(mod.fq_py_name))

    seen.append(mod)

    # Make sure all the direct imports are done first.
    for direct_import in mod.imports:
        _set_all_imports(direct_import, error_log, seen=seen)

    # Now build the list from our direct imports' lists but ignoring
    # duplicates.
    def _append_unique_import(imported):
        if imported not in mod.all_imports:
            mod.all_imports.append(imported)

    for direct_import in mod.imports:
        for imported in direct_import.all_imports:
            _append_unique_import(imported)

        _append_unique_import(direct_import)

    seen.remove(mod)

    # Set the module's number, ie. it's index in the all_imports list.
    for nr, imported_mod in enumerate(mod.all_imports):
        imported_mod.module_nr = nr


def _move_main_module_casts_slots(spec, error_log):
    """ Move the casts and slots to the correct place for a main module (ie.
    the one we are generating code for).
    """

    for klass in spec.classes:
        if klass.iface_file.module is spec.module:
            _move_class_casts(spec, klass, error_log)

    for member in tuple(spec.module.global_functions):
        if member.py_slot is not None and member.module is spec.module:
            _move_global_slot(spec, member, error_log)


def _move_class_casts(spec, klass, error_log):
    """ Move any class casts to its correct class, or publish as a ctor
    extender.
    """

    for cast in klass.casts:
        dst_klass = cast.definition

        # Create the new ctor.
        arg = Argument(ArgumentType.CLASS, definition=klass,
                derefs=list(cast.derefs), is_reference=cast.is_reference,
                is_const=cast.is_const, is_in=True,
                source_location=cast.source_location)
        signature = Signature(args=[arg])
        ctor = Constructor(AccessSpecifier.PUBLIC, py_signature=signature,
                cpp_signature=signature, is_cast=True)

        # If the destination class is in a different module then use a proxy.
        if dst_klass.iface_file.module is not spec.module:
            _iface_file_is_used(spec.module.used, arg)
            dst_klass = _get_extender(spec.module, dst_klass)
            ctor.no_typehint = True

        _iface_file_is_used(dst_klass.iface_file.used, arg)

        # Check it hasn't already been defined.
        for dst_ctor in dst_klass.ctors:
            if same_signature(spec, dst_ctor.py_signature, ctor.py_signature, strict=False):
                error_log.log(
                        " operator '{0}::{0}({1})' already defined".format(
                                dst_klass.iface_file.fq_cpp_name,
                                klass.iface_file.fq_cpp_name))

        dst_klass.ctors.append(ctor)


def _move_global_slot(spec, global_slot, error_log):
    """ If possible, move a global slot to its correct class. """

    remove_member = True

    for overload in tuple(spec.module.overloads):
        if overload.common is not global_slot:
            continue

        # We know that the slot has the right number of arguments, but the
        # first or second one needs to be a class or enum defined in the same
        # module.  Otherwise we leave it as it is and publish it as a slot
        # extender.
        arg0 = overload.py_signature.args[0]

        try:
            arg1 = overload.py_signature.args[1]
        except IndexError:
            arg1 = None

        arg_members = None
        arg_overloads = None
        arg_module = None
        arg_enum = None
        is_second = False

        if arg0.type is ArgumentType.CLASS:
            arg_members = arg0.definition.members
            arg_overloads = arg0.definition.overloads
            arg_module = arg0.definition.iface_file.module

        elif arg0.type is ArgumentType.ENUM:
            arg_members = arg0.definition.slots
            arg_overloads = arg0.definition.overloads;
            arg_module = arg0.definition.module
            arg_enum = arg0.definition

        elif arg1 is None:
            if arg0.type is ArgumentType.NONE:
                # The error has already been logged.
                pass
            else:
                _log_overload_error(error_log,
                        "the argument must be a class or enum", overload)
            continue

        elif arg1.type is ArgumentType.CLASS:
            arg_members = arg1.definition.members
            arg_overloads = arg1.definition.overloads
            arg_module = arg1.definition.iface_file.module
            is_second = True

        elif arg1.type is ArgumentType.ENUM:
            arg_members = arg1.definition.slots
            arg_overloads = arg1.definition.overloads
            arg_module = arg1.definition.module
            arg_enum = arg1.definition
            is_second = True

        else:
            if arg0.type is ArgumentType.NONE or arg1.type is ArgumentType.NONE:
                # The error has already been logged.
                pass
            else:
                _log_overload_error(error_log,
                        "one of the arguments must be a class or enum",
                        overload)
            continue

        if spec.target_abi >= (14, 0):
            _move_slot_v14(spec, error_log, global_slot, overload, arg_module,
                    arg_members, arg_overloads, arg_enum, is_second)
        else:
            slot_extender = _move_slot_v12v13(spec, error_log, global_slot,
                    overload, arg_module, arg_members, arg_overloads, arg_enum,
                    is_second)

            if slot_extender:
                remove_member = False

    if remove_member:
        # Remove the slot itself as it has no remaining overloads.
        spec.module.global_functions.remove(global_slot)


def _move_slot_v14(spec, error_log, global_slot, overload, arg_module,
        arg_members, arg_overloads, arg_enum, is_second):
    """ Move a slot and adjust it's signature appropriately. """

    if arg_module is not global_slot.module:
        # We don't support cross-module enum extenders, ie. you can't specify
        # a global operator in a module that takes an enum defined in another
        # module.  This limitation could be overcome (potentially the most
        # difficult issue is having to override the __getattr__ of the Python
        # Enum type).  It is also a little different to the limitations imposed
        # by ABIs v12 and v13 so it's possible that some things are possible in
        # those that aren't supported by v14.  If so we'll consider them as v14
        # bugs and deal with them as they arise.
        extending = overload.py_signature.args[1 if is_second else 0]

        if extending.type is not ArgumentType.CLASS:
            _log_overload_error(error_log, "argument 1 must be a class",
                    overload)
            return

        extender = _get_extender(global_slot.module, extending.definition)
        arg_members = extender.members
        arg_overloads = extender.overloads

    if arg_enum is not None:
        overload.dont_deref_self = True
        _enum_iface_file_is_used(arg_enum, arg_module)

    # Fix the member name if it is a reflected slot.
    member_name = reflected_slot(global_slot.py_slot)
    if member_name is not None and is_second:
        member_name = CachedName(member_name)
        overload.is_reflected = True
    else:
        member_name = global_slot.py_name

    # See if there is already a member or create a new one.
    for arg_member in arg_members:
        if arg_member.py_name is member_name:
            break
    else:
        arg_member = copy(global_slot)

        arg_member.module = arg_module
        arg_member.py_name = member_name
        arg_members.append(arg_member)

    # Move the overload to the end of the destination list.
    overload.access_specifier = AccessSpecifier.PUBLIC
    overload.common = arg_member
    overload.is_global = True

    arg_overloads.append(overload)

    # Remove the self argument.
    del overload.py_signature.args[1 if is_second else 0]

    # Remove from the list.
    spec.module.overloads.remove(overload)


def _move_slot_v12v13(spec, error_log, global_slot, overload, arg_module,
        arg_members, arg_overloads, arg_enum, is_second):
    """ Move a slot and adjust it's signature appropriately.  Return True if
    the slot wasn't moved so it can be handled at a later stage.
    """

    arg0 = overload.py_signature.args[0]

    # For rich comparisons the first argument must be a class or an enum.  For
    # cross-module slots then it may only be a class.  (This latter limitation
    # is artificial, but is unlikely to be a problem in practice.)
    if is_rich_compare_slot(global_slot.py_slot):
        if is_second:
            _log_overload_error(error_log,
                    "argument 1 must be a class or enum", overload)
            return False

        if arg_module is not global_slot.module and arg0.type is ArgumentType.ENUM:
            _log_overload_error(error_log, "argument 1 must be a class",
                    overload)
            return False

    if arg_module is not global_slot.module:
        if is_rich_compare_slot(global_slot.py_slot):
            proxy = _get_extender(arg_module, arg0.definition)

            # Create a new proxy member if needed.
            for proxy_member in proxy.members:
                if proxy_member.py_slot is global_slot.py_slot:
                    break
            else:
                proxy_member = Member(arg_module, global_slot.py_name,
                        py_slot=global_slot.py_slot,
                        namespace_iface_file=global_slot.namespace_iface_file)

                proxy.members.insert(0, proxy_member)

            # Remove the overload from the list.
            spec.module.overloads.remove(overload)

            # Add the overload to the proxy.
            overload.common = proxy_member
            overload.no_typehint = True

            proxy.overloads.insert(0, overload)

            # Remove the overload's first argument.
            del overload.py_signature.args[0]

            slot_extender = False
        else:
            slot_extender = True

        return slot_extender

    # Remove from the list.
    spec.module.overloads.remove(overload)

    if arg_enum is not None:
        _enum_iface_file_is_used(arg_enum, arg_module)
        arg_enum.py_name.used = True

    # See if there is already a member or create a new one.
    inject_equality_slot = False

    for arg_member in arg_members:
        if arg_member.py_slot is global_slot.py_slot:
            break
    else:
        arg_member = copy(global_slot)

        arg_member.module = arg_module
        arg_members.insert(0, arg_member)

        # Legacy enum members, when accessed as scoped values, are created on
        # the fly.  By default these members compare for equality correctly
        # (ie. 'E.M == E.M' works as expected).  However if there is another
        # equality operator defined then it will fail so we have to explicitly
        # inject the comparison.
        if SipModuleConfiguration.CustomEnums in spec.sip_module_configuration and arg0.type is ArgumentType.ENUM and arg_member.py_slot is PySlot.EQ and not is_second:
            inject_equality_slot = True

    # Move the overload to the end of the destination list.
    if is_second:
        overload.is_reflected = True

    overload.access_specifier = AccessSpecifier.PUBLIC
    overload.common = arg_member
    overload.is_global = True

    arg_overloads.append(overload)

    # Inject an additional equality slot if necessary.
    if inject_equality_slot:
        eq_overload = copy(overload)
        eq_overload.py_signature = copy(eq_overload.py_signature)
        eq_overload.py_signature.args = copy(eq_overload.py_signature.args)
        eq_overload.cpp_signature = eq_overload.py_signature

        eq_overload.py_signature.args[0].derefs = [False]
        del eq_overload.py_signature.args[1]

        arg_overloads.append(eq_overload)

    # Remove the first argument of inplace numeric operators and comparison
    # operators.
    if is_inplace_number_slot(arg_member.py_slot) or is_rich_compare_slot(arg_member.py_slot):
        # Remember if the argument was a pointer.
        if len(arg0.derefs) > 0:
            overload.dont_deref_self = True

        del overload.py_signature.args[0]

    # Remove the only argument of unary operators.
    if is_zero_arg_slot(arg_member.py_slot):
        del overload.py_signature.args[0]

    return False


def _get_extender(mod, klass):
    """ Create an extender for a class if it doesn't already exist. """

    for extender in mod.extenders:
        if extender.iface_file is klass.iface_file:
            return extender

    extender = WrappedClass(klass.iface_file, klass.py_name, scope=klass.scope,
            mro=klass.mro, real_class=klass, superclasses=klass.superclasses)

    # We insert to be consistent with old code.
    mod.extenders.insert(0, extender)

    return extender


def _add_auto_overload(spec, auto_klass, auto_overload):
    """ Add an overload that is automatically generated. """

    # Find every class that has this one in its hierarchy.
    for klass in spec.classes:
        if klass is auto_klass:
            continue

        for mro_klass in klass.mro:
            if mro_klass is auto_klass:
                # Another overload may already exist.
                for member in klass.members:
                    if member.py_name is auto_overload.common.py_name:
                        break
                else:
                    member = copy(auto_overload.common)
                    member.module = klass.iface_file.module
                    klass.members.insert(0, member)

                overload = copy(auto_overload)
                overload.common = member
                overload.is_autogenerated = False
                klass.overloads.insert(0, overload)

                if klass.iface_file.module is spec.module:
                    member.py_name.used = True

                break


def _set_mro(spec, klass, error_log, seen=None):
    """ Set the MRO for a class and add it to the list of classes so that it is
    after any classes it depends on.
    """

    # See if it has already been done.
    if fast_contains(spec.classes.by_fq_cpp_name(klass.iface_file.fq_cpp_name),
                     klass):
        return

    # Initialise the detection of recursive hierarchies.
    if seen is None:
        seen = []

    # Handle any enclosing scope.
    if klass.scope is not None:
        _set_mro(spec, klass.scope, error_log, seen=seen)

        if klass.scope.deprecated and not klass.deprecated:
            klass.deprecated = klass.scope.deprecated

    if klass.iface_file.type is IfaceFileType.CLASS:
        # The first thing is itself.
        klass.mro.append(klass)

        if klass.convert_to_subclass_code is not None:
            klass.subclass_base = klass

        # Now do it's super-classes.
        seen.append(klass)

        for superklass in klass.superclasses:
            if fast_contains(seen, superklass):
                error_log.log(
                        "recursive class hierarchy detected: '{0}' and '{1}'".format(
                                klass.iface_file.fq_cpp_name,
                                superklass.iface_file.fq_cpp_name))
                continue

            # Unions cannot be super-classes.
            if superklass.class_key is ClassKey.UNION:
                error_log.log(
                        "union '{0}' cannot be a super-class".format(
                                superklass.iface_file.fq_cpp_name))

            # Make sure the super-class's hierarchy has been done. */
            _set_mro(spec, superklass, error_log, seen=seen)

            # Append the super-class's MRO.
            for superklass_mro in superklass.mro:
                if superklass_mro not in klass.mro:
                    klass.mro.append(superklass_mro)

                if klass.iface_file.module is spec.module:
                    superklass_mro.iface_file.needed = True

                if superklass_mro.deprecated and not klass.deprecated:
                    klass.deprecated = superklass_mro.deprecated

                # If the super-class is a QObject sub-class then this one is as
                # well.
                if superklass_mro.is_qobject:
                    klass.is_qobject = True

                # If the super-class can't be assigned to then this one cannot
                # either.
                if superklass_mro.cannot_assign:
                    klass.cannot_assign = True

                # If the super-class needs a shadow then this one should have
                # one as well.
                if superklass_mro.needs_shadow:
                    klass.needs_shadow = True

                # Ensure that the sub-class base class is the furthest up the
                # hierarchy.
                if superklass_mro.subclass_base is not None:
                    klass.subclass_base = superklass_mro.subclass_base;

        seen.remove(klass)

        # If the class doesn't have an explicit meta-type then inherit from the
        # module's default.
        if klass.metatype is None and len(klass.superclasses) == 0:
            # The class may not have been defined.
            if klass.iface_file.module is not None:
                klass.metatype = klass.iface_file.module.default_metatype

        if klass.metatype is not None and klass.iface_file.module is spec.module:
            klass.metatype.used = True

        # If the class doesn't have an explicit super-type then inherit from
        # the module's default.
        if klass.supertype is None and len(klass.superclasses) == 0:
            # The class may not have been defined.
            if klass.iface_file.module is not None:
                klass.supertype = klass.iface_file.module.default_supertype

        if klass.supertype is not None:
            # If the super-type ends with '.wrapper' then assume it is the
            # default.
            if klass.supertype.name.endswith('.wrapper'):
                klass.supertype = None

        if klass.supertype is not None and klass.iface_file.module is spec.module:
            klass.supertype.used = True

    # Make sure that the module in which a sub-class convertor will be created
    # knows about the base class.
    if klass.subclass_base is not None:
        append_iface_file(klass.iface_file.module.used,
                klass.subclass_base.iface_file)

    # We can't have a shadow if the specification is incomplete, there is a
    # private dtor, there are no non-private ctors or there are private
    # abstract methods.
    if klass.is_incomplete or klass.dtor is AccessSpecifier.PRIVATE or not klass.can_create:
        klass.has_shadow = False
    else:
        # Note that we should be able to provide better support for abstract
        # private methods than we do at the moment.
        for overload in klass.overloads:
            if overload.is_abstract and overload.access_specifier is AccessSpecifier.PRIVATE:
                klass.has_shadow = False

                # It also means we cannot create an instance from Python.
                klass.can_create = False

                break

    # Add it to the new list of classes.
    spec.classes.append(klass)


def _resolve_typedefs(spec, mod, error_log):
    """ Resolve the base types for all typedefs of a module. """

    for typedef in spec.typedefs:
        if typedef.module is mod:
            _resolve_type(spec, typedef.module, typedef.scope, typedef.type,
                    error_log)


def _resolve_mapped_types(spec, mod, error_log, final_checks):
    """ Resolve the data types for mapped types based on a template. """

    for mapped_type in spec.mapped_types:
        if mapped_type.iface_file.module is mod:
            if mapped_type.type.type is ArgumentType.TEMPLATE:
                _resolve_mapped_type_types(spec, mapped_type, error_log)
            else:
                _resolve_scope_overloads(spec, mapped_type.overloads,
                        error_log, final_checks, scope=mapped_type)


def _resolve_ctors(spec, klass, error_log):
    """ Resolve the data types for a class's ctors. """

    for ctor in klass.ctors:
        _resolve_ctor_types(spec, klass, ctor, error_log)

        # Now check that the Python signature doesn't conflict with an earlier
        # one.  If there is %MethodCode then assume that it will handle any
        # potential conflicts.
        if ctor.method_code is None:
            for previous_ctor in klass.ctors:
                if previous_ctor is ctor:
                    break

                if previous_ctor.method_code is not None:
                    continue

                sig_state = _same_python_signature(spec,
                        previous_ctor.py_signature, ctor.py_signature)

                if sig_state is None:
                    # The error has already been logged.
                    break

                if sig_state:
                    error_log.log(
                            "class '{0}' has ctors with the same Python signature".format(
                                    klass.iface_file.fq_cpp_name))
                    break


        if klass.deprecated and not ctor.deprecated :
            ctor.deprecated = klass.deprecated   


def _transform_casts(spec, klass, error_log):
    """ Resolve the data type for a list of casts. """

    for cast in klass.casts:
        _resolve_type(spec, klass.iface_file.module, klass, cast, error_log)

        if cast.type is ArgumentType.NONE:
            # The error has already been logged.
            pass
        elif cast.type is not ArgumentType.CLASS:
            error_log.log(
                    "operator cast '{0}' must be to a class".format(
                            klass.iface_file.fq_cpp_name))


def _add_default_copy_ctor(klass):
    """ Add a default copy ctor if required. """

    # See if there is a private copy ctor in the hierarchy.
    for mro_klass in klass.mro:
        for ctor in mro_klass.ctors:
            # See if is a copy ctor.
            if len(ctor.py_signature.args) != 1:
                continue

            arg = ctor.py_signature.args[0]
 
            if len(arg.derefs) == 0 and arg.is_reference:
                if arg.type is ArgumentType.CLASS and arg.definition.iface_file is mro_klass.iface_file:
                    break
        else:
            continue

        # If the copy ctor is private then the class can't be copied.
        if ctor.access_specifier is AccessSpecifier.PRIVATE:
            klass.cannot_copy = True
            return

        # If the ctor is in the class itself then there is nothing to do.
        if mro_klass is klass.mro[0]:
            return
 
        # Otherwise we need to create a default.
        break
 
    # Create a default public copy ctor.
    arg = Argument(ArgumentType.CLASS, definition=klass, is_reference=True,
            is_const=True, is_in=True)
    result = Argument(ArgumentType.VOID)
    signature = Signature(args=[arg], result=result)
    ctor = Constructor(AccessSpecifier.PUBLIC, py_signature=signature,
            cpp_signature=signature)

    if klass.deprecated and not ctor.deprecated :
        ctor.deprecated = klass.deprecated

    if not klass.is_abstract:
        klass.can_create = True

    # Append it to the list.
    klass.ctors.append(ctor)


def _resolve_scope_overloads(spec, overloads, error_log, final_checks,
        scope=None):
    """ Resolve the data types for a scope's overloads. """

    for overload in overloads:
        _resolve_func_types(spec, overload.common.module, scope, overload,
                error_log, final_checks)

        # Now check that the Python signature doesn't conflict with an earlier
        # one.  If there is %MethodCode then assume that it will handle any
        # potential conflicts.
        if overload.method_code is None and spec.is_strict:
            for previous_overload in overloads:
                if previous_overload is overload:
                    break

                if previous_overload.common is not overload.common:
                    continue

                if previous_overload.method_code is not None:
                    continue

                sig_state = _same_python_signature(spec,
                        previous_overload.py_signature, overload.py_signature)

                if sig_state is None:
                    # The error has already been logged.
                    break

                if sig_state:
                    _log_overload_error(error_log,
                            "has overloaded functions with the same Python signature",
                            overload, scope=scope)
                    break

        if isinstance(scope, WrappedClass):
            if scope.deprecated and not overload.deprecated :
                overload.deprecated = scope.deprecated

            if overload.is_abstract:
                scope.is_abstract = True


# The supported enum base types.  Note that we use the STRING types and the
# BYTE types because there is no opportunity to apply the /PyInt/ annotation
# and there can be no confusion about context.  We still need the BYTE types
# because /PyInt/ could have been specified in a typedef.
_ENUM_BASE_TYPES = (
    ArgumentType.STRING,
    ArgumentType.SSTRING,
    ArgumentType.USTRING,
    ArgumentType.BYTE,
    ArgumentType.SBYTE,
    ArgumentType.UBYTE,
    ArgumentType.SHORT,
    ArgumentType.USHORT,
    ArgumentType.INT,
    ArgumentType.UINT,
    ArgumentType.BOOL,
)

# ABI v14 also supports integer base types larger than int.
_ENUM_BASE_TYPES_V14 = (
    ArgumentType.LONG,
    ArgumentType.ULONG,
    ArgumentType.LONGLONG,
    ArgumentType.ULONGLONG,
)

def _resolve_enums(spec, error_log):
    """ Resolve the base types for all the enums. """

    for enum in spec.enums:
        base_type = enum.enum_base_type

        if base_type is None:
            # There is no explicit C++ base type so if the Python base type
            # implies it has unsigned value then default the C++ base type to
            # unsigned.
            if enum.base_type is EnumBaseType.UINT_ENUM:
                enum.enum_base_type = Argument(ArgumentType.UINT)

            continue

        _resolve_type(spec, enum.module, enum.scope, base_type, error_log)

        if len(base_type.derefs) != 0:
            bad_base_type = True
        elif base_type.type in _ENUM_BASE_TYPES:
            bad_base_type = False
        elif spec.target_abi >= (14, 0) and base_type.type in _ENUM_BASE_TYPES_V14:
            bad_base_type = False
        else:
            bad_base_type = True

        if bad_base_type:
            error_log.log(f"unsupported enum base type",
                    source_location=base_type.source_location)

        # The default is int.
        if base_type.type is ArgumentType.INT:
            enum.enum_base_type = None


def _resolve_variables(spec, mod, error_log):
    """ Resolve the data types for the variables of a module. """

    for variable in spec.variables:
        if variable.module is mod:
            _resolve_variable_type(spec, variable, error_log)


def _get_visible_py_members(spec, klass):
    """ Set the list of visible Python member functions for a class. """

    for mro_klass in klass.mro:
        for member in mro_klass.members:
            # See if it is already in the list.  This has the desired side
            # effect of eliminating any functions that have an implementation
            # closer to this class in the hierarchy.  This is the only reason
            # to define private functions.
            for visible_member in klass.visible_members:
                if visible_member.member.py_name is member.py_name:
                    break
            else:
                visible_member = VisibleMember(member, mro_klass)
                klass.visible_members.insert(0, visible_member)

                for overload in mro_klass.overloads:
                    if overload.common is member:
                        need_types = False

                        # If the visible overload is abstract then it hasn't
                        # had a concrete implementation so this class must also
                        # be abstract.
                        if overload.is_abstract:
                            klass.is_abstract = True

                        if klass.iface_file.module is spec.module and (klass is mro_klass or (overload.access_specifier is AccessSpecifier.PROTECTED and klass.has_shadow)):
                            need_types = True
                            member.py_name.used = True

                        _iface_files_are_used_by_overload(spec,
                                klass.iface_file.used, overload,
                                need_types=need_types);


def _get_virtuals(spec, klass, error_log):
    """ Get all the virtuals for a particular class. """

    # Copy the collected virtuals of each super-class updating from what we
    # find in this class.
    for superklass in klass.superclasses:
        for virtual_overload in superklass.virtual_overloads:
            implicit = True

            for overload in klass.overloads:
                if virtual_overload.overload.cpp_name != overload.cpp_name:
                    continue

                implicit = False

                if overload.is_final:
                    break

                # See if it re-implements rather than hides.
                if _same_cpp_overload(spec, virtual_overload.overload, overload):
                    # What if is is private?
                    overload.is_virtual = True
                    overload.is_virtual_reimplementation = True

                    # Use the base implementation's virtual handler code if
                    # there is any.  We cannot just use its virtual handler
                    # because this re-implementation may have different
                    # annotations which means the complete handler would be
                    # different.  In practice there is no reason why it would
                    # be different (and maybe this should be detected as an
                    # error) but if they are the same then the same handler
                    # will eventually be chosen.
                    if overload.virtual_catcher_code is None:
                        overload.virtual_catcher_code = virtual_overload.overload.virtual_catcher_code

                    # Use the base implementation's virtual error handler if
                    # one isn't explicitly specified.
                    if overload.virtual_error_handler is None:
                        overload.virtual_error_handler = virtual_overload.overload.virtual_error_handler

                    _add_virtual_overload(spec, overload, klass, error_log)

            # Add it if it wasn't explicitly mentioned in the class.
            if implicit:
                _add_virtual_overload(spec, virtual_overload.overload, klass,
                        error_log)

    # Handle any new virtuals.
    for overload in klass.overloads:
        if overload.is_virtual and not overload.is_virtual_reimplementation and not overload.is_final:
            _add_virtual_overload(spec, overload, klass, error_log)


def _add_virtual_overload(spec, overload, klass, error_log):
    """ Add an overload to the list of virtuals for a class. """

    # If this class is defined in the main module then make sure the virtuals
    # have a handler.
    if klass.iface_file.module is spec.module:
        virtual_handler = _get_virtual_handler(spec, overload, klass,
                error_log)
        virtual_error_handler = _get_virtual_error_handler(spec, overload,
                klass, error_log);

        # Make sure we get the name.
        overload.common.py_name.used = True

        # Make sure we have the interface files and type definitions for the
        # virtual handler.
        _iface_files_are_used_by_overload(spec, spec.module.used, overload,
                need_types=True)
    else:
        virtual_handler = None
        virtual_error_handler = None

    # Add it to the class.
    virtual_overload = VirtualOverload(overload, virtual_handler,
            virtual_error_handler)
    klass.virtual_overloads.insert(0, virtual_overload)


def _get_virtual_error_handler(spec, overload, klass, error_log):
    """ Get the virtual error handler for a function. """

    # Handle the trivial case.
    if overload.no_virtual_error_handler:
        return None

    klass_mod = klass.iface_file.module

    # Check the function itself.
    handler_name = overload.virtual_error_handler

    if handler_name is None:
        # Check the class hierarchy.
        for mro_klass in klass.mro:
            handler_name = mro_klass.virtual_error_handler

            if handler_name is not None:
                break
        else:
            # Check the class's module.
            handler_name = klass_mod.default_virtual_error_handler

            if handler_name is None:
                # Check the module hierarchy.
                for mod in klass_mod.all_imports:
                    handler_name = mod.default_virtual_error_handler

                    if handler_name is not None:
                        break
                else:
                    return None

    # Find the handler with the name.
    for handler in spec.virtual_error_handlers:
        if handler.name == handler_name:
            break
    else:
        error_log.log(
                "unknown virtual error handler '{0}'".format(handler_name))
        return None

    # Assign it an index if we need to import the handler.
    if klass_mod is not handler.module and handler.handler_nr < 0:
        handler.handler_nr = handler.module.nr_virtual_error_handlers
        handler.module.nr_virtual_error_handlers += 1

    return handler


def _get_virtual_handler(spec, overload, klass, error_log):
    """ Get the virtual handler for an overload. """

    # See if there is an existing handler that is suitable.
    for handler in spec.virtual_handlers:
        if _check_virtual_handler(spec, overload, handler):
            return handler

    # Create a new one.
    handler = VirtualHandler(overload.cpp_signature, overload.py_signature,
            overload.virtual_catcher_code)

    handler.handler_nr = spec.nr_virtual_handlers
    spec.nr_virtual_handlers += 1

    if overload.factory or overload.transfer is Transfer.TRANSFER_BACK:
        handler.transfer_result = True

    if overload.abort_on_exception:
        handler.abort_on_exception = True

    spec.virtual_handlers.insert(0, handler)

    return handler


def _check_virtual_handler(spec, overload, virtual_handler):
    """ Return True if a virtual handler is appropriate for an overload. """

    if overload.virtual_catcher_code is not virtual_handler.virtual_catcher_code:
        return False

    if (overload.factory or overload.transfer is Transfer.TRANSFER_BACK) and  not virtual_handler.transfer_result:
        return False

    if overload.abort_on_exception is not virtual_handler.abort_on_exception:
        return False

    if not same_argument_type(spec, overload.py_signature.result, virtual_handler.py_signature.result):
        return False

    if overload.py_signature.result.allow_none is not virtual_handler.py_signature.result.allow_none:
        return False

    if overload.py_signature.result.disallow_none is not virtual_handler.py_signature.result.disallow_none:
        return False

    if not same_signature(spec, overload.py_signature, virtual_handler.py_signature):
        return False

    # Take into account the argument directions in the Python signatures.
    for arg1, arg2 in zip(overload.py_signature.args, virtual_handler.py_signature.args):
        if arg1.is_in is not arg2.is_in:
            return False

        if arg1.is_out is not arg2.is_out:
            return False

    if overload.py_signature is overload.cpp_signature and virtual_handler.py_signature is virtual_handler.cpp_signature:
        return True

    if not same_argument_type(spec, overload.cpp_signature.result, virtual_handler.cpp_signature.result):
        return False

    return same_signature(spec, overload.cpp_signature,
            virtual_handler.cpp_signature)


def _resolve_mapped_type_types(spec, mapped_type, error_log):
    """ Resolve the types of a mapped type based on a template. """

    template_types = mapped_type.type.definition.types

    for template_type in template_types.args:
        # Leave templates as they are.
        if template_type.type is not ArgumentType.TEMPLATE:
            _resolve_type(spec, mapped_type.iface_file.module, None,
                    template_type, error_log, allow_defined=True)

    # Make sure that the signature result won't cause problems.
    template_types.result = None

    _iface_files_are_used_by_signature(mapped_type.iface_file.used,
            template_types)


def _resolve_ctor_types(spec, scope, ctor, error_log):
    """ Resolve the types of a ctor. """

    # Handle any exceptions.
    _set_needed_exceptions(spec, scope.iface_file.module, ctor.throw_args)

    # Handle any C++ signature.
    if ctor.cpp_signature is not None and ctor.cpp_signature is not ctor.py_signature:
        for arg in ctor.cpp_signature.args:
            _resolve_type(spec, scope.iface_file.module, scope, arg, error_log,
                    allow_defined=True)
 
    # Handle the Python signature.
    for arg_nr, arg in enumerate(ctor.py_signature.args):
        _resolve_type(spec, scope.iface_file.module, scope, arg, error_log)

        if arg.type is ArgumentType.NONE:
            # The error has already been logged.
            continue

        if not _supported_type(spec, scope, None, arg, error_log):
            error_log.log(
                    "argument {0} of ctor '{1}' has an unsupported type and/or annotation for a Python signature - provide a valid type, %MethodCode and a C++ signature".format(
                            arg_nr + 1, scope.iface_file.fq_cpp_name))
            continue

        _iface_file_is_used(scope.iface_file.used, arg)
        _scope_default_value(spec, scope, arg)


def _resolve_func_types(spec, mod, scope, overload, error_log, final_checks):
    """ Resolve the types of a function. """

    # Handle any exceptions.
    _set_needed_exceptions(spec, mod, overload.throw_args)

    # Handle any C++ signature.
    if overload.cpp_signature is not overload.py_signature:
        result = overload.cpp_signature.result

        _resolve_type(spec, mod, scope, result, error_log, allow_defined=True)

        if (result.type is not ArgumentType.VOID or len(result.derefs) != 0) and overload.is_virtual and not _supported_type(spec, scope, overload, result, error_log) and overload.virtual_catcher_code is None:
            _log_overload_error(error_log,
                    "has an unsupported virtual function return type - provide %VirtualCatcherCode",
                    overload, scope=scope)

        for arg in overload.cpp_signature.args:
            _resolve_type(spec, mod, scope, arg, error_log, allow_defined=True)
 
    # Handle the Python signature.
    _resolve_py_signature_types(spec, mod, scope, overload, error_log,
            final_checks)

    result = overload.py_signature.result

    # These slots must return Py_ssize_t.
    if is_ssize_return_slot(overload.common.py_slot):
        if spec.target_abi >= (13, 0):
            required_types = (ArgumentType.SSIZE, )
        else:
            required_types = (ArgumentType.SSIZE, ArgumentType.INT)

        if result.type not in required_types or len(result.derefs) != 0 or result.is_reference or result.is_const:
            _log_overload_error(error_log, "must return a Py_ssize_t",
                    overload, scope=scope)

    # These slots must return int.
    if is_int_return_slot(overload.common.py_slot):
        if result.type is not ArgumentType.INT or len(result.derefs) != 0 or result.is_reference or result.is_const:
            _log_overload_error(error_log, "must return an int", overload,
                    scope=scope)

    # These slots must return void.
    if is_void_return_slot(overload.common.py_slot):
        if result.type is not ArgumentType.VOID or len(result.derefs) != 0 or result.is_reference or result.is_const:
            _log_overload_error(error_log, "must return void", overload,
                    scope=scope)

    # These slots must return Py_hash_t.
    if is_hash_return_slot(overload.common.py_slot):
        if spec.target_abi >= (13, 0):
            required_type = ArgumentType.HASH
            required_type_name = 'Py_hash_t'
        else:
            required_type = ArgumentType.LONG
            required_type_name = 'long'

        if result.type is not required_type or len(result.derefs) != 0 or result.is_reference or result.is_const:
            _log_overload_error(error_log,
                    "must return a {0}".format(required_type_name), overload,
                    scope=scope)


def _resolve_py_signature_types(spec, mod, scope, overload, error_log,
        final_checks):
    """ Resolve the types of a Python signature. """

    result = overload.py_signature.result
    nr_derefs = len(result.derefs)

    if result.type is not ArgumentType.VOID or nr_derefs != 0:
        if overload.pyqt_method_specifier is PyQtMethodSpecifier.SIGNAL:
            _log_overload_error(error_log, "is a signal and must return void",
                    overload, scope=scope)

        _resolve_type(spec, mod, scope, result, error_log)

        # Results must be simple.
        if result.type is ArgumentType.NONE:
            # The error has already been logged.
            pass
        elif not _supported_type(spec, scope, overload, result, error_log):
            if overload.cpp_signature is overload.py_signature or overload.method_code is None:
                _log_overload_error(error_log,
                        "has an unsupported return type - provide %MethodCode and a {0} signature".format(
                                'C' if spec.c_bindings else 'C++'),
                        overload, scope=scope)

        # Additional checks for results that are copied.
        if nr_derefs == 0 and not result.is_reference:
            # Make sure a mapped type can be copied or moved.
            if result.type is ArgumentType.MAPPED:
                mapped_type = result.definition

                if mapped_type.no_assignment_operator and not mapped_type.movable:
                    _log_overload_error(error_log,
                            "the mapped type result needs an assignment operator or be movable",
                            overload, scope=scope)

    for arg_nr, arg in enumerate(overload.py_signature.args):
        _resolve_type(spec, mod, scope, arg, error_log)

        if arg.type is ArgumentType.NONE:
            continue

        # Note signal arguments are restricted in their types because we don't
        # (yet) support handwritten code for them.
        if overload.pyqt_method_specifier is PyQtMethodSpecifier.SIGNAL:
            if not _supported_type(spec, scope, overload, arg, error_log):
                _log_overload_error(error_log,
                        "argument {0} has an unsupported type and/or annotation for a Python signature".format(
                                arg_nr + 1),
                        overload, scope=scope)

        elif not _supported_type(spec, scope, overload, arg, error_log, outputs=True):
            if overload.is_virtual:
                _log_overload_error(error_log,
                        "argument {0} has an unsupported type and/or annotation for a Python signature - provide a valid type, %MethodCode, %VirtualCatcherCode and a C++ signature".format(
                                arg_nr + 1),
                        overload, scope=scope)

            _log_overload_error(error_log,
                    "argument {0} has an unsupported type and/or annotation for a Python signature - provide a valid type, %MethodCode and a C++ signature".format(
                            arg_nr + 1),
                    overload, scope=scope)

        # Check that the argument support /Array/.
        if arg.type in (ArgumentType.CLASS, ArgumentType.MAPPED) and arg.array is ArrayArgument.ARRAY:
            # This is delayed because the class must be complete.  Be careful
            # not to pass any non-local changing variables.
            local_arg_nr = arg_nr
            final_checks.append(
                    lambda: _check_array_support(overload, local_arg_nr, scope,
                            error_log))

        # If the type of the argument is a /Movable/ mapped type then add
        # /Transfer/.
        if arg.type is ArgumentType.MAPPED and arg.definition.movable:
            arg.transfer = Transfer.TRANSFER

        if scope is not None:
            _scope_default_value(spec, scope, arg)


def _check_array_support(overload, arg_nr, scope, error_log):
    """ Check that an argument supports /Array/. """

    arg = overload.py_signature.args[arg_nr]

    if len(arg.derefs) != 1 or not arg.is_in or arg.is_reference:
        _log_overload_error(error_log,
                "argument {0} is a mapped type or class with /Array/ and so must be a pointer".format(
                        arg_nr + 1),
                overload, scope=scope)

    if arg.type is ArgumentType.MAPPED and arg.definition.no_release:
        _log_overload_error(error_log,
                "argument {0} is a mapped type that does not support /Array/ because /NoRelease/ has been specified for it".format(
                        arg_nr + 1),
                overload, scope=scope)

    if arg.type is ArgumentType.CLASS and not arg.definition.needs_array_helper:
        _log_overload_error(error_log,
                "argument {0} is a class that does not support /Array/".format(
                        arg_nr + 1),
                overload, scope=scope)


# Various type classifications.
_CLASS_TYPES = (ArgumentType.MAPPED, ArgumentType.CLASS)
_SIMPLE_TYPES = (ArgumentType.CFLOAT, ArgumentType.FLOAT, ArgumentType.CDOUBLE,
        ArgumentType.DOUBLE, ArgumentType.ENUM, ArgumentType.BOOL,
        ArgumentType.CBOOL, ArgumentType.BYTE, ArgumentType.SBYTE,
        ArgumentType.UBYTE, ArgumentType.USHORT, ArgumentType.SHORT,
        ArgumentType.UINT, ArgumentType.CINT, ArgumentType.INT,
        ArgumentType.ULONG, ArgumentType.LONG, ArgumentType.ULONGLONG,
        ArgumentType.LONGLONG, ArgumentType.HASH, ArgumentType.SSIZE,
        ArgumentType.SIZE, ArgumentType.PYOBJECT, ArgumentType.PYTUPLE,
        ArgumentType.PYLIST, ArgumentType.PYDICT, ArgumentType.PYCALLABLE,
        ArgumentType.PYSLICE, ArgumentType.PYTYPE, ArgumentType.PYBUFFER,
        ArgumentType.PYENUM, ArgumentType.CAPSULE)
_POINTER_TYPES = (ArgumentType.STRUCT, ArgumentType.UNION, ArgumentType.VOID)
_STRING_TYPES = (ArgumentType.ASCII_STRING, ArgumentType.LATIN1_STRING,
        ArgumentType.UTF8_STRING, ArgumentType.SSTRING, ArgumentType.USTRING,
        ArgumentType.STRING, ArgumentType.WSTRING)


def _resolve_variable_type(spec, variable, error_log):
    """ Resolve the type of a variable. """

    if spec.target_abi < (14, 0) and variable.scope is None:
        if variable.get_code is not None or variable.set_code is not None:
            error_log.log("%GetCode or %SetCode cannot be specified for global variables")

    bad_type = True
    variable_type = variable.type

    _resolve_type(spec, variable.module, variable.scope, variable_type,
            error_log)

    if variable_type is ArgumentType.NONE:
        return

    if variable_type.type in _CLASS_TYPES:
        # Class, Class & and Class * are supported.
        if len(variable_type.derefs) <= 1:
            bad_type = False

    elif variable_type.type in _STRING_TYPES:
        # (signed/unsigned) char, (signed/unsigned) char *, wchar_t, wchar_t *
        # are supported.
        if not variable_type.is_reference and len(variable_type.derefs) <= 1:
            bad_type = False

    elif variable_type.type in _SIMPLE_TYPES:
        # These are supported without pointers or references.
        if not variable_type.is_reference and len(variable_type.derefs) == 0:
            bad_type = False

    elif variable_type.type in _POINTER_TYPES:
        # A simple pointer is supported.
        if not variable_type.is_reference and len(variable_type.derefs) == 1:
            bad_type = False

    if bad_type and (variable.get_code is None or (not variable.no_setter and variable.set_code is None)):
        if variable.no_setter:
            set_s = ''
        else:
            set_s = " and %SetCode"

        error_log.log(f"'{variable.fq_cpp_name}' has an unsupported type and/or annotation - provide %GetCode{set_s}")
 
    if variable.access_code is not None:
        if spec.target_abi >= (14, 0):
            error_log.log(f"'{variable.fq_cpp_name}' has %AccessCode which is not supported by ABI v14 and later, use %GetCode instead")
        elif variable_type.type is not ArgumentType.CLASS:
            error_log.log(f"'{variable.fq_cpp_name}' has %AccessCode but isn't a class instance")

    if variable.scope is not None:
        _iface_file_is_used(variable.scope.iface_file.used, variable_type)
    else:
        _iface_file_is_used(variable.module.used, variable_type)

    # Scoped variables need a handler unless they have %AccessCode.
    if spec.target_abi < (14, 0) and variable.access_code is None:
        if variable.scope is not None and not variable.scope.is_hidden_namespace:
            variable.needs_handler = True
            variable.scope.has_variable_handlers = True


def _supported_type(spec, klass, overload, arg, error_log, outputs=False):
    """ See if a type and annotations are supported by the generated code. """

    if arg.type in _CLASS_TYPES:
        if arg.is_reference:
            if len(arg.derefs) == 0:
                _default_input(arg)
                return True

            if len(arg.derefs) == 1 and outputs:
                _default_output(arg)
                return True

        elif len(arg.derefs) == 0:
            _ensure_input(klass, overload, arg, error_log)
            return True

        elif len(arg.derefs) == 1:
            if outputs:
                _default_input(arg)
            else:
                _ensure_input(klass, overload, arg, error_log)

            return True

        elif len(arg.derefs) == 2 and outputs:
            _default_output(arg)
            return True

    elif arg.type in _STRING_TYPES:
        if arg.is_reference:
            if len(arg.derefs) <= 1 and outputs:
                _default_output(arg)
                return True

        elif len(arg.derefs) == 0:
            _ensure_input(klass, overload, arg, error_log)
            return True

        elif len(arg.derefs) == 1:
            if spec.target_abi < (14, 0) and arg.disallow_none:
                return False

            if outputs:
                _default_input(arg)
            else:
                _ensure_input(klass, overload, arg, error_log)

            return True

        elif len(arg.derefs) == 2 and outputs:
            _default_output(arg)
            return True

    elif arg.type in _SIMPLE_TYPES:
        if arg.is_reference:
            if arg.is_const:
                _ensure_input(klass, overload, arg, error_log)
                return True

            if len(arg.derefs) == 0 and outputs:
                _default_output(arg)
                return True

        elif len(arg.derefs) == 0:
            _ensure_input(klass, overload, arg, error_log)
            return True

        elif len(arg.derefs) == 1 and outputs:
            _default_output(arg)
            return True

    elif arg.type in _POINTER_TYPES:
        if arg.is_reference:
            if len(arg.derefs) == 1 and outputs:
                _default_output(arg)
                return True

        elif len(arg.derefs) == 1:
            _ensure_input(klass, overload, arg, error_log)
            return True

        elif len(arg.derefs) == 2 and outputs:
            _default_output(arg)
            return True

    elif arg.type is ArgumentType.ELLIPSIS:
        # This can only appear in argument lists without * or &.
        _ensure_input(klass, overload, arg, error_log)
        return True

    # Unsupported if we got this far.
    return False


def _ensure_input(klass, overload, arg, error_log):
    """ Ensure the direction of an argument is an input. """

    if arg.is_out:
        _log_overload_error(error_log,
                "has an invalid argument type for /Out/", overload,
                scope=klass)

    arg.is_in = True


def _default_input(arg):
    """ Default the direction of an argument to an input. """

    if not arg.is_out:
        arg.is_in = True


def _default_output(arg):
    """ Default the direction of an argument to an output unless the argument
    is const.
    """

    if not arg.is_out and not arg.is_in:
        if arg.is_const:
            arg.is_in = True
        else:
            arg.is_out = True


def _same_cpp_overload(spec, overload1, overload2):
    """ Compare two overloads and return True if they are the same. """

    # They must both be const, or both not.
    if overload1.is_const is not overload2.is_const:
        return False

    return same_signature(spec, overload1.cpp_signature,
            overload2.cpp_signature)


def _same_python_signature(spec, signature1, signature2):
    """ See if two Python signatures are the same as far as Python is
    concerned.  Return None if any argument's type is unknown.
    """

    a1 = a2 = -1

    while True:
        a1, arg1 = _next_significant_arg(signature1, a1)
        a2, arg2 = _next_significant_arg(signature2, a2)

        if arg1 is not None and arg1.type is ArgumentType.NONE:
            return None

        if arg2 is not None and arg2.type is ArgumentType.NONE:
            return None

        if a1 < 0 or a2 < 0:
            break

        if not same_argument_type(spec, arg1, arg2, strict=False):
            return False

    return a1 < 0 and a2 < 0


def _next_significant_arg(signature, a):
    """ Return the next significant argument from a Python signature (ie. one
    that is not optional or an output only argument.  Return -1 if there isn't
    one.
    """

    a += 1

    while a < len(signature.args):
        arg = signature.args[a]

        if arg.default_value is not None:
            break

        if arg.type is ArgumentType.NONE or arg.is_in:
            return a, arg

        a += 1

    return -1, None


def _scope_default_value(spec, klass, arg):
    """ Add an explicit scope to the default value of an argument if possible.
    """

    # We do a quick check to see if we need to do anything.  This means we can
    # limit the times we need to copy the default value.  It needs to be copied
    # because it will be shared by class versions that have been created on the
    # fly and it may need to be scoped differently for each of those versions.
    # Note that, as we no longer support API versions, this comment may no
    # longer apply.
    if arg.default_value is None:
        return

    for value in arg.default_value:
        if value.value_type is ValueType.SCOPED and value.value.is_simple:
            break
    else:
        return

    # It's not certain that we will do anything, but we assume we will and
    # start copying.
    new_default_value = []

    for value in arg.default_value:
        # Make the copy.
        new_value = copy(value)
        new_default_value.append(new_value)

        # Skip this part of the expression if it isn't a named value or it
        # already has a scope.
        if value.value_type is not ValueType.SCOPED or not value.value.is_simple:
            continue

        # Search the class hierarchy for an enum member with the same name.  If
        # we don't find one, leave it as it is (the compiler will find out if
        # this is a problem).
        original_name = value.value.base_name

        for mro_klass in klass.mro:
            for enum in spec.enums:
                if enum.scope is not mro_klass:
                    continue

                for enum_member in enum.members:
                    if enum_member.cpp_name == original_name:
                        # Take the scope from the class that the enum was
                        # defined in.
                        new_name = ScopedName(mro_klass.iface_file.fq_cpp_name)
                        new_name.append(original_name)
                        new_value.value = new_name

                        # Nothing more to do.
                        break
                else:
                    continue

                break
            else:
                continue

            break

    arg.default_value = new_default_value


def _resolve_type(spec, mod, scope, type, error_log, allow_defined=False):
    """ Resolve a type if possible. """

    # Loop until we've got to a base type.
    while type.type is ArgumentType.DEFINED:
        scoped_name = type.definition
        type.type = ArgumentType.NONE

        # Search the local scopes unless what we are looking for has an
        # explicit global scope.
        if not scoped_name.is_absolute:
            klass_scope = scope
            while klass_scope is not None:
                if klass_scope.iface_file.type is IfaceFileType.CLASS:
                    _search_class_scope(spec, klass_scope, scoped_name, type)
                else:
                    _search_scope(spec, klass_scope, scoped_name, type)

                if type.type is not ArgumentType.NONE:
                    break

                klass_scope = klass_scope.scope

            if type.type is not ArgumentType.NONE:
                break

            # We now need an absolute name.
            scoped_name = scoped_name.absolute

        _name_lookup(spec, mod, scoped_name, type)

        if type.type is ArgumentType.NONE:
            if allow_defined:
                type.type = ArgumentType.DEFINED
            else:
                error_log.log("'{0}' is undefined".format(scoped_name),
                        source_location=type.source_location)

            return

    # See if the type refers to an instantiated template.
    _resolve_instantiated_class_template(spec, type)

    # Replace the base type if it has been mapped.
    if type.type in (ArgumentType.STRUCT, ArgumentType.UNION, ArgumentType.TEMPLATE):
        _search_mapped_types(spec, mod, type)

        # If we still have a template then see if we need to automatically
        # instantiate it.
        if type.type is ArgumentType.TEMPLATE:
            for mapped_type_template in spec.mapped_type_templates:
                if mapped_type_template.mapped_type.type.definition.cpp_name == type.definition.cpp_name and same_template_signature(mapped_type_template.mapped_type.type.definition.types, type.definition.types, deep=True):
                    _instantiate_mapped_type_template(spec, mod,
                            mapped_type_template, type, error_log)
                    break

    # If we are in the main module then mark any generated types as being
    # needed.
    if mod is spec.module:
        _set_needed_type(type)


def _set_needed_type(arg):
    """ Specify that a generated type is needed. """

    if arg.type in (ArgumentType.CLASS, ArgumentType.MAPPED):
        arg.definition.iface_file.needed = True
    elif arg.type is ArgumentType.ENUM:
        arg.definition.needed = True


def _set_needed_exceptions(spec, mod, throw_args):
    """ Specify that a set of thrown arguments are needed. """

    if mod is spec.module and throw_args is not None and throw_args.arguments is not None:
        for exception in throw_args.arguments:
            _set_needs_exception(exception)


def _set_needs_exception(exception):
    """ Specify that an exception is needed. """

    if exception.class_exception is not None:
        exception.class_exception.iface_file.needed = True
    else:
        exception.needed = True


def _resolve_instantiated_class_template(spec, type):
    """ If the type corresponds to a previously instantiated class template
    then replace it with the class that was created.
    """

    if type.type is not ArgumentType.TEMPLATE:
        return

    template = type.definition
    template_signature = template.types

    for arg in template_signature.args:
        _resolve_instantiated_class_template(spec, arg)

    for klass in spec.classes:
        if klass.template is not None and klass.template.cpp_name == template.cpp_name and same_signature(spec, klass.template.types, template_signature):
            type.type = ArgumentType.CLASS
            type.definition = klass
            break


def _instantiate_mapped_type_template(spec, mod, mapped_type_template, type,
        error_log):
    """ Instantiate a mapped type template. """

    expansions = template_expansions(
            mapped_type_template.mapped_type.type.definition.types,
            type.definition.types,
            declared_names=mapped_type_template.signature)

    iface_file = find_iface_file(spec, mod,
            encoded_template_name(type.definition), IfaceFileType.MAPPED_TYPE,
            error_log.log, cpp_type=type)
    iface_file.module = mod

    mapped_type = MappedType(iface_file, copy(type))
    mapped_type.type.derefs = []
    mapped_type.type.is_const = False
    mapped_type.type.is_reference = False
    mapped_type.cpp_name = cached_name(spec, argument_as_str(mapped_type.type))

    if mod is spec.module:
        mapped_type.cpp_name.used = True

    proto_mapped_type = mapped_type_template.mapped_type

    mapped_type.handles_none = proto_mapped_type.handles_none
    mapped_type.movable = proto_mapped_type.movable
    mapped_type.needs_user_state = proto_mapped_type.needs_user_state
    mapped_type.no_assignment_operator = proto_mapped_type.no_assignment_operator
    mapped_type.no_copy_ctor = proto_mapped_type.no_copy_ctor
    mapped_type.no_default_ctor = proto_mapped_type.no_default_ctor
    mapped_type.no_release = proto_mapped_type.no_release
    mapped_type.pyqt_flags = proto_mapped_type.pyqt_flags

    if proto_mapped_type.type_hints is not None:
        mapped_type.type_hints = instantiate_type_hints(spec,
                proto_mapped_type.type_hints, expansions)

    used = mapped_type.iface_file.used

    mapped_type.iface_file.type_header_code = template_code_blocks(spec, used,
            proto_mapped_type.iface_file.type_header_code, expansions)

    if proto_mapped_type.convert_from_type_code is not None:
        mapped_type.convert_from_type_code = template_code(spec, used,
                proto_mapped_type.convert_from_type_code, expansions)

    if proto_mapped_type.convert_to_type_code is not None:
        mapped_type.convert_to_type_code = template_code(spec, used,
                proto_mapped_type.convert_to_type_code, expansions)

    if proto_mapped_type.release_code is not None:
        mapped_type.release_code = template_code(spec, used,
                proto_mapped_type.release_code, expansions)

    spec.mapped_types.insert(0, mapped_type)

    _replace_template_type(mapped_type, type)


def _search_class_scope(spec, scope, scoped_name, type):
    """ Search for a name in a class scope and resolve the corresponding type.
    """

    for mro_klass in scope.mro:
        _search_scope(spec, mro_klass, scoped_name, type)

        if type.type is not ArgumentType.NONE:
            break


def _search_scope(spec, scope, scoped_name, type):
    """ Search for a name in a scope and resolve the corresponding type. """

    # Prepend the scope and see if it exists.
    fq_name = ScopedName(scoped_name)
    fq_name.prepend(scope.iface_file.fq_cpp_name)

    _name_lookup(spec, scope.iface_file.module, fq_name, type)


def _name_lookup(spec, mod, scoped_name, type):
    """ Look up a name and resolve the corresponding type. """

    _search_mapped_types(spec, mod, type, scoped_name)
    if type.type is not ArgumentType.NONE:
        return

    search_typedefs(spec, scoped_name, type)
    if type.type is not ArgumentType.NONE:
        return

    _search_enums(spec, scoped_name, type)
    if type.type is not ArgumentType.NONE:
        return

    _search_classes(spec, mod, scoped_name, type)


def _search_mapped_types(spec, mod, type, scoped_name=None):
    """ Search the mapped types for a name and resolve the type. """

    # Patch back to defined types so we can use same_base_type().
    if scoped_name is not None:
        orig_name = type.definition
        type.definition = scoped_name
        type.type = ArgumentType.DEFINED

    if type.type is ArgumentType.TEMPLATE:
        name = type.definition.cpp_name
    elif type.type in (ArgumentType.STRUCT, ArgumentType.UNION, ArgumentType.DEFINED, ArgumentType.NONE):
        name = type.definition
    else:
        assert False, f"_search_mapped_types got {type.type}"

    for mapped_type in spec.mapped_types.by_readable_base_name(name.readable_base_name):
        if same_base_type(mapped_type.type, type):
            break
    else:
        # Restore because we didn't find anything.
        if scoped_name is not None:
            type.definition = orig_name
            type.type = ArgumentType.NONE

        return

    _replace_template_type(mapped_type, type)


def _replace_template_type(mapped_type, type):
    """ If a mapped type is based on a template then update the type with a
    copy that keeps the original types of the template arguments.
    """

    if mapped_type.type.type is ArgumentType.TEMPLATE:
        dst_types = None

        for arg_nr, arg in enumerate(type.definition.types.args):
            if arg.original_typedef is None:
                continue

            # Create an appropriately deep copy now that we know it is needed
            # and if it hasn't already been done.
            if dst_types is None:
                mapped_type = copy(mapped_type)
                mapped_type.type = copy(mapped_type.type)
                mapped_type.type.definition = copy(mapped_type.type.definition)
                mapped_type.type.definition.types = copy(mapped_type.type.definition.types)
                mapped_type.type.definition.types.args = [copy(arg) for arg in
                        mapped_type.type.definition.types.args]
                dst_types = mapped_type.type.definition.types

            dst_types.args[arg_nr].original_typedef = arg.original_typedef

    # Replace the template with the mapped type.
    type.type = ArgumentType.MAPPED
    type.definition = mapped_type
    type.type_hints = mapped_type.type_hints


def _merged_type_hints(type_hints, defaults):
    """ Return a TypeHints object that is the merge of another and some
    defaults.
    """

    if type_hints is None:
        return defaults

    if defaults is None:
        return type_hints

    if type_hints.hint_in is None:
        type_hints.hint_in = defaults.hint_in

    if type_hints.hint_out is None:
        type_hints.hint_out = defaults.hint_out

    if type_hints.default_value is None:
        type_hints.default_value = defaults.default_value

    return type_hints


def _search_enums(spec, scoped_name, type):
    """ Search the enums for a name and resolve the type. """

    enum = spec.enums.by_fq_cpp_name(scoped_name)
    if enum is None:
        return
    type.type = ArgumentType.ENUM
    type.definition = enum


def _search_classes(spec, mod, scoped_name, type):
    """ Search the classes for one with a particular name and resolve it as a
    type.
    """

    for klass in spec.classes.by_fq_cpp_name(scoped_name):
        # Ignore an external class unless it was declared in the same module as
        # the name is being used.
        if klass.external and klass.iface_file.module is not mod:
            continue

        type.type = ArgumentType.CLASS
        type.definition = klass
        type.type_hints = _merged_type_hints(type.type_hints,
                klass.type_hints)
        break


def _iface_files_are_used_by_signature(used, signature, need_types=False):
    """ Make sure all interface files for a signature are used. """

    if signature.result is not None:
        _iface_file_is_used(used, signature.result, need_types=need_types)

    for arg in signature.args:
        _iface_file_is_used(used, arg, need_types=need_types)


def _iface_files_are_used_by_overload(spec, used, overload, need_types=False):
    """ Make sure all interface files for an overload are used. """

    _iface_files_are_used_by_signature(used, overload.py_signature,
            need_types=need_types)

    if overload.cpp_signature is not overload.py_signature:
        _iface_files_are_used_by_signature(used, overload.cpp_signature,
                need_types=need_types)

    # Don't bother with %TypeHeaderCode from %Exception for later ABI versions.
    if spec.target_abi >= (13, 1) or (spec.target_abi >= (12, 9) and spec.target_abi < (13, 0)):
        return

    throw_args = overload.throw_args
    if throw_args is not None and throw_args.arguments is not None:
        for exception in throw_args.arguments:
            append_iface_file(used, exception.iface_file)

            if need_types:
                _set_needs_exception(exception)


def _iface_file_is_used(used, arg, need_types=False):
    """ If a type has an interface file then add it to the the given list of
    used interface files so that the header file is #included in the generated
    code.
    """

    if arg.type in (ArgumentType.CLASS, ArgumentType.MAPPED):
        iface_file = arg.definition.iface_file
    elif arg.type is ArgumentType.ENUM:
        iface_file = _get_iface_file_for_enum(arg.definition)
    else:
        iface_file = None

    if iface_file is not None:
        append_iface_file(used, iface_file)

        # For mapped type templates we also need the template arguments.  These
        # will be in the mapped type's used list (which itself will be empty
        # for non-template mapped types).
        if arg.type is ArgumentType.MAPPED:
            for used_iface_file in iface_file.used:
                append_iface_file(used, used_iface_file)

    if need_types:
        _set_needed_type(arg)


def _enum_iface_file_is_used(enum, mod):
    """ Add an enum's interface file to that used by a module. """

    enum_iface_file = _get_iface_file_for_enum(enum)

    if enum_iface_file is not None:
        append_iface_file(mod.used, enum_iface_file)


def _get_iface_file_for_enum(enum):
    """ Return the interface file for an enum, or None if it doesn't have one.
    """

    if enum.fq_cpp_name is not None:
        if enum.scope is not None:
            return enum.scope.iface_file

    return None


def _create_sorted_numbered_types(spec, mod, error_log):
    """ Create the sorted list of numbered types for a module.  For the main
    module this will be every type defined in the module.  For other modules
    this will be every type needed by the main module.
    """

    # Collect the needed types.
    for klass in spec.classes:
        if klass.iface_file.module is not mod:
            continue

        if mod is spec.module or klass.iface_file.needed:
            if klass.is_hidden_namespace:
                continue

            if spec.target_abi >= (14, 0) and is_namespace_extender(klass):
                continue

            mod.needed_types.append(Argument(ArgumentType.CLASS,
                    definition=klass, name=klass.iface_file.cpp_name))

    for mapped_type in spec.mapped_types:
        if mapped_type.iface_file.module is not mod:
            continue

        if mod is spec.module or mapped_type.iface_file.needed:
            mod.needed_types.append(Argument(ArgumentType.MAPPED,
                    definition=mapped_type, name=mapped_type.cpp_name))

    for enum in spec.enums:
        if enum.module is not mod:
            continue

        if enum.fq_cpp_name is None:
            continue

        if mod is spec.module or enum.needed:
            mod.needed_types.append(Argument(ArgumentType.ENUM,
                    definition=enum, name=enum.cached_fq_cpp_name))

    # ABI v14 includes exceptions in the list of needed types.
    if spec.target_abi >= (14, 0):
        for exception in spec.exceptions:
            if _exception_needed(spec, exception):
                mod.needed_types.append(Argument(ArgumentType.EXCEPTION,
                        definition=exception,
                        name=exception.iface_file.cpp_name))

    # Sort the list and assign type numbers.
    mod.needed_types.sort(key=lambda t: t.name.name)

    for type_nr, needed_type in enumerate(mod.needed_types):
        if needed_type.type is ArgumentType.CLASS:
            needed_type.definition.iface_file.type_nr = type_nr

            # If we find a class called QObject, assume it's Qt.
            if needed_type.name.name == 'QObject':
                if spec.pyqt_qobject is not None:
                    error_log.log(
                            "class 'QObject' has been defined more than once")

                spec.pyqt_qobject = needed_type.definition

        elif needed_type.type is ArgumentType.MAPPED:
            needed_type.definition.iface_file.type_nr = type_nr

        elif needed_type.type is ArgumentType.ENUM:
            needed_type.definition.type_nr = type_nr

        elif needed_type.type is ArgumentType.EXCEPTION:
            needed_type.definition.iface_file.type_nr = type_nr


def _exception_needed(spec, exception):
    """ Return True if an exception is needed by the module. """

    # Skip those that don't require a Python exception object to be created.
    if exception.iface_file.type is not IfaceFileType.EXCEPTION:
        return False

    if exception.builtin_base_exception is None and exception.defined_base_exception is None:
        return False

    return exception.iface_file.module is spec.module or exception.needed


def _check_properties(klass, error_log):
    """ Check that any properties are valid. """

    for prop in klass.properties:
        if find_method(klass, prop.getter) is None:
            error_log.log(
                    "property '{0}.{1}' has no getter '{3}'".format(
                            klass.py_name.name, prop.name.name, prop.getter))

        if prop.setter is not None and find_method(klass, prop.setter) is None:
            error_log.log(
                    "property '{0}.{1}' has no setter '{3}'".format(
                            klass.py_name.name, prop.name.name, prop.setter))


def _log_overload_error(error_log, text, overload, scope=None):
    """ Log an error about an overload. """

    if scope is None:
        fq_cpp_name = overload.cpp_name
    else:
        fq_cpp_name = f'{scope.iface_file.fq_cpp_name}::{overload.cpp_name}'

    error_log.log(f"'{fq_cpp_name}' {text}",
            source_location=overload.source_location)
