# Copyright 2016 Quora, Inc.
#
# 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.

__doc__ = """

Code inspection helpers.

"""

import functools
import inspect
import six
import sys


def get_original_fn(fn):
    """Gets the very original function of a decorated one."""

    fn_type = type(fn)
    if fn_type is classmethod or fn_type is staticmethod:
        return get_original_fn(fn.__func__)
    if hasattr(fn, "original_fn"):
        return fn.original_fn
    if hasattr(fn, "fn"):
        fn.original_fn = get_original_fn(fn.fn)
        return fn.original_fn
    return fn


def get_full_name(src):
    """Gets full class or function name."""

    if hasattr(src, "_full_name_"):
        return src._full_name_
    if hasattr(src, "is_decorator"):
        # Our own decorator or binder
        if hasattr(src, "decorator"):
            # Our own binder
            _full_name_ = str(src.decorator)
            # It's a short-living object, so we don't cache result
        else:
            # Our own decorator
            _full_name_ = str(src)
            try:
                src._full_name_ = _full_name_
            except AttributeError:
                pass
            except TypeError:
                pass
    elif hasattr(src, "im_class"):
        # Bound method
        cls = src.im_class
        _full_name_ = get_full_name(cls) + "." + src.__name__
        # It's a short-living object, so we don't cache result
    elif hasattr(src, "__module__") and hasattr(src, "__name__"):
        # Func or class
        _full_name_ = (
            ("<unknown module>" if src.__module__ is None else src.__module__)
            + "."
            + src.__name__
        )
        try:
            src._full_name_ = _full_name_
        except AttributeError:
            pass
        except TypeError:
            pass
    else:
        # Something else
        _full_name_ = str(get_original_fn(src))
    return _full_name_


def get_function_call_str(fn, args, kwargs):
    """Converts method call (function and its arguments) to a str(...)-like string."""

    def str_converter(v):
        try:
            return str(v)
        except Exception:
            try:
                return repr(v)
            except Exception:
                return "<n/a str raised>"

    result = get_full_name(fn) + "("
    first = True
    for v in args:
        if first:
            first = False
        else:
            result += ","
        result += str_converter(v)
    for k, v in kwargs.items():
        if first:
            first = False
        else:
            result += ","
        result += str(k) + "=" + str_converter(v)
    result += ")"
    return result


def get_function_call_repr(fn, args, kwargs):
    """Converts method call (function and its arguments) to a repr(...)-like string."""

    result = get_full_name(fn) + "("
    first = True
    for v in args:
        if first:
            first = False
        else:
            result += ","
        result += repr(v)
    for k, v in kwargs.items():
        if first:
            first = False
        else:
            result += ","
        result += str(k) + "=" + repr(v)
    result += ")"
    return result


def getargspec(func):
    """Variation of inspect.getargspec that works for more functions.

    This function works for Cythonized, non-cpdef functions, which expose argspec information but
    are not accepted by getargspec. It also works for Python 3 functions that use annotations, which
    are simply ignored. However, keyword-only arguments are not supported.

    """
    if inspect.ismethod(func):
        func = func.__func__
    # Cythonized functions have a .__code__, but don't pass inspect.isfunction()
    try:
        code = func.__code__
    except AttributeError:
        raise TypeError("{!r} is not a Python function".format(func))
    if hasattr(code, "co_kwonlyargcount") and code.co_kwonlyargcount > 0:
        raise ValueError("keyword-only arguments are not supported by getargspec()")
    args, varargs, varkw = inspect.getargs(code)
    return inspect.ArgSpec(args, varargs, varkw, func.__defaults__)


def is_cython_or_generator(fn):
    """Returns whether this function is either a generator function or a Cythonized function."""
    if hasattr(fn, "__func__"):
        fn = fn.__func__  # Class method, static method
    if inspect.isgeneratorfunction(fn):
        return True
    name = type(fn).__name__
    return (
        name == "generator"
        or name == "method_descriptor"
        or name == "cython_function_or_method"
        or name == "builtin_function_or_method"
    )


def is_cython_function(fn):
    """Checks if a function is compiled w/Cython."""
    if hasattr(fn, "__func__"):
        fn = fn.__func__  # Class method, static method
    name = type(fn).__name__
    return (
        name == "method_descriptor"
        or name == "cython_function_or_method"
        or name == "builtin_function_or_method"
    )


def is_cython_class(cls):
    """Returns whether a class is a Cython extension class."""
    return "__pyx_vtable__" in cls.__dict__


def is_classmethod(fn):
    """Returns whether f is a classmethod."""
    # This is True for bound methods
    if not inspect.ismethod(fn):
        return False
    if not hasattr(fn, "__self__"):
        return False
    im_self = fn.__self__
    # This is None for instance methods on classes, but True
    # for instance methods on instances.
    if im_self is None:
        return False
    # This is True for class methods of new- and old-style classes, respectively
    return isinstance(im_self, six.class_types)


def wraps(
    wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, updated=functools.WRAPPER_UPDATES
):
    """Cython-compatible functools.wraps implementation."""
    if not is_cython_function(wrapped):
        return functools.wraps(wrapped, assigned, updated)
    else:
        return lambda wrapper: wrapper


def get_subclass_tree(cls, ensure_unique=True):
    """Returns all subclasses (direct and recursive) of cls."""
    subclasses = []
    # cls.__subclasses__() fails on classes inheriting from type
    for subcls in type.__subclasses__(cls):
        subclasses.append(subcls)
        subclasses.extend(get_subclass_tree(subcls, ensure_unique))
    return list(set(subclasses)) if ensure_unique else subclasses


def lazy_stack():
    """Return a generator of records for the stack above the caller's frame.

    Equivalent to inspect.stack() but potentially faster because it does not compute info for all
    stack frames.

    As a further optimization, yields raw frame objects instead of tuples describing the frame. To
    get the full information, call inspect.getframeinfo(frame).

    """
    frame = sys._getframe(1)

    while frame:
        yield frame
        frame = frame.f_back