"""
This file opens the XML `index.xml` file generated by Doxygen
which has the following structure:

    <doxygenindex ...>
      <compound ... >
      </compound>
      ...
    </doxygenindex>

Each <compound> element has a 'kind' attribute that is one of the following:

    - define
    - dir
    - enum
    - enumvalue
    - example
    - file
    - function
    - page
    - struct
    - typedef
    - union
    - variable

Most <compound> elements have child <member> elements with their own contents
depending on the 'kind' of <compound> element they are in.

This file defines classes for each of these except for

    - dir
    - page
    - example

The remaining 'kind' values are:

    - define
    - enum
    - enumvalue
    - file
    - function
    - struct
    - typedef
    - union
    - variable

The list of classes is:

    Class     | Adds Self to  Dictionary in __init__()
    --------- | --------------------------------------
    DEFINE    => `defines`
    ENUM      => `enums`
    VARIABLE  => `variables`
    NAMESPACE => `namespaces`
    STRUCT    => `structures`
    UNION    appears to have a different purpose
    TYPEDEF   => `typedefs`
    FUNCTION  => `functions`
    GROUP     => `groups`
    FILE      => `files`
    CLASS     => `classes`

Additional classes:

    - NAMESPACE(object):
    - FUNC_ARG(object):      (becomes members of FUNCTION objects)
    - STRUCT_FIELD(object):  (becomes members of STRUCT objects)
    - GROUP(object):
    - CLASS(object):
    - XMLSearch(object):

Each of the above Dictionary variables has entries with

    - keys   = actual name of the code elements Doxygen found in the .H files.
    - values = XML node generated by `xml.etree::ElementTree`

Samples:

    'defines': {'ZERO_MEM_SENTINEL': <doc_builder.DEFINE object at 0x000001FB5D866420>,
                'LV_GLOBAL_DEFAULT': <doc_builder.DEFINE object at 0x000001FB5D866210>,
                'LV_ASSERT_OBJ': <doc_builder.DEFINE object at 0x000001FB5D1EC080>,
                'LV_TRACE_OBJ_CREATE': <doc_builder.DEFINE object at 0x000001FB5D8660F0>,

    'enums': {'lv_key_t': <doc_builder.ENUM object at 0x000001FB5D1EEB40>,
              'lv_group_refocus_policy_t': <doc_builder.ENUM object at 0x000001FB5D1E3DA0>,
              'lv_obj_flag_t': <doc_builder.ENUM object at 0x000001FB5D29F830>,
              'lv_obj_class_editable_t': <doc_builder.ENUM object at 0x000001FB5D29E300>,

    'variables': {'lv_global': <doc_builder.VARIABLE object at 0x000001FB5D1E3FE0>,
                  'lv_obj_class': <doc_builder.VARIABLE object at 0x000001FB5D1EE1E0>,
                  'lv_font_montserrat_8': <doc_builder.VARIABLE object at 0x000001FB5DAB41A0>,
                  'lv_font_montserrat_10': <doc_builder.VARIABLE object at 0x000001FB5D99D040>,

    'namespaces': {},

    'structures': {'_lv_anim_t::_lv_anim_path_para_t': <doc_builder.UNION object at 0x000001FB5C4240E0>,
                   '_lv_anim_t': <doc_builder.STRUCT object at 0x000001FB5C45F680>,
                   '_lv_animimg_t': <doc_builder.STRUCT object at 0x000001FB5C4FE390>,
                   '_lv_arc_t': <doc_builder.STRUCT object at 0x000001FB59D350A0>,

    'unions': {},

    'typedefs': {'lv_global_t': <doc_builder.TYPEDEF object at 0x000001FB5D1EFFE0>,
                 'lv_group_focus_cb_t': <doc_builder.TYPEDEF object at 0x000001FB5D1F1CA0>,
                 'lv_group_edge_cb_t': <doc_builder.TYPEDEF object at 0x000001FB5D1EE7E0>,

    'functions': {'lv_group_create': <doc_builder.FUNCTION object at 0x000001FB5D1E0470>,
                  'lv_group_delete': <doc_builder.FUNCTION object at 0x000001FB5D1F3800>,
                  'lv_group_set_default': <doc_builder.FUNCTION object at 0x000001FB5D1ECAA0>,

Additional dictionaries:
    'files': {'lv_global.h': <doc_builder.FILE object at 0x000001FB5D864E00>,
              'lv_group.h': <doc_builder.FILE object at 0x000001FB5D1EFD40>,
              'lv_group_private.h': <doc_builder.FILE object at 0x000001FB5D0D7DD0>,

    'html_files': {'lvgl': 'lvgl.html',
                   'lv_api_map_v8': 'lv_api_map_v8.html',
                   'lv_api_map_v9_0': 'lv_api_map_v9_0.html',
                   'lv_api_map_v9_1': 'lv_api_map_v9_1.html',

"""
import os
import sys
from xml.etree import ElementTree as ET

base_path = ''
xml_path = ''

EMIT_WARNINGS = True
DOXYGEN_OUTPUT = True

MISSING_FUNC = 'MissingFunctionDoc'
MISSING_FUNC_ARG = 'MissingFunctionArgDoc'
MISSING_FUNC_RETURN = 'MissingFunctionReturnDoc'
MISSING_FUNC_ARG_MISMATCH = 'FunctionArgMissing'
MISSING_STRUCT = 'MissingStructureDoc'
MISSING_STRUCT_FIELD = 'MissingStructureFieldDoc'
MISSING_UNION = 'MissingUnionDoc'
MISSING_UNION_FIELD = 'MissingUnionFieldDoc'
MISSING_ENUM = 'MissingEnumDoc'
MISSING_ENUM_ITEM = 'MissingEnumItemDoc'
MISSING_TYPEDEF = 'MissingTypedefDoc'
MISSING_VARIABLE = 'MissingVariableDoc'
MISSING_MACRO = 'MissingMacroDoc'


def warn(warning_type, *args):
    if EMIT_WARNINGS:
        args = ' '.join(str(arg) for arg in args)

        if warning_type is None:
            output = f'\033[31;1m    {args}\033[0m\n'
        else:
            output = f'\033[31;1m{warning_type}: {args}\033[0m\n'

        sys.stdout.write(output)
        sys.stdout.flush()


def build_docstring(element):
    docstring = None
    if element.tag == 'parameterlist':
        return None

    if element.text:
        docstring = element.text.strip()

    for item in element:
        ds = build_docstring(item)
        if ds:
            if docstring:
                docstring += ' ' + ds
            else:
                docstring = ds.strip()

    if element.tag == 'para':
        if docstring:
            docstring = '\n\n' + docstring

    if element.tag == 'ref':
        docstring = f':ref:`{docstring}`'

    if element.tail:
        if docstring:
            docstring += ' ' + element.tail.strip()
        else:
            docstring = element.tail.strip()

    return docstring


def read_as_xml(d):
    try:
        return ET.fromstring(d)
    except:  # NOQA
        return None


def load_xml(fle):
    fle = os.path.join(xml_path, fle + '.xml')

    with open(fle, 'rb') as f:
        d = f.read().decode('utf-8')

    # This code is to correct a bug in Doxygen. That bug incorrectly parses
    # a typedef and it causes an error to occur building the docs. The Error
    # doesn't stop the documentation from being generated, I just don't want
    # to see the ugly red output.
    #
    # if 'typedef void() lv_lru_free_t(void *v)' in d:
    #     d = d.replace(
    #         '<type>void()</type>\n        '
    #         '<definition>typedef void() lv_lru_free_t(void *v)</definition>',
    #         '<type>void</type>\n        '
    #         '<definition>typedef void(lv_lru_free_t)(void *v)</definition>'
    #     )
    #     with open(fle, 'wb') as f:
    #         f.write(d.encode('utf-8'))

    return ET.fromstring(d)


structures = {}
functions = {}
enums = {}
typedefs = {}
variables = {}
unions = {}
namespaces = {}
files = {}


# things to remove from description
# <para> </para>


class STRUCT_FIELD(object):

    def __init__(self, name, type, description, file_name, line_no):
        self.name = name
        self.type = type
        self.description = description
        self.file_name = file_name
        self.line_no = line_no


class STRUCT(object):
    _missing = MISSING_STRUCT
    _missing_field = MISSING_STRUCT_FIELD

    template = '''\
.. doxygenstruct:: {name}
   :project: lvgl
   :members:
   :protected-members:
   :private-members:
   :undoc-members:
'''

    def __init__(self, parent, refid, name, **_):
        if name in structures:
            self.__dict__.update(structures[name].__dict__)
        else:
            structures[name] = self
            self.parent = parent
            self.refid = refid
            self.name = name
            self.types = set()
            self._deps = None
            self.header_file = ''
            self.description = None
            self.fields = []
            self.file_name = None
            self.line_no = None

        if parent and refid:
            root = load_xml(refid)

            for compounddef in root:
                if compounddef.attrib['id'] != self.refid:
                    continue

                for child in compounddef:
                    if child.tag == 'includes':
                        self.header_file = os.path.splitext(child.text)[0]
                        continue

                    elif child.tag == 'location':
                        self.file_name = child.attrib['file']
                        self.line_no = child.attrib['line']

                    elif child.tag == 'detaileddescription':
                        self.description = build_docstring(child)

                    elif child.tag == 'sectiondef':
                        for memberdef in child:
                            t = get_type(memberdef)
                            description = None
                            name = ''
                            file_name = None
                            line_no = None

                            for element in memberdef:
                                if element.tag == 'location':
                                    file_name = element.attrib['file']
                                    line_no = element.attrib['line']

                                elif element.tag == 'name':
                                    name = element.text

                                elif element.tag == 'detaileddescription':
                                    description = build_docstring(element)

                            field = STRUCT_FIELD(name, t, description, file_name, line_no)
                            self.fields.append(field)

                            if t is None:
                                continue

                            self.types.add(t)

            if not self.description:
                warn(self._missing, self.name)
                warn(None, 'FILE:', self.file_name)
                warn(None, 'LINE:', self.line_no)
                warn(None)

            for field in self.fields:
                if not field.description:
                    warn(self._missing_field, self.name)
                    warn(None, 'FIELD:', field.name)
                    warn(None, 'FILE:', field.file_name)
                    warn(None, 'LINE:', field.line_no)
                    warn(None)

    def get_field(self, name):
        for field in self.fields:
            if field.name == name:
                return field

    @property
    def deps(self):
        if self._deps is None:
            self._deps = dict(
                typedefs=set(),
                functions=set(),
                enums=set(),
                structures=set(),
                unions=set(),
                namespaces=set(),
                variables=set(),
            )
            for type_ in self.types:
                if type_ in typedefs:
                    self._deps['typedefs'].add(typedefs[type_])
                elif type_ in structures:
                    self._deps['structures'].add(structures[type_])
                elif type_ in unions:
                    self._deps['unions'].add(unions[type_])
                elif type_ in enums:
                    self._deps['enums'].add(enums[type_])
                elif type_ in functions:
                    self._deps['functions'].add(functions[type_])
                elif type_ in variables:
                    self._deps['variables'].add(variables[type_])
                elif type_ in namespaces:
                    self._deps['namespaces'].add(namespaces[type_])
        return self._deps

    def __str__(self):
        return self.template.format(name=self.name)


class UNION(STRUCT):
    _missing = MISSING_UNION
    _missing_field = MISSING_UNION_FIELD

    template = '''\
.. doxygenunion:: {name}
   :project: lvgl
'''


def get_type(node):
    def gt(n):
        for c in n:
            if c.tag == 'ref':
                t = c.text.strip()
                break
        else:
            t = node.text.strip()

        return t.replace('*', '').replace('(', '').replace(')', '').strip()

    for child in node:
        if child.tag == 'type':
            return gt(child)


class VARIABLE(object):
    template = '''\
.. doxygenvariable:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        if name in variables:
            self.__dict__.update(variables[name].__dict__)
        else:
            variables[name] = self
            self.parent = parent
            self.refid = refid
            self.name = name
            self.description = None
            self.type = ''
            self.file_name = None
            self.line_no = None

        if parent is not None:
            root = load_xml(parent.refid)

            for compounddef in root:
                if compounddef.attrib['id'] != parent.refid:
                    continue

                for child in compounddef:
                    if (
                        child.tag == 'sectiondef' and
                        child.attrib['kind'] == 'var'
                    ):
                        for memberdef in child:
                            if memberdef.attrib['id'] == refid:
                                break
                        else:
                            continue

                        self.type = get_type(memberdef)

                        for element in memberdef:
                            if element.tag == 'location':
                                self.file_name = element.attrib['file']
                                self.line_no = element.attrib['line']
                            elif element.tag == 'detaileddescription':
                                self.description = build_docstring(element)

            if not self.description:
                warn(MISSING_VARIABLE, self.name)
                warn(None, 'FILE:', self.file_name)
                warn(None, 'LINE:', self.line_no)
                warn(None)

    def __str__(self):
        return self.template.format(name=self.name)


class NAMESPACE(object):
    template = '''\
.. doxygennamespace:: {name}
   :project: lvgl
   :members:
   :protected-members:
   :private-members:
   :undoc-members:
'''

    def __init__(self, parent, refid, name, **_):
        if name in namespaces:
            self.__dict__.update(namespaces[name].__dict__)
        else:
            namespaces[name] = self
            self.parent = parent
            self.refid = refid
            self.name = name
            self.description = None
            self.line_no = None
            self.file_name = None
            self.enums = []
            self.funcs = []
            self.vars = []
            self.typedefs = []
            self.structs = []
            self.unions = []
            self.classes = []

        # root = load_xml(refid)
        #
        # for compounddef in root:
        #     if compounddef.attrib['id'] != refid:
        #         continue
        #
        #     for sectiondef in compounddef:
        #         if sectiondef.tag != 'sectiondef':
        #             continue
        #
        #         enum
        #         typedef
        #         func
        #         struct
        #         union
        #
        #
        #         cls = globals()[sectiondef.attrib['kind'].upper()]
        #         if cls == ENUM:
        #             if sectiondef[0].text:
        #                 sectiondef.attrib['name'] = sectiondef[0].text.strip()
        #                 enums_.append(cls(self, **sectiondef.attrib))
        #             else:
        #                 sectiondef.attrib['name'] = None
        #                 enums_.append(cls(self, **sectiondef.attrib))
        #
        #         elif cls == ENUMVALUE:
        #             if enums_[-1].is_member(sectiondef):
        #                 enums_[-1].add_member(sectiondef)
        #
        #         else:
        #             sectiondef.attrib['name'] = sectiondef[0].text.strip()
        #             cls(self, **sectiondef.attrib)

    def __str__(self):
        return self.template.format(name=self.name)


class FUNC_ARG(object):

    def __init__(self, name, type):
        self.name = name
        self.type = type
        self.description = None


groups = {}


class GROUP(object):
    template = '''\
.. doxygengroup:: {name}
    :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        if name in groups:
            self.__dict__.update(functions[name].__dict__)
        else:
            functions[name] = self
            self.parent = parent
            self.refid = refid
            self.name = name
            self.description = None

    def __str__(self):
        return self.template.format(name=self.name)



class FUNCTION(object):
    template = '''\
.. doxygenfunction:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        if name in functions:
            self.__dict__.update(functions[name].__dict__)
        else:
            functions[name] = self
            self.parent = parent
            self.refid = refid
            self.name = name
            self.types = set()
            self.restype = None
            self.args = []
            self._deps = None
            self.description = None
            self.res_description = None
            self.file_name = None
            self.line_no = None
            self.void_return = False

        if parent is not None:
            root = load_xml(parent.refid)

            for compounddef in root:
                if compounddef.attrib['id'] != parent.refid:
                    continue

                for child in compounddef:
                    if child.tag != 'sectiondef':
                        continue

                    if child.attrib['kind'] != 'func':
                        continue

                    for memberdef in child:
                        if 'id' not in memberdef.attrib:
                            continue

                        if memberdef.attrib['id'] == refid:
                            break
                    else:
                        continue

                    break
                else:
                    continue

                break
            else:
                return

            self.restype = get_type(memberdef)

            for child in memberdef:
                if child.tag == 'type':
                    if child.text and child.text.strip() == 'void':
                        self.void_return = True

                if child.tag == 'param':
                    t = get_type(child)
                    if t is not None:
                        self.types.add(t)

                    for element in child:
                        if element.tag == 'declname':
                            arg = FUNC_ARG(element.text, t)
                            self.args.append(arg)

            for child in memberdef:
                if child.tag == 'location':
                    self.file_name = child.attrib['file']
                    self.line_no = child.attrib['line']

                elif child.tag == 'detaileddescription':
                    self.description = build_docstring(child)
                    for element in child:
                        if element.tag != 'para':
                            continue

                        for desc_element in element:
                            if desc_element.tag == 'simplesect' and desc_element.attrib['kind'] == 'return':
                                self.res_description = build_docstring(desc_element)

                            if desc_element.tag != 'parameterlist':
                                continue

                            for parameter_item in desc_element:
                                parameternamelist = parameter_item[0]
                                if parameternamelist.tag != 'parameternamelist':
                                    continue

                                parameter_name = parameternamelist[0].text

                                try:
                                    parameterdescription = parameter_item[1]
                                    if parameterdescription.tag == 'parameterdescription':
                                        parameter_description = build_docstring(parameterdescription)
                                    else:
                                        parameter_description = None
                                except IndexError:
                                    parameter_description = None

                                if parameter_name is not None:
                                    for arg in self.args:
                                        if arg.name != parameter_name:
                                            continue

                                        arg.description = parameter_description
                                        break
                                    else:
                                        warn(MISSING_FUNC_ARG_MISMATCH, self.name)
                                        warn(None, 'ARG:', parameter_name)
                                        warn(None, 'FILE:', self.file_name)
                                        warn(None, 'LINE:', self.line_no)
                                        warn(None)

            if not self.description:
                warn(MISSING_FUNC, self.name)
                warn(None, 'FILE:', self.file_name)
                warn(None, 'LINE:', self.line_no)
                warn(None)
            else:
                for arg in self.args:
                    if not arg.description:
                        warn(MISSING_FUNC_ARG, self.name)
                        warn(None, 'ARG:', arg.name)
                        warn(None, 'FILE:', self.file_name)
                        warn(None, 'LINE:', self.line_no)
                        warn(None)

                if not self.res_description and not self.void_return:
                    warn(MISSING_FUNC_RETURN, self.name)
                    warn(None, 'FILE:', self.file_name)
                    warn(None, 'LINE:', self.line_no)
                    warn(None)

        if self.restype in self.types:
            self.restype = None

    @property
    def deps(self):
        if self._deps is None:
            self._deps = dict(
                typedefs=set(),
                functions=set(),
                enums=set(),
                structures=set(),
                unions=set(),
                namespaces=set(),
                variables=set(),
            )
            if self.restype is not None:
                self.types.add(self.restype)

            for type_ in self.types:
                if type_ in typedefs:
                    self._deps['typedefs'].add(typedefs[type_])
                elif type_ in structures:
                    self._deps['structures'].add(structures[type_])
                elif type_ in unions:
                    self._deps['unions'].add(unions[type_])
                elif type_ in enums:
                    self._deps['enums'].add(enums[type_])
                elif type_ in functions:
                    self._deps['functions'].add(functions[type_])
                elif type_ in variables:
                    self._deps['variables'].add(variables[type_])
                elif type_ in namespaces:
                    self._deps['namespaces'].add(namespaces[type_])
        return self._deps

    def __str__(self):
        return self.template.format(name=self.name)


class FILE(object):

    def __init__(self, _, refid, name, node, **__):
        if name in files:
            self.__dict__.update(files[name].__dict__)
            return

        files[name] = self

        self.refid = refid
        self.name = name
        self.header_file = os.path.splitext(name)[0]

        enums_ = []

        for member in node:
            if member.tag != 'member':
                continue

            cls = globals()[member.attrib['kind'].upper()]
            if cls == ENUM:
                if member[0].text:
                    member.attrib['name'] = member[0].text.strip()
                    enums_.append(cls(self, **member.attrib))
                else:
                    member.attrib['name'] = None
                    enums_.append(cls(self, **member.attrib))

            elif cls == ENUMVALUE:
                if enums_[-1].is_member(member):
                    enums_[-1].add_member(member)

            else:
                member.attrib['name'] = member[0].text.strip()
                cls(self, **member.attrib)


class ENUM(object):
    template = '''\
.. doxygenenum:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        if name in enums:
            self.__dict__.update(enums[name].__dict__)
        else:

            enums[name] = self

            self.parent = parent
            self.refid = refid
            self.name = name
            self.members = []
            self.description = None
            self.file_name = None
            self.line_no = None

        if parent is not None:
            root = load_xml(parent.refid)

            for compounddef in root:
                if compounddef.attrib['id'] != parent.refid:
                    continue

                for child in compounddef:
                    if child.tag != 'sectiondef':
                        continue

                    if child.attrib['kind'] != 'enum':
                        continue

                    for memberdef in child:
                        if 'id' not in memberdef.attrib:
                            continue

                        if memberdef.attrib['id'] == refid:
                            break
                    else:
                        continue

                    break
                else:
                    continue

                break
            else:
                return
                # raise RuntimeError(f'not able to locate enum {name} ({refid})')

            for element in memberdef:
                if element.tag == 'location':
                    self.file_name = element.attrib['file']
                    self.line_no = element.attrib['line']

                if element.tag == 'detaileddescription':
                    self.description = build_docstring(element)
                elif element.tag == 'enumvalue':
                    item_name = None
                    item_description = None
                    item_file_name = None
                    item_line_no = None

                    for s_element in element:
                        if s_element.tag == 'name':
                            item_name = s_element.text
                        elif s_element.tag == 'detaileddescription':
                            item_description = build_docstring(s_element)

                        elif s_element.tag == 'location':
                            item_file_name = child.attrib['file']
                            item_line_no = child.attrib['line']

                    if item_name is not None:
                        for ev in self.members:
                            if ev.name != item_name:
                                continue
                            break
                        else:
                            ev = ENUMVALUE(
                                self,
                                element.attrib['id'],
                                item_name
                            )

                            self.members.append(ev)

                        ev.description = item_description

            if not self.description:
                warn(MISSING_ENUM, self.name)
                warn(None, 'FILE:', self.file_name)
                warn(None, 'LINE:', self.line_no)
                warn(None)

            for member in self.members:
                if not member.description:
                    warn(MISSING_ENUM_ITEM, self.name)
                    warn(None, 'MEMBER:', member.name)
                    warn(None, 'FILE:', self.file_name)
                    warn(None, 'LINE:', self.line_no)
                    warn(None)

    def is_member(self, member):
        return (
            member.attrib['kind'] == 'enumvalue' and
            member.attrib['refid'].startswith(self.refid)
        )

    def add_member(self, member):
        name = member[0].text.strip()
        for ev in self.members:
            if ev.name == name:
                return

        self.members.append(
            ENUMVALUE(
                self,
                member.attrib['refid'],
                name
            )
        )

    def __str__(self):
        template = [self.template.format(name=self.name)]
        template.extend(list(str(member) for member in self.members))

        return '\n'.join(template)


defines = {}


def build_define(element):
    define = None

    if element.text:
        define = element.text.strip()

    for item in element:
        ds = build_define(item)
        if ds:
            if define:
                define += ' ' + ds
            else:
                define = ds.strip()

    if element.tail:
        if define:
            define += ' ' + element.tail.strip()
        else:
            define = element.tail.strip()

    return define


class DEFINE(object):
    template = '''\
.. doxygendefine:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        if name in defines:
            self.__dict__.update(defines[name].__dict__)
        else:
            defines[name] = self

            self.parent = parent
            self.refid = refid
            self.name = name
            self.description = None
            self.file_name = None
            self.line_no = None
            self.params = None
            self.initializer = None

        if parent is not None:
            root = load_xml(parent.refid)

            for compounddef in root:
                if compounddef.attrib['id'] != parent.refid:
                    continue

                for child in compounddef:
                    if child.tag != 'sectiondef':
                        continue

                    if child.attrib['kind'] != 'define':
                        continue

                    for memberdef in child:
                        if memberdef.attrib['id'] == refid:
                            break
                    else:
                        continue

                    break
                else:
                    continue

                break
            else:
                return

            for element in memberdef:
                if element.tag == 'location':
                    self.file_name = element.attrib['file']
                    self.line_no = element.attrib['line']

                elif element.tag == 'detaileddescription':
                    self.description = build_docstring(element)

                elif element.tag == 'param':
                    for child in element:
                        if child.tag == 'defname':
                            if self.params is None:
                                self.params = []

                            if child.text:
                                self.params.append(child.text)

                elif element.tag == 'initializer':
                    initializer = build_define(element)
                    if initializer is None:
                        self.initializer = ''
                    else:
                        self.initializer = initializer

            if not self.description:
                warn(MISSING_MACRO, self.name)
                warn(None, 'FILE:', self.file_name)
                warn(None, 'LINE:', self.line_no)
                warn(None)

    def __str__(self):
        return self.template.format(name=self.name)


class ENUMVALUE(object):
    template = '''\
.. doxygenenumvalue:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        self.parent = parent
        self.refid = refid
        self.name = name
        self.description = None
        self.file_name = None
        self.line_no = None

    def __str__(self):
        return self.template.format(name=self.name)


class TYPEDEF(object):
    template = '''\
.. doxygentypedef:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        if name in typedefs:
            self.__dict__.update(typedefs[name].__dict__)
        else:
            typedefs[name] = self

            self.parent = parent
            self.refid = refid
            self.name = name
            self.type = None
            self._deps = None
            self.description = None
            self.file_name = None
            self.line_no = None

        if parent is not None:
            root = load_xml(parent.refid)

            for compounddef in root:
                if compounddef.attrib['id'] != parent.refid:
                    continue

                for child in compounddef:
                    if child.tag != 'sectiondef':
                        continue
                    if child.attrib['kind'] != 'typedef':
                        continue

                    for memberdef in child:
                        if 'id' not in memberdef.attrib:
                            continue

                        if memberdef.attrib['id'] == refid:
                            break
                    else:
                        continue

                    break
                else:
                    continue

                break
            else:
                return

            for element in memberdef:
                if element.tag == 'location':
                    self.file_name = element.attrib['file']
                    self.line_no = element.attrib['line']

                if element.tag == 'detaileddescription':
                    self.description = build_docstring(element)

            if not self.description:
                warn(MISSING_TYPEDEF, self.name)
                warn(None, 'FILE:', self.file_name)
                warn(None, 'LINE:', self.line_no)
                warn(None)

            self.type = get_type(memberdef)

    @property
    def deps(self):
        if self._deps is None:
            self._deps = dict(
                typedefs=set(),
                functions=set(),
                enums=set(),
                structures=set(),
                unions=set(),
                namespaces=set(),
                variables=set(),
            )
            if self.type is not None:
                type_ = self.type

                if type_ in typedefs:
                    self._deps['typedefs'].add(typedefs[type_])
                elif type_ in structures:
                    self._deps['structures'].add(structures[type_])
                elif type_ in unions:
                    self._deps['unions'].add(unions[type_])
                elif type_ in enums:
                    self._deps['enums'].add(enums[type_])
                elif type_ in functions:
                    self._deps['functions'].add(functions[type_])
                elif type_ in variables:
                    self._deps['variables'].add(variables[type_])
                elif type_ in namespaces:
                    self._deps['namespaces'].add(namespaces[type_])

        return self._deps

    def __str__(self):
        return self.template.format(name=self.name)


classes = {}


class CLASS(object):

    def __init__(self, _, refid, name, node, **__):
        if name in classes:
            self.__dict__.update(classes[name].__dict__)
            return

        classes[name] = self

        self.refid = refid
        self.name = name

        enums_ = []

        for member in node:
            if member.tag != 'member':
                continue

            cls = globals()[member.attrib['kind'].upper()]
            if cls == ENUM:
                member.attrib['name'] = member[0].text.strip()
                enums_.append(cls(self, **member.attrib))
            elif cls == ENUMVALUE:
                if enums_[-1].is_member(member):
                    enums_[-1].add_member(member)

            else:
                member.attrib['name'] = member[0].text.strip()
                cls(self, **member.attrib)


lvgl_src_path = ''
api_path = ''
html_files = {}


def iter_src(n, p):
    if p:
        out_path = os.path.join(api_path, p)
    else:
        out_path = api_path

    index_file = None

    if p:
        src_path = os.path.join(lvgl_src_path, p)
    else:
        src_path = lvgl_src_path

    folders = []

    for file in os.listdir(src_path):
        if 'private' in file:
            continue

        if os.path.isdir(os.path.join(src_path, file)):
            folders.append((file, os.path.join(p, file)))
            continue

        if not file.endswith('.h'):
            continue

        if not os.path.exists(out_path):
            os.makedirs(out_path)

        if index_file is None:
            index_file = open(os.path.join(out_path, 'index.rst'), 'w')
            if n:
                index_file.write('=' * len(n))
                index_file.write('\n' + n + '\n')
                index_file.write('=' * len(n))
                index_file.write('\n\n\n')

            index_file.write('.. toctree::\n    :maxdepth: 2\n\n')

        name = os.path.splitext(file)[0]
        index_file.write('    ' + name + '\n')

        rst_file = os.path.join(out_path, name + '.rst')
        html_file = os.path.join(p, name + '.html')
        html_files[name] = html_file

        with open(rst_file, 'w') as f:
            f.write('.. _{0}_h:'.format(name))
            f.write('\n\n')
            f.write('=' * len(file))
            f.write('\n')
            f.write(file)
            f.write('\n')
            f.write('=' * len(file))
            f.write('\n\n\n')

            f.write('.. doxygenfile:: ' + file)
            f.write('\n')
            f.write('    :project: lvgl')
            f.write('\n\n')

    for name, folder in folders:
        if iter_src(name, folder):
            if index_file is None:
                index_file = open(os.path.join(out_path, 'index.rst'), 'w')

                if n:
                    index_file.write('=' * len(n))
                    index_file.write('\n' + n + '\n')
                    index_file.write('=' * len(n))
                    index_file.write('\n\n\n')

                index_file.write('.. toctree::\n    :maxdepth: 2\n\n')

            index_file.write('    ' + os.path.split(folder)[-1] + '/index\n')

    if index_file is not None:
        index_file.write('\n')
        index_file.close()
        return True

    return False


def clean_name(nme):
    # Handle error:
    #     AttributeError: 'NoneType' object has no attribute 'startswith'
    if nme is None:
        return nme

    if nme.startswith('_lv_'):
        nme = nme[4:]
    elif nme.startswith('lv_'):
        nme = nme[3:]

    if nme.endswith('_t'):
        nme = nme[:-2]

    return nme


# Definitions:
# - "section" => The name "abc_def" has 2 sections.
# - N = number of sections in `item_name`.
# After removing leading '_lv_', 'lv_' and trailing '_t' from `obj_name`,
# do the remaining first N "sections" of `obj_name` match `item_name`
# (case sensitive)?
def is_name_match(item_name, obj_name):
    # Handle error:
    #     AttributeError: 'NoneType' object has no attribute 'split'
    if obj_name is None:
        return False

    u_num = item_name.count('_') + 1

    obj_name = obj_name.split('_')

    # Reject (False) if `obj_name` doesn't have as many sections as `item_name`.
    if len(obj_name) < u_num:
        return False

    obj_name = '_'.join(obj_name[:u_num])

    return item_name == obj_name


def get_includes(name1, name2, obj, includes):
    name2 = clean_name(name2)

    if not is_name_match(name1, name2):
        return

    if obj.parent is not None and hasattr(obj.parent, 'header_file'):
        header_file = obj.parent.header_file
    elif hasattr(obj, 'header_file'):
        header_file = obj.header_file
    else:
        return

    if not header_file:
        return

    if header_file not in html_files:
        return

    includes.add((header_file, html_files[header_file]))


class XMLSearch(object):

    def __init__(self, temp_directory):
        global xml_path
        import subprocess
        import re
        import sys

        bp = os.path.abspath(os.path.dirname(__file__))

        lvgl_path = os.path.join(temp_directory, 'lvgl')
        src_path = os.path.join(lvgl_path, 'src')

        doxy_path = os.path.join(bp, 'Doxyfile')

        with open(doxy_path, 'rb') as f:
            data = f.read().decode('utf-8')

        data = data.replace(
            '#*#*LV_CONF_PATH*#*#',
            os.path.join(temp_directory, 'lv_conf.h')
        )
        data = data.replace('*#*#SRC#*#*', '"{0}"'.format(src_path))

        with open(os.path.join(temp_directory, 'Doxyfile'), 'wb') as f:
            f.write(data.encode('utf-8'))

        # -----------------------------------------------------------------
        # Populate LVGL_URLPATH and LVGL_GITCOMMIT environment variables:
        #   - LVGL_URLPATH   <= 'master' or '8.4' '9.2' etc.
        #   - LVGL_GITCOMMIT <= commit hash of HEAD.
        # The previous version of this was populating LVGL_URLPATH with
        # the multi-line list of all existing branches in the repository,
        # which was not what was intended.
        # -----------------------------------------------------------------
        status, branch = subprocess.getstatusoutput("git branch --show-current")
        _, gitcommit = subprocess.getstatusoutput("git rev-parse HEAD")

        # If above failed (i.e. `branch` not valid), default to 'master'.
        if status != 0:
            branch = 'master'
        elif branch == 'master':
            # Expected in most cases.  Nothing to change.
            pass
        else:
            # `branch` is valid.  Capture release version if in a 'release/' branch.
            if branch.startswith('release/'):
                branch = branch[8:]
            else:
                # Default to 'master'.
                branch = 'master'

        os.environ['LVGL_URLPATH'] = branch
        os.environ['LVGL_GITCOMMIT'] = gitcommit

        # ---------------------------------------------------------------------
        # Provide a way to run an external command and abort build on error.
        #
        # This is necessary because when tempdir created by tempfile.mkdtemp()`
        # is on a different drive, the "cd tmpdir && doxygen Doxyfile" syntax
        # fails because of the different semantics of the `cd` command on
        # Windows:  it doesn't change the default DRIVE if `cd` is executed
        # from a different drive.  The result, when this is the case, is that
        # Doxygen runs in the current working directory instead of in the
        # temporary directory as was intended.
        # ---------------------------------------------------------------------
        def cmd(cmd_str, start_dir=None):
            if start_dir is None:
                start_dir = os.getcwd()

            saved_dir = os.getcwd()
            os.chdir(start_dir)

            # This method of running Doxygen is used because if it
            # succeeds, we do not want anything going to STDOUT.
            # Running it via `os.system()` would send its output
            # to STDOUT.
            p = subprocess.Popen(
                cmd_str,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=True
            )

            out, err = p.communicate()
            if p.returncode:
                if out:
                    # Note the `.decode("utf-8")` is required here
                    # because `sys.stdout.write()` requires a string,
                    # and `out` by itself is a byte array -- it causes
                    # it to generate an exception and abort the script.
                    sys.stdout.write(out.decode("utf-8"))
                    sys.stdout.flush()
                if err:
                    sys.stderr.write(err.decode("utf-8"))
                    sys.stdout.flush()

                sys.exit(p.returncode)

            # If execution arrived here, Doxygen exited with code 0.
            os.chdir(saved_dir)

        # -----------------------------------------------------------------
        # Run Doxygen in temporary directory.
        # -----------------------------------------------------------------
        cmd('doxygen Doxyfile', temp_directory)

        xml_path = os.path.join(temp_directory, 'xml')

        self.index = load_xml('index')

        for compound in self.index:
            compound.attrib['name'] = compound[0].text.strip()
            if compound.attrib['kind'] in ('example', 'page', 'dir'):
                continue

            globals()[compound.attrib['kind'].upper()](
                None,
                node=compound,
                **compound.attrib
            )

    def get_macros(self):
        return list(defines.values())

    def get_enum_item(self, e_name):
        for enum, obj in enums.items():
            for enum_item in obj.members:
                if enum_item.name == e_name:
                    return enum_item

    def get_enum(self, e_name):
        return enums.get(e_name, None)

    def get_function(self, f_name):
        return functions.get(f_name, None)

    def get_variable(self, v_name):
        return variables.get(v_name, None)

    def get_union(self, u_name):
        return unions.get(u_name, None)

    def get_structure(self, s_name):
        return structures.get(s_name, None)

    def get_typedef(self, t_name):
        return typedefs.get(t_name, None)

    def get_macro(self, m_name):
        return defines.get(m_name, None)


def announce(*args):
    args = ' '.join(repr(arg) for arg in args)
    print(f'{os.path.basename(__file__)}: ', args)


def run(project_path, temp_directory, *doc_paths):
    """
    This function does 2 things:
    1.  Generates .RST files for the LVGL header files that will have API
        pages generated for them.  It places these in <tmp_dir>/API/...
        following the <project_path>/src/ directory structure.
    2.  Add Sphinx hyperlinks to the end of source .RST files found
        in the `doc_paths` array directories, whose file-name stems
        match code-element names found by Doxygen.

    :param project_path:  platform-appropriate path to LVGL root directory
    :param temp_directory:  platform-appropriate path to temp dir being operated on
    :param doc_paths:  list of platform-appropriate paths to find source .RST files.
    :return:  n/a
    """
    global base_path
    global xml_path
    global lvgl_src_path
    global api_path

    base_path = temp_directory
    xml_path = os.path.join(base_path, 'xml')
    api_path = os.path.join(base_path, 'API')
    lvgl_src_path = os.path.join(project_path, 'src')

    announce("Generating API documentation .RST files...")

    if not os.path.exists(api_path):
        os.makedirs(api_path)

    # Generate .RST files for API pages.
    iter_src('API', '')
    # Load index.xml -- core of what was generated by Doxygen.
    index = load_xml('index')

    # Populate these dictionaries.
    #     Keys  :  C-code-element names (str) found by Doxygen.
    #     Values:  The <compound> XML-node created by `xml.etree::ElementTree` in `load_xml()` above.
    #
    #    - defines,
    #    - enums,
    #    - variables,
    #    - namespaces,
    #    - structures,
    #    - unions,
    #    - typedefs,
    #    - functions.
    announce("Building source-code symbol tables...")

    for compound in index:
        compound.attrib['name'] = compound[0].text.strip()
        if compound.attrib['kind'] in ('example', 'page', 'dir'):
            continue

        # This below highly-compressed command effectively does this:
        #
        #     namespace_dict = globals()
        #     compound_elem_kind_upper = compound.attrib['kind'].upper()
        #         e.g. 'FUNCTION'
        #     class_obj = namespace_dict['FUNCTION']
        #         # In each case of `class_obj`, the __init__ args are:
        #         #     (self, parent, refid, name, **_)
        #         # So we get...
        #     attrib_keyword_args = **compound.attrib
        #         # Passing (**compound.attrib) as an argument creates and
        #         # passes a set of keyword arguments produced from the
        #         # dictionary `compound.attrib`.
        #     new_obj = class_obj(None, node=compound, attrib_keyword_args)
        #
        # Note carefully that `new_obj` gets thrown away, but the new object created
        # doesn't go away because during its execution of __init__(), the new object
        # adds itself to the global dictionary matching its "kind":
        #
        # Class          Dictionary New Object Adds Itself To
        # ------------ | ------------------------------------
        # - DEFINE    => `defines`
        # - ENUM      => `enums`
        # - VARIABLE  => `variables`
        # - NAMESPACE => `namespaces`
        # - STRUCT    => `structures`
        # - UNION    appears to have a different purpose
        # - TYPEDEF   => `typedefs`
        # - FUNCTION  => `functions`
        # - GROUP     => `groups`
        # - FILE      => `files`
        # - CLASS     => `classes`
        #
        # Populating these dictionaries takes quite a while:
        # ~18-seconds on a medium-speed system.
        globals()[compound.attrib['kind'].upper()](
            None,
            node=compound,
            **compound.attrib
        )

    # For each directory entry in `doc_paths` array...
    announce("Adding API-page hyperlinks to source docs...")

    for folder in doc_paths:
        # Fetch a list of '.rst' files excluding 'index.rst'.
        rst_files = list(
            (os.path.splitext(item)[0], os.path.join(folder, item))
            for item in os.listdir(folder)
            if item.endswith('.rst') and 'index.rst' not in item
        )

        # For each .RST file in that directory...
        for stem, path in rst_files:
            # Start with an empty set.
            html_includes = set()

            # Build `html_includes` set as a list of tuples containing
            # (name, html_file).  Example:  "draw.rst" has `stem` == 'draw',
            # and generates a list of tuples from .H files where matching
            # C-code-element names were found.  Example:
            # {('lv_draw_line', 'draw\\lv_draw_line.html'),
            #  ('lv_draw_sdl', 'draw\\sdl\\lv_draw_sdl.html'),
            #  ('lv_draw_sw_blend_to_i1', 'draw\\sw\\blend\\lv_draw_sw_blend_to_i1.html'),
            #  etc.}
            for container in (
                defines,
                enums,
                variables,
                namespaces,
                structures,
                unions,
                typedefs,
                functions
            ):
                for n, o in container.items():
                    get_includes(stem, n, o, html_includes)

            if html_includes:
                # Convert `html_includes` set to a list of strings containing the
                # Sphinx hyperlink syntax "link references".  Example from above:
                # [':ref:`lv_draw_line_h`\n',
                #  ':ref:`lv_draw_sdl_h`\n',
                #  ':ref:`lv_draw_sw_blend_to_i1_h`\n',
                #  etc.]
                html_includes = list(
                    ':ref:`{0}_h`\n'.format(inc)
                    for inc, _ in html_includes
                )

                # Convert that list to a single string of Sphinx hyperlink
                # references with blank lines between them.
                # :ref:`lv_draw_line_h`
                #
                # :ref:`lv_draw_sdl_h`
                #
                # :ref:`lv_draw_sw_blend_to_i1_h`
                #
                # etc.
                output = ('\n'.join(html_includes)) + '\n'

                # Append that string to the  source .RST file being processed.
                with open(path, 'rb') as f:
                    try:
                        data = f.read().decode('utf-8')
                    except UnicodeDecodeError:
                        print(path)
                        raise

                data = data.split('.. Autogenerated', 1)[0]

                data += '.. Autogenerated\n\n'
                data += output

                with open(path, 'wb') as f:
                    f.write(data.encode('utf-8'))
