"""Machinery for documenting traitlets config options with Sphinx.

This includes:

- A Sphinx extension defining directives and roles for config options.
- A function to generate an rst file given an Application instance.

To make this documentation, first set this module as an extension in Sphinx's
conf.py::

    extensions = [
        # ...
        'traitlets.config.sphinxdoc',
    ]

Autogenerate the config documentation by running code like this before
Sphinx builds::

    from traitlets.config.sphinxdoc import write_doc
    from myapp import MyApplication

    writedoc('config/options.rst',    # File to write
             'MyApp config options',  # Title
             MyApplication()
            )

The generated rST syntax looks like this::

    .. configtrait:: Application.log_datefmt

        Description goes here.

    Cross reference like this: :configtrait:`Application.log_datefmt`.
"""
from traitlets import Undefined
from traitlets.utils.text import indent
from collections import defaultdict

from textwrap import indent as _indent, dedent


def setup(app):
    """Registers the Sphinx extension.

    You shouldn't need to call this directly; configure Sphinx to use this
    module instead.
    """
    app.add_object_type('configtrait', 'configtrait', objname='Config option')
    metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
    return metadata

def interesting_default_value(dv):
    if (dv is None) or (dv is Undefined):
        return False
    if isinstance(dv, (str, list, tuple, dict, set)):
        return bool(dv)
    return True

def format_aliases(aliases):
    fmted = []
    for a in aliases:
        dashes = '-' if len(a) == 1 else '--'
        fmted.append('``%s%s``' % (dashes, a))
    return ', '.join(fmted)

def class_config_rst_doc(cls, trait_aliases):
    """Generate rST documentation for this class' config options.

    Excludes traits defined on parent classes.
    """
    lines = []
    classname = cls.__name__
    for k, trait in sorted(cls.class_traits(config=True).items()):
        ttype = trait.__class__.__name__

        fullname = classname + '.' + trait.name
        lines += ['.. configtrait:: ' + fullname,
                  ''
                 ]

        help = trait.help.rstrip() or "No description"
        lines.append(indent(dedent(help)) + "\n")

        # Choices or type
        if 'Enum' in ttype:
            # include Enum choices
            lines.append(
                indent(":options: " + ", ".join("``%r``" % x for x in trait.values))
            )
        else:
            lines.append(indent(":trait type: " + ttype))

        # Default value
        # Ignore boring default values like None, [] or ''
        if interesting_default_value(trait.default_value):
            try:
                dvr = trait.default_value_repr()
            except Exception:
                dvr = None  # ignore defaults we can't construct
            if dvr is not None:
                if len(dvr) > 64:
                    dvr = dvr[:61] + '...'
                # Double up backslashes, so they get to the rendered docs
                dvr = dvr.replace("\\n", "\\\\n")
                lines.append(indent(":default: ``%s``" % dvr))

        # Command line aliases
        if trait_aliases[fullname]:
            fmt_aliases = format_aliases(trait_aliases[fullname])
            lines.append(indent(":CLI option: " + fmt_aliases))

        # Blank line
        lines.append('')

    return '\n'.join(lines)

def reverse_aliases(app):
    """Produce a mapping of trait names to lists of command line aliases.
    """
    res = defaultdict(list)
    for alias, trait in app.aliases.items():
        res[trait].append(alias)

    # Flags also often act as aliases for a boolean trait.
    # Treat flags which set one trait to True as aliases.
    for flag, (cfg, _) in app.flags.items():
        if len(cfg) == 1:
            classname = list(cfg)[0]
            cls_cfg = cfg[classname]
            if len(cls_cfg) == 1:
                traitname = list(cls_cfg)[0]
                if cls_cfg[traitname] is True:
                    res[classname+'.'+traitname].append(flag)

    return res

def write_doc(path, title, app, preamble=None):
    """Write a rst file documenting config options for a traitlets application.

    Parameters
    ----------
    path : str
        The file to be written
    title : str
        The human-readable title of the document
    app : traitlets.config.Application
        An instance of the application class to be documented
    preamble : str
        Extra text to add just after the title (optional)
    """
    trait_aliases = reverse_aliases(app)
    with open(path, 'w') as f:
        f.write(title + '\n')
        f.write(('=' * len(title)) + '\n')
        f.write('\n')
        if preamble is not None:
            f.write(preamble + '\n\n')

        for c in app._classes_inc_parents():
            f.write(class_config_rst_doc(c, trait_aliases))
            f.write('\n')
