import os, re, pydoc, inspect, urllib, types, imp, stat, time
import XmlFormatter

# Get the list of types to not document
_builtin_types = vars(types).values()
_builtin_types = [ t for t in _builtin_types if type(t) is types.TypeType ]

# Regex for extracting the argument list from a builtin function's docstring 
# WARNING - tuple arguments in the arglist will break this
_re_arglist = re.compile(' *[a-zA-Z_][a-zA-Z0-9_]* *\((?P<arglist>[^)]*) *\)')

def _visiblename(name):
    """Decide whether to show documentation on a variable."""
    # Certain special names are redundant.
    if name in ['__builtins__', '__doc__', '__file__', '__path__',
                '__module__', '__name__']: return 0
    # Private names are hidden, but special names are displayed.
    if name.startswith('__') and name.endswith('__'): return 1
    return not name.startswith('_')

class ApiFormatter(XmlFormatter.XmlFormatter):

    def __init__(self, command, modules):
        XmlFormatter.XmlFormatter.__init__(self, command)
        self.documented_modules = modules
        return

    def ispublic(self, name):
        if hasattr(self.module, '__all__'):
            return (name in self.module.__all__) and 'yes' or 'no'
        else:
            return (not name.startswith('_')) and 'yes' or 'no'

    def isdocumented(self, name):
        return self.documented_modules.has_key(name) and 'yes' or 'no'

    def doc_module(self, module):
        """
        Produce documentation for a given module object.
        """
        if not inspect.ismodule(module):
            raise TypeError("doc_module() requires a 'module' object "
                            "but received a '%s'" % type(module).__name__)

        attributes = {'name' : module.__name__}
        try:
            absfile = inspect.getabsfile(module)
        except TypeError:
            absfile = None
        else:
            attributes['file'] = urllib.pathname2url(absfile)

        self.start_element('module', attributes)

        if absfile:
            mtime = os.stat(absfile)[stat.ST_MTIME]
            mtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))
        else:
            mtime = 'unknown'
        self.write_element('modification-date', content=mtime)
            
        self.write_description(module)

        # standard module metadata
        for attr in ['author', 'credits', 'date', 'version']:
            if hasattr(module, '__%s__' % attr):
                content = self.escape(str(getattr(module, '__%s__' % attr)))
                self.write_element(attr, content=content)

        modules = []
        if hasattr(module, '__path__'):
            # Don't add the __init__ for this module (it IS this module)
            handled = {'__init__' : 1}
            submodules = []
            path = module.__path__[0]
            for filename in os.listdir(path):
                modname = inspect.getmodulename(filename)
                if modname and not handled.get(modname):
                    handled[modname] = 1
                    submodules.append(modname)
                elif pydoc.ispackage(os.path.join(path, filename)):
                    # 'file' will be a bare directory name
                    submodules.append(filename)
            for name in submodules:
                fullname = module.__name__ + '.' + name
                try:
                    submod = pydoc.safeimport(fullname)
                except:
                    submod = None
                if submod is None:
                    # Could not import for some reason, create a stub
                    submod = imp.new_module(fullname)
                modules.append((name, submod))
                
        all = getattr(module, '__all__', [])

        # Only document the modules listed in __all__.
        for name, member in inspect.getmembers(module, inspect.ismodule):
            if name in all:
                modules.append((name, member))
        if modules:
            modules.sort()
            self.section('modules', modules, self.doc_submodule)


        # Only document the classes defined in this module or
        # are listed in __all__.
        classes = [ t for t in inspect.getmembers(module, inspect.isclass)
                    if ((inspect.getmodule(t[1]) or module) is module or
                        t[0] in all) ]
        if classes:
            self.section('classes', classes, self.doc_class)

        # Only document the functions defined in this module or
        # are listed in __all__.
        funcs = [ t for t in inspect.getmembers(module, inspect.isroutine)
                  if ((inspect.getmodule(t[1]) or module) is module or
                      t[0] in all) ]
        if funcs:
            self.section('functions', funcs, self.doc_function)

        # Only document the "visible" objects or those listed in __all__.
        fields = [ t for t in inspect.getmembers(module, pydoc.isdata)
                   if t[0] in all or _visiblename(t[0]) ]
        if fields:
            self.section('fields', fields, self.doc_field)

        self.end_element('module')
        return

    def doc_submodule(self, module, name):
        """Produce XML documentation for a submodule"""
        realname = module.__name__
        name = name or realname

        attributes = {'name' : name,
                      'realname' : realname,
                      'public' : self.ispublic(name),
                      'documented' : self.isdocumented(realname),
                      }

        self.start_element('module-reference', attributes)

        self.write_description(module)

        self.end_element('module-reference')
        return

    def doc_class(self, klass, name):
        """Produce XML documentation for a given class object."""
        realname = klass.__name__
        name = name or realname

        attributes = {'name' : name,
                      'public' : self.ispublic(name),
                      }
        if name != realname:
            attributes['realname'] = realname

        self.start_element('class', attributes)

        self.start_element('bases')
        for base in klass.__bases__:
            attributes = {
                'class' : base.__name__,
                'module' : base.__module__,
                'documented' : self.isdocumented(base.__module__),
                }
            self.write_element('base', attributes)
        self.end_element('bases')

        self.write_description(klass)

        self.start_element('method-resolution-order')
        mro = list(inspect.getmro(klass))
        bases = {}
        for base in mro:
            attributes = {'module' : base.__module__,
                          'name' : base.__name__,
                          }
            self.write_element('base', attributes)
        self.end_element('method-resolution-order')

        attrs = inspect.classify_class_attrs(klass)
        attrs = [ t for t in attrs if _visiblename(t[0]) ]
        while attrs:
            if mro:
                thisclass = mro.pop(0)
            else:
                thisclass = attrs[0][2]

            inherited_attrs = [ t for t in attrs if t[2] is not thisclass ]
            attrs = [ t for t in attrs if t[2] is thisclass ]

            # Sort attrs by name (which is the first item in the tuple)
            attrs.sort()

            # Group the attributes into 'methods' and 'fields', where:
            #  methods = the method types and functions from 'data'
            #  fields = properties and the non-functions from 'data'
            methods = []
            fields = []
            for info in attrs:
                if info[1].endswith('method') or inspect.isbuiltin(info[3]):
                    methods.append(info)
                else:
                    fields.append(info)

            inherited = thisclass is not klass
            
            attributes = {
                'class' : thisclass.__name__,
                'module' : thisclass.__module__,
                'documented' : self.isdocumented(thisclass.__module__),
                'inherited' : inherited and 'yes' or 'no',
                }

            if methods:
                self.start_element('methods', attributes)
                for name, kind, home_class, method in methods:
                    if inherited:
                        self.doc_inherited(name)
                    else:
                        if inspect.isbuiltin(method):
                            self.doc_function(method, name)
                        else:
                            # inspect.classify_class_attrs gets the methods
                            # from the class __dict__ which returns functions,
                            # not methods
                            method = getattr(klass, name)
                            self.doc_method(method, name, klass, kind)
                       
                self.end_element('methods')

            if fields:
                self.start_element('fields', attributes)
                for name, kind, home_class, field in fields:
                    if inherited:
                        self.doc_inherited(name)
                    else:
                        # inspect.classify_class_attrs gets the attributes from
                        # the class __dict__
                        #other = getattr(klass, name)
                        self.doc_field(field, name, klass)
                        
                self.end_element('fields')

            attrs = inherited_attrs

        self.end_element('class')
        return

    def format_arg(self, arg, default=None):
        attributes = {}
        if default is not None:
            attributes['default'] = default
        if type(arg) in [types.TupleType, types.ListType]:
            self.start_element('sequence', attributes)
            for a in arg:
                self.format_arg(a)
            self.end_element('sequence')
        else:
            attributes['name'] = arg
            self.write_element('arg', attributes)
        return

    def doc_arguments(self, object):
        self.start_element('arguments')

        if inspect.isfunction(object):
            args, varargs, varkw, defaults = inspect.getargspec(object)

            if defaults:
                firstdefault = len(args) - len(defaults)

            for i in xrange(len(args)):
                if defaults and i >= firstdefault:
                    default = repr(defaults[i - firstdefault])
                else:
                    default = None
                self.format_arg(args[i], default)

            if varargs:
                self.write_element('var-args', {'name' : varargs})

            if varkw:
                self.write_element('var-keywords', {'name' : varkw})
        else:
            arglist = '...'
            if inspect.isbuiltin(object):
                # Extract argument list from docstring
                match = _re_arglist.match(pydoc.getdoc(object))
                if match:
                    arglist = match.group('arglist')
            self.write_element('unknown', content=arglist)

        self.end_element('arguments')
        return

    def doc_method(self, method, name, klass, kind):
        """
        Document a method, class method or static method as given by 'kind'
        """
        realname = method.__name__
        name = name or realname
        attributes = {'name' : name,
                      'id' : klass.__name__ + '-' + name,
                      'public' : self.ispublic(name),
                      }
        if name != realname:
            attributes['realname'] = realname
            # aliased methods fail the 'is' test but compare true
            if getattr(klass, realname, None) == method:
                attributes['realid'] = klass.__name__ + '-' + realname

        tagname = kind.replace(' ', '-')
        self.start_element(tagname, attributes)

        self.write_description(method)

        if inspect.ismethod(method):
            self.doc_arguments(method.im_func)
        else:
            self.doc_arguments(method)

        # In Python 2.2 it could also be a method descriptor which is
        # just a wrapper that acts like a method
        if inspect.ismethod(method):
            imclass = method.im_class
            if imclass is not klass:
                attributes = {'module' : imclass.__module__,
                              'name' : imclass.__name__,
                              }
                self.write_element('inherited-from', attributes)
            elif imclass:
                # This always returns the passed in class as the first item
                for base in inspect.getmro(imclass)[1:]:
                    overridden = getattr(base, name, None)
                    if overridden:
                        attributes = {'module' : base.__module__,
                                      'name' : base.__name__,
                                      }
                        self.write_element('overrides', attributes)
                        break

        self.end_element(tagname)
        return

    def doc_function(self, func, name):
        """
        Document a function
        """
        realname = func.__name__
        if realname == '<lambda>':
            realname = 'lambda'
        name = name or realname

        attributes = {'name' : name,
                      'id' : name,
                      'public' : self.ispublic(name),
                      }
        if name != realname:
            attributes['realname'] = realname

        self.start_element('function', attributes)

        self.write_description(func)

        self.doc_arguments(func)

        self.end_element('function')
        return

    def doc_field(self, object, name, klass=None):
        """Produce XML documentation for a data object."""
        if klass:
            field_id = klass.__name__ + '-' + name
        else:
            field_id = name
        attributes = {'name' : name,
                      'id' : field_id,
                      'public' : self.ispublic(name),
                      }
        self.start_element('field', attributes)

        # SWIG wrapped objects throw exceptions with getattr(obj, attr, None)
        # but not with hasattr(obj, attr)
        if ((isinstance(object, types.InstanceType) and
             object.__doc__ != object.__class__.__doc__) or
            (type(object) not in _builtin_types and
             hasattr(object, '__doc__') and getattr(object, '__doc__'))
            ):
            self.write_description(object)

        self.write_element('value', content=self.repr(object))

        self.end_element('field')
        return

    def doc_inherited(self, name):
        """Produce XML documentation for an inherited object."""
        attributes = {'name' : name,
                      'public' : self.ispublic(name),
                      }
        self.write_element('member-reference', attributes)
        return

    def write_description(self, object):
        """Produce XML tag(s) for an object description."""
        docstring = self.escape(pydoc.getdoc(object))
        
        paragraphs = docstring.split('\n\n')
        if paragraphs:
            abstract = paragraphs[0]
            description = '\n\n'.join(paragraphs[1:])
        else:
            abstract = None
            description = None

        self.write_element('abstract', content=abstract)
        self.write_element('description', content=description)
        return

# function not available in Python 2.1
if not hasattr(inspect, 'getmro'):
    def _searchbases(cls, accum):
        # Simulate the "classic class" search order.
        if cls in accum:
            return
        accum.append(cls)
        for base in cls.__bases__:
            _searchbases(base, accum)

    def getmro(cls):
        "Return list of base classes (including cls) in method resolution order."
        result = []
        _searchbases(cls, result)
        return result
    inspect.getmro = getmro

# function not available in Python 2.1
if not hasattr(inspect, 'classify_class_attrs'):
    def classify_class_attrs(cls):
        """Return list of attribute-descriptor tuples.

        For each name defined on class, cls, the return list contains a
        4-tuple with these elements:

            0. The name (a string).

            1. The kind of attribute this is, one of these strings:
                   'method'   any flavor of method
                   'data'     not a method

            2. The class which defined this attribute (a class).

            3. The object as obtained directly from the defining class's
               __dict__, not via getattr.
        """

        # This mimics a Python 2.2 dir(), which returns an alphabetized list
        # of all reachable attribute names.
        bases = getmro(cls)
        bases.reverse()
        combined = {}
        for baseclass in bases:
            for name, value in baseclass.__dict__.items():
                combined[name] = (baseclass, value)
        names = combined.keys()
        names.sort()

        result = []
        for name in names:
            true_class, obj = combined[name]
            # Classify the object.
            if inspect.ismethod(getattr(cls, name)):
                kind = "method"
            else:
                kind = "data"
            result.append((name, kind, true_class, obj))
        return result
    inspect.classify_class_attrs = classify_class_attrs
