##############################################################################
# MDTraj: A Python Library for Loading, Saving, and Manipulating
#         Molecular Dynamics Trajectories.
# Copyright 2012-2013 Stanford University and the Authors
#
# Authors: Robert McGibbon
# Contributors:
#
# MDTraj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 2.1
# of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with MDTraj. If not, see <http://www.gnu.org/licenses/>.
##############################################################################


##############################################################################
# Imports
##############################################################################
from __future__ import print_function, division
import sys
import types
import warnings
import pkgutil
import importlib
from inspect import (isclass, ismodule, isfunction, ismethod,
                     getmembers, getdoc, getmodule, getargs, isbuiltin)
from mdtraj.testing.docscrape import NumpyDocString
from mdtraj.utils.six import get_function_code, get_function_closure, PY2

__all__ = ['docstring_verifiers', 'import_all_modules']

##############################################################################
# functions
##############################################################################


def docstring_verifiers(module, error_on_none=False):
    """Yield an iterable of tests that verify the docstrings (for
    adherance to the NumpyDoc format) of all functions/classes defined
    in a module.

    Parameters
    ----------
    module : module
        The module to test
    error_on_none : bool, default=False
        Throw an error if no docstring is defined
    """

    # These are the types that we want to check
    # currently, the docstring on classes are not being checked, since
    # isclass() is not in the list
    acceptors = [isfunction, ismethod, isbuiltin]
    accept = lambda f: any([acc(f) for acc in acceptors])
    functions = [f for f in walk(module) if accept(f)]

    def format(f):
        """
        Format a method/function/class as a string

        Parameters
        ----------
        f : function, method, class

        Returns
        -------
        repr : string
            A string represntation
        """
        if ismethod(f):
            if PY2:
                return '.'.join([getmodule(f).__name__, f.im_class.__name__, f.__name__])
            else:
                return '.'.join([getmodule(f).__name__, f.__self__.__class__.__name__, f.__name__])
        if isfunction(f) or isbuiltin(f):
            return '.'.join([getmodule(f).__name__, f.__name__])
        if isclass(f):
            return f.__name__
        return 'Error'

    def check_docstring(f):
        """
        Ensure the docstring of `f` is in accordance with the numpy standard

        Currently, only the Parameters section of the docstring is checked.

        Parameters
        ----------
        f : function, method, class

        Returns
        -------
        repr : string
            A string represntation
        """

        doc = getdoc(f)
        if doc is None:
            if error_on_none:
                raise ValueError('no docstring for %s' % format(f))
        else:
            with warnings.catch_warnings():
                warnings.simplefilter('error')
                parsed = NumpyDocString(doc)

            param_names = {e[0] for e in parsed['Parameters']}

            if isbuiltin(f):
                # You can't get the arglist from a builtin, which
                # is how cython functions turn up

                # but you can, hackily, get the number of arguments it wants
                # by parseing the error hen you supply too many
                import re
                try:
                    f(*list(range(100)))
                except TypeError as e:
                    m = re.search('takes at most (\d+) positional arguments', str(e))
                    if not m:
                        return
                    n_args = int(m.group(1))

                if len(param_names) != n_args:
                    raise ValueError("In %s, number of arguments, %d, doesn't "
                        " match the length of the Parameters in the "
                        "docstring, %d" % (format(f), n_args, len(param_names)))
                return

            args = set(getargs(get_function_code(f)).args)
            if 'self' in args:
                args.remove('self')
            if 'cls' in args:
                args.remove('cls')

            if args != param_names:
                raise ValueError("In %s, arguments %s don't "
                    "match Parameters list %s" % (format(f),
                        list(args), list(param_names)))

    for f in functions:
        qq = lambda: check_docstring(f)
        qq.description = 'NumpyDoc: %s.%s' % (module.__name__, f.__name__)
        qq.fname = f.__name__
        yield qq


def ispackage(obj):
    """
    Check if obj is a package. Simply look for whether its a module whose
    filename is __init__.py(c)

    Parameters
    ----------
    obj : module
    """
    if ismodule(obj):
        return obj.__file__.endswith("__init__.pyc") or \
            obj.__file__.endswith("__init__.py")
    return False


def walk(module):
    """
    Get all of the functions, classes and their methods defined within
    a python module

    Parameters
    ----------
    module : module

    Returns
    -------
    funcs : list
        List of functions, classes and methods
    """
    assert ismodule(module)
    if ispackage(module):
        raise ValueError('Sorry, you need to supply me a module, not a package')

    def is_valid(obj):
        if getmodule(obj) == module:
            # cython specific stuff
            if module.__file__.endswith('.so'):
                if isbuiltin(obj):
                    return not obj.__name__.startswith('_')

            if ismethod(obj) or isfunction(obj):
                return not obj.__name__.startswith('_')
            if isclass(obj):
                return True
        return False

    instack = [v for k, v in getmembers(module) if is_valid(v)]
    outstack = []

    while True:
        try:
            item = instack.pop()
        except IndexError:
            break
        outstack.append(item)

        if isclass(item):
            instack.extend([v for k, v in getmembers(item) if is_valid(v)])

    return outstack


def import_all_modules(pkg):
    result = []
    for _, modname, ispkg in pkgutil.iter_modules(pkg.__path__):
        c = '%s.%s' % (pkg.__name__, modname)
        if modname.startswith('test_'):
            continue
        try:
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", category=DeprecationWarning)
                mod = importlib.import_module(c)
            if ispkg:
                result.extend(import_all_modules(mod))
            else:
                result.append(mod)
        except ImportError as e:
            print('e', e)
            continue

    return result
