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

import compiler
import posixpath
import re
import textwrap

from trac.core import *
from trac.util.html import html, Markup
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 html.DL(class_='pydoc')(
                html.DT(class_='module')(html.H2(
                    html.A(self.modname, href=self.href.browser(self.modpath))
                )),
                html.DD(class_='module')(self._format_docstring(self.ast.doc)),
                html.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 = html.A(node.name, href=self.href.browser(self.modpath) +
                                               '#L%d' % node.lineno)
            else:
                label = node.name

            Hn = getattr(html, 'H%d' % min(self._depth + 3, 6))
            yield html.DT(class_='class')(
                Hn(id_=self._generate_id(node, parents))(label)
            )
            yield html.DD(self._format_docstring(node.doc))
            if node.getChildNodes():
                self._depth += 1
                yield html.DD(html.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 = html.A(node.name, href=self.href.browser(self.modpath) +
                                               '#L%d' % node.lineno)
            else:
                label = node.name

            Hn = getattr(html, 'H%d' % min(self._depth + 2, 6))
            yield html.DT(class_='function')(
                Hn(id_=self._generate_id(node, parents))(
                    html.CODE(html.B(label), '(', _render_args(), ')')
                )
            )
            yield html.DD(self._format_docstring(node.doc))

        def _format_docstring(self, docstring):
            if not docstring:
                return html.P(html.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 html.DIV(class_='wiki-toc')(
                html.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 html.OL(children)
            yield html.LI(class_='class')(
                html.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 html.LI(class_='function')(
                html.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()
