#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#

'''Extensions to Python's ``inspect`` module.

You may use it as drop-in replacement of ``inspect``.  Although we don't
document all items here.  Refer to `inspect's <inspect>`:mod: documentation.

'''

from inspect import *    # noqa
from inspect import (_sentinel, _static_getmro, _check_instance,    # noqa
                     _check_class, _is_type, _shadowed_dict)


def get_attr_value(obj, name, *default):
    '''Get a named attribute from an object in a safe way.

    Similar to `getattr` but without triggering dynamic look-up via the
    descriptor protocol, `__getattr__` or `__getattribute__` by using
    `getattr_static`:func:.

    '''
    from xotl.tools.params import check_default, Undefined
    default = check_default()(*default)
    is_type = isinstance(obj, type)
    res = getattr_static(obj, name, Undefined)
    if isdatadescriptor(res):    # noqa
        try:
            owner = type if is_type else type(obj)
            res = res.__get__(obj, owner)
        except Exception:  # TODO: @med Which expections.
            res = Undefined
    if res is Undefined and not is_type:
        cls = type(obj)
        res = getattr_static(cls, name, Undefined)
        if isdatadescriptor(res):    # noqa
            try:
                res = res.__get__(obj, cls)
            except Exception:  # TODO: @med Which?
                try:
                    res = res.__get__(cls, type)
                except Exception:  # TODO: @med Which?
                    res = Undefined
    if res is not Undefined:
        return res
    elif default is not Undefined:
        return default
    else:
        msg = "'%s' object has no attribute '%s'"
        raise AttributeError(msg % (type(obj).__name__, name))


def safe_name(obj, affirm=False):
    '''Return the internal name for a type or a callable.

    This function is safe.  If :param obj: is not an instance of a proper type
    then returns the following depending on :param affirm:

    - If ``False`` returns None.

    - If ``True`` convert a single object to its type before returns the name,
      but if is a tuple, list or set; returns a string with a representation
      of contained types.

    Examples::

      >>> safe_name(int)
      'int'

      >>> safe_name(0) is None
      True

      >>> safe_name(0, affirm=True)
      'int'

      >>> safe_name((0, 1.1)) is None
      True

      >>> safe_name((0, 1.1), affirm=True)
      '(int, float)'

    '''
    from types import FunctionType, MethodType
    from types import BuiltinFunctionType, BuiltinMethodType
    named_types = (FunctionType, MethodType, BuiltinFunctionType,
                   BuiltinMethodType, type)
    if isinstance(obj, (staticmethod, classmethod)):
        fn = get_attr_value(obj, '__func__', None)
        if fn:
            obj = fn
    if isinstance(obj, named_types):
        # TODO: Why not use directly `get_attr_value``
        try:
            res = getattr_static(obj, '__name__', None)
            if res:
                if isdatadescriptor(res):    # noqa
                    res = res.__get__(obj, type)
        except Exception:
            res = None
        if res is None:
            try:
                res = obj.__name__
            except AttributeError:
                res = None
    else:
        res = None
    if res is None:
        # TODO: Why not use directly `get_attr_value``
        # FIX: Improve and standardize the combination of next code
        res = getattr_static(obj, '__name__', None)
        if res and isdatadescriptor(res):    # noqa
            res = res.__get__(obj, type(obj))
    if isinstance(res, str):
        return res
    elif affirm:
        if isinstance(obj, (tuple, list, set)):
            if isinstance(obj, tuple):
                head, tail = '()'
            elif isinstance(obj, list):
                head, tail = '[]'
            else:
                head, tail = '{}'
            items = ', '.join(safe_name(t, affirm) for t in obj)
            return str('%s%s%s' % (head, items, tail))
        else:
            return safe_name(type(obj))
    else:
        return None


def _static_issubclass(C, B):
    '''like ``issubclass(C, B) -> bool`` but without using ABCs.

    Return whether class C is a strict subclass (i.e., a derived class) of
    class B.

    When using a tuple as the second argument it's a shortcut for::

      any(_static_issubclass(C, b) for b in B)

    This function returns False instead raising "TypeError: issubclass() arg 2
    must be a class or tuple of classes" if `B` any tuple member) is not
    instance of `type`.

    '''
    mro = _static_getmro(C)
    if isinstance(B, tuple):
        return any(b in mro for b in B)
    else:
        return B in mro
