# Doxy2PyDocsStrings.py
#
# Convert the C++ documentation generated by Doxygen to Python docs strings.
# This saves the manual synchronization between the C++ code documentation and
# Python doc strings.
#
# Copyright (c) 2019 Bertrand Coconnier
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, see <http://www.gnu.org/licenses/>
#

import re
from textwrap import wrap
import xml.etree.ElementTree as et

LASTCOL = 79


def wrap_last_line(txt, tab):
    col = txt.rfind("\n" + tab) + len(tab) + 1
    lastline = txt[col:].strip()
    n = len(tab)
    if len(lastline) > LASTCOL - n:
        return txt[:col] + ("\n" + tab).join(wrap(lastline, LASTCOL - n))

    return txt


def wrap_list_item(item, bullet, indent):
    text = " ".join(convert_para(item, indent).split())
    tab = " " * (indent + len(bullet))
    return ("\n" + tab).join(wrap(text, LASTCOL - len(tab)))


def convert_para(para, indent):
    tab = " " * indent
    docstring = ""
    if para.text:
        text = " ".join(para.text.split())
        docstring += ("\n" + tab).join(wrap(text, LASTCOL - indent))
    for elem in list(para):
        if elem.tag == "heading":
            if docstring:
                docstring = docstring.strip() + "\n\n" + tab
            docstring += ".. rubric:: " + elem.text
        elif elem.tag == "linebreak":
            docstring += "\n" + tab
            if elem.tail:
                docstring += elem.tail.strip()
        elif elem.tag == "bold":
            docstring += "**" + elem.text + "**"
            if elem.tail:
                docstring += elem.tail
        elif elem.tag == "itemizedlist":
            if docstring:
                docstring = docstring.strip() + "\n\n" + tab
            for item in elem.findall("listitem/para"):
                docstring += "* " + wrap_list_item(item, "* ", indent) + "\n" + tab
            docstring = docstring.rstrip()
        elif elem.tag == "orderedlist":
            if docstring:
                docstring = docstring.strip() + "\n\n" + tab
            num = 1
            for item in elem.findall("listitem/para"):
                bullet = "{}. ".format(num)
                docstring += bullet + wrap_list_item(item, bullet, indent) + "\n" + tab
                num += 1
            docstring = docstring.rstrip()
        elif elem.tag == "programlisting":
            if docstring:
                docstring = docstring.strip() + "\n\n" + tab
            language = "xml"
            if "filename" in elem.attrib.keys():
                language = elem.attrib["filename"][1:]
            docstring += ".. code-block:: " + language + "\n\n"
            for codeline in elem.findall("codeline"):
                line = " " * (indent + 3)
                for cl in codeline.iter():
                    if cl.tag in ("highlight", "ref"):
                        if cl.text:
                            line += cl.text.strip()
                    elif cl.tag == "sp":
                        line += " "
                    if cl.tail:
                        line += cl.tail.strip()
                docstring += line + "\n"
            if elem.tail:
                docstring += elem.tail
        elif elem.tag == "ref":
            if elem.attrib["kindref"] == "compound" and elem.text in klasses:
                docstring += " :ref:`" + elem.text + "`"
            else:
                docstring += " " + elem.text
            if elem.tail:
                docstring += elem.tail
            docstring = wrap_last_line(docstring, tab)
        elif elem.tag == "parameterlist":
            if docstring.rstrip():
                docstring += "\n\n" + tab
            for item in elem.findall("parameteritem"):
                pname = item.find("parameternamelist/parametername").text
                pdesc = item.find("parameterdescription/para")
                bullet = ":param " + pname + ": "
                docstring += bullet + wrap_list_item(pdesc, bullet, indent) + "\n" + tab
        elif elem.tag == "simplesect" and elem.attrib["kind"] == "return":
            ret = elem.find("para")
            if ret is not None and ret.text:
                if docstring.rstrip():
                    docstring += "\n\n" + tab
                bullet = ":return: "
                docstring += bullet + wrap_list_item(ret, bullet, indent) + "\n"
        elif elem.tag == "ulink":
            docstring += "`" + elem.text + " <" + elem.attrib["url"] + ">`_"
            if elem.tail:
                docstring += elem.tail
            docstring = wrap_last_line(docstring, tab)

    return docstring


with open("${CMAKE_CURRENT_BINARY_DIR}/_jsbsim.pyx") as source:
    pyx_data = source.read()

klasses = re.findall(r"cdef\s+class\s+(\w+)", pyx_data)

# Autogenerate the documentation page for each class
for klass in klasses:
    with open("${CMAKE_BINARY_DIR}/documentation/" + klass + ".rst", "w") as f:
        title = klass
        f.write(".. _" + klass + ":\n\n")
        f.write("=" * len(title) + "\n" + title + "\n" + "=" * len(title) + "\n\n")
        f.write(".. autoclass:: jsbsim." + klass + "\n   :members:\n")

tree = et.parse("${CMAKE_BINARY_DIR}/documentation/xml/indexpage.xml")
root = tree.getroot()
mainpage = ""
doxymain = re.search(r"@DoxMainPage", pyx_data)
col = doxymain.start() - pyx_data[: doxymain.start()].rfind("\n")
tab = " " * (col - 1)

for sect in root.findall(".//sect1"):
    # mainpage += '.. '+sect.attrib['id']+':\n\n'
    title = sect.find("title").text
    mainpage += title + "\n" + tab + "=" * len(title) + "\n\n" + tab
    for para in sect.findall("para"):
        mainpage += convert_para(para, col - 1).strip() + "\n\n" + tab

pyx_data = pyx_data[: doxymain.start()] + pyx_data[doxymain.start() :].replace(
    doxymain.group(), mainpage.rstrip()
)

with open("${CMAKE_BINARY_DIR}/documentation/mainpage.rst", "w") as f:
    f.write("\n".join(mainpage.split("\n" + tab)))

request = re.compile(r"@Dox\(([\w:]+)(\(([\w:,&\s]+)\))?\)")
doxytag = re.search(request, pyx_data)

while doxytag:
    names = doxytag.group(1).split("::")
    xmlfilename = "class" + "_1_1".join(names[:2]) + ".xml"
    tree = et.parse("${CMAKE_BINARY_DIR}/documentation/xml/" + xmlfilename)
    root = tree.getroot()
    docstring = ""
    col = doxytag.start() - pyx_data[: doxytag.start()].rfind("\n")
    tab = " " * (col - 1)

    for tag in root.findall("compounddef/compoundname"):
        if tag.text != "::".join(names[:2]):
            raise IOError(
                "File {} does not contain {}".format(xmlfilename, doxytag.group(1))
            )

    if len(names) == 2:
        # Class docs
        member = root.find("compounddef")
    else:
        # Member function docs
        if doxytag.group(3):
            params_type = [ptype.strip() for ptype in doxytag.group(3).split(",")]
        else:
            params_type = None
        for member in root.findall(".//memberdef"):
            if member.find("name").text == names[2]:
                if params_type is not None:
                    ptypes = [ptype.text for ptype in member.findall("param/type")]
                    if ptypes != params_type:
                        continue
                break
        else:
            raise IOError(
                "File {} does not contain {}".format(xmlfilename, doxytag.group(1))
            )
    para = member.find("briefdescription/para")
    if para is not None and para.text:
        docstring = para.text.strip() + "\n\n" + tab

    tag = member.find("detaileddescription")
    if tag is not None:
        for para in tag.findall("para"):
            docstring += convert_para(para, col - 1).strip() + "\n\n" + tab

    if len(docstring) == 0:
        docstring = (
            "\n"
            + tab
            + ".. note::\n\n   "
            + tab
            + "This feature is not yet documented."
        )

    pyx_data = pyx_data[: doxytag.start()] + pyx_data[doxytag.start() :].replace(
        doxytag.group(), docstring.rstrip()
    )
    doxytag = re.search(request, pyx_data)

with open("${CMAKE_CURRENT_BINARY_DIR}/_jsbsim.pyx", "w") as dest:
    dest.write(pyx_data)

