# -*- coding: utf-8 -*-
# Copyright 2014 Rob Ruana
# Licensed under the BSD License, see LICENSE file for details.

"""A collection of development related build tasks."""

import fnmatch
import os
import shutil
import sys
from paver.easy import (Bunch, call_task, consume_args, needs, no_help,
                        options, sh, task)


reqs = (open('requirements_docs.txt', 'r').read().strip().splitlines() +
        open('requirements_test.txt', 'r').read().strip().splitlines())
reqs = filter(lambda s: not s.startswith('paver'), reqs)

options(
    bootstrap=Bunch(no_site_packages=True),
    sphinx=Bunch(
        # See `sphinx-apidoc --help` for details on apidoc options
        apidoc_excludes=[],
        apidoc_moduledir='sphinxcontrib',
        apidoc_outputdir='docs/source',  # -o DESTDIR, --output-dir=DESTDIR
        apidoc_overwrite=True,  # -f, --force
        builddir='build',
        docroot='docs',
        sourcedir='source'
    ),
    virtualenv=Bunch(
        no_site_packages=True,
        packages_to_install=reqs,
    ),
)


@task
@consume_args
def bootstrap():
    """Create a virtualenv in the specified directory.

    From a clean project working directory::

        $ paver bootstrap
        $ source virtualenv/bin/activate
        (virtualenv)$ python setup.py develop

    Or if you prefer, you can specify an alternate virtualenv directory::

        $ paver bootstrap ~/virtualenv/napoleon
        $ source ~/virtualenv/napoleon/bin/activate
        (napoleon)$ python setup.py develop

    """
    try:
        import virtualenv
        assert virtualenv
    except ImportError:
        raise RuntimeError('virtualenv is required for bootstrap')

    bootstrap_dir = 'virtualenv'
    if options.args:
        bootstrap_dir = options.args[0]
    if not os.path.exists(bootstrap_dir):
        os.makedirs(bootstrap_dir)

    bootstrap_script = 'bootstrap.py'
    bootstrap_script_path = os.path.join(bootstrap_dir, bootstrap_script)
    options.virtualenv.script_name = bootstrap_script_path

    call_task('paver.virtual.bootstrap')

    current_dir = os.getcwd()
    os.chdir(bootstrap_dir)
    try:
        sh(' '.join([sys.executable, bootstrap_script, '--no-site-packages']))
    finally:
        os.chdir(current_dir)


@task
@needs(['clean_coverage', 'paver.doctools.doc_clean', 'clean_pyc'])
def clean():
    """Delete all files generated by the build process."""
    shutil.rmtree('build', True)
    shutil.rmtree('dist', True)


@task
def clean_coverage():
    """Delete coverage output."""
    if os.path.exists('.coverage'):
        os.remove('.coverage')
    if os.path.exists('coverage.xml'):
        os.remove('coverage.xml')


@task
def clean_pyc():
    """Delete compiled *.py[cod] files."""
    for root, dirnames, filenames in os.walk('.'):
        for filename in fnmatch.filter(filenames, '*.py[cod]'):
            os.remove(os.path.join(root, filename))


@task
@needs('_monkey_patch_sphinx', 'sphinxcontrib.napoleon.pavertasks.html')
def html():
    """Build Napoleon's HTML documentation."""
    pass


@task
@consume_args
def flake8():
    """Validate PEP-8 compliance and perform static analysis."""
    cmd = [
        'flake8',
        '--exclude=tests/docs/source/conf.py',
        '*.py',
        'sphinxcontrib',
        'tests'
    ]
    if options.args:
        cmd.extend(options.args)
    sh(' '.join(cmd))


@task
@consume_args
def nosetests():
    """Run nosetests with coverage enabled."""
    cmd = [
        'nosetests',
        '--with-doctest',
        '--with-coverage',
        '--cover-package=sphinxcontrib.napoleon',
    ]
    if options.args:
        cmd.extend(options.args)
    sh(' '.join(cmd))


@task
@consume_args
@needs('nosetests')
@needs('flake8')
def test():
    """Run nosetests, PEP-8 compliance, and static analysis."""
    pass


@task
@no_help
def _monkey_patch_sphinx():
    from docutils import nodes
    from sphinx.util import docfields
    from sphinx.util.docfields import _is_single_paragraph

    def _patched__DocFieldTransformer__transform(self, node):
        """Transform a single field list *node*."""
        typemap = self.typemap

        entries = []
        groupindices = {}
        types = {}

        # step 1: traverse all fields and collect field types and content
        for field in node:
            fieldname, fieldbody = field
            try:
                # split into field type and argument
                fieldtype, fieldarg = fieldname.astext().split(None, 1)
            except ValueError:
                # maybe an argument-less field type?
                fieldtype, fieldarg = fieldname.astext(), ''
            typedesc, is_typefield = typemap.get(fieldtype, (None, None))

            # sort out unknown fields
            if typedesc is None or typedesc.has_arg != bool(fieldarg):
                # either the field name is unknown, or the argument doesn't
                # match the spec; capitalize field name and be done with it
                new_fieldname = fieldtype[0:1].upper() + fieldtype[1:]
                if fieldarg:
                    new_fieldname = new_fieldname + ' ' + fieldarg
                fieldname[0] = nodes.Text(new_fieldname)
                entries.append(field)
                continue

            typename = typedesc.name

            # collect the content, trying not to keep unnecessary paragraphs
            if _is_single_paragraph(fieldbody):
                content = fieldbody.children[0].children
            else:
                content = fieldbody.children

            # if the field specifies a type, put it in the types collection
            if is_typefield:
                # filter out only inline nodes; others will result in invalid
                # markup being written out
                content = filter(
                    lambda n: (isinstance(n, nodes.Inline) or
                               isinstance(n, nodes.Text)),
                    content)
                if content:
                    types.setdefault(typename, {})[fieldarg] = content
                continue

            # also support syntax like ``:param type name:``
            if typedesc.is_typed:
                try:
                    argtype, argname = fieldarg.split(None, 1)
                except ValueError:
                    pass
                else:
                    types.setdefault(typename, {})[argname] = \
                        [nodes.Text(argtype)]
                    fieldarg = argname

            translatable_content = nodes.inline(fieldbody.rawsource,
                                                translatable=True)
            translatable_content.source = fieldbody.parent.source
            translatable_content.line = fieldbody.parent.line
            translatable_content += content

            # grouped entries need to be collected in one entry, while others
            # get one entry per field
            if typedesc.is_grouped:
                if typename in groupindices:
                    group = entries[groupindices[typename]]
                else:
                    groupindices[typename] = len(entries)
                    group = [typedesc, []]
                    entries.append(group)
                entry = typedesc.make_entry(fieldarg, translatable_content)
                group[1].append(entry)
            else:
                entry = typedesc.make_entry(fieldarg, translatable_content)
                entries.append([typedesc, entry])

        # step 2: all entries are collected, construct the new field list
        new_list = nodes.field_list()
        for entry in entries:
            if isinstance(entry, nodes.field):
                # pass-through old field
                new_list += entry
            else:
                fieldtype, content = entry
                fieldtypes = types.get(fieldtype.name, {})
                new_list += fieldtype.make_field(fieldtypes, self.domain,
                                                 content)

        node.replace_self(new_list)
    docfields.DocFieldTransformer.transform = \
        _patched__DocFieldTransformer__transform
