#!python3
"""
Clean up of API information generated by Doxygen before passing it to Doxybook2 for markdown generation.
"""
import argparse
import logging
import os
import shutil
import sys

import xml.etree.ElementTree as ET

import doc_helpers

LOGGER = logging.getLogger("")

def process_doxygen_file(input_dir, output_dir, file_path):
    """
    Fix up XML file generated by Doxygen.
    """
    filename = os.path.basename(file_path)
    output_path = os.path.join(output_dir, filename)
    if not filename.startswith('class_'):
        shutil.copy(file_path, output_path)
        return True
    tree = ET.parse(file_path)
    root = tree.getroot()
    fix_include_path(tree)
    for memberdef in tree.iterfind('//memberdef'):
        kind = memberdef.attrib.get('kind')
        if kind == 'function':
            # fix all instances of UPROPERTY classified as a function
            if memberdef.find('name').text == 'UPROPERTY':
                fix_uproperty(memberdef)
        elif kind == 'variable':
            _set_subelem_text(memberdef, 'definition', '[delete_this_placeholder_line]')
    header = "<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n"
    doc_helpers.update_file(output_path,
                            contents=header + ET.tostring(root, encoding='utf-8').decode('utf-8'))
    return True

def _remove_attributes(elem, attrib_names):
    for attrib_name in attrib_names:
        if attrib_name in elem.attrib:
            del elem.attrib[attrib_name]

def _get_elem_text(elem):
    if elem is not None:
        return elem.text
    return ''

def _set_subelem_text(elem, subelem_name, text):
    if not text:
        text = ' '  # None / empty string not allowed
    subelem = elem.find(subelem_name)
    subelem.text = text

def _find_matching_brackets(args):
    """
    Given a string starting with an open bracket, return:
    - substring from the opening bracket to the matching closing bracket
    - the remainder
    """
    open_count = 0
    for index in range(len(args) - 1):
        if args[index] == '(':
            open_count += 1
        elif args[index] == ')':
            open_count -= 1
        if open_count == 0:
            return args[0:index + 1], args[index + 1:]
    if open_count > 1:
        raise Exception("Unmatched brackets in: %s", repr(args))
    return args

def fix_uproperty(memberdef):
    memberdef.attrib['kind'] = 'variable'
    _remove_attributes(memberdef, ['const', 'explicit', 'inline', 'virt'])
    for param in memberdef.findall('param'):
        memberdef.remove(param)
    # arguments of the UPROPERTY macro followed by member declaration, e.g. ((EditAnywhere, BlueprintReadWrite, Category=...) float var
    argsstring_text = _get_elem_text(memberdef.find("argsstring"))
    uproperty_params, member_declaration = _find_matching_brackets(argsstring_text)
    member_declaration = member_declaration.strip()
    member_initializer = ""
    if '=' in member_declaration:
        # e.g. "float ProximityConeAngleLerp=0.9f"
        member_type_and_name, member_initializer = member_declaration.split('=')
    else:
        member_type_and_name = member_declaration
    member_type, member_name = member_type_and_name.rsplit(maxsplit=1)
    _set_subelem_text(memberdef, 'type', member_type)
    _set_subelem_text(memberdef, 'name', member_name)
    memberdef.attrib['mutable'] = 'no' if member_type.startswith('const ') else 'yes'
    memberdef.attrib['static'] = 'no'
    _set_subelem_text(memberdef, 'definition', 'UPROPERTY{}'.format(uproperty_params))
    # set the initializer (e.g. "=90.0f")
    if member_initializer:
        initializer_elem = ET.SubElement(memberdef, 'initializer')
        initializer_elem.text = '= {}'.format(member_initializer)

def fix_include_path(tree):
    """
    Make #include relative to include path used by the UE4 projects.
    E.g. #include "AUxtHandInteractionActor.h" => #include "Input/AUxtHandInteractionActor.h"
    """
    include_elem = tree.find(".//includes")
    if include_elem is None:
        LOGGER.warning("Could not find includes tag")  # TODO: use ADO pipeline warning syntax
        return
    include_filename = os.path.basename(include_elem.text)
    header_path = include_elem.text
    # find the full path, e.g. "UXToolsGame/Plugins/UXTools/Source/UXTools/Public/Input/UxtHandInteractionActor.h"
    for location_elem in tree.findall(".//memberdef/location[@bodyfile]"):
        if location_elem.attrib['bodyfile'].endswith(include_elem.text):
            header_path = location_elem.attrib['bodyfile']
            break
    # find Public or Private folder in path and use it as the new base for relative paths
    include_root = os.path.dirname(header_path)
    for base_folder in ('Public', 'Private'):
        index = include_root.rfind(base_folder)
        if index > 0:
            include_root = include_root[:index + len(base_folder)]
            break
    # make include path relative to include_root
    include_path = os.path.relpath(header_path, include_root)
    include_elem.text = include_path.replace('\\', '/') 

if __name__ == "__main__":
    PARSER = argparse.ArgumentParser(description=__doc__)
    PARSER.add_argument("input", help="Path to folder with .md files")
    PARSER.add_argument("-o", "--output", help="Path to output directory (if not processing in place)", required=False)
    PARSER.add_argument("-v", "--verbose", help="Verbose output")
    ARGS = PARSER.parse_args(sys.argv[1:])
    logging.basicConfig(format="%(asctime)s - %(levelname)s: %(message)s", level=logging.DEBUG if ARGS.verbose else logging.INFO)
    result = doc_helpers.process_folder(input_dir=ARGS.input,
                                        output_dir=ARGS.output or ARGS.input,
                                        filter_ext=doc_helpers.DOXYGEN_EXT,
                                        callback=process_doxygen_file)
    sys.exit(0 if result else 1)
