##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""
API documentation help topics
"""

import types
import HelpTopic
from Globals import DTMLFile, Persistent

_ignore_objects = {}

try:
    import Interface
    _ignore_objects.update(Interface.__dict__)
except ImportError:
    pass

class APIHelpTopic(HelpTopic.HelpTopic):
    """
    Provides API documentation.
    """

    isAPIHelpTopic=1
    funcs=() # for backward compatibility

    def __init__(self, id, title, file):
        self.id=id
        self.title=title
        dict={}
        execfile(file, dict)
        self.doc=dict.get('__doc__','')

        self.apis=[]
        self.funcs=[]
        for k, v in dict.items():
            if (not _ignore_objects.has_key(k) or
                _ignore_objects[k] is not v):
                if type(v)==types.ClassType:
                    # A class.
                    self.apis.append(APIDoc(v, 0))
                elif (hasattr(v, 'isImplementedByInstancesOf')):
                    # A scarecrow interface.
                    self.apis.append(APIDoc(v, 1))
                elif type(v)==types.FunctionType:
                    # A function
                    self.funcs.append(MethodDoc(v, 0))
        # try to get title from first non-blank line
        # of module docstring
        if not self.title:
            lines=self.doc.split('\n')
            while 1:
                line=lines[0].strip()
                if line:
                    # get rid of anything after a colon in the line
                    self.title=line.split(':')[0]
                    break
                lines.pop(0)
                if not lines:
                    break
        # otherwise get title from first class name
        if not self.title:
            self.title=self.apis[0].name

    index_html=DTMLFile('dtml/APIHelpView', globals())

    def SearchableText(self):
        "The full text of the Help Topic, for indexing purposes"
        text="%s %s" % (self.title, self.doc)
        for api in self.apis + self.funcs:
            text="%s %s" % (text, api.SearchableText())
        return text


class APIDoc(Persistent):
    """
    Describes an API.
    """

    extends=()

    def __init__(self, klass, isInterface=0):
        if isInterface:
            self._createFromInterface(klass)
        else:
            self._createFromClass(klass)

    def _createFromInterface(self, klass):
        # Creates an APIDoc instance given an interface object.
        self.name=klass.__name__
        self.doc=trim_doc_string(klass.__doc__)

        # inheritence information
        self.extends=[]
##        for base in klass.getBases():
##            names = string.split(base.__name__, '.')
##            url="%s/Help/%s.py#%s" % (names[0], names[1], names[2])
##            self.extends.append((names[2], url))

        # constructor information
##        if hasattr(klass, '__constructor__'):
##            self.constructor=MethodDoc(klass.__constructor__)

        # Get info on methods and attributes, ignore special items
        self.attributes=[]
        self.methods=[]
        from Interface.Method import Method
        for k,v in klass.namesAndDescriptions():
            if hasattr(v, 'getSignatureInfo'):
                self.methods.append(MethodDoc(v, 1))
            else:
                self.attributes.append(AttributeDoc(k, v.__doc__))

    def _createFromClass(self, klass):
        # Creates an APIDoc instance given a python class.
        # the class describes the API; it contains
        # methods, arguments and doc strings.
        #
        # The name of the API is deduced from the name
        # of the class.
        #
        # The base APIs are deduced from the __extends__
        # attribute.

        self.name=klass.__name__
        self.doc=trim_doc_string(klass.__doc__)

        # inheritence information
        if hasattr(klass,'__extends__'):
            self.extends=[]
            for base in klass.__extends__:
                names=base.split( '.')
                url="%s/Help/%s.py#%s" % (names[0], names[1], names[2])
                self.extends.append((names[2], url))

        # constructor information
        if hasattr(klass, '__constructor__'):
            self.constructor=MethodDoc(klass.__constructor__)

        # Get info on methods and attributes, ignore special items
        self.attributes=[]
        self.methods=[]
        for k,v in klass.__dict__.items():
            if k not in ('__extends__', '__doc__', '__constructor__'):
                if type(v)==types.FunctionType:
                    self.methods.append(MethodDoc(v, 0))
                else:
                    self.attributes.append(AttributeDoc(k, v))

    def SearchableText(self):
        """
        The full text of the API, for indexing purposes.
        """
        text="%s %s" % (self.name, self.doc)
        for attribute in self.attributes:
            text="%s %s" % (text, attribute.name)
        for method in self.methods:
            text="%s %s %s" % (text, method.name, method.doc)
        return text

    view=DTMLFile('dtml/APIView', globals())


class AttributeDoc(Persistent):
    """
    Describes an attribute of an API.
    """

    def __init__(self, name, value):
        self.name=name
        self.value=value

    view=DTMLFile('dtml/attributeView', globals())


class MethodDoc(Persistent):
    """
    Describes a method of an API.

    required - a sequence of required arguments
    optional - a sequence of tuples (name, default value)
    varargs - the name of the variable argument or None
    kwargs - the name of the kw argument or None
    """

    varargs=None
    kwargs=None

    def __init__(self, func, isInterface=0):
        if isInterface:
            self._createFromInterfaceMethod(func)
        else:
            self._createFromFunc(func)

    def _createFromInterfaceMethod(self, func):
        self.name = func.__name__
        self.doc = trim_doc_string(func.__doc__)
        self.required = func.required
        opt = []
        for p in func.positional[len(func.required):]:
            opt.append((p, func.optional[p]))
        self.optional = tuple(opt)
        if func.varargs:
            self.varargs = func.varargs
        if func.kwargs:
            self.kwargs = func.kwargs

    def _createFromFunc(self, func):
        if hasattr(func, 'im_func'):
            func=func.im_func

        self.name=func.__name__
        self.doc=trim_doc_string(func.__doc__)

        # figure out the method arguments
        # mostly stolen from pythondoc
        CO_VARARGS = 4
        CO_VARKEYWORDS = 8
        names = func.func_code.co_varnames
        nrargs = func.func_code.co_argcount
        if func.func_defaults:
            nrdefaults = len(func.func_defaults)
        else:
            nrdefaults = 0
        self.required = names[:nrargs-nrdefaults]
        if func.func_defaults:
            self.optional = tuple(map(None, names[nrargs-nrdefaults:nrargs],
                                 func.func_defaults))
        else:
            self.optional = ()
        varargs = []
        ix = nrargs
        if func.func_code.co_flags & CO_VARARGS:
            self.varargs=names[ix]
            ix = ix+1
        if func.func_code.co_flags & CO_VARKEYWORDS:
            self.kwargs=names[ix]

    view=DTMLFile('dtml/methodView', globals())


def trim_doc_string(text):
    """
    Trims a doc string to make it format
    correctly with structured text.
    """
    text=text.strip()
    text=text.replace( '\r\n', '\n')
    lines=text.split('\n')
    nlines=[lines[0]]
    if len(lines) > 1:
        min_indent=None
        for line in lines[1:]:
            if not line:
                continue
            indent=len(line) - len(line.lstrip())
            if indent < min_indent or min_indent is None:
                min_indent=indent
        for line in lines[1:]:
            nlines.append(line[min_indent:])
    return '\n'.join(nlines)
