# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
"""
Generate HTML documentation for Device Interface Functions (DIFs)
"""

import copy
import logging as log
import subprocess
import xml.etree.ElementTree as ET


# Turn the Doxygen multi-file XML output into one giant XML file (and parse it
# into a python object), using the provided XLST file.
def get_combined_xml(doxygen_xml_path):
    xsltproc_args = [
        "xsltproc",
        str(doxygen_xml_path.joinpath("combine.xslt")),
        str(doxygen_xml_path.joinpath("index.xml")),
    ]

    combined_xml_res = subprocess.run(
        xsltproc_args, check=True,
        cwd=str(doxygen_xml_path), stdout=subprocess.PIPE,
        universal_newlines=True,
    )
    return ET.fromstring(combined_xml_res.stdout)


# Get all information about individual DIF functions that are specified in one
# DIF header. This returns only the Info from the XML that we require.
def get_difref_info(combined_xml, dif_header):
    compound = _get_dif_file_compound(combined_xml, dif_header)
    if compound is None:
        return []

    file_id = _get_dif_file_id(compound)
    functions = _get_dif_function_info(compound, file_id)
    return functions


# Create HTML List of DIFs, using the info from the combined xml
def gen_listing_html(html_path: str, combined_xml, dif_header, dif_listings_html):
    compound = _get_dif_file_compound(combined_xml, dif_header)
    if compound is None:
        log.error("Doxygen output not found for {}".format(dif_header))
        return

    file_id = _get_dif_file_id(compound)
    functions = _get_dif_function_info(html_path, compound, file_id)

    if len(functions) == 0:
        log.error("No DIF functions found for {}".format(dif_header))
        return

    # Generate DIF listing header
    dif_listings_html.write('<p>To use this DIF, include the following C header:</p>')
    dif_listings_html.write('<pre><code class=language-c data-lang=c>')
    dif_listings_html.write('#include "<a href="{}/{}.html">{}</a>"'.format(
        html_path, file_id, dif_header,
    ))
    dif_listings_html.write('</code></pre>\n')

    # Generate DIF function list.
    dif_listings_html.write('<p>This header provides the following device interface functions:</p>')
    dif_listings_html.write('<ul>\n')
    for f in sorted(functions, key=lambda x: x['name']):
        dif_listings_html.write('<li title="{prototype}" id="Dif_{name}">'.format(**f))
        dif_listings_html.write('<a href="{full_url}">'.format(**f))
        dif_listings_html.write('<code>{name}</code>'.format(**f))
        dif_listings_html.write('</a>\n')
        dif_listings_html.write(f['description'])
        dif_listings_html.write('</li>\n')
    dif_listings_html.write('</ul>\n')


# Generate HTML link for single function, using info returned from
# get_difref_info
def gen_difref_html(function_info, difref_html):
    difref_html.write('<a href="{full_url}">'.format(**function_info))
    difref_html.write('<code>{name}</code>'.format(**function_info))
    difref_html.write('</a>\n')


def _get_dif_file_compound(combined_xml, dif_header):
    for c in combined_xml.findall('compounddef[@kind="file"]'):
        if c.find("location").attrib["file"] == dif_header:
            return c
    return None


def _get_dif_file_id(compound):
    return compound.attrib["id"]


def _get_dif_function_info(html_path: str, compound, file_id):
    funcs = compound.find('sectiondef[@kind="func"]')
    if funcs is None:
        return []

    # Collect useful info on each function
    functions = []
    for m in funcs.findall('memberdef[@kind="function"]'):
        func_id = m.attrib['id']
        # Strip refid prefix, which is separated from the funcid by `_1`
        if func_id.startswith(file_id + '_1'):
            # The +2 here is because of the weird `_1` separator
            func_id = func_id[len(file_id) + 2:]
        else:
            # I think this denotes that this function isn't from this file
            continue

        func_info = {}
        func_info["id"] = m.attrib["id"]
        func_info["file_id"] = file_id
        func_info["local_id"] = func_id
        func_info["full_url"] = "{}/{}.html#{}".format(html_path, file_id, func_id)

        func_info["name"] = _get_text_or_empty(m, "name")
        func_info["prototype"] = _get_text_or_empty(
            m, "definition") + _get_text_or_empty(m, "argsstring")
        func_info["description"] = _get_html_or_empty(m,
                                                      "briefdescription/para")

        functions.append(func_info)

    return functions


def _get_html_or_empty(element: ET.Element, xpath: str) -> str:
    """ Get a minimal HTML-rendering of the children in an element.

    element is expected to be a docCmdGroup according to the DoxyGen schema [1],
    but only a very minimal subset of formatting is transferred to semantic
    HTML. However, the tag structure is retained by transforming all tags into
    HTML `span` tags with a class attribute `doxygentag-ORIGINALTAGNAME`. This
    can be used to write CSS targeting specific Doxygen tags and recreate the
    intended formatting.

    In addtion, the following semantic transformations are performed:
    - `computeroutput` is transformed to `code`

    [1] https://github.com/doxygen/doxygen/blob/master/templates/xml/compound.xsd"""
    inner = element.find(xpath)
    # if the element isn't found, return ""
    if inner is None:
        return ""

    # Avoid modifying the passed element argument.
    inner_copy = copy.deepcopy(inner)

    for c in inner_copy.iter():
        c.set('class', 'doxygentag-' + c.tag)
        if c.tag == 'computeroutput':
            c.tag = 'code'
        else:
            c.tag = 'span'

    # Create a string from all subelements
    text = ET.tostring(inner_copy, encoding="unicode", method="html")
    return text or ""


def _get_text_or_empty(element: ET.Element, xpath: str) -> str:
    """ Get all text of an element, without any tags """
    inner = element.find(xpath)
    if inner is None:
        return ""

    return ' '.join([e for e in inner.itertext()]) or ""
