# -*- coding: utf-8 -*-

import compiler
import inspect
import posixpath
import re
import textwrap

from genshi import Markup
from genshi.builder import tag
from trac.core import *
from trac.wiki.api import IWikiMacroProvider

try:
    from docutils.core import publish_parts
except ImportError:
    def publish_parts(text, **kwargs):
        return {'body': '<pre>%s</pre>' % text}


class PythonDocMacro(Component):
    """Renders Python API documentation using the PythonDoc library.

    Usage:
    {{{
        [[PythonDoc(path/in/repos)]]
    }}}
    """

    implements(IWikiMacroProvider)

    class _BaseMacro(object):

        def __init__(self, modname, modpath, content, href):
            self.modname = modname
            self.modpath = modpath
            self.content = content
            self.href = href

            self.ast = compiler.parse(self.content, 'exec')
            self._depth = 0

        def _dump_node(self, node, parents):
            attrname = '_dump_%s' % node.__class__.__name__.lower()
            if hasattr(self, attrname):
                return getattr(self, attrname)(node, parents)
            return []

        def _dump_stmt(self, node, parents):
            for child in node.getChildNodes():
                for output in self._dump_node(child, parents + [node]):
                    yield output

        def _generate_id(self, node, parents):
            buf = [parent.name for parent in parents if hasattr(parent, 'name')]
            return ':'.join([self.modname] + buf + [node.name])


    class _PythonDoc(_BaseMacro):

        def render(self):
            return tag.dl(class_='pydoc')(
                tag.dt(class_='module')(tag.h2(
                    tag.a(tag.code(self.modname), href=self.href.browser(self.modpath))
                )),
                tag.dd(class_='module')(self._format_docstring(self.ast.doc)),
                tag.dd(class_='module')(
                    [self._dump_node(node, [self.ast])
                     for node in self.ast.getChildNodes()]
                )
            )

        def _dump_class(self, node, parents):
            if node.lineno:
                label = tag.a(node.name, href=self.href.browser(self.modpath) + '#L%d' % node.lineno)
            else:
                label = node.name

            Hn = getattr(tag, 'h%d' % min(self._depth + 3, 6))
            yield tag.dt(class_='class')(
                Hn(id_=self._generate_id(node, parents))(tag.code(label))
            )
            yield tag.dd(self._format_docstring(node.doc))
            if node.getChildNodes():
                self._depth += 1
                yield tag.dd(tag.dl(
                    [self._dump_node(child, parents + [node])
                     for child in node.getChildNodes()]
                ))
                self._depth -= 1

        def _dump_function(self, node, parents):
            if node.name.startswith('_'):
                return

            def _render_args():
                varargs_offset = kwargs_offset = len(node.argnames)
                if node.varargs:
                    varargs_offset -= 1
                    if node.kwargs:
                        kwargs_offset -= 1
                if node.kwargs:
                    kwargs_offset -= 1
                defaults_offset = min(varargs_offset, kwargs_offset) - len(node.defaults)

                for idx, argname in enumerate(node.argnames):
                    if idx > 0:
                        yield ', '
                    if idx == varargs_offset:
                        yield '*'
                    elif idx == kwargs_offset:
                        yield '**'
                    yield argname
                    if idx >= defaults_offset and idx < min(varargs_offset, kwargs_offset):
                        yield '='
                        val = node.defaults[idx - defaults_offset]
                        if hasattr(val, 'value'):
                            val = repr(val.value)
                        yield val

            if node.lineno:
                label = tag.a(node.name, href=self.href.browser(self.modpath) + '#L%d' % node.lineno)
            else:
                label = node.name

            Hn = getattr(tag, 'h%d' % min(self._depth + 2, 6))
            yield tag.dt(class_='function')(
                Hn(id_=self._generate_id(node, parents))(
                    tag.code(tag.b(label), '(', _render_args(), ')')
                )
            )
            yield tag.dd(self._format_docstring(node.doc))

        def _format_docstring(self, docstring):
            if not docstring:
                return tag.p(tag.em('(Not documented)'))
            text = docstring.expandtabs()

            if '\n' in text: # Fix indentation
                indent = 0
                for line in text.split('\n'):
                    content = line.lstrip()
                    indent = len(line) - len(content)
                if indent:
                    text = ' ' * indent + text
                    text = textwrap.dedent(text)

            return Markup(publish_parts(text, writer_name='html')['body'])


    class _PythonOutline(_BaseMacro):

        def render(self):
            return tag.div(class_='wiki-toc')(
                tag.ol([self._dump_node(node, [self.ast])
                        for node in self.ast.getChildNodes()])
            )

        def _dump_class(self, node, parents):
            def _render_children():
                children = [self._dump_node(child, parents + [node])
                            for child in node.getChildNodes()]
                if children:
                    yield tag.ol(children)
            yield tag.li(class_='class')(
                tag.a(href='#' + self._generate_id(node, parents),
                      title=self._get_summary(node.doc))(node.name),
                _render_children()
            )

        def _dump_function(self, node, parents):
            if node.name.startswith('_'):
                return

            yield tag.li(class_='function')(
                tag.a(href='#' + self._generate_id(node, parents))(node.name)
            )

        def _get_summary(self, docstring):
            if not docstring:
                return None
            text = docstring.expandtabs()

            if '\n' in text: # Fix indentation
                indent = 0
                for line in text.split('\n'):
                    content = line.lstrip()
                    indent = len(line) - len(content)
                if indent:
                    text = ' ' * indent + text
                    text = textwrap.dedent(text)

            lines = text.strip().split('\n\n', 1)
            if len(lines) > 1:
                summary = lines[0]
            else:
                summary = text

            return re.sub(r'\s{2,}', r'\s', summary.replace('\n', ' '))

    # IWikiMacroProvider methods

    def get_macros(self):
        yield 'PythonDoc'
        yield 'PythonOutline'

    def get_macro_description(self, name):
        """Return the subclass's docstring."""
        return inspect.getdoc(getattr(self, '_' + name))

    def render_macro(self, req, name, content):
        basepath, modname = [x.strip() for x in content.split(',', 1)]
        modpath = posixpath.join(basepath, modname.replace('.', '/')) + '.py'

        repos = self.env.get_repository()
        node = repos.get_node(modpath)
        content = node.get_content().read()

        instance = getattr(self, '_' + name)(modname, modpath, content, req.href)
        return instance.render()


