# This script generates a set of .md (MarkDown) files
# from xml files generated by doxygen.

# The script does two sweeps. 
# First all xml are parsed and stored in dictionaries
# with their id as key.
# The second sweep generates one page (a markdown file) pr class
# and collects the information for main page which is generated
# at the end.

import sys, os
from xml.etree import ElementTree
import subprocess

input_dir = os.path.abspath(sys.argv[1])
output_dir = os.path.abspath(sys.argv[2])

if not os.path.exists(output_dir) :
    os.mkdir(output_dir)

if not os.path.exists(os.path.join(output_dir, "API")) :
    os.mkdir(os.path.join(output_dir, "API"))


# searches through the detaileddescription section
# for parameter documentation with the same name
# and returns it
def get_parameter_documentation(functiondefinition, declname) :
    details = functiondefinition.find("detaileddescription")
    para = details.find("para")
    if para is not None :
        paralist = para.find("parameterlist")
        if paralist is not None :
            items = paralist.findall("parameteritem")
            for parameteritem in items :
                name = parameteritem.find("parameternamelist").find("parametername").text.strip()

                if name == declname.strip() :
                    # We got the right parameter
                    description = parameteritem.find("parameterdescription")
                    if description is not None :
                        para = description.find("para")
                        if para is not None and para.text is not None:
                            return para.text.strip()
                        else :
                            return ""
    return ""

def get_extra_data(classdefinition) :
    details = classdefinition.find("detaileddescription")
    paragraphs = details.findall("para")
    for para in paragraphs :
        text = para.text.strip()
        if text.startswith("{") and text.endswith("}") :
            return eval(text)
    return {}

print("Reading files from %s" % input_dir)

# save all the classes as dictionary with doxygens id as key.
cls = {}

# First sweep
for filename in os.listdir(input_dir) :
    if not os.path.isfile(os.path.join(input_dir, filename)) :
        continue

    if not filename.endswith(".xml") :
        continue

    # Only classes are documented now
    if not filename.startswith("class") :
        continue

    # print("Processing %s" % filename)

    root = ElementTree.parse(os.path.join(input_dir, filename)).getroot()
    assert root.tag == "doxygen"
    assert len(root) == 1

    class_def = root.find("compounddef")
    assert not cls.has_key(class_def.attrib["id"])

    class_name = class_def.find("compoundname").text
    outfilename_base = class_name if not class_name.startswith("mshr::") else class_name[6:]
    briefdescription = class_def.find("briefdescription")
    # Description should at most one paragraph
    assert (len(briefdescription) <= 1) 
    briefdescription = briefdescription.find("para").text if briefdescription.find("para") is not None else ""
    
    data = {'filename'    : outfilename_base,
            'description' : briefdescription.strip() }

    extra_data = get_extra_data(class_def)
    data.update(extra_data)

    cls[class_def.attrib["id"]] = (class_def, data)

longest_class_name  = max([ len(c[0].find("compoundname").text) for k, c in cls.iteritems()])
longest_description = max([ len(c[1]["description"]) for k, c in cls.iteritems()])

primitives2d = []
primitives3d = []
operators    = []
others       = []


# Second sweep
for k,c in cls.iteritems() :
    theclass = c[0]
    classname = theclass.find("compoundname").text
    classid = theclass.attrib["id"]
    print "Processing "+classname

    classpage = []
    category = others

    # inheritance
    # also decides which category the class belong to
    inheritance = theclass.find("inheritancegraph")
    if inheritance is not None :
        inheritancenodes = {}
        nodes = inheritance.findall("node")

        # collect all inheritance nodes in a dictionary with the refid (which is not the same
        # as the class id as key
        myid = False
        for x in nodes : 
            inheritancenodes[x.attrib["id"]] = x
            link = x.find("link")
            
            # save the refid to self
            if link is not None and link.attrib["refid"] == classid :
                myid = x.attrib["id"]

        # traverse the tree from self through all super classes
        current = inheritancenodes[myid]
        current = inheritancenodes[current.find("childnode").attrib["refid"]]
        inheritance_list = []
        while current is not None :
            label = current.find("label").text
            if label == "mshr::CSGPrimitive2D"   : category = primitives2d
            elif label == "mshr::CSGPrimitive3D" : category = primitives3d
            elif label == "mshr::CSGOperator"    : category = operators

            ref = current.find("link")

            if ref is not None and cls.has_key(ref.attrib["refid"]) :
                inheritance_list.append("[%s](%s)" % (current.find("label").text.strip(),
                                                      cls[ref.attrib["refid"]][1]["filename"]))
            else :
                inheritance_list.append(x.find("label").text.strip())

            child = current.find("childnode")
            if child is not None :
                current = inheritancenodes[child.attrib["refid"]]
            else :
                current = None


        inheritance_list.reverse()
        classpage.append("_" + " > ".join(inheritance_list) + "_")
        classpage.append("\n\n")

    icon_txt = " |"
    if c[1].has_key("small-icon") :
        icon_txt = "![%s icon](icons/%s)|" % (classname, c[1]["small-icon"])

    print "Appending to category"
    category.append("%s[%s](API/%s)|%s|\n" % (icon_txt,
                                              classname.ljust(longest_class_name),
                                              c[1]["filename"],
                                              c[1]["description"].ljust(longest_description)))



    classpage.append("# %s\n\n" % (classname))
    classpage.append("_%s_\n\n" % c[1]["description"])

    classpage.append("#### Public functions\n\n")
    for member in c[0].findall("sectiondef") :

        # Only public functions are considered part of the public API for now
        if member.attrib["kind"] != "public-func" :
            continue

        for definition in member.findall("memberdef") :
            membername = definition.find("name").text

            argsstr    = definition.find("argsstring").text
            description = definition.find("briefdescription").find("para")
            description = description.text.strip() if description is not None else ""

            returntype = definition.find("type").text
            if returntype is not None and len(returntype) > 0 :
                returntype = returntype+" "
            if returntype is None :
                returntype = ""

            # name
            classpage.append("%s**%s**%s\n" % (returntype, membername, argsstr))

            # description
            if description :
                classpage.append("> _%s_\n" % description)

            classpage.append("\n")

            param_table = []

            # table of parameters
            for param in definition.findall("param") :
                declname = param.find("declname").text
                paramtype = param.find("type").text
                if paramtype is None :
                    paramtype = ""

                desc = get_parameter_documentation(definition, declname)
                param_table.append( (declname, paramtype, desc) )
                
            if len(param_table) > 0 :
                longest_param_name = max([len(x[0]) for x in param_table])
                longest_param_type = max([len(x[1]) for x in param_table])
                longest_param_desc = max([len(x[2]) for x in param_table])

                longest_param_name = max(longest_param_name, len("Parameters"))

                classpage.append("> %s|%s|%s\n" % ("Parameters".ljust(longest_param_name),
                                                   " "*longest_param_type,
                                                   " "*longest_param_desc))
                classpage.append("> %s|%s|%s\n" % ("-"*longest_param_name,
                                                   "-"*longest_param_type,
                                                   "-"*longest_param_desc))

                for p in param_table :
                    classpage.append("> %s|%s|%s\n" % (p[0].ljust(longest_param_name),
                                                       p[1].ljust(longest_param_type),
                                                       p[2].ljust(longest_param_desc)))

                classpage.append("\n")
                

    with open(os.path.join(output_dir, "API", c[1]["filename"]+".md"), "w") as f:
        f.write("".join(classpage))
        f.close()



# Create the main page for 
mainpage = ["""

# API reference

"""]

mainpage.append("- |%s|%s|\n" % ("2D primitives".ljust(longest_class_name),
                                "Description".ljust(longest_description)))

mainpage.append("--|%s|%s|\n" % ( ":".ljust(longest_class_name, "-"),
                                 ":".ljust(longest_description, "-")))


################# Append 2D primitives
#mainpage.append("%s|%s|\n" % ("**2D primitives**".ljust(longest_class_name),
#                              "".ljust(longest_description)))

# mainpage.append("%s|%s|\n" % ( ":".ljust(longest_class_name, "-"),
#                                ":".ljust(longest_description, "-")))

mainpage.append("".join(primitives2d))
#mainpage.append("\n\n")

################ Append 3D primitives
mainpage.append("- |%s|%s|\n" % ("**3D primitives**".ljust(longest_class_name),
                              " "*longest_description))

# mainpage.append("%s|%s|\n" % ( ":".ljust(longest_class_name, "-"),
#                                ":".ljust(longest_description, "-")))

mainpage.append("".join(primitives3d))
#mainpage.append("\n\n")

############### Append operators
mainpage.append("- |%s|%s|\n" % ("**Operators**".ljust(longest_class_name),
                              " "*longest_description))

# mainpage.append("%s|%s|\n" % ( ":".ljust(longest_class_name, "-"),
#                               ":".ljust(longest_description, "-")))

mainpage.append("".join(operators))
# mainpage.append("\n\n")

############### Append other classes
mainpage.append("- |%s|%s|\n" % ("**Other classes**".ljust(longest_class_name),
                              " "*longest_description))

# mainpage.append("%s|%s|\n" % ( ":".ljust(longest_class_name, "-"),
#                               ":".ljust(longest_description, "-")))

mainpage.append("".join(others))
mainpage.append("\n\n")

# Read the current revision from git
revision_info = subprocess.check_output(["git", "log", "--pretty=format:'%ad %H'", "-1"]).strip("'")
mainpage.append("_Generated from revision: {}. Please don't edit these files manually._\n".format(revision_info))



with open(os.path.join(output_dir, "API.md"), "w") as f:
    f.write("".join(mainpage))
    f.close()

