"""
===========
autosummary
===========

Sphinx extension that adds an autosummary:: directive, which can be
used to generate function/method/attribute/etc. summary lists, similar
to those output eg. by Epydoc and other API doc generation tools.

An :autolink: role is also provided.

autosummary directive
---------------------

The autosummary directive has the form::

    .. autosummary::
       :nosignatures:
       :toctree: generated/

       module.function_1
       module.function_2
       ...

and it generates an output table (containing signatures, optionally)

    ========================  =============================================
    module.function_1(args)   Summary line from the docstring of function_1
    module.function_2(args)   Summary line from the docstring
    ...
    ========================  =============================================

If the :toctree: option is specified, files matching the function names
are inserted to the toctree with the given prefix:

    generated/module.function_1
    generated/module.function_2
    ...

Note: The file names contain the module:: or currentmodule:: prefixes.

.. seealso:: autosummary_generate.py


autolink role
-------------

The autolink role functions as ``:obj:`` when the name referred can be
resolved to a Python object, and otherwise it becomes simple emphasis.
This can be used as the default role to make links 'smart'.

"""
import sys
import posixpath
import re

try:
    from docutils import nodes
except ImportError:
    # This won't work, but we have to have the module importable,
    # so that nose can do its discovery scan, so we stub docutils
    class nodes(object):
        comment = object


from docscrape_sphinx import get_doc_object


def setup(app):
    from docutils.parsers.rst import directives
    app.add_directive('autosummary',
                                 autosummary_directive, True, (0, 0, False),
                                 toctree=directives.unchanged,
                                 nosignatures=directives.flag)
    app.add_role('autolink', autolink_role)

    app.add_node(autosummary_toc,
                            html=(autosummary_toc_visit_html,
                                       autosummary_toc_depart_noop),
                            latex=(autosummary_toc_visit_latex,
                                       autosummary_toc_depart_noop))

    app.connect('doctree-read', process_autosummary_toc)


#------------------------------------------------------------------------------
# autosummary_toc node
#------------------------------------------------------------------------------


class autosummary_toc(nodes.comment):
    pass


def process_autosummary_toc(app, doctree):
    """
    Insert items described in autosummary:: to the TOC tree, but do
    not generate the toctree:: list.

    """
    import sphinx.addnodes
    env = app.builder.env
    crawled = {}

    def crawl_toc(node, depth=1):
        crawled[node] = True
        for j, subnode in enumerate(node):
            try:
                if (isinstance(subnode, autosummary_toc)
                    and isinstance(subnode[0], sphinx.addnodes.toctree)):
                    env.note_toctree(env.docname, subnode[0])
                    continue
            except IndexError:
                continue
            if not isinstance(subnode, nodes.section):
                continue
            if subnode not in crawled:
                crawl_toc(subnode, depth + 1)
    crawl_toc(doctree)


def autosummary_toc_visit_html(self, node):
    """Hide autosummary toctree list in HTML output"""
    raise nodes.SkipNode


def autosummary_toc_visit_latex(self, node):
    """Show autosummary toctree (= put the referenced pages here) in Latex"""
    pass


def autosummary_toc_depart_noop(self, node):
    pass


#------------------------------------------------------------------------------
# .. autosummary::
#------------------------------------------------------------------------------


def autosummary_directive(dirname, arguments, options, content, lineno,
                          content_offset, block_text, state, state_machine):
    """
    Pretty table containing short signatures and summaries of functions etc.

    autosummary also generates a (hidden) toctree:: node.

    """
    import sphinx.addnodes

    names = []
    names += [x.strip() for x in content if x.strip()]

    table, warnings, real_names = get_autosummary(names, state,
                                                  'nosignatures' in options)
    node = table

    env = state.document.settings.env
    suffix = env.config.source_suffix
    all_docnames = env.found_docs.copy()
    dirname = posixpath.dirname(env.docname)

    if 'toctree' in options:
        tree_prefix = options['toctree'].strip()
        docnames = []
        for name in names:
            name = real_names.get(name, name)

            docname = tree_prefix + name
            if docname.endswith(suffix):
                docname = docname[:-len(suffix)]
            docname = posixpath.normpath(posixpath.join(dirname, docname))
            if docname not in env.found_docs:
                warnings.append(state.document.reporter.warning(
                    'toctree references unknown document %r' % docname,
                    line=lineno))
            docnames.append(docname)

        tocnode = sphinx.addnodes.toctree()
        tocnode['includefiles'] = docnames
        tocnode['maxdepth'] = -1
        tocnode['glob'] = None
        tocnode['entries'] = []

        tocnode = autosummary_toc('', '', tocnode)
        return warnings + [node] + [tocnode]
    else:
        return warnings + [node]


def get_autosummary(names, state, no_signatures=False):
    """
    Generate a proper table node for autosummary:: directive.

    Parameters
    ----------
    names : list of str
        Names of Python objects to be imported and added to the table.
    document : document
        Docutils document object

    """

    from docutils.statemachine import ViewList
    document = state.document

    real_names = {}
    warnings = []

    prefixes = ['']
    prefixes.insert(0, document.settings.env.currmodule)

    table = nodes.table('')
    group = nodes.tgroup('', cols=2)
    table.append(group)
    group.append(nodes.colspec('', colwidth=30))
    group.append(nodes.colspec('', colwidth=70))
    body = nodes.tbody('')
    group.append(body)

    def append_row(*column_texts):
        row = nodes.row('')
        for text in column_texts:
            node = nodes.paragraph('')
            vl = ViewList()
            vl.append(text, '<autosummary>')
            state.nested_parse(vl, 0, node)
            row.append(nodes.entry('', node))
        body.append(row)

    for name in names:
        try:
            obj, real_name = import_by_name(name, prefixes=prefixes)
        except ImportError:
            warnings.append(document.reporter.warning(
                'failed to import %s' % name))
            append_row(":obj:`%s`" % name, "")
            continue

        real_names[name] = real_name

        doc = get_doc_object(obj)

        if doc['Summary']:
            title = " ".join(doc['Summary'])
        else:
            title = ""

        col1 = ":obj:`%s <%s>`" % (name, real_name)
        if doc['Signature']:
            sig = re.sub('^[a-zA-Z_0-9.-]*', '', doc['Signature'])
            if '=' in sig:
                # abbreviate optional arguments
                sig = re.sub(r', ([a-zA-Z0-9_]+)=', r'[, \1=', sig, count=1)
                sig = re.sub(r'\(([a-zA-Z0-9_]+)=', r'([\1=', sig, count=1)
                sig = re.sub(r'=[^,)]+,', ',', sig)
                sig = re.sub(r'=[^,)]+\)$', '])', sig)
                # shorten long strings
                sig = re.sub(r'(\[.{16,16}[^,)]*?),.*?\]\)', r'\1, ...])', sig)
            else:
                sig = re.sub(r'(\(.{16,16}[^,)]*?),.*?\)', r'\1, ...)', sig)
            col1 += " " + sig
        col2 = title
        append_row(col1, col2)

    return table, warnings, real_names


def import_by_name(name, prefixes=[None]):
    """
    Import a Python object that has the given name, under one of the prefixes.

    Parameters
    ----------
    name : str
        Name of a Python object, eg. 'numpy.ndarray.view'
    prefixes : list of (str or None), optional
        Prefixes to prepend to the name (None implies no prefix).
        The first prefixed name that results to successful import is used.

    Returns
    -------
    obj
        The imported object
    name
        Name of the imported object (useful if `prefixes` was used)

    """

    for prefix in prefixes:
        try:
            if prefix:
                prefixed_name = '.'.join([prefix, name])
            else:
                prefixed_name = name
            return _import_by_name(prefixed_name), prefixed_name
        except ImportError:
            pass
    raise ImportError


def _import_by_name(name):
    """Import a Python object given its full name"""
    try:
        # try first interpret `name` as MODNAME.OBJ
        name_parts = name.split('.')
        try:
            modname = '.'.join(name_parts[:-1])
            __import__(modname)
            return getattr(sys.modules[modname], name_parts[-1])
        except (ImportError, IndexError, AttributeError):
            pass

        # ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ...
        last_j = 0
        modname = None
        for j in reversed(range(1, len(name_parts) + 1)):
            last_j = j
            modname = '.'.join(name_parts[:j])
            try:
                __import__(modname)
            except ImportError:
                continue
            if modname in sys.modules:
                break

        if last_j < len(name_parts):
            obj = sys.modules[modname]
            for obj_name in name_parts[last_j:]:
                obj = getattr(obj, obj_name)
            return obj
        else:
            return sys.modules[modname]
    except (ValueError, ImportError, AttributeError, KeyError), e:
        raise ImportError(e)

#------------------------------------------------------------------------------
# :autolink: (smart default role)
#------------------------------------------------------------------------------


def autolink_role(typ, rawtext, etext, lineno, inliner,
                  options={}, content=[]):
    """
    Smart linking role.

    Expands to ":obj:`text`" if `text` is an object that can be imported;
    otherwise expands to "*text*".
    """
    import sphinx.roles
    r = sphinx.roles.xfileref_role('obj', rawtext, etext, lineno, inliner,
                                   options, content)
    pnode = r[0][0]

    prefixes = [None]
    #prefixes.insert(0, inliner.document.settings.env.currmodule)
    try:
        obj, name = import_by_name(pnode['reftarget'], prefixes)
    except ImportError:
        content = pnode[0]
        r[0][0] = nodes.emphasis(rawtext, content[0].astext(),
                                 classes=content['classes'])
    return r
