#!/usr/bin/env python

import re
import os
import sys

# -----------------
# Sintax: OpenLayers Natural Docs --> Closure Compiler jsDoc 
# -----------------

# Translate comment blocks
syntax ={
    "public": { # No effect: should be studied
        "keywords": ["APIFunction:", "APIMethod:"]
    },
    "private": { # No effect: should be studied
        "keywords": ["Function:", "Method:"]
    },
    "jsDoc": { # See Format/WKT.js:260-349
        "keywords": ["@param ", "@return ", "@returns "],
        "keywordLineCnv": "cnvKeywordJsDoc"
    },
    "extends": {
        "keywords": ["Inherits from: *$"],
        "lineConv": "lineSuperClass",
        "maxLines": 1, # Closure Compiler does not understand the multiple-inheritance.
        "prefixLine": "@extends"
    },    
    "constructor": {
        "keywords": ["Constructor:"],
        "prefixKeyword": "@constructor"
    },
    "const": {
        "keywords": ["Constant:"],
        "prefixKeyword": "@const",
        "lineConv": "lineSingleDecl",
        "prefixLine": "@type"
    },    
    "namespace": {
        "keywords": ["Namespace:"],
        "prefixKeyword": "@type {Object}"
    },
    "var": {
        "keywords": ["Property:", "APIProperty:"],
        "lineConv": "lineSingleDecl",
        "prefixLine": "@type",
        "maxLines": 1, # forced end of block
        "maxKeywords": 1  ## TODO: 
            ## A property can be a function with parameters 
            ##      documented in other lines within the block, 
            ##      this means stop working line by line to analyze 
            ##      the whole block together. uff!
            ##      See APIProperty:onStart on Control/DragFeature.js
            # On the other hand it is very rare case and there is few
            #   additional informations that can provide to the compiler.
    },
    "params": {
        "keywords": ["Parameters: *$", "Parameter: *$"],
        "lineConv": "lineParam",
        "prefixLine": "@param"
    },
    "optionalParams": { # New block: To debate
        "keywords": ["Optional Parameters: *$", "Optional Parameter: *$"],
        "lineConv": "lineOptParam",
        "prefixLine": "@param"
    },
    "optionsParam": { # Any "options" argument is forced to set optional.
        "keywords": ["options - {Object}"],
        "prefixKeyword": "@param {Object=} options",
        "lineConv" : "lineOptParam", # Causes compiler warnings that allows 
        "prefixLine": "@param"       #    better verification of documentation.
                                     #    See below "optionsProperties"
    },
    "optionsProperties": { # The properties of the "options" are not translated into jsDoc
                           #    It is necessary to complete the set of parameters 
                           #    for the compiler and also for the documentation 
                           #    generated by Natural Docs.
        "keywords": [
            "Options: *$", 
            "Allowed Options: *$", 
            "Valid Options: *$", 
            "Valid options properties: *$"]
    },
    "scope": { # New block: To debate
        "keywords": ["Scope: *$"],
        "lineConv": "lineSingleDecl",
        "prefixLine": "@this"
    },
    "return": {
        "keywords": ["Returns: *$", "Return: *$"],
        "lineConv": "lineSingleDecl",
        "maxLines": 1, # forced end of block
        "prefixLine": "@return"
    }
}

# Translate types
typesOL = [
    # For DOM types see Closure Compiler source in the folder: closure-compiler/externs
    ("XMLNode",      "Node"), # Closure said: "we put XMLNode properties on Node" (see: ie_dom.js)
    ("DOMElement",   "Element"),
    ("HTMLDOMElement",   "Element"), # Used in: Events.js
    # js types
    ("Number",       "number"),
    ("Integer",      "number"),
    ("int",          "number"), # Used in: Events.js
    ("Float",        "number"),
    ("String",       "string"),
    ("Boolean",      "boolean"),
    ("Function",     "function(...[*])"),
    # Composed types
    (r"Array\((.*)\)",   r"Array.<\1>"), # Array(...) to Array.<...>
    (r"Array\[(.*)\]",   r"Array.<\1>"), # Used in: Popup/Framed.js
    (r"Array\<(.*)\>",   r"Array.<\1>"), # Used in: Renderer/Elements.js:26
    (r"\<(.*)>\((.*)\)", r"\1.<\2>")     # Used in Tween.js:24
]

# -----------------
# Detect "Natural Docs" comments.
# -----------------
M_CODE = 0
M_COM2_BLOC = 1
reStarCom2Bloc =        re.compile(r"^ *\/\*\* *(?! )")
reLineCom2 =            re.compile(r"^ *\* *(?! )")
reEndComBloc =          re.compile(r"\*\/")
reEndLine =             re.compile(r"\n")
reProblematicEndLine =  re.compile(r"\\\n")

def cnv4JsDoc (inputFilename, outputFilename):
    print "Translating into jsDoc: ", outputFilename, " ",

    if not os.path.isfile(inputFilename):
        print "\nProcess aborted due to errors."
        sys.exit('ERROR: Input file "%s" does not exist!' % inputFilename)

    dirOut = os.path.dirname(outputFilename)
    if dirOut == "":
        print "\nProcess aborted due to errors."
        sys.exit('ERROR: Output file "%s" without path!' % outputFilename)

    if not os.path.exists(dirOut):
        os.makedirs(dirOut)

    fOut = open(outputFilename,"w")
    fIn = open(inputFilename)
    
    mode = M_CODE
    previousProblematicEndLine = False
    nat = Com2()
    lineNumber = 0
    for line in fIn:
        lineNumber += 1
        startCom2 = -1
        endCom2 = -1
        if mode == M_CODE and not previousProblematicEndLine:
            oo = reStarCom2Bloc.search(line)
            if oo: 
                startCom2 = oo.end()
                mode = M_COM2_BLOC
                nat.clearBlock()
                
        # Com2 line?
        if mode == M_COM2_BLOC:
            previousProblematicEndLine = False
            oo = reEndComBloc.search(line)
            if oo: 
                endCom2 = oo.start()
                mode = M_CODE

            if startCom2 == -1:
                oo = reLineCom2.search(line)
                if oo: 
                    startCom2 = oo.end()

            if endCom2 == -1:
                endCom2 = reEndLine.search(line).start()

            if startCom2 >= 0 and startCom2 < endCom2:
                # Com2 line? Yeah!
                line = ( line[:startCom2] + 
                         nat.toJsDoc(line[startCom2:endCom2]) + 
                         line[endCom2:] )
        fOut.write(line)
        if mode == M_CODE: 
            if reProblematicEndLine.search(line):
                previousProblematicEndLine = True
            else:
                previousProblematicEndLine = False
                
    print "   Done!"
    fIn.close()
    fOut.close()
    return outputFilename
    
# -----------------
# Analyze comment blocks
# -----------------    
class Com2:
    def __init__(self):
        self.clearBlock()

    def clearBlock(self):
        self.maxKeywords = sys.maxint
        self.processedKeywords = 0
        self.blockActive = True
        self.clearKeyword()

    def clearKeyword(self):
        self.maxLines = sys.maxint
        self.processedLines = 0
        self.currentModeBlock = None
        self.blockName = None

    def toJsDoc(self, line):
        if self.blockActive == False:
            return line
        for k, v in syntax.iteritems():
            for j in v["keywords"]:
                if re.match(j, line, flags=re.IGNORECASE):
                    # Close the previous keyword
                    if self.maxKeywords <= self.processedKeywords:
                        self.blockActive = False
                        return line
                    self.clearKeyword()
                    
                    # Start keyword
                    self.blockName = k
                    self.currentModeBlock = v
                    
                    if "keywordLineCnv" in self.currentModeBlock:
                        conv = self.currentModeBlock["keywordLineCnv"]
                        if conv == "cnvKeywordJsDoc":
                            line = self.cnvKeywordJsDoc(line)
                        # cnvKeywordType
                    if v.get("maxLines"):
                        self.maxLines = v.get("maxLines")
                    if v.get("maxKeywords"):
                        self.maxKeywords = v.get("maxKeywords")
                    if "prefixKeyword" in self.currentModeBlock:
                        line = self.currentModeBlock["prefixKeyword"] + " " + line
                    self.processedKeywords += 1
                    break
            else:
                continue
            break
        else:
            if self.currentModeBlock and self.currentModeBlock.get("lineConv"):
                conv = self.currentModeBlock.get("lineConv")
                if conv == "lineParam":
                    line = self.cnvLineParam(line, "", "")
                elif conv == "lineOptParam":
                    line = self.cnvLineParam(line, "", "|null|undefined=")
                elif conv == "lineSingleDecl":
                    line = self.cnvLineSingleDecl(line)
                elif conv == "lineSuperClass":
                    line = self.cnvLineSuperClass(line)

                if self.maxLines <= self.processedLines:
                    self.clearKeyword() # forced end of block
        return line

    # Keyword line converters
    def cnvKeywordJsDoc(self, subLine):
        words = subLine.split(None,1)
        if len(words) < 2:
            return subLine

        decl = self.cnvChkDeclaration(words[1])
        if decl == None:
            return subLine

        return ( words[0] + " {" + decl[0] + "} " + " " + decl[1])

    # Line converters
    def cnvLineSuperClass(self, subLine):
        superClass = subLine.replace("- <", 
            self.currentModeBlock.get("prefixLine") + " ")
        if subLine == superClass:
            return subLine

        self.processedLines += 1
        superClass = superClass.replace(">","")
        return superClass

    def cnvLineParam(self, subLine, start, end):
        words = subLine.split(None,2)
        if len(words) < 3:
            return subLine

        if words[1] != "-":
            return subLine

        decl = self.cnvChkDeclaration(words[2])
        if decl == None:
            return subLine

        self.processedLines += 1
        return (self.currentModeBlock.get("prefixLine") +
            " {" + start + decl[0] + end + "} " +
            words[0] + " "+ decl[1])

    def cnvLineSingleDecl(self, subLine):
        decl = self.cnvChkDeclaration(subLine)
        if decl == None:
            return subLine

        self.processedLines += 1
        return self.currentModeBlock.get("prefixLine") + " {" + decl[0] + "} " + decl[1]

    # Type converter
    def cnvChkDeclaration(self, subLine):
        if subLine[0:1] != "{":
            return None

        declEnd = re.search(r"\}(\s|\n|$)", subLine)
        if not declEnd:
            return None

        return [self.cnvTypeList(subLine[1:declEnd.start()]), 
                subLine[declEnd.start()+1:]] 

    def cnvTypeList(self, typeList):
        repetitiveParameter = ""
        if typeList[-4:] == " ...":
            typeList = typeList[:-4]
            repetitiveParameter = "..."
        
        return repetitiveParameter + self.cnvTypeName(typeList)

    def cnvTypeName(self, typeName):
        # print "ini:", typeName
        if typeName == "":
            return "*" # Any type from declaration as {}

        for p, r in typesOL:
            if r.find(r"\2") > 0:
                spl = re.split(p,typeName)
                if len(spl) >= 4 and spl[1] != "" and spl[2] != "":
                    typeName = re.sub(p,r,typeName)
                    typeName = typeName.replace(spl[1], self.cnvTypeName(spl[1]))
                    typeName = typeName.replace(spl[2], self.cnvTypeName(spl[2]))
                    # print "/2:",typeName
                    return self.cnvTypeNameSplit(typeName)
            elif r.find(r"\1") > 0:
                spl = re.split(p,typeName)
                if len(spl) >= 3 and spl[1] != "":
                    typeName = re.sub(p,r,typeName)
                    typeName = typeName.replace(spl[1], self.cnvTypeName(spl[1]))
                    # print "/1:",typeName
                    return self.cnvTypeNameSplit(typeName)
            else:
                if typeName.lower() == p.lower():
                    return r

        return self.cnvTypeNameSplit(typeName)

    def cnvTypeNameSplit(self, typeName):
        # print "or:",typeName
        typeName = typeName.replace(" or ","|")
        typeName = typeName.replace("||","|") # Used in Event.js
        typeName = typeName.replace(" ","")
        types = typeName.split("|")
        if len(types) > 1:
            for i in range(len(types)):
                types[i] = self.cnvTypeName(self.cnvTypeNameClear(types[i]))
            # print types
            return "|".join(types)
        else:
            # print typeName
            return self.cnvTypeNameClear(typeName)

    def cnvTypeNameClear(self, typeAux):
        if typeAux[0:1] == "{" and typeAux[len(typeAux)-1:] == "}": # See Protocol.js:105
            typeAux = typeAux[1:-1]
        if typeAux[0:1] == "<" and typeAux[len(typeAux)-1:] == ">":
            typeAux = typeAux[1:-1]
        return typeAux

# -----------------
# main
# -----------------
if __name__ == '__main__':
    cnv4JsDoc(sys.argv[1],sys.argv[2])
