"""doxygen_xml.py

Python Interface to Doxygen XML Output

DoxygenXml is the primary class defined herein.
It uses all the other classes, and is used by:

- ./docs/build.py via ./docs/api_doc_builder.py for the LVGL doc build to:
  - edit Doxyfile to replace 2 string tokens
  - run Doxygen
  - load Doxygen XML output and use it to:
    - generate API `.rst` pages,
    - populate a set of `doxygen_xml.<...>` dictionaries, and
    - use those dictionaries to determine in what LVGL documents
      hyperlinks to API pages might be useful and appends them if they
      are not already present.

- ./scripts/gen_json/gen_json.py => pycparser_monkeypatch.py to
  - edit Doxyfile to replace 2 string tokens
  - silently run Doxygen
  - load Doxygen XML output and use it to:
    - populate a set of `doxygen_xml.<...>` dictionaries, and
    - monkey-patch pycparser AST (Abstract Syntax Tree) output to
      extract API documentation into its JSON output.

"Loading Doxygen XML output" consists of

- opening and parsing the XML `index.xml` file generated by Doxygen which
  is a summary of Doxygen-generated documentation linked to additional `.xml`
  filenames that have more details.  Note that `index.xml` has a complete list
  of documented symbol-names from the C code.  `./docs/api_doc_builder.py`
  searches those names using search term(s) to determine in what LVGL
  documents hyperlinks to API pages might be useful, and appends them
  to those documents if they are not already present.

  `index.xml` structure is described in the `doxygen/doxygen` repository
  on GitHub under `./templates/xml/index.xsd`, and the structure of the
  other files is described in `./templates/xml/compound.xsd`.

    <doxygenindex ...>
      Unlimited-length list of <compound> nodes.
      <compound refid="..." kind="..."><name>...</name>
        <member refid="..." kind="..."><name>...</name></member>
        ...
      </compound>
      ...
    </doxygenindex>

In `index.xml`, every <compound> element:

    - always has attributes "refid" and "kind",
    - always has exactly 1 <name> element, and
    - is followed by an unbounded number of <member> elements.

Each <compound> element's 'kind' attribute will be one of the following:

    class        struct      union        interface
    protocol     category    exception    file
    namespace    group       page         example
    dir          type        concept      module

which is a group that applies to many languages.  Since Doxygen
is parsing a C-language project, we will only find these in the
`index.xml` file:

    - struct
    - union
    - file
    - page       (unused)
    - dir        (unused)

In `'index.xml`, every <member> element:

    - always has attributes "refid" and "kind",
    - always has exactly 1 <name> element.

The contents of the <member> elements differ based on the "kind" of
<compound> element they are contained by.

kind="struct"

    - <member> sub-elements only have `kind="variable"` attributes.

kind="union"

    - <member> sub-elements only have `kind="variable"` attributes.

kind="file"

    - <member> sub-elements can have `kind` attributes:
        - define
        - enum
            - enumvalue has refid that begins with containing enum's refid
        - variable
        - function
        - typedef

    - <member kind="define"...>
        Has <name> child element with macro name.  refid contains something
        that looks like this:

            "lv__obj_8h_1a8ec0fe9743c5c015162d64df591cd719"
            /____________________________________________/
            /_________/            |
                |                  +-- ``id`` attribute of applicable item
                |
                +-- stem of `.xml` filename containing details

lv__obj_8h.xml has this format:
    <doxygen ...>
      <compounddef ...>
        <compoundname>lv_obj.h</compoundname>
        Section dealing with #include's (list, dependency graph, etc.)
        <includes list>
        <includedby list>
        <incdepgraph ...>
        </incdepgraph>
        <invincdepgraph ...>
        </invincdepgraph>
        <sectiondef kind="...">
        </sectiondef>
        ...List of <sectiondef> elements.
        ...kind can be:
            - define
            - enum
            - var
            - func
        <sectiondef kind="define">
          <memberdef kind="define" id="refid from index.xml">...</memberdef>
          <memberdef>...</memberdef>
          <memberdef>...</memberdef>
          ...List of all defines in file.
        </sectiondef>
        <sectiondef kind="enum">
          <memberdef kind="enum" id="refid from index.xml">...</memberdef>
            <type></type>
            <name></name>
            <enumvalue id="refid from index.xml" prot="public">
              <name>LV_STATE_DEFAULT</name>
              <initializer>=  0x0000</initializer>
              <briefdescription>
              </briefdescription>
              <detaileddescription>
              </detaileddescription>
            </enumvalue>
            ...List of all the enumeration's enumvalue's.
          </memberdef>
          ...List of all enums in the file.
        </sectiondef>
        <sectiondef kind="var">
          <memberdef kind="variable" id="refid from index.xml" prot="public" static="no" extern="yes" mutable="no">
          ...List of all variables in file.
        </sectiondef>
        <sectiondef kind="func">
          <memberdef kind="function" id="refid from index.xml" ...>
          ...List of all functions in file.
        </sectiondef>
        <briefdescription>      @file @brief description
        </briefdescription>
        <detaileddescription>   @file detailed description
        </detaileddescription>
        <programlisting>        Only included if XML_PROGRAMLISTING = YES.
          <codeline lineno="1">...</codeline>
          <codeline lineno="2">...</codeline>
          <codeline lineno="3">...</codeline>
        </programlisting>
      </compounddef>
    </doxygen>

kind="page"

    - Has no child members, but does have a child element <name>
      (e.g. "deprecated" and "todo").

kind="dir"

    - do not have <member> sub-elements
    - contents of <name> element is a full path to a directory
    - If sorted alphabetically, they form a sequence that can be
      used to create something:
      E:
      E:/Dev
      E:/Dev/lvgl
      E:/Dev/lvgl/lvgl
      E:/Dev/lvgl/lvgl/src
      etc.
    - refid="..." contains the stem of another `.xml` file that
      contains more information on the directory, including any
      documentation on it found by Doxygen.  That XML file's structure is:

      <refid>.xml:
      ------------
      <compounddef id="<refid_from_above" kind="dir">
        <compoundname>full_path_of_dir_wo_trailing_slash</compoundname>
        <innerdir refid="stem_of_xml_file_containing_dir_details">subdirectory_path</innerdir>
        <innerdir refid="stem_of_xml_file_containing_dir_details">subdirectory_path</innerdir>
        <innerdir refid="stem_of_xml_file_containing_dir_details">subdirectory_path</innerdir>
        ... (list <innerdir> elements --- one for each subdirectory in directory)
        ...
        <innerfile refid="stem_of_xml_file_containing_file_details">filename</innerfile>
        <innerfile refid="stem_of_xml_file_containing_file_details">filename</innerfile>
        <innerfile refid="stem_of_xml_file_containing_file_details">filename</innerfile>
        ... (list <innerfile> elements --- one for each file in directory)
        ...
        <briefdescription>
          Brief description of directory (Doxygen documentation)
        </briefdescription>
        <detaileddescription>
          Detailed description of directory (Doxygen documentation)
        </detaileddescription>
        <location file="full_path_of_dir_with_trailing_slash"/>
      </compounddef>


list

    - define
    - dir        (unused)
    - enum
    - enumvalue
    - example    (unused)
    - file
    - function
    - page       (unused)
    - 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, whose class names are the upper-case names above
except for the unused ones.  These classes are used to build a data structure
that matches that of the `index.xml` file.

The list of classes is:

  +-----------+----------------------------------------------------+
  | Class     | Adds New Instance to this Dictionary in __init__() |
  +===========+====================================================+
  | DEFINE    | defines                                            |
  +-----------+----------------------------------------------------+
  | ENUM      | enums       (have child ENUMVALUE objects)         |
  +-----------+----------------------------------------------------+
  | VARIABLE  | variables                                          |
  +-----------+----------------------------------------------------+
  | NAMESPACE | namespaces                                         |
  +-----------+----------------------------------------------------+
  | STRUCT    | structures  (have child STRUCT_FIELD objects)      |
  +-----------+----------------------------------------------------+
  | UNION     | unions      (have child STRUCT_FIELD objects)      |
  +-----------+----------------------------------------------------+
  | TYPEDEF   | typedefs                                           |
  +-----------+----------------------------------------------------+
  | FUNCTION  | functions   (have child FUNC_ARG objects)          |
  +-----------+----------------------------------------------------+
  | GROUP     | groups                                             |
  +-----------+----------------------------------------------------+
  | FILE      | files                                              |
  +-----------+----------------------------------------------------+
  | CLASS     | classes                                            |
  +-----------+----------------------------------------------------+

Additional classes:

    - FUNC_ARG(object):      (becomes child members of FUNCTION objects)
    - STRUCT_FIELD(object):  (becomes child members of STRUCT objects)
    - ENUMVALUE(object):     (becomes child members of ENUM objects)
    - XMLSearch(object):     (used only by ./scripts/gen_json/gen_json.py)

Each of the above Dictionary variables has entries with

    - keys   = documented symbol name from C code that Doxygen found
    - values = instances of classes named above

Samples:

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

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

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

    'namespaces': {},

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

    'unions': {},

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

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

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

"""
import os
import sys
import subprocess
from xml.etree import ElementTree
import doxygen_config
from announce import *

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'

# Dictionaries built from Doxygen XML output via `xml.etree.ElementTree`
defines = {}     # dictionary of doxygen_xml.DEFINE objects
enums = {}       # dictionary of doxygen_xml.ENUM objects
variables = {}   # dictionary of doxygen_xml.VARIABLE objects
namespaces = {}  # dictionary of doxygen_xml.NAMESPACE objects
structures = {}  # dictionary of doxygen_xml.STRUCT objects
typedefs = {}    # dictionary of doxygen_xml.TYPEDEF objects
functions = {}   # dictionary of doxygen_xml.FUNCTION objects
groups = {}      # dictionary of doxygen_xml.GROUP objects
files = {}       # dictionary of doxygen_xml.FILE objects
classes = {}     # dictionary of doxygen_xml.CLASS objects
unions = {}      # appears to be unused at this time (unions => structures dict).

# Module-Global Variables
xml_path = ''


def run_ext_cmd(cmd_str: str, start_dir: str = None, quiet: bool = False, exit_on_error: bool = True):
    """
    Run external command `cmd_str` (possibly silently) in directory
    `start_dir` (if provided), and optionally abort execution on error.

    :param cmd_str:        String to pass to OS.
    :param start_dir:      Directory to start in, or `None` to run in "cwd".
    :param quiet:          Should STDOUT and STDERR be suppressed?
    :param exit_on_error:  Should this function abort execution on
                             a non-zero exit status?
    :return:               n/a
    """
    saved_dir = None

    if start_dir is not None:
        saved_dir = os.getcwd()
        os.chdir(start_dir)

    if quiet:
        # This method of running Doxygen is used because 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 because
                # `sys.stdout.write()` requires a string, and `out` is
                # a byte array; generates an exception if passed alone.
                sys.stdout.write(out.decode("utf-8"))
                sys.stdout.flush()
            if err:
                sys.stderr.write(err.decode("utf-8"))
                sys.stdout.flush()

            if exit_on_error:
                sys.exit(p.returncode)
    else:
        announce_start(__file__, f'Running [{cmd_str}] in [{os.getcwd()}]...')
        return_code = os.system(cmd_str)
        announce_finish()

        if return_code != 0 and exit_on_error:
            print(f'Exiting due to error [{return_code}] running [{cmd_str}].')
            sys.exit(return_code)

    if saved_dir is not None:
        os.chdir(saved_dir)


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 ElementTree.fromstring(d)
    except:  # NOQA
        return None


def load_xml_etree(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 ElementTree.fromstring(d)


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)


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 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):
    """<compound kind="struct"> elements in Doxygen `index.xml`"""
    _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, **_):
        global structures
        global unions

        if type(self) is UNION:
            # UNION inherits from STRUCT.
            if name in unions:
                self.__dict__.update(unions[name].__dict__)
            else:
                unions[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
        else:
            # STRUCT type
            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

        # Prior to 9-Mar-2025, the code below was never executing since this
        # __init__() was never called with a `parent` value other than `None`.
        # Reason:  `kind="struct"` only occurs in `index.xml` as a top-level
        # entry, and not as a child element of `kind="file"` as do <sectiondef>
        # elements with kind = define, var, enum and func.
        # Original code:
        # if parent and refid:
        if refid:
            root = load_xml_etree(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 each struct member...
                            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 EMIT_WARNINGS:
                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):
    """<compound kind="union"> elements in Doxygen `index.xml`"""
    _missing = MISSING_UNION
    _missing_field = MISSING_UNION_FIELD

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


class VARIABLE(object):
    """<compound kind="struct" ...>
        <member ... kind="variable"> elements in Doxygen `index.xml`"""
    template = '''\
.. doxygenvariable:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        global variables

        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_etree(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):
    """<compound kind="namespace"> elements in Doxygen `index.xml`"""
    template = '''\
.. doxygennamespace:: {name}
   :project: lvgl
   :members:
   :protected-members:
   :private-members:
   :undoc-members:
'''

    def __init__(self, parent, refid, name, **_):
        global namespaces

        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 GROUP(object):
    """<compound kind="group"> elements in Doxygen `index.xml`"""
    template = '''\
.. doxygengroup:: {name}
    :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        global groups

        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 FUNC_ARG(object):
    """<compound kind="?"><member ...> elements in Doxygen `index.xml`"""

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


class FUNCTION(object):
    """<compound kind="file" ...>
        <member ... kind="function"> elements in Doxygen `index.xml`"""
    template = '''\
.. doxygenfunction:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        global functions

        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_etree(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):
    """<compound kind="file"> elements in Doxygen `index.xml`"""

    def __init__(self, _, refid, name, node, **__):
        global files

        if name.endswith('lv_types.h'):
            return

        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]
        self.types_contained = set()

        enums_ = []

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

            cls_name = member.attrib['kind'].upper()
            self.types_contained.add(cls_name)
            cls = globals()[cls_name]
            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 ENUMVALUE(object):
    """<compound kind="file"...>
        <member kind="enum"...>
        <member kind="enumvalue"> elements in Doxygen `index.xml`"""
    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

        if parent is not None:
            if parent.file_name is not None:
                self.file_name = parent.file_name
            elif hasattr(parent, 'parent') and parent.parent is not None:
                if parent.parent.name is not None:
                    self.file_name = parent.parent.name

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


class ENUM(object):
    """<compound kind="file"...>
        <member kind="enum"...> elements in Doxygen `index.xml`"""
    template = '''\
.. doxygenenum:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        global enums

        if name in enums:
            # This happens when `name` is `None`, for example.
            # This is true for unnamed enumerations.
            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_etree(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)


class DEFINE(object):
    """<compound kind="file"...>
        <member kind="define"...> elements in Doxygen `index.xml`"""
    template = '''\
.. doxygendefine:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        global defines

        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_etree(parent.refid)
            memberdef = []

            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 TYPEDEF(object):
    template = '''\
.. doxygentypedef:: {name}
   :project: lvgl
'''

    def __init__(self, parent, refid, name, **_):
        global typedefs

        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_etree(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)


class CLASS(object):
    """<compound kind="file" ...>
        <member ... kind="class"> elements in Doxygen `index.xml`"""

    def __init__(self, _, refid, name, node, **__):
        global classes

        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)


class DoxygenXml(object):
    """Opens, parses and loads a Doxygen-generated `index.xml` file
    and makes it available as a set of dictionary attributes of this
    module, documented at the top of this file and named below in
    `global` statements.
    """

    def __init__(self,
                 lvgl_src_dir: str,
                 intermediate_dir: str,
                 doxyfile_src_file: str,
                 silent_mode=False,
                 doxy_tagfile=''):
        """
        - Prepare and run Doxygen, generating XML output.
        - Load that XML output, and use it to populate
        :param lvgl_src_dir:
        :param intermediate_dir:
        :param doxyfile_src_file:
        :param silent_mode:
        """
        # Dictionaries to Be Populated:
        global defines
        global enums
        global variables
        global namespaces
        global structures
        global typedefs
        global functions
        global groups
        global files
        global classes
        global unions

        global xml_path

        announce_set_silent_mode(silent_mode)
        base_dir = os.path.abspath(os.path.dirname(__file__))
        doxyfile_filename = str(os.path.split(doxyfile_src_file)[1])
        doxyfile_dst_file = str(os.path.join(intermediate_dir, doxyfile_filename))
        lv_conf_file = os.path.join(intermediate_dir, 'lv_conf.h')
        xml_path = os.path.join(intermediate_dir, 'xml')

        # In case DoxygenXml() is ever instantiated twice in 1 session,
        # clear these dictionaries before they are (re-)populated below.
        defines.clear()
        enums.clear()
        variables.clear()
        namespaces.clear()
        structures.clear()
        typedefs.clear()
        functions.clear()
        groups.clear()
        files.clear()
        classes.clear()
        unions.clear()

        # -----------------------------------------------------------------
        # Prep and run Doxygen
        # -----------------------------------------------------------------
        # Generate Doxyfile into `intermediate_dir` replacing certain
        # config options for this run.
        # 1. Load from Doxyfile
        cfg = doxygen_config.DoxygenConfig()
        cfg.load(doxyfile_src_file)
        # 2. Update cfg.
        cfg.set('OUTPUT_DIRECTORY', '.')
        cfg.set('XML_OUTPUT', "xml")
        cfg.set('HTML_OUTPUT', 'doxygen_html')
        cfg.set('INPUT', lvgl_src_dir)
        cfg.set('QUIET', 'YES')
        cfg.set('GENERATE_HTML', 'NO')
        cfg.set('GENERATE_DOCSET', 'NO')
        cfg.set('GENERATE_HTMLHELP', 'NO')
        cfg.set('GENERATE_CHI', 'NO')
        cfg.set('GENERATE_QHP', 'NO')
        cfg.set('GENERATE_ECLIPSEHELP', 'NO')
        cfg.set('GENERATE_LATEX', 'NO')
        cfg.set('GENERATE_RTF', 'NO')
        cfg.set('GENERATE_MAN', 'NO')
        cfg.set('GENERATE_XML', 'YES')
        cfg.set('GENERATE_DOCBOOK', 'NO')
        cfg.set('GENERATE_PERLMOD', 'NO')

        # The predefined definitions are:
        # - DOXYGEN (defines it as 1 and allows conditional directives [e.g. #if]
        #   to do special things when DOXYGEN is processing it, as opposed to
        #   a C compiler.
        # - LV_CONF_PATH predefines the path where `lv_conf_internal.h` will
        #   include the `lv_conf.h` file.
        # - All the others here are used when macros prefix a line of code like this:
        #
        #       LV_ATTRIBUTE_EXTERN_DATA extern const lv_obj_class_t lv_obj_class;
        #
        #   which occurs in `lv_obj.h`.  When these are added to the PREDEFINED list as
        #   "MACRO_NAME=" with no value, Doxygen expands the macro to an empty string
        #   allowing Doxygen to correctly parse the line.  The additional macros that
        #   are treated that way are found in the middle of `lv_conf_template.h` in a
        #   section called "COMPILER SETTINGS" with no definitions, made for prefixing
        #   various code.  (This works around Doxygen's failure to correctly deal with
        #   macros that are defined with no values, as these are in lv_conf.h.)
        #   This list is current as of 28-Apr-2025.
        predefined_symbols = [
            'DOXYGEN',
            f'LV_CONF_PATH="{lv_conf_file}"',
            'LV_ATTRIBUTE_TICK_INC=',
            'LV_ATTRIBUTE_TIMER_HANDLER=',
            'LV_ATTRIBUTE_FLUSH_READY=',
            'LV_ATTRIBUTE_MEM_ALIGN=',
            'LV_ATTRIBUTE_LARGE_CONST=',
            'LV_ATTRIBUTE_LARGE_RAM_ARRAY=',
            'LV_ATTRIBUTE_FAST_MEM=',
            'LV_ATTRIBUTE_EXTERN_DATA=',
        ]

        cfg.set('PREDEFINED', predefined_symbols)

        # Include TAGFILES if requested.
        if doxy_tagfile:
            cfg.set('GENERATE_TAGFILE', doxy_tagfile)

        # 3. Store it for use by Doxygen in intermediate directory.
        cfg.save(doxyfile_dst_file)

        # Run Doxygen in intermediate directory.
        run_ext_cmd('doxygen Doxyfile', intermediate_dir, quiet=silent_mode)

        # -----------------------------------------------------------------
        # Load root of Doxygen output (index.xml) as an `xml.etree.ElementTree`.
        # -----------------------------------------------------------------
        index_xml_etree = load_xml_etree('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,  typedefs,
        #     functions,   unions,      groups,
        #     files,       classes.
        announce_start(__file__, "Building source-code symbol dictionaries...")
        module_namespace = globals()

        for compound in index_xml_etree:
            # Here we will encounter these "kind" in the index.xml
            # <compound> elements: dir, file, page, struct, union.
            compound.attrib['name'] = compound[0].text.strip()

            # Filter out dir, page, example.
            if compound.attrib['kind'] not in ('example', 'page', 'dir'):
                class_name = compound.attrib['kind'].upper()
                class_obj = module_namespace[class_name]
                _ = class_obj(None, node=compound, **compound.attrib)

        announce_finish()

        # Additional data:  the above instantiates the class, but doesn't
        # store the resulting object anywhere, since each class' __init__()
        # function adds the new object to the appropriate dictionary,
        # and uses the arguments it gets to "build out" additional
        # structure and/or populate additional dictionaries based on
        # content.  Each class __init__() function specifies args it
        # in its parameter list.  They match needed key values in the
        # `**compound.attrib` dictionary.  The remaining (unused)
        # keys in that dictionary are accepted in the `**_` parameter
        # at the end of the parameter list.
        #
        # FILE class populates `files` dict plus:
        #   - `defines` dictionary
        #   - `enums` dictionary with children:
        #       - ENUMVALUEs
        #   - `variables` dictionary
        #   - `typedefs` dictionary
        #   - `functions` dictionary with children:
        #       - FUNC_ARGs
        # STRUCT class populates the `structures` dictionary with children:
        #   - STRUCT_FIELDs
        #
        # and possibly `namespaces`, `groups` and `classes` if
        # they are present in `index.xml`.


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

    def get_enum_item(self, e_name):
        global enums
        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):
        global enums
        return enums.get(e_name, None)

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

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

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

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

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

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