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

#    Copyright (C) 2012-2013 Yahoo! Inc. All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""
Reflection module.

.. versionadded:: 1.1
"""

import inspect
import types

import six

try:
    _TYPE_TYPE = types.TypeType
except AttributeError:
    _TYPE_TYPE = type

# See: https://docs.python.org/2/library/__builtin__.html#module-__builtin__
# and see https://docs.python.org/2/reference/executionmodel.html (and likely
# others)...
_BUILTIN_MODULES = ('builtins', '__builtin__', '__builtins__', 'exceptions')

if six.PY3:
    Parameter = inspect.Parameter
    Signature = inspect.Signature
    get_signature = inspect.signature
else:
    # Provide an equivalent but use funcsigs instead...
    import funcsigs
    Parameter = funcsigs.Parameter
    Signature = funcsigs.Signature
    get_signature = funcsigs.signature


def get_members(obj, exclude_hidden=True):
    """Yields the members of an object, filtering by hidden/not hidden.

    .. versionadded:: 2.3
    """
    for (name, value) in inspect.getmembers(obj):
        if name.startswith("_") and exclude_hidden:
            continue
        yield (name, value)


def get_member_names(obj, exclude_hidden=True):
    """Get all the member names for a object."""
    return [name for (name, _obj) in
            get_members(obj, exclude_hidden=exclude_hidden)]


def get_class_name(obj, fully_qualified=True, truncate_builtins=True):
    """Get class name for object.

    If object is a type, returns name of the type. If object is a bound
    method or a class method, returns its ``self`` object's class name.
    If object is an instance of class, returns instance's class name.
    Else, name of the type of the object is returned. If fully_qualified
    is True, returns fully qualified name of the type. For builtin types,
    just name is returned. TypeError is raised if can't get class name from
    object.
    """
    if inspect.isfunction(obj):
        raise TypeError("Can't get class name.")

    if inspect.ismethod(obj):
        obj = get_method_self(obj)
    if not isinstance(obj, six.class_types):
        obj = type(obj)
    if truncate_builtins:
        try:
            built_in = obj.__module__ in _BUILTIN_MODULES
        except AttributeError:  # nosec
            pass
        else:
            if built_in:
                return obj.__name__
    if fully_qualified and hasattr(obj, '__module__'):
        return '%s.%s' % (obj.__module__, obj.__name__)
    else:
        return obj.__name__


def get_all_class_names(obj, up_to=object,
                        fully_qualified=True, truncate_builtins=True):
    """Get class names of object parent classes.

    Iterate over all class names object is instance or subclass of,
    in order of method resolution (mro). If up_to parameter is provided,
    only name of classes that are sublcasses to that class are returned.
    """
    if not isinstance(obj, six.class_types):
        obj = type(obj)
    for cls in obj.mro():
        if issubclass(cls, up_to):
            yield get_class_name(cls,
                                 fully_qualified=fully_qualified,
                                 truncate_builtins=truncate_builtins)


def get_callable_name(function):
    """Generate a name from callable.

    Tries to do the best to guess fully qualified callable name.
    """
    method_self = get_method_self(function)
    if method_self is not None:
        # This is a bound method.
        if isinstance(method_self, six.class_types):
            # This is a bound class method.
            im_class = method_self
        else:
            im_class = type(method_self)
        try:
            parts = (im_class.__module__, function.__qualname__)
        except AttributeError:
            parts = (im_class.__module__, im_class.__name__, function.__name__)
    elif inspect.ismethod(function) or inspect.isfunction(function):
        # This could be a function, a static method, a unbound method...
        try:
            parts = (function.__module__, function.__qualname__)
        except AttributeError:
            if hasattr(function, 'im_class'):
                # This is a unbound method, which exists only in python 2.x
                im_class = function.im_class
                parts = (im_class.__module__,
                         im_class.__name__, function.__name__)
            else:
                parts = (function.__module__, function.__name__)
    else:
        im_class = type(function)
        if im_class is _TYPE_TYPE:
            im_class = function
        try:
            parts = (im_class.__module__, im_class.__qualname__)
        except AttributeError:
            parts = (im_class.__module__, im_class.__name__)
    return '.'.join(parts)


def get_method_self(method):
    """Gets the ``self`` object attached to this method (or none)."""
    if not inspect.ismethod(method):
        return None
    try:
        return six.get_method_self(method)
    except AttributeError:
        return None


def is_same_callback(callback1, callback2, strict=True):
    """Returns if the two callbacks are the same."""
    if callback1 is callback2:
        # This happens when plain methods are given (or static/non-bound
        # methods).
        return True
    if callback1 == callback2:
        if not strict:
            return True
        # Two bound methods are equal if functions themselves are equal and
        # objects they are applied to are equal. This means that a bound
        # method could be the same bound method on another object if the
        # objects have __eq__ methods that return true (when in fact it is a
        # different bound method). Python u so crazy!
        try:
            self1 = six.get_method_self(callback1)
            self2 = six.get_method_self(callback2)
            return self1 is self2
        except AttributeError:  # nosec
            pass
    return False


def is_bound_method(method):
    """Returns if the given method is bound to an object."""
    return get_method_self(method) is not None


def is_subclass(obj, cls):
    """Returns if the object is class and it is subclass of a given class."""
    return inspect.isclass(obj) and issubclass(obj, cls)


def get_callable_args(function, required_only=False):
    """Get names of callable arguments.

    Special arguments (like ``*args`` and ``**kwargs``) are not included into
    output.

    If required_only is True, optional arguments (with default values)
    are not included into output.
    """
    sig = get_signature(function)
    function_args = list(six.iterkeys(sig.parameters))
    for param_name, p in six.iteritems(sig.parameters):
        if (p.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD)
                or (required_only and p.default is not Parameter.empty)):
            function_args.remove(param_name)
    return function_args


def accepts_kwargs(function):
    """Returns ``True`` if function accepts kwargs otherwise ``False``."""
    sig = get_signature(function)
    return any(p.kind == Parameter.VAR_KEYWORD
               for p in six.itervalues(sig.parameters))
