#!/usr/bin/env python
################################################################################
#
#  qooxdoo - the new era of web development
#
#  http://qooxdoo.org
#
#  Copyright:
#    2006-2010 1&1 Internet AG, Germany, http://www.1und1.de
#
#  License:
#    LGPL: http://www.gnu.org/licenses/lgpl.html
#    EPL: http://www.eclipse.org/org/documents/epl-v10.php
#    See the LICENSE file in the project's top-level directory for details.
#
#  Authors:
#    * Sebastian Werner (wpbasti)
#    * Fabian Jakobs (fjakobs)
#
################################################################################

import sys, string, re

from ecmascript.frontend import tree
from textile import textile

##
# Many Regexp's
S_INLINE_COMMENT = "//.*"
R_INLINE_COMMENT = re.compile("^" + S_INLINE_COMMENT + "$")

R_INLINE_COMMENT_TIGHT = re.compile("^//\S+")
R_INLINE_COMMENT_PURE = re.compile("^//")



S_BLOCK_COMMENT = "/\*(?:[^*]|[\n]|(?:\*+(?:[^*/]|[\n])))*\*+/"
R_BLOCK_COMMENT = re.compile("^" + S_BLOCK_COMMENT + "$")

R_BLOCK_COMMENT_JAVADOC = re.compile("^/\*\*")
R_BLOCK_COMMENT_QTDOC = re.compile("^/\*!")
R_BLOCK_COMMENT_AREA = re.compile("^/\*\n\s*\*\*\*\*\*")
R_BLOCK_COMMENT_DIVIDER = re.compile("^/\*\n\s*----")
R_BLOCK_COMMENT_HEADER = re.compile("^/\* \*\*\*\*")

R_BLOCK_COMMENT_TIGHT_START = re.compile("^/\*\S+")
R_BLOCK_COMMENT_TIGHT_END = re.compile("\S+\*/$")
R_BLOCK_COMMENT_PURE_START = re.compile("^/\*")
R_BLOCK_COMMENT_PURE_END = re.compile("\*/$")

R_ATTRIBUTE = re.compile('[^{]@(\w+)\s*')
R_JAVADOC_STARS = re.compile(r'^\s*\*')



R_NAMED_TYPE = re.compile(r'^\s*([a-zA-Z0-9_\.#-]+)\s*({([^}]+)})?')
R_SIMPLE_TYPE = re.compile(r'^\s*({([^}]+)})?')




VARPREFIXES = {
    "a" : "Array",
    "b" : "Boolean",
    "d" : "Date",
    "f" : "Function",
    "i" : "Integer",
    "h" : "Map",
    "m" : "Map",
    "n" : "Number",
    "o" : "Object",
    "r" : "RegExp",
    "s" : "String",
    "v" : "var",
    "w" : "Widget"
}

VARNAMES = {
    "a" : "Array",
    "arr" : "Array",

    "doc" : "Document",

    "e" : "Event",
    "ev" : "Event",
    "evt" : "Event",

    "el" : "Element",
    "elem" : "Element",
    "elm" : "Element",

    "ex" : "Exception",
    "exc" : "Exception",

    "flag" : "Boolean",
    "force" : "Boolean",

    "f" : "Function",
    "func" : "Function",

    "h" : "Map",
    "hash" : "Map",
    "map" : "Map",

    "node" : "Node",

    "n" : "Number",
    "num" : "Number",

    "o" : "Object",
    "obj" : "Object",

    "reg" : "RegExp",

    "s" : "String",
    "str" : "String",

    "win" : "Window"
}

VARDESC = {
    "propValue" : "Current value",
    "propOldValue" : "Previous value",
    "propData" : "Property configuration map"
}




def outdent(source, indent): # indent is number!
    return re.compile("\n\s{%s}" % indent).sub("\n", source)



#def indent(source, indent):
#  return re.compile("\n").sub("\n" + (" " * indent), source)
def indent(source, indent):  # indent is string!
    return re.compile("\n").sub("\n" + indent, source)



def correctInline(source):
    if R_INLINE_COMMENT_TIGHT.match(source):
        return R_INLINE_COMMENT_PURE.sub("// ", source)

    return source



def correctBlock(source):
    if not getFormat(source) in ["javadoc", "qtdoc"]:
        if R_BLOCK_COMMENT_TIGHT_START.search(source):
            source = R_BLOCK_COMMENT_PURE_START.sub("/* ", source)

        if R_BLOCK_COMMENT_TIGHT_END.search(source):
            source = R_BLOCK_COMMENT_PURE_END.sub(" */", source)

    return source



def correct(source):
    if source[:2] == "//":
        return correctInline(source)
    else:
        return correctBlock(source)



def isMultiLine(source):
    return source.find("\n") != -1



def getFormat(source):
    if R_BLOCK_COMMENT_JAVADOC.search(source):
        return "javadoc"
    elif R_BLOCK_COMMENT_QTDOC.search(source):
        return "qtdoc"
    elif R_BLOCK_COMMENT_AREA.search(source):
        return "area"
    elif R_BLOCK_COMMENT_DIVIDER.search(source):
        return "divider"
    elif R_BLOCK_COMMENT_HEADER.search(source):
        return "header"

    return "block"








def hasThrows(node):
    if node.type == "throw":
        return True

    if node.hasChildren():
        for child in node.children:
            if hasThrows(child):
                return True

    return False




def getReturns(node, found):
    if node.type == "function":
        pass

    elif node.type == "return":
        if node.getChildrenLength(True) > 0:
            val = "var"
        else:
            val = "void"

        if node.hasChild("expression"):
            expr = node.getChild("expression")
            if expr.hasChild("variable"):
                var = expr.getChild("variable")
                if var.getChildrenLength(True) == 1 and var.hasChild("identifier"):
                    val = nameToType(var.getChild("identifier").get("name"))
                else:
                    val = "var"

            elif expr.hasChild("constant"):
                val = expr.getChild("constant").get("constantType")

                if val == "number":
                    val = expr.getChild("constant").get("detail")

            elif expr.hasChild("array"):
                val = "Array"

            elif expr.hasChild("map"):
                val = "Map"

            elif expr.hasChild("function"):
                val = "Function"

            elif expr.hasChild("call"):
                val = "var"

        if not val in found:
            found.append(val)

    elif node.hasChildren():
        for child in node.children:
            getReturns(child, found)

    return found



def nameToType(name):
    typ = "var"

    # Evaluate type from name
    if name in VARNAMES:
        typ = VARNAMES[name]

    elif len(name) > 1:
        if name[1].isupper():
            if name[0] in VARPREFIXES:
                typ = VARPREFIXES[name[0]]

    return typ



def nameToDescription(name):
    desc = "TODOC"

    if name in VARDESC:
        desc = VARDESC[name]

    return desc




def qt2javadoc(text):
    attribList = parseText(text, False)
    res = "/**"

    desc = getAttrib(attribList, "description")
    if desc.has_key("text"):
        desc = desc["text"]
    else:
        desc = ""

    if "\n" in desc:
        res += "\n"

        for line in desc.split("\n"):
            res += " * %s\n" % line

        res += " "

    else:
        res += " %s " % desc

    res += "*/"

    return res



def findComment(node):
    
    def findCommentBefore(node):
        while node:
            if node.hasChild("commentsBefore"):
                for comment in node.getChild("commentsBefore").children:
                    if comment.get("detail") in ["javadoc", "qtdoc"]:
                        comments = parseNode(node)
                        return comments
            if node.hasParent():
                node = node.parent
            else:
                return None
            
    def findCommentAfter(node):
        while node:
            if node.hasChild("commentsBefore"):
                for comment in node.getChild("commentsBefore").children:
                    if comment.get("detail") in ["javadoc", "qtdoc"]:
                        comments = parseNode(node)
                        return comments
            if node.hasChildren():
                node = node.children[0]
            else:
                return None   
            
    if node.type == "file":
        return findCommentAfter(node)
    else:
        return findCommentBefore(node)  


def parseNode(node):
    """Takes the last doc comment from the commentsBefore child, parses it and
    returns a Node representing the doc comment"""

    # Find the last doc comment
    commentsBefore = node.getChild("commentsBefore", False)
    if commentsBefore and commentsBefore.hasChildren():
        for child in commentsBefore.children:
            if child.type == "comment" and child.get("detail") in ["javadoc", "qtdoc"]:
                return parseText(child.get("text"))

    return []



def parseText(intext, format=True):
    # print "Parse: " + intext

    # Strip "/**", "/*!" and "*/"
    intext = intext[3:-2]

    # Strip leading stars in every line
    text = ""
    for line in intext.split("\n"):
        text += R_JAVADOC_STARS.sub("", line) + "\n"

    # Autodent
    text = autoOutdent(text)

    # Search for attributes
    desc = { "category" : "description", "text" : "" }
    attribs = [desc]
    pos = 0

    while True:
        # this is necessary to match ^ at the beginning of a line
        if pos > 0 and  text[pos-1] == "\n": pos -= 1
        match = R_ATTRIBUTE.search(text, pos)

        if match == None:
            prevText = text[pos:].rstrip()

            if len(attribs) == 0:
                desc["text"] = prevText
            else:
                attribs[-1]["text"] = prevText

            break

        prevText = text[pos:match.start(0)].rstrip()
        pos = match.end(0)

        if len(attribs) == 0:
            desc["text"] = prevText
        else:
            attribs[-1]["text"] = prevText

        attribs.append({ "category" : match.group(1), "text" : "" })

    # parse details
    for attrib in attribs:
        parseDetail(attrib, format)

    return attribs


def parseDetail(attrib, format=True):
    text = attrib["text"]

    if attrib["category"] in ["param", "event", "see", "state", "appearance", "childControl"]:
        match = R_NAMED_TYPE.search(text)
    else:
        match = R_SIMPLE_TYPE.search(text)

    if match:
        text = text[match.end(0):]

        if attrib["category"] in ["param", "event", "see", "state", "appearance", "childControl"]:
            attrib["name"] = match.group(1)
            #print ">>> NAME: %s" % match.group(1)
            remain = match.group(3)
        else:
            remain = match.group(2)

        if remain != None:
            defIndex = remain.rfind("?")
            if defIndex != -1:
                attrib["defaultValue"] = remain[defIndex+1:].strip()
                remain = remain[0:defIndex].strip()
                #print ">>> DEFAULT: %s" % attrib["defaultValue"]

            typValues = []
            for typ in remain.split("|"):
                typValue = typ.strip()
                arrayIndex = typValue.find("[")

                if arrayIndex != -1:
                    arrayValue = (len(typValue) - arrayIndex) / 2
                    typValue = typValue[0:arrayIndex]
                else:
                    arrayValue = 0

                typValues.append({ "type" : typValue, "dimensions" : arrayValue })

            if len(typValues) > 0:
                attrib["type"] = typValues
                #print ">>> TYPE: %s" % attrib["type"]

    if format:
        attrib["text"] = formatText(text)
    else:
        attrib["text"] = cleanupText(text)

    if attrib["text"] == "":
        del attrib["text"]




def autoOutdent(text):
    lines = text.split("\n")

    if len(lines) <= 1:
        return text.strip()

    for line in lines:
        if len(line) > 0 and line[0] != " ":
            return text

    result = ""
    for line in lines:
        if len(line) >= 0:
            result += line[1:]

        result += "\n"

    return result



def cleanupText(text):
    #print "============= INTEXT ========================="
    #print text

    text = text.replace("<p>", "\n")
    text = text.replace("<br/>", "\n")
    text = text.replace("<br>", "\n")
    text = text.replace("</p>", " ")

    # on single lines strip the content
    if not "\n" in text:
        text = text.strip()

    else:
        newline = False
        lines = text.split("\n")
        text = u""

        for line in lines:
            if line == "":
                if not newline:
                    newline = True

            else:
                if text != "":
                    text += "\n"

                if newline:
                    text += "\n"
                    newline = False

                text += line

    #print "============= OUTTEXT ========================="
    #print text

    # Process TODOC the same as no text
    if text == "TODOC":
        return ""

    return text



def formatText(text):
    text = cleanupText(text)

    #if "\n" in text:
    #  print
    #  print "------------- ORIGINAL ----------------"
    #  print text

    text = text.replace("<pre", "\n\n<pre").replace("</pre>", "</pre>\n\n")

    # encode to ascii leads into a translation of umlauts to their XML code.
    text = unicode(textile.textile(text.encode("utf-8"), output="ascii"))

    #if "\n" in text:
    #  print "------------- TEXTILED ----------------"
    #  print text

    return text








def getAttrib(attribList, category):
    for attrib in attribList:
        if attrib["category"] == category:
            return attrib



def getParam(attribList, name):
    for attrib in attribList:
        if attrib["category"] == "param":
            if attrib.has_key("name") and attrib["name"] == name:
                return attrib



def attribHas(attrib, key):
    if attrib != None and attrib.has_key(key) and not attrib[key] in ["", None]:
        return True

    return False



def splitText(orig, attrib=True):
    res = ""
    first = True

    for line in orig.split("\n"):
        if attrib:
            if first:
                res += " %s\n" % line
            else:
                res += " *   %s\n" % line

        else:
            res += " * %s\n" % line

        first = False

    if not res.endswith("\n"):
        res += "\n"

    return res



def parseType(vtype):
    typeText = ""

    firstType = True
    for entry in vtype:
        if not firstType:
            typeText += " | "

        typeText += entry["type"]

        if entry.has_key("dimensions") and entry["dimensions"] > 0:
            typeText += "[]" * entry["dimensions"]

        firstType = False

    return typeText




def fromNode(node, assignType, name, alternative, old=[]):
    #
    # description
    ##############################################################
    oldDesc = getAttrib(old, "description")

    if attribHas(oldDesc, "text"):
        newText = oldDesc["text"]
    else:
        newText = "{var} TODOC"

    if "\n" in newText:
        s = "/**\n%s\n-*/" % splitText(newText, False)
    else:
        s = "/** %s */" % newText

    s = s.replace("/**  ", "/** ").replace("  */", " */")



    #
    # other @attributes
    ##############################################################

    for attrib in old:
        cat = attrib["category"]

        if cat != "description":
            print "  * Found unallowed attribute %s in comment for %s (node)" % (cat, name)

    return s




def fromFunction(func, assignType, name, alternative, old=[]):
    #
    # open comment
    ##############################################################
    s = "/**\n"


    #
    # description
    ##############################################################
    oldDesc = getAttrib(old, "description")

    if attribHas(oldDesc, "text"):
        newText = oldDesc["text"]
    else:
        newText = "TODOC"

    s += splitText(newText, False)
    s += " *\n"




    #
    # add @type
    ##############################################################
# TODO: Remove the @type annotation as it conflicts with JSdoc
#    if assignType != None:
#        s += " * @type %s\n" % assignType
#    else:
#        s += " * @type unknown TODOC\n"




    #
    # add @abstract
    ##############################################################
    oldAbstract = getAttrib(old, "abstract")

    first = func.getChild("body").getChild("block").getFirstChild(False, True)
    abstract = first and first.type == "throw"

    if abstract:
        if attribHas(oldAbstract, "text"):
            newText = oldDesc["text"]
        else:
            newText = ""

        s += " * @abstract%s" % splitText(newText)

        if not s.endswith("\n"):
            s += "\n"

    elif oldAbstract:
        print " * Removing old @abstract for %s" % name






    #
    # add @param
    ##############################################################
    params = func.getChild("params")
    if params.hasChildren():
        for child in params.children:
            if child.type == "variable":
                newName = child.getChild("identifier").get("name")
                newType = newTypeText = nameToType(newName)
                newDefault = ""
                newText = nameToDescription(newName)

                oldParam = getParam(old, newName)

                # Get type and text from old content
                if oldParam:
                    if attribHas(oldParam, "type"):
                        newTypeText = parseType(oldParam["type"])

                    if attribHas(oldParam, "defaultValue"):
                        newDefault = " ? %s" % oldParam["defaultValue"]

                    if attribHas(oldParam, "text"):
                        newText = oldParam["text"].strip()

                s += " * @param %s {%s%s}%s" % (newName, newTypeText, newDefault, splitText(newText))

                if not s.endswith("\n"):
                    s += "\n"





    #
    # add @return
    ##############################################################
    if name != "construct":
        oldReturn = getAttrib(old, "return")

        newType = "void"
        newText = ""

        # Get type and text from old content
        if oldReturn:
            if attribHas(oldReturn, "type"):
                newType = parseType(oldReturn["type"])

            if attribHas(oldReturn, "text"):
                newText = oldReturn["text"].strip()

        # Try to autodetect the type
        if newType == "void":
            returns = getReturns(func.getChild("body"), [])

            if len(returns) > 0:
                newType = " | ".join(returns)
            elif name != None and name[:2] == "is" and name[3].isupper():
                newType = "Boolean"

        # Add documentation hint in non void cases
        if newType != "void" and newText == "":
            newText = "TODOC"

        s += " * @return {%s}%s" % (newType, splitText(newText))

        if not s.endswith("\n"):
            s += "\n"






    #
    # add @throws
    ##############################################################
    oldThrows = getAttrib(old, "throws")

    if hasThrows(func):
        if oldThrows and attribHas(oldThrows, "text"):
            newText = oldThrows["text"]
        elif abstract:
            newText = "the abstract function warning."
        else:
            newText = "TODOC"

        s += " * @throws%s" % splitText(newText)

        if not s.endswith("\n"):
            s += "\n"

    elif oldThrows:
        print "  * Removing old @throw attribute in comment for %s" % name




    #
    # other @attributes
    ##############################################################

    for attrib in old:
        cat = attrib["category"]

        if cat in ["see", "author", "deprecated", "exception", "since", "version", "abstract", "overridden"]:
            s += " * @%s" % cat

            if cat == "see":
                if attribHas(attrib, "name"):
                    s += splitText(attrib["name"])
            elif attribHas(attrib, "text"):
                s += splitText(attrib["text"])

            if not s.endswith("\n"):
                s += "\n"

        elif not cat in ["description", "type", "abstract", "param", "return", "throws", "link", "internal", "signature"]:
            print "  * Found unallowed attribute %s in comment for %s (function)" % (cat, name)





    #
    # close comment
    ##############################################################
    s += " */"

    return s



##
# fill(node) -- look for function definitions in the tree represented by <node>,
# look for their corresponding comment and amend it, or create it in the first
# place
#
def fill(node):
    if node.type in ["comment", "commentsBefore", "commentsAfter"]:
        return

    if node.hasParent():
        target = node

        if node.type == "function":
            name = node.get("name", False)
        else:
            name = ""

        alternative = False
        assignType = None

        if name != None:
            assignType = "function"

        # move to hook operation
        while target.parent.type in ["first", "second", "third"] and target.parent.parent.type == "operation" and target.parent.parent.get("operator") == "HOOK":
            alternative = True
            target = target.parent.parent

        # move comment to assignment
        while target.parent.type == "right" and target.parent.parent.type == "assignment":
            target = target.parent.parent
            if target.hasChild("left"):
                left = target.getChild("left")
                if left and left.hasChild("variable"):
                    var = left.getChild("variable")
                    last = var.getLastChild(False, True)
                    if last and last.type == "identifier":
                        name = last.get("name")
                        assignType = "object"

                    for child in var.children:
                        if child.type == "identifier":
                            if child.get("name") in ["prototype", "Proto"]:
                                assignType = "member"
                            elif child.get("name") in ["class", "base", "Class"]:
                                assignType = "static"

            elif target.parent.type == "definition":
                name = target.parent.get("identifier")
                assignType = "definition"

        # move to definition
        if target.parent.type == "assignment" and target.parent.parent.type == "definition" and target.parent.parent.parent.getChildrenLength(True) == 1:
            target = target.parent.parent.parent
            assignType = "function"


        # move comment to keyvalue
        if target.parent.type == "value" and target.parent.parent.type == "keyvalue":
            target = target.parent.parent
            name = target.get("key")
            assignType = "map"

            if name == "construct":
                assignType = "constructor"

            if target.parent.type == "map" and target.parent.parent.type == "value" and target.parent.parent.parent.type == "keyvalue":
                paname = target.parent.parent.parent.get("key")

                if paname == "members":
                    assignType = "member"

                elif paname == "statics":
                    assignType = "static"

        # filter stuff, only add comments to member and static values and to all functions
        if assignType in ["member", "static"] and node.type == "function":

            if not hasattr(target, "documentationAdded") and target.parent.type != "params":
                old = []

                commentNode = None

                # create commentsBefore
                if target.hasChild("commentsBefore"):
                    commentsBefore = target.getChild("commentsBefore")

                    if commentsBefore.hasChild("comment"):
                        for child in commentsBefore.children:
                            if child.get("detail") in ["javadoc", "qtdoc"]:
                                old = parseText(child.get("text"), False)
                                commentNode = child
                                commentNodeIndex = commentsBefore.children.index(child)
                                break

                else:
                    commentsBefore = tree.Node("commentsBefore")
                    target.addChild(commentsBefore)

                # create comment node
                if commentNode == None:
                  commentNodeIndex = None
                  commentNode = tree.Node("comment")
                  commentNode.set("detail", "javadoc")

                """
                if node.type == "function":
                    commentNode.set("text", fromFunction(node, assignType, name, alternative, old))
                else:
                    commentNode.set("text", fromNode(node, assignType, name, alternative, old))
                """

                commentNode.set("text", fromFunction(node, assignType, name, alternative, old))

                commentNode.set("multiline", True)

                commentsBefore.addChild(commentNode,commentNodeIndex)

                # in case of alternative methods, use the first one, ignore the others
                target.documentationAdded = True





    if node.hasChildren():
        for child in node.children:
            fill(child)
