# -*- coding: utf-8 -*-

# Copyright (c) 2003 - 2005 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the builtin documentation generator.

The different parts of the module document are assembled from the parsed
Python file. The appearance is determined by several templates defined within
this module.
"""

import sys
import re

import TemplatesListsStyle
import TemplatesListsStyleCSS

from Utilities import html_uencode
from Utilities.ModuleParser import RB_SOURCE

_signal = re.compile(r"""
    ^@signal [ \t]+ 
    (?P<SignalName1>
        [a-zA-Z_] \w* [ \t]* \( [^)]* \)
    )
    [ \t]* (?P<SignalDescription1> .*)
|
    ^@signal [ \t]+ 
    (?P<SignalName2>
        [a-zA-Z_] \w*
    )
    [ \t]+ (?P<SignalDescription2> .*)
""", re.VERBOSE | re.DOTALL | re.MULTILINE).search

class TagError(Exception):
    """
    Exception class raised, if an invalid documentation tag was found.
    """
    def __init__(self, args=None):
        """
        Constructor
        
        @param args The arguments.
        """
        self.args = args
        
class ModuleDocument:
    """
    Class implementing the builtin documentation generator.
    """
    def __init__(self, module, css=0):
        """
        Constructor
        
        @param module The information of the parsed Python file.
        @param css flag indicating a css style sheet should be used (boolean)
        """
        self.module = module
        self.empty = 1
        
        if css:
            self.headerTemplate = TemplatesListsStyleCSS.headerTemplate
            self.footerTemplate = TemplatesListsStyleCSS.footerTemplate
            self.moduleTemplate = TemplatesListsStyleCSS.moduleTemplate
            self.rbFileTemplate = TemplatesListsStyleCSS.rbFileTemplate
            self.classTemplate = TemplatesListsStyleCSS.classTemplate
            self.methodTemplate = TemplatesListsStyleCSS.methodTemplate
            self.constructorTemplate = TemplatesListsStyleCSS.constructorTemplate
            self.rbModuleTemplate = TemplatesListsStyleCSS.rbModuleTemplate
            self.rbModulesClassTemplate = TemplatesListsStyleCSS.rbModulesClassTemplate
            self.functionTemplate = TemplatesListsStyleCSS.functionTemplate
            self.listTemplate = TemplatesListsStyleCSS.listTemplate
            self.listEntryTemplate = TemplatesListsStyleCSS.listEntryTemplate
            self.listEntryNoneTemplate = TemplatesListsStyleCSS.listEntryNoneTemplate
            self.listEntryDeprecatedTemplate = TemplatesListsStyleCSS.listEntryDeprecatedTemplate
            self.descriptionTemplate = TemplatesListsStyleCSS.descriptionTemplate
            self.paragraphTemplate = TemplatesListsStyleCSS.paragraphTemplate
            self.parametersListTemplate = TemplatesListsStyleCSS.parametersListTemplate
            self.parametersListEntryTemplate = TemplatesListsStyleCSS.parametersListEntryTemplate
            self.returnsTemplate = TemplatesListsStyleCSS.returnsTemplate
            self.exceptionsListTemplate = TemplatesListsStyleCSS.exceptionsListTemplate
            self.exceptionsListEntryTemplate = TemplatesListsStyleCSS.exceptionsListEntryTemplate
            self.signalsListTemplate = TemplatesListsStyleCSS.signalsListTemplate
            self.signalsListEntryTemplate = TemplatesListsStyleCSS.signalsListEntryTemplate
            self.deprecatedTemplate = TemplatesListsStyleCSS.deprecatedTemplate
            self.authorInfoTemplate = TemplatesListsStyleCSS.authorInfoTemplate
        else:
            self.headerTemplate = TemplatesListsStyle.headerTemplate
            self.footerTemplate = TemplatesListsStyle.footerTemplate
            self.moduleTemplate = TemplatesListsStyle.moduleTemplate
            self.rbFileTemplate = TemplatesListsStyle.rbFileTemplate
            self.classTemplate = TemplatesListsStyle.classTemplate
            self.methodTemplate = TemplatesListsStyle.methodTemplate
            self.constructorTemplate = TemplatesListsStyle.constructorTemplate
            self.rbModuleTemplate = TemplatesListsStyle.rbModuleTemplate
            self.rbModulesClassTemplate = TemplatesListsStyle.rbModulesClassTemplate
            self.functionTemplate = TemplatesListsStyle.functionTemplate
            self.listTemplate = TemplatesListsStyle.listTemplate
            self.listEntryTemplate = TemplatesListsStyle.listEntryTemplate
            self.listEntryNoneTemplate = TemplatesListsStyle.listEntryNoneTemplate
            self.listEntryDeprecatedTemplate = TemplatesListsStyle.listEntryDeprecatedTemplate
            self.descriptionTemplate = TemplatesListsStyle.descriptionTemplate
            self.paragraphTemplate = TemplatesListsStyle.paragraphTemplate
            self.parametersListTemplate = TemplatesListsStyle.parametersListTemplate
            self.parametersListEntryTemplate = TemplatesListsStyle.parametersListEntryTemplate
            self.returnsTemplate = TemplatesListsStyle.returnsTemplate
            self.exceptionsListTemplate = TemplatesListsStyle.exceptionsListTemplate
            self.exceptionsListEntryTemplate = TemplatesListsStyle.exceptionsListEntryTemplate
            self.signalsListTemplate = TemplatesListsStyle.signalsListTemplate
            self.signalsListEntryTemplate = TemplatesListsStyle.signalsListEntryTemplate
            self.deprecatedTemplate = TemplatesListsStyle.deprecatedTemplate
            self.authorInfoTemplate = TemplatesListsStyle.authorInfoTemplate
        
    def isEmpty(self):
        """
        Method to determine, if the module contains any classes or functions.
        
        @return Flag indicating an empty module (i.e. __init__.py without
            any contents)
        """
        return self.empty
        
    def name(self):
        """
        Method used to get the module name.
        
        @return The name of the module. (string)
        """
        return self.module.name
        
    def description(self):
        """
        Method used to get the description of the module.
        
        @return The description of the module. (string)
        """
        return self.formatDescription(self.module.description)
        
    def shortDescription(self):
        """
        Method used to get the short description of the module.
        
        The short description is just the first line of the modules
        description.
        
        @return The short description of the module. (string)
        """
        return self.getShortDescription(self.module.description)
        
    def genDocument(self):
        """
        Method to generate the source code documentation.
        
        @return The source code documentation. (string)
        """
        doc = self.headerTemplate % {'Title' : self.module.name} + \
              self.genModuleSection() + \
              self.footerTemplate
        return doc
        
    def genModuleSection(self):
        """
        Private method to generate the body of the document.
        
        @return The body of the document. (string)
        """
        classList = self.genClassListSection()
        functionList = self.genFunctionListSection()
        try:
            if self.module.type == RB_SOURCE:
                rbModulesList = self.genRbModulesListSection()
                modBody = self.rbFileTemplate % { \
                    'Module' : self.module.name,
                    'ModuleDescription' : self.formatDescription(self.module.description),
                    'ClassList' : classList,
                    'RbModulesList' : rbModulesList,
                    'FunctionList' : functionList,
                }
            else:
                modBody = self.moduleTemplate % { \
                    'Module' : self.module.name,
                    'ModuleDescription' : self.formatDescription(self.module.description),
                    'ClassList' : classList,
                    'FunctionList' : functionList,
                }
        except TagError, e:
            sys.stderr.write("Error in tags of description of module %s.\n" % \
                self.module.name)
            sys.stderr.write("%s\n" % e.args)
            return ""
            
        classesSection = self.genClassesSection()
        functionsSection = self.genFunctionsSection()
        if self.module.type == RB_SOURCE:
            rbModulesSection = self.genRbModulesSection()
        else:
            rbModulesSection = ""
        return "%s%s%s%s" % (modBody, classesSection, rbModulesSection, functionsSection)
        
    def genListSection(self, names, dict, className=''):
        """
        Private method to generate a list section of the document.
        
        @param names The names to appear in the list. (list of strings)
        @param dict A dictionary containing all relevant information.
        @param className The class name containing the names.
        @return The list section. (string)
        """
        lst = []
        for name in names:
            lst.append(self.listEntryTemplate % { \
                'Link' : "%s%s" % (className, name),
                'Name' : dict[name].name,
                'Description' : self.getShortDescription(dict[name].description),
                'Deprecated' : self.checkDeprecated(dict[name].description) and \
                    self.listEntryDeprecatedTemplate or "",
            })
        return ''.join(lst)
        
    def genClassListSection(self):
        """
        Private method to generate the section listing all classes of the module.
        
        @return The classes list section. (string)
        """
        names = self.module.classes.keys()
        names.sort()
        if names:
            self.empty = 0
            s = self.genListSection(names, self.module.classes)
        else:
            s = self.listEntryNoneTemplate
        return self.listTemplate % { \
            'Entries' : s,
        }
        
    def genRbModulesListSection(self):
        """
        Private method to generate the section listing all modules of the file (Ruby only).
        
        @return The modules list section. (string)
        """
        names = self.module.modules.keys()
        names.sort()
        if names:
            self.empty = 0
            s = self.genListSection(names, self.module.modules)
        else:
            s = self.listEntryNoneTemplate
        return self.listTemplate % { \
            'Entries' : s,
        }
        
    def genFunctionListSection(self):
        """
        Private method to generate the section listing all functions of the module.
        
        @return The functions list section. (string)
        """
        names = self.module.functions.keys()
        names.sort()
        if names:
            self.empty = 0
            s = self.genListSection(names, self.module.functions)
        else:
            s = self.listEntryNoneTemplate
        return self.listTemplate % { \
            'Entries' : s,
        }
        
    def genClassesSection(self):
        """
        Private method to generate the document section with details about classes.
        
        @return The classes details section. (string)
        """
        classNames = self.module.classes.keys()
        classNames.sort()
        classes = []
        for className in classNames:
            _class = self.module.classes[className]
            supers = _class.super
            if len(supers) > 0:
                supers = ', '.join(supers)
            else:
                supers = 'None'
                
            methList, methBodies = self.genMethodSection(_class, className)
            
            try:
                clsBody = self.classTemplate % { \
                    'Anchor' : className,
                    'Class' : _class.name,
                    'ClassSuper' : supers,
                    'ClassDescription' : self.formatDescription(_class.description),
                    'MethodList' : methList,
                    'MethodDetails' : methBodies,
                }
            except TagError, e:
                sys.stderr.write("Error in tags of description of class %s.\n" % \
                    className)
                sys.stderr.write("%s\n" % e.args)
                clsBody = ""
            
            classes.append(clsBody)
            
        return ''.join(classes)
        
    def genMethodsListSection(self, names, dict, className, clsName):
        """
        Private method to generate the methods list section of a class.
        
        @param names The names to appear in the list. (list of strings)
        @param dict A dictionary containing all relevant information.
        @param className The class name containing the names.
        @param clsName The visible class name containing the names.
        @return The list section. (string)
        """
        lst = []
        try:
            lst.append(self.listEntryTemplate % { \
                'Link' : "%s.%s" % (className, '__init__'),
                'Name' : clsName,
                'Description' : self.getShortDescription(dict['__init__'].description),
                'Deprecated' : self.checkDeprecated(dict['__init__'].description) and \
                               self.listEntryDeprecatedTemplate or "",
            })
        except KeyError:
            pass
            
        for name in names:
            lst.append(self.listEntryTemplate % { \
                'Link' : "%s.%s" % (className, name),
                'Name' : dict[name].name,
                'Description' : self.getShortDescription(dict[name].description),
                'Deprecated' : self.checkDeprecated(dict[name].description) and \
                               self.listEntryDeprecatedTemplate or "",
            })
        return ''.join(lst)
        
    def genMethodSection(self, obj, className):
        """
        Private method to generate the method details section.
        
        @param obj Reference to the object being formatted.
        @param className Name of the class containing the method. (string)
        @return The method list and method details section. (tuple of two string)
        """
        methList = []
        methBodies = []
        methods = obj.methods.keys()
        methods.sort()
        
        # first do the constructor
        if '__init__' in methods:
            methods.remove('__init__')
            try:
                methBody = self.constructorTemplate % { \
                    'Anchor' : className,
                    'Class' : obj.name,
                    'Method' : '__init__',
                    'MethodDescription' : self.formatDescription(obj.methods['__init__'].description),
                    'Params' : ', '.join(obj.methods['__init__'].parameters[1:]),
                }
            except TagError, e:
                sys.stderr.write("Error in tags of description of method %s.%s.\n" % \
                    (className, '__init__'))
                sys.stderr.write("%s\n" % e.args)
                methBody = ""
            methBodies.append(methBody)
            
        for method in methods:
            try:
                methBody = self.methodTemplate % { \
                    'Anchor' : className,
                    'Class' : obj.name,
                    'Method' : obj.methods[method].name,
                    'MethodDescription' : self.formatDescription(obj.methods[method].description),
                    'Params' : ', '.join(obj.methods[method].parameters[1:]),
                }
            except TagError, e:
                sys.stderr.write("Error in tags of description of method %s.%s.\n" % \
                    (className, method))
                sys.stderr.write("%s\n" % e.args)
                methBody = ""
            methBodies.append(methBody)
            
        methList = self.genMethodsListSection(methods, obj.methods, className, obj.name)
        
        if not methList:
            methList = self.listEntryNoneTemplate
        return self.listTemplate % { \
            'Entries' : methList,
            }, ''.join(methBodies)
        
    def genRbModulesSection(self):
        """
        Private method to generate the document section with details about Ruby modules.
        
        @return The Ruby modules details section. (string)
        """
        rbModulesNames = self.module.modules.keys()
        rbModulesNames.sort()
        rbModules = []
        for rbModuleName in rbModulesNames:
            rbModule = self.module.modules[rbModuleName]
            methList, methBodies = self.genMethodSection(rbModule, rbModuleName)
            classList, classBodies = self.genRbModulesClassesSection(rbModule, rbModuleName)
            
            try:
                rbmBody = self.rbModuleTemplate % { \
                    'Anchor' : rbModuleName,
                    'Module' : rbModule.name,
                    'ModuleDescription' : self.formatDescription(rbModule.description),
                    'ClassesList' : classList,
                    'ClassesDetails' : classBodies,
                    'FunctionsList' : methList,
                    'FunctionsDetails' : methBodies,
                }
            except TagError, e:
                sys.stderr.write("Error in tags of description of Ruby module %s.\n" % \
                    rbModuleName)
                sys.stderr.write("%s\n" % e.args)
                rbmBody = ""
            
            rbModules.append(rbmBody)
            
        return ''.join(rbModules)

    def genRbModulesClassesSection(self, obj, modName):
        """
        Private method to generate the Ruby module classes details section.
        
        @param obj Reference to the object being formatted.
        @param modName Name of the Ruby module containing the classes. (string)
        @return The classes list and classes details section. (tuple of two string)
        """
        classNames = obj.classes.keys()
        classNames.sort()
        classes = []
        for className in classNames:
            _class = obj.classes[className]
            supers = _class.super
            if len(supers) > 0:
                supers = ', '.join(supers)
            else:
                supers = 'None'
            
            classname = "%s.%s" % (modName, className)
            methList, methBodies = self.genMethodSection(_class, className)
            
            try:
                clsBody = self.rbModulesClassTemplate % { \
                    'Anchor' : className,
                    'Class' : _class.name,
                    'ClassSuper' : supers,
                    'ClassDescription' : self.formatDescription(_class.description),
                    'MethodList' : methList,
                    'MethodDetails' : methBodies,
                }
            except TagError, e:
                sys.stderr.write("Error in tags of description of class %s.\n" % \
                    className)
                sys.stderr.write("%s\n" % e.args)
                clsBody = ""
            
            classes.append(clsBody)
            
            
        classesList = self.genRbModulesClassesListSection(classNames, obj.classes, modName)
        
        if not classesList:
            classesList = self.listEntryNoneTemplate
        return self.listTemplate % { \
            'Entries' : classesList,
            }, ''.join(classes)
        
    def genRbModulesClassesListSection(self, names, dict, moduleName):
        """
        Private method to generate the classes list section of a Ruby module.
        
        @param names The names to appear in the list. (list of strings)
        @param dict A dictionary containing all relevant information.
        @param moduleName Name of the Ruby module containing the classes. (string)
        @return The list section. (string)
        """
        lst = []
        for name in names:
            lst.append(self.listEntryTemplate % { \
                'Link' : "%s.%s" % (moduleName, name),
                'Name' : dict[name].name,
                'Description' : self.getShortDescription(dict[name].description),
                'Deprecated' : self.checkDeprecated(dict[name].description) and \
                               self.listEntryDeprecatedTemplate or "",
            })
        return ''.join(lst)
        
    def genFunctionsSection(self):
        """
        Private method to generate the document section with details about functions.
        
        @return The functions details section. (string)
        """
        funcBodies = []
        funcNames = self.module.functions.keys()
        funcNames.sort()
        for funcName in funcNames:
            try:
                funcBody = self.functionTemplate % { \
                    'Anchor' : funcName,
                    'Function' : self.module.functions[funcName].name,
                    'FunctionDescription' : self.formatDescription(self.module.functions[funcName].description),
                    'Params' : ', '.join(self.module.functions[funcName].parameters),
                }
            except TagError, e:
                sys.stderr.write("Error in tags of description of function %s.\n" % \
                    funcName)
                sys.stderr.write("%s\n" % e.args)
                funcBody = ""
            
            funcBodies.append(funcBody)
            
        return ''.join(funcBodies)
        
    def getShortDescription(self, desc):
        """
        Private method to determine the short description of an object.
        
        The short description is just the first non empty line of the
        documentation string.
        
        @param desc The documentation string. (string)
        @return The short description. (string)
        """
        dlist = desc.splitlines()
        for desc in dlist:
            if desc:
                return html_uencode(desc)
        else:
            return ''
            
    def checkDeprecated(self, descr):
        """
        Private method to check, if the object to be documented contains a deprecated flag.
        
        @param desc The documentation string. (string)
        @return Flag indicating the deprecation status. (boolean)
        """
        dlist = descr.splitlines()
        for desc in dlist:
            desc = desc.strip()
            if desc.startswith("@deprecated"):
                return 1
        return 0
        
    def genParagraphs(self, lines):
        """
        Private method to assemble the descriptive paragraphs of a docstring.
        
        A paragraph is made up of a number of consecutive lines without
        an intermediate empty line. Empty lines are treated a paragraph
        delimiter.
        
        @param lines A list of individual lines. (list of strings)
        @return Ready formatted paragraphs. (string)
        """
        lst = []
        linelist = []
        for line in lines:
            if line:
                if line == '.':
                    linelist.append("")
                else:
                    linelist.append(html_uencode(line))
            else:
                lst.append(self.paragraphTemplate % { \
                    'Lines' : '\n'.join(linelist)
                })
                linelist = []
        if linelist:
            lst.append(self.paragraphTemplate % { \
                'Lines' : '\n'.join(linelist)
            })
        return ''.join(lst)
        
    def genDescriptionListSection(self, dictionary, template):
        """
        Private method to generate the list section of a description.
        
        @param dictionary Dictionary containing the info for the
            list section.
        @param template The template to be used for the list. (string)
        @return The list section. (string)
        """
        lst = []
        keys = dictionary.keys()
        keys.sort()
        for key in keys:
            lst.append(template % { \
                'Name' : key,
                'Description' : html_uencode('\n'.join(dictionary[key])),
            })
        return ''.join(lst)
        
    def genParamDescriptionListSection(self, _list, template):
        """
        Private method to generate the list section of a description.
        
        @param _list List containing the info for the
            list section.
        @param template The template to be used for the list. (string)
        @return The list section. (string)
        """
        lst = []
        for name, lines in _list:
            lst.append(template % { \
                'Name' : name,
                'Description' : html_uencode('\n'.join(lines)),
            })
        return ''.join(lst)
        
    def formatDescription(self, descr):
        """
        Private method to format the contents of the documentation string.
        
        @param descr The contents of the documentation string. (string)
        @exception TagError A tag doesn't have the correct number
            of arguments.
        @return The formated contents of the documentation string. (string)
        """
        if not descr:
            return ""
        
        paragraphs = []
        paramList = []
        returns = []
        exceptionDict = {}
        signalDict = {}
        deprecated = []
        authorInfo = []
        lastItem = paragraphs
        inTagSection = 0
        
        dlist = descr.splitlines()
        while dlist and not dlist[0]:
            del dlist[0]
        for ditem in dlist:
            desc = ditem.strip()
            if desc:
                if desc.startswith("@param") or desc.startswith("@keyparam"):
                    inTagSection = 1
                    parts = desc.split(None, 2)
                    if len(parts) < 2:
                        raise TagError, "Wrong format in %s line.\n" % parts[0]
                    paramName = parts[1]
                    if parts[0] == "@keyparam":
                        paramName += '='
                    try:
                        paramList.append([paramName, [parts[2]]])
                    except IndexError:
                        paramList.append([paramName, []])
                    lastItem = paramList[-1][1]
                    continue
                elif desc.startswith("@return"):
                    inTagSection = 1
                    parts = desc.split(None, 1)
                    if len(parts) < 2:
                        raise TagError, "Wrong format in %s line.\n" % parts[0]
                    returns = [parts[1]]
                    lastItem = returns
                    continue
                elif desc.startswith("@exception") or \
                     desc.startswith("@throws") or \
                     desc.startswith("@raise"):
                    inTagSection = 1
                    parts = desc.split(None, 2)
                    if len(parts) < 2:
                        raise TagError, "Wrong format in %s line.\n" % parts[0]
                    excName = parts[1]
                    try:
                        exceptionDict[excName] = [parts[2]]
                    except IndexError:
                        exceptionDict[excName] = []
                    lastItem = exceptionDict[excName]
                    continue
                elif desc.startswith("@signal"):
                    inTagSection = 1
                    m = _signal(desc,0)
                    if m is None:
                        raise TagError, "Wrong format in %s line.\n" % parts[0]
                    signalName = 1 and m.group("SignalName1") \
                                   or m.group("SignalName2")
                    signalDesc = 1 and m.group("SignalDescription1") \
                                   or m.group("SignalDescription2")
                    signalDict[signalName] = [signalDesc]
                    lastItem = signalDict[signalName]
                    continue
                elif desc.startswith("@deprecated"):
                    inTagSection = 1
                    parts = desc.split(None, 1)
                    if len(parts) < 2:
                        raise TagError, "Wrong format in %s line.\n" % parts[0]
                    deprecated = [parts[1]]
                    lastItem = deprecated
                    continue
                elif desc.startswith("@author"):
                    inTagSection = 1
                    parts = desc.split(None, 1)
                    if len(parts) < 2:
                        raise TagError, "Wrong format in %s line.\n" % parts[0]
                    authorInfo = [parts[1]]
                    lastItem = authorInfo
                    continue
                elif desc.startswith("@@"):
                    lastItem.append(desc[1:])
                elif desc.startswith("@"):
                    tag = desc.split(None, 1)[0]
                    raise TagError, "Unknown tag encountered, %s.\n" % tag
                else:
                    lastItem.append(ditem)
            elif not inTagSection:
                lastItem.append(ditem)
                
        if paragraphs:
            description = self.genParagraphs(paragraphs)
        else:
            description = ""
        
        if paramList:
            parameterSect = self.parametersListTemplate % { \
                'Parameters' : self.genParamDescriptionListSection(paramList,
                               self.parametersListEntryTemplate)
            }
        else:
            parameterSect = ""
            
        if returns:
            returnSect = self.returnsTemplate % html_uencode('\n'.join(returns))
        else:
            returnSect = ""
            
        if exceptionDict:
            exceptionSect = self.exceptionsListTemplate % { \
                'Exceptions' : self.genDescriptionListSection(exceptionDict,
                               self.exceptionsListEntryTemplate)
            }
        else:
            exceptionSect = ""
            
        if signalDict:
            signalSect = self.signalsListTemplate % { \
                'Signals' : self.genDescriptionListSection(signalDict,
                               self.signalsListEntryTemplate)
            }
        else:
            signalSect = ""
            
        if deprecated:
            deprecatedSect = self.deprecatedTemplate % { \
                'Lines' : html_uencode('\n'.join(deprecated)),
            }
        else:
            deprecatedSect = ""
        
        if authorInfo:
            authorInfoSect = self.authorInfoTemplate % { \
                'Authors' : html_uencode('\n'.join(authorInfo)),
            }
        else:
            authorInfoSect = ""
        
        return "%s%s%s%s%s%s%s" % ( \
            deprecatedSect, description, parameterSect, returnSect,
            exceptionSect, signalSect, authorInfoSect,
        )
