import os
import sys
import shutil
import zipfile
import json
import subprocess
import platform
import html
import re

from lib import tools_lib as Tools

class HtmlGenerator:
    def __init__ (self, eol):
        self.html = ''
        self.eol = eol

    def AddText (self, content):
        self.html += content

    def AddLine (self, content):
        self.AddText (content + self.eol)

    def AddTag (self, tagName, content):
        self.AddLine ('<{0}>{1}</{0}>'.format (tagName, content))

    def AddTagWithClass (self, tagName, className, content):
        self.AddLine ('<{0} class="{1}">{2}</{0}>'.format (tagName, className, content))

    def AddTagWithAttributes (self, tagName, attributes, content):
        line = '<{0}'.format (tagName);
        if len (attributes) > 0:
            attributeStrings = map (lambda x : '{0}="{1}"'.format (x[0], x[1]), attributes)
            line += ' ' + ' '.join (attributeStrings)
        line += '>{1}</{0}>'.format (tagName, content)
        self.AddLine (line)

    def BeginTag (self, tagName):
        self.AddLine ('<{0}>'.format (tagName))

    def BeginTagWithClass (self, tagName, className):
        self.AddLine ('<{0} class="{1}">'.format (tagName, className))

    def EndTag (self, tagName):
        self.AddLine ('</{0}>'.format (tagName))

    def GetHtml (self):
        return self.html

def CleanUpText (text):
    if text == None:
        return ''
    invalidChars = ['\r', '\n', '\t']
    for invalidChar in invalidChars:
        text = text.replace (invalidChar, ' ')
    text = html.escape (text)
    return text

def GenerateLink (entityName, entityLink):
    target = '_blank' if entityLink.startswith ('http') else '_self'
    return '<a href="{1}" target="{2}">{0}</a>'.format (entityName, entityLink, target)

def FinalizeType (text, entityLinks):
    text = CleanUpText (text)
    arrayMatch = re.match ('Array\.&lt;(.+)&gt', text)
    if arrayMatch != None:
        matchedName = arrayMatch.group (1)
        if matchedName in entityLinks:
            return GenerateLink (matchedName, entityLinks[matchedName]) + '[]'
        else:
            return arrayMatch.group (1) + '[]'
    if text in entityLinks:
        return GenerateLink (text, entityLinks[text])
    return text

def FinalizeDescription (text, entityLinks):
    text = CleanUpText (text)
    links = re.findall ('{@link (.+)}', text)
    for link in links:
        if link in entityLinks:
            text = text.replace ('{@link ' + link + '}', GenerateLink (link, entityLinks[link]))
        else:
            text = text.replace ('{@link ' + link + '}', link)
    return text

class NavigationGroup:
    def __init__ (self, name, sort):
        self.name = name
        self.sort = sort
        self.links = []

    def AddLink (self, name, url):
        self.links.append ({
            'name' : name,
            'url' : url
        })

class Navigation:
    def __init__ (self):
        self.groups = []
        self.entityLinks = {}

    def AddGroup (self, group):
        self.groups.append (group)

    def AddEntityLink (self, name, url):
        self.entityLinks[name] = url

    def GenerateHtml (self, eol):
        generator = HtmlGenerator (eol)
        for group in self.groups:
            if len (group.links) == 0:
                continue
            generator.BeginTagWithClass ('div', 'navigation_section')
            generator.AddTagWithClass ('div', 'navigation_title', group.name)
            finalLinks = group.links
            if group.sort:
                finalLinks = sorted (group.links, key = lambda x : x['name'])
            for link in finalLinks:
                linkHtml = GenerateLink (link['name'], link['url'])
                generator.AddTagWithAttributes ('div', [('id', 'nav-' + link['name']), ('class', 'navigation_item')], linkHtml)
            generator.EndTag ('div')
        return generator.GetHtml ()

class EnumMemberDoc:
    def __init__ (self, name, description):
        self.name = name
        self.description = description

class EnumDoc:
    def __init__ (self, name, description):
        self.name = name
        self.description = description
        self.members = []

    def AddMember (self, member):
        self.members.append (member)

    def GenerateHtml (self, navigation, eol):
        generator = HtmlGenerator (eol)
        generator.AddTag ('h1', self.name)
        generator.AddTagWithClass ('div', 'description', FinalizeDescription (self.description, navigation.entityLinks))
        if len (self.members) > 0:
            generator.AddTag ('h2', 'Values')
            for member in self.members:
                generator.BeginTagWithClass ('div', 'parameter_header')
                generator.AddTagWithClass ('span', 'parameter_name', member.name)
                generator.EndTag ('div')
                generator.BeginTagWithClass ('div', 'parameter_main')
                generator.AddTagWithClass ('div', 'parameter_description', FinalizeDescription (member.description, navigation.entityLinks))
                generator.EndTag ('div')

        return generator.GetHtml ()

class ParameterDoc:
    def __init__ (self, name, types, isOptional, description):
        self.name = name
        self.types = types
        self.isOptional = isOptional
        self.description = description
        self.subParameters = []

    def AddSubParameter (self, parameter):
        self.subParameters.append (parameter)

class ReturnsDoc:
    def __init__ (self, types, description):
        self.types = types
        self.description = description

class MethodDoc:
    def __init__ (self, name, description, parameters, returns):
        self.name = name
        self.description = description
        self.parameters = parameters
        self.returns = returns

    def AddParameter (self, parameter):
        self.parameters.append (parameter)

    def GenerateHtml (self, navigation, eol):
        generator = HtmlGenerator (eol)
        generator.AddTag ('h1', self.name)
        GenerateMethodHtml (self, generator, navigation, False)
        return generator.GetHtml ()

class ClassDoc:
    def __init__ (self, name, description):
        self.name = name
        self.description = description
        self.constructor = None
        self.methods = []

    def SetConstructor (self, constructor):
        self.constructor = constructor

    def AddMethod (self, method):
        self.methods.append (method)

    def GenerateHtml (self, navigation, eol):
        generator = HtmlGenerator (eol)
        generator.AddTag ('h1', self.name)
        generator.AddTagWithClass ('div', 'description', FinalizeDescription (self.description, navigation.entityLinks))
        if self.constructor != None:
            generator.AddTag ('h2', 'Constructor')
            GenerateMethodHtml (self.constructor, generator, navigation, True)
        if len (self.methods) > 0:
           generator.AddTag ('h2', 'Methods')
           for method in self.methods:
               GenerateMethodHtml (method, generator, navigation, False)
        return generator.GetHtml ()

def GenerateParameterTypesHtml (paramTypes, generator, navigation):
    for i in range (0, len (paramTypes)):
        paramType = paramTypes[i]
        paramTypeHtml = FinalizeType (paramType, navigation.entityLinks)
        generator.AddTagWithClass ('span', 'type parameter_type', paramTypeHtml)
        if (i < len (paramTypes) - 1):
            generator.AddTagWithClass ('span', 'parameter_type_separator', '|')

def GenerateParameterListHtml (parameters, generator, navigation):
    for param in parameters:
        generator.BeginTagWithClass ('div', 'parameter_header')
        generator.AddTagWithClass ('span', 'parameter_name', param.name)
        GenerateParameterTypesHtml (param.types, generator, navigation)
        if param.isOptional:
            generator.AddTagWithClass ('span', 'parameter_attributes', '(optional)')
        generator.EndTag ('div')
        generator.BeginTagWithClass ('div', 'parameter_main')
        generator.AddTagWithClass ('div', 'parameter_description', FinalizeDescription (param.description, navigation.entityLinks))
        if len (param.subParameters) > 0:
            GenerateParameterListHtml (param.subParameters, generator, navigation)
        generator.EndTag ('div')

def GenerateMethodHtml (method, generator, navigation, isConstructor):
    paramNames = map (lambda x : x.name, method.parameters)
    methodSignature = method.name + ' (' + ', '.join (paramNames) + ')'
    if isConstructor:
        methodSignature = 'new ' + methodSignature
    generator.BeginTagWithClass ('div', 'method_container')
    generator.AddTagWithAttributes ('div', [('id', method.name), ('class', 'method_signature')], methodSignature)
    if method.description != None:
        generator.AddTagWithClass ('div', 'method_title', 'Description')
        generator.AddTagWithClass ('div', 'method_description', FinalizeDescription (method.description, navigation.entityLinks))
    if method.parameters != None and len (method.parameters) > 0:
        generator.AddTagWithClass ('div', 'method_title', 'Parameters')
        GenerateParameterListHtml (method.parameters, generator, navigation)
    if method.returns != None:
        generator.AddTagWithClass ('div', 'method_title', 'Returns')
        generator.BeginTagWithClass ('div', 'method_returns')
        if method.returns.types != None:
            GenerateParameterTypesHtml (method.returns.types, generator, navigation)
        if method.returns.description != None:
            generator.AddTagWithClass ('span', 'return_description', FinalizeDescription (method.returns.description, navigation.entityLinks))
        generator.EndTag ('div')
    generator.EndTag ('div')

def GetDictValue (dict, key):
    if not key in dict:
        return None
    return dict[key]

def GetDocumentedDoclets (doclets):
    documented = []
    for doclet in doclets:
        kind = doclet['kind']
        if kind not in ['class', 'function', 'constant', 'member']:
            continue
        if 'undocumented' in doclet and doclet['undocumented'] == True:
            continue
        documented.append (doclet)
    return documented

def GetParametersFromDoclet (doclet):
    parameters = []
    paramNamespaceToDoc = {}
    if not 'params' in doclet:
        return parameters
    for param in doclet['params']:
        paramName = GetDictValue (param, 'name')
        paramIsOptional = 'optional' in param and param['optional'] == True
        paramNameParts = paramName.split ('.')
        paramTypes = None
        if 'type' in param:
            paramTypes = param['type']['names']
        if len (paramNameParts) == 1:
            paramDoc = ParameterDoc (
                paramName,
                paramTypes,
                paramIsOptional,
                GetDictValue (param, 'description')
            )
            parameters.append (paramDoc)
            paramNamespaceToDoc[paramName] = paramDoc
        else:
            paramDoc = ParameterDoc (
                paramNameParts[len (paramNameParts) - 1],
                paramTypes,
                paramIsOptional,
                GetDictValue (param, 'description')
            )
            paramNamespace = '.'.join (paramNameParts[0:-1])
            paramNamespaceToDoc[paramNamespace].AddSubParameter (paramDoc)
            paramNamespaceToDoc[paramName] = paramDoc
    return parameters

def GetReturnsFromDoclet (doclet):
    if not 'returns' in doclet:
        return None
    assert (len (doclet['returns']) == 1)
    returns = doclet['returns'][0]
    returnType = None
    if 'type' in returns:
        returnType = returns['type']['names']
    return ReturnsDoc (
        returnType,
        GetDictValue (returns, 'description')
    )

def BuildHierarchy (doclets):
    hierarchy = {
        'classes' : [],
        'functions' : [],
        'enums' : []
    }
    classNameToDoc = {}
    enumNameToDoc = {}
    for doclet in doclets:
        kind = doclet['kind']
        name = doclet['name']
        parameters = GetParametersFromDoclet (doclet)
        returns = GetReturnsFromDoclet (doclet)
        description = GetDictValue (doclet, 'description')
        if kind == 'class':
            classDoc = ClassDoc (name, doclet['classdesc'])
            constructorDoc = MethodDoc (name, description, parameters, returns)
            classDoc.SetConstructor (constructorDoc)
            hierarchy['classes'].append (classDoc)
            classNameToDoc[name] = classDoc
        elif kind == 'function':
            if 'memberof' in doclet:
                parentName = doclet['memberof']
                if parentName in classNameToDoc:
                    classDoc = classNameToDoc[parentName]
                    methodDoc = MethodDoc (name, description, parameters, returns)
                    classDoc.AddMethod (methodDoc)
            else:
                methodDoc = MethodDoc (name, description, parameters, returns)
                hierarchy['functions'].append (methodDoc)
        elif kind == 'constant':
            if 'isEnum' in doclet and doclet['isEnum'] == True:
                enumDoc = EnumDoc (name, description)
                hierarchy['enums'].append (enumDoc)
                enumNameToDoc[name] = enumDoc
        elif kind == 'member':
            parentName = doclet['memberof']
            if parentName in enumNameToDoc:
                enumDoc = enumNameToDoc[parentName]
                memberDoc = EnumMemberDoc (name, description)
                enumDoc.AddMember (memberDoc)
    return hierarchy

def CreateFromTemplate (templateHtmlPath, resultHtmlPath, navigation, title, content, eol):
    shutil.copy (templateHtmlPath, resultHtmlPath)
    Tools.ReplaceStringsInFile (resultHtmlPath, [
        ('$$$TITLE$$$', title),
        ('$$$NAVIGATION$$$', navigation.GenerateHtml (eol)),
        ('$$$MAIN$$$', content)
    ])

def BuildNavigation (pageGroups, hierarchy):
    navigation = Navigation ()

    for pageGroup in pageGroups:
        navGroup = NavigationGroup (pageGroup['name'], False)
        for page in pageGroup['pages']:
            navGroup.AddLink (page['name'], page['url'])
        navigation.AddGroup (navGroup)

    classesGroup = NavigationGroup ('Classes', True)
    for classDoc in hierarchy['classes']:
        navigation.AddEntityLink (classDoc.name, classDoc.name + '.html')
        classesGroup.AddLink (classDoc.name, classDoc.name + '.html')
    navigation.AddGroup (classesGroup)

    functionsGroup = NavigationGroup ('Functions', True)
    for methodDoc in hierarchy['functions']:
        navigation.AddEntityLink (methodDoc.name, methodDoc.name + '.html')
        functionsGroup.AddLink (methodDoc.name, methodDoc.name + '.html')
    navigation.AddGroup (functionsGroup)

    enumsGroup = NavigationGroup ('Enums', True)
    for enumDoc in hierarchy['enums']:
        navigation.AddEntityLink (enumDoc.name, enumDoc.name + '.html')
        enumsGroup.AddLink (enumDoc.name, enumDoc.name + '.html')
    navigation.AddGroup (enumsGroup)

    return navigation

def BuildDocumentationFiles (navigation, pageGroups, hierarchy, sourceDir, resultDir):
    templateHtmlPath = os.path.join (sourceDir, 'Template.html')
    eol = Tools.GetEOLCharFromFile (templateHtmlPath)

    for pageGroup in pageGroups:
        for page in pageGroup['pages']:
            if page['url'].startswith ('http'):
                continue
            sourceHtmlPath = os.path.join (sourceDir, page['url'])
            pageHtmlPath = os.path.join (resultDir, page['url'])
            pageContent = '<div class="page">' + eol + Tools.GetFileContent (sourceHtmlPath) + eol + '</div>'
            CreateFromTemplate (templateHtmlPath, pageHtmlPath, navigation, page['name'], pageContent, eol)

    for classDoc in hierarchy['classes']:
        classHtmlPath = os.path.join (resultDir, classDoc.name + '.html')
        docContent = classDoc.GenerateHtml (navigation, eol)
        CreateFromTemplate (templateHtmlPath, classHtmlPath, navigation, classDoc.name, docContent, eol)

    for methodDoc in hierarchy['functions']:
        methodHtmlPath = os.path.join (resultDir, methodDoc.name + '.html')
        docContent = methodDoc.GenerateHtml (navigation, eol)
        CreateFromTemplate (templateHtmlPath, methodHtmlPath, navigation, methodDoc.name, docContent, eol)

    for enumDoc in hierarchy['enums']:
        enumHtmlPath = os.path.join (resultDir, enumDoc.name + '.html')
        docContent = enumDoc.GenerateHtml (navigation, eol)
        CreateFromTemplate (templateHtmlPath, enumHtmlPath, navigation, enumDoc.name, docContent, eol)

def Main (argv):
    toolsDir = os.path.dirname (os.path.abspath (__file__))
    rootDir = os.path.dirname (toolsDir)
    os.chdir (rootDir)

    shell = True
    if platform.system () != 'Windows':
        shell = False
    result = subprocess.run (['jsdoc', '-c', 'tools/jsdoc.json'], stdout = subprocess.PIPE, shell = shell)
    resultJson = json.loads (result.stdout)

    resultDir = os.path.join (rootDir, 'docs')
    sourceDir = os.path.join (resultDir, 'source')

    for fileName in os.listdir (resultDir):
        filePath = os.path.join (resultDir, fileName)
        if not os.path.isdir (filePath):
            os.remove (filePath)

    config = None
    with open (os.path.join (sourceDir, 'config.json')) as configJson:
        config = json.load (configJson)

    doclets = GetDocumentedDoclets (resultJson)
    pageGroups = config['page_groups']
    hierarchy = BuildHierarchy (doclets)

    navigation = BuildNavigation (pageGroups, hierarchy)
    for name in config['external_refs']:
        navigation.AddEntityLink (name, config['external_refs'][name])

    BuildDocumentationFiles (navigation, pageGroups, hierarchy, sourceDir, resultDir)

    return 0

sys.exit (Main (sys.argv))
