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

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


from xml.etree.ElementTree import Element, SubElement

from ..python_slots import is_number_slot, reflected_slot
from ..scoped_name import ScopedName, STRIP_GLOBAL
from ..specification import (AccessSpecifier, ArgumentType, ArrayArgument,
        IfaceFileType, KwArgs, PyQtMethodSpecifier, PySlot, Transfer)

from .formatters import (fmt_argument_as_py_default_value,
        fmt_argument_as_rest_ref, fmt_class_as_rest_ref, fmt_scoped_py_name,
        fmt_signature_as_cpp_declaration, fmt_value_list_as_rest_ref)


# The schema version number.
_XML_VERSION_NR = '0'


def output_xml(spec, module_name):
    """ Return the root Module element of the XML for a module. """

    # Note that we don't yet handle mapped types, templates or exceptions.

    module = spec.module

    if module.py_name != module_name:
        return None

    root = Element('Module', version=_XML_VERSION_NR, name=module.py_name)

    for klass in spec.classes:
        if klass.iface_file.module is module and not klass.external:
            _class(root, spec, module, klass)

    for klass in module.proxies:
        _class(root, spec, module, klass)

    _enums(root, spec, module)
    _variables(root, spec, module)

    for member in module.global_functions:
        _function(root, spec, member, module.overloads)

    return root


def _realname(scope, member=None):
    """ Return a 'realname' attribute containing a fully qualified C/C++ name.
    """

    if scope is None:
        # The member should have a name in this context.
        fq_cpp_name = member
    else:
        if isinstance(scope, ScopedName):
            fq_cpp_name = scope
        else:
            fq_cpp_name = scope.iface_file.fq_cpp_name

        fq_cpp_name = fq_cpp_name.cpp_stripped(STRIP_GLOBAL)

        if member is not None:
            fq_cpp_name += '::' + member

    return fq_cpp_name


def _class(parent, spec, module, klass):
    """ Output the XML for a class. """

    if klass.is_opaque:
        return SubElement(parent, 'OpaqueClass',
                name=fmt_scoped_py_name(klass.scope, klass.py_name.name))

    if klass.is_hidden_namespace:
        parent_klass = parent
    else:
        attrib = {}

        if klass.pickle_code is not None:
            attrib['pickle'] = '1'

        if klass.convert_to_type_code is not None:
            attrib['convert'] = '1'

        if klass.convert_from_type_code is not None:
            attrib['convertfrom'] = '1'

        if klass.real_class is not None:
            attrib['extends'] = klass.real_class.iface_file.module.py_name

        if klass.pyqt_flags_enums is not None:
            attrib['flagsenums'] = ' '.join(klass.pyqt_flags_enums)

        if len(klass.superclasses) != 0:
            attrib['inherits'] = ' '.join(
                    [fmt_class_as_rest_ref(sc) for sc in klass.superclasses])

        parent_klass = SubElement(parent, 'Class', attrib,
                name=fmt_scoped_py_name(klass.scope, klass.py_name.name),
                realname=_realname(klass.iface_file.fq_cpp_name))

    for ctor in klass.ctors:
        if ctor.access_specifier is not AccessSpecifier.PRIVATE:
            _ctor(parent_klass, spec, klass, ctor)

    _enums(parent_klass, spec, module, klass)
    _variables(parent_klass, spec, module, klass)

    for member in klass.members:
        _function(parent_klass, spec, member, klass.overloads, klass)


def _enums(parent, spec, module, scope=None):
    """ Output the XML for all the enums in a scope. """

    for enum in spec.enums:
        if enum.module is module and enum.scope is scope:
            if enum.py_name is None:
                for member in enum.members:
                    SubElement(parent, 'Member',
                            name=fmt_scoped_py_name(enum.scope,
                                    member.py_name.name),
                            realname=_realname(enum.scope, member.cpp_name),
                            const='1', typename='int')
            else:
                enum_el = SubElement(parent, 'Enum',
                        name=fmt_scoped_py_name(enum.scope,
                                enum.py_name.name),
                        realname=_realname(enum.fq_cpp_name))

                for member in enum.members:
                    name = fmt_scoped_py_name(enum.scope, enum.py_name.name) + '.' + member.py_name.name

                    SubElement(enum_el, 'EnumMember', name=name,
                            realname=_realname(enum.fq_cpp_name,
                                    member.cpp_name))


def _variables(parent, spec, module, scope=None):
    """ Output the XML for all the variables in a scope. """

    for variable in spec.variables:
        if variable.module is module and variable.scope is scope:
            attrib = {}

            if variable.type.is_const or scope is None:
                attrib['const'] = '1'

            if variable.is_static:
                attrib['static'] = '1'

            SubElement(parent, 'Member', attrib,
                    name=fmt_scoped_py_name(variable.scope,
                            variable.py_name.name),
                    realname=_realname(variable.fq_cpp_name),
                    typename=_typename(spec, variable.type))


def _ctor(parent, spec, scope, ctor):
    """ Output the XML for a ctor. """

    attrib = {}

    if _has_cpp_signature(ctor.cpp_signature):
        attrib['cppsig'] = _cpp_signature(spec, ctor.cpp_signature)

    function_el = SubElement(parent, 'Function', attrib,
            name=fmt_scoped_py_name(scope, '__init__'),
            realname=_realname(scope, '__init__'))

    for arg in ctor.py_signature.args:
        if arg.is_in:
            _argument(function_el, spec, arg, ctor.kw_args)

        if arg.is_out:
            _argument(function_el, spec, arg, ctor.kw_args, out=True)


def _function(parent, spec, member, overloads, scope=None):
    """ Output the XML for a function. """

    for overload in overloads:
        if overload.common is member and overload.access_specifier is not AccessSpecifier.PRIVATE:
            if overload.pyqt_method_specifier is PyQtMethodSpecifier.SIGNAL:
                attrib = {}

                if _has_cpp_signature(overload.cpp_signature):
                    attrib['cppsig'] = _cpp_signature(spec,
                            overload.cpp_signature)

                signal_el = SubElement(parent, 'Signal', attrib,
                        name=fmt_scoped_py_name(scope, member.py_name.name),
                        realname=_realname(scope, overload.cpp_name))

                for arg in overload.py_signature.args:
                    _argument(signal_el, spec, arg, overload.kw_args)
            else:
                extends = None
                is_static = (scope is None or scope.iface_file.type is IfaceFileType.NAMESPACE or overload.is_static)

                if scope is None and member.py_slot is not None and overload.py_signature.args[0].type is ArgumentType.CLASS:
                    extends = overload.py_signature.args[0].definition
                    is_static = False

                _overload(parent, spec, scope, overload, extends,
                        is_static)


def _overload(parent, spec, scope, overload, extends, is_static):
    """ Output the XML for an overload. """

    if overload.is_reflected:
        name = reflected_slot(overload.common.py_slot)
    else:
        name = None

    if name is None:
        name = overload.common.py_name.name
        cpp_name = overload.cpp_name
    else:
        cpp_name = name

    attrib = {}

    if _has_cpp_signature(overload.cpp_signature):
        attrib['cppsig'] = _cpp_signature(spec, overload.cpp_signature,
                is_const=overload.is_const)

    if overload.is_abstract:
        attrib['abstract'] = '1'

    if is_static:
        attrib['static'] = '1'

    if overload.pyqt_method_specifier is PyQtMethodSpecifier.SLOT:
        attrib['slot'] = '1'

    if overload.is_virtual:
        attrib['virtual'] = '1'

    if extends is not None:
        attrib['extends'] = fmt_scoped_py_name(extends.scope,
                extends.py_name.name)

    function_el = SubElement(parent, 'Function', attrib,
            name=fmt_scoped_py_name(scope, name),
            realname=_realname(scope, cpp_name))

    # An empty type hint specifies a void return.
    result = overload.py_signature.result

    if result.type_hints is not None and result.type_hints.hint_out is not None and result.type_hints.hint_out == '':
        no_result = True
    else:
        no_result = (result.type is ArgumentType.VOID and len(result.derefs) == 0)

    if not no_result:
        _argument(function_el, spec, overload.py_signature.result, KwArgs.NONE,
                out=True,
                transfer_result=overload.transfer is Transfer.TRANSFER_BACK)

    # Ignore the first argument of non-reflected number slots and the second
    # argument of reflected number slots.
    might_ignore = (is_number_slot(overload.common.py_slot) and len(overload.py_signature.args) == 2)

    for a, arg in enumerate(overload.py_signature.args):
        if might_ignore:
            if a == 0 and not overload.is_reflected:
                continue

            if a == 1 and overload.is_reflected:
                continue

        if arg.is_in:
            _argument(function_el, spec, arg, overload.kw_args)

        if arg.is_out:
            _argument(function_el, spec, arg, overload.kw_args, out=True)


# Argument types that imply handwritten code.
_HANDWRITTEN_CODE_TYPES = (
    ArgumentType.PYOBJECT,
    ArgumentType.PYTUPLE,
    ArgumentType.PYLIST,
    ArgumentType.PYDICT,
    ArgumentType.PYCALLABLE,
    ArgumentType.PYSLICE,
    ArgumentType.PYTYPE,
    ArgumentType.PYBUFFER,
    ArgumentType.PYENUM,
    ArgumentType.CAPSULE,
)

def _has_cpp_signature(signature):
    """ Return True if there is a C/C++ signature. """

    if signature is None:
        return False

    # See if there are any arguments that could only have come from handwritten
    # code.
    for arg in signature.args:
        if arg.type in _HANDWRITTEN_CODE_TYPES:
            return False

    return True


def _cpp_signature(spec, cpp_signature, is_const=False):
    """ Return the XML for a C++ signature. """

    args = fmt_signature_as_cpp_declaration(spec, cpp_signature,
            strip=STRIP_GLOBAL, make_public=True, as_xml=True)
    const = ' const' if is_const else ''

    return f'({args}){const}'


def _argument(parent, spec, arg, kw_args, out=False,
        transfer_result=False):
    """ Ouput the XML for an argument. """

    if arg.array is not ArrayArgument.ARRAY_SIZE:
        attrib = {}

        if not out:
            if arg.allow_none:
                attrib['allownone'] = '1'

            if arg.disallow_none:
                attrib['disallownone'] = '1'

            if arg.transfer is Transfer.TRANSFER:
                attrib['transfer'] = 'to'
            elif arg.transfer is Transfer.TRANSFER_THIS:
                attrib['transfer'] = 'this'

        if transfer_result or arg.transfer is Transfer.TRANSFER_BACK:
            attrib['transfer'] = 'back'

        SubElement(parent, 'Return' if out else 'Argument', attrib,
                typename=_typename(spec, arg, kw_args=kw_args, out=out))


def _typename(spec, arg, kw_args=KwArgs.NONE, out=False):
    """ Return the XML for a type. """

    s = ''

    # Handle the argument name.
    if not out and arg.name is not None:
        if kw_args is KwArgs.ALL or (kw_args is KwArgs.OPTIONAL and arg.default_value is not None):
            s += arg.name.name + ': '

    arg_rest_ref = fmt_argument_as_rest_ref(spec, arg, out, as_xml=True)
    s += arg_rest_ref

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

        # Try and convert the value to a reST reference.  We don't try very
        # hard but will get most cases.
        rest_ref = fmt_value_list_as_rest_ref(spec, arg.default_value)
        if rest_ref is None:
            rest_ref = fmt_argument_as_py_default_value(spec, arg,
                    arg_rest_ref, as_xml=True)

        s += rest_ref

    return s
