'''
License: Apache 2.0
Author: Yuli Fitterman
'''
# noinspection PyBroadException
import types

from _pydevd_bundle.pydevd_constants import IS_JYTHON, IS_PY3K, IS_PY311_OR_GREATER

try:
    import inspect
except:
    try:
        from _pydev_imps import _pydev_inspect as inspect
    except:
        import traceback;

        traceback.print_exc()  # Ok, no inspect available (search will not work)from _pydevd_bundle.pydevd_constants import IS_JYTHON, IS_PY3K

from _pydev_bundle._pydev_imports_tipper import signature_from_docstring


def is_bound_method(obj):
    if isinstance(obj, types.MethodType):
        return getattr(obj, '__self__', getattr(obj, 'im_self', None)) is not None
    else:
        return False


def get_class_name(instance):
    return getattr(getattr(instance, "__class__", None), "__name__", None)


def get_bound_class_name(obj):
    my_self = getattr(obj, '__self__', getattr(obj, 'im_self', None))
    if my_self is None:
        return None
    return get_class_name(my_self)


def getargspec_py2(obj):
    doc_str = obj.__doc__
    args_str = ''

    args = []
    varargs = None
    keywords = None
    defaults = []

    if doc_str:
        lines = doc_str.split('\n')
        if lines:
            func_descr = lines[0]
            s = func_descr.replace(obj.__name__, '')
            idx1 = s.find('(')
            idx2 = s.find(')', idx1)
            if idx1 != -1 and idx2 != -1 and (idx2 > idx1 + 1):
                args_str = s[idx1+1 : idx2]

    if args_str == '':
        return inspect.ArgSpec(args=args, varargs=varargs, keywords=keywords, defaults=tuple(defaults))

    args_list = args_str.split(",")
    for arg in args_list:
        arg = arg.replace(" ", "")
        idx = arg.find("=")

        if idx != -1:
            default = arg[idx+1 :]
            defaults.append(default)
            arg = arg[:idx]

        if arg.startswith("**"):
            keywords = arg
        elif arg.startswith("*"):
            varargs = arg
        else:
            args.append(arg)

    return inspect.ArgSpec(args=args, varargs=varargs, keywords=keywords, defaults=tuple(defaults))


def get_description(obj):
    try:
        ob_call = obj.__call__
    except:
        ob_call = None

    is_init_func = False
    if isinstance(obj, type) or type(obj).__name__ == 'classobj':
        fob = getattr(obj, '__init__', lambda: None)
        is_init_func = True
        if not callable(fob):
            fob = obj
    elif is_bound_method(ob_call):
        fob = ob_call
    else:
        fob = obj

    argspec = ""
    fn_class = None
    if callable(fob):
        try:
            if IS_PY311_OR_GREATER:
                spec_info = inspect.signature(fob)
                argspec = str(spec_info)
            else:
                if IS_PY3K:
                    spec_info = inspect.getfullargspec(fob)
                else:
                    spec_info = inspect.getargspec(fob)
                argspec = inspect.formatargspec(*spec_info)
        except TypeError:
            if is_init_func:
                spec_info = getargspec_py2(obj)
            else:
                spec_info = getargspec_py2(fob)
            argspec = inspect.formatargspec(*spec_info)
        except ValueError:
            # function/method defined in C for example str.count
            argspec = None

        fn_name = getattr(fob, '__name__', None)
        if isinstance(obj, type) or type(obj).__name__ == 'classobj':
            fn_name = "__init__"
            fn_class = getattr(obj, "__name__", "UnknownClass")
        elif is_bound_method(obj) or is_bound_method(ob_call):
            fn_class = get_bound_class_name(obj) or "UnknownClass"

    else:
        fn_name = getattr(fob, '__name__', None)
        fn_self = getattr(fob, '__self__', None)
        if fn_self is not None and not isinstance(fn_self, types.ModuleType):
            fn_class = get_class_name(fn_self)

    doc_string = get_docstring(ob_call) if is_bound_method(ob_call) else get_docstring(obj)
    return create_method_stub(fn_name, fn_class, argspec, doc_string)


def create_method_stub(fn_name, fn_class, argspec, doc_string):
    if fn_name and argspec:
        doc_string = "" if doc_string is None else doc_string
        fn_stub = create_function_stub(fn_name, argspec, doc_string, indent=1 if fn_class else 0)
        if fn_class:
            expr = fn_class if fn_name == '__init__' else fn_class + '().' + fn_name
            return create_class_stub(fn_class, fn_stub) + "\n" + expr
        else:
            expr = fn_name
            return fn_stub + "\n" + expr
    elif doc_string:
        if fn_name:
            restored_signature, _ = signature_from_docstring(doc_string, fn_name)
            if restored_signature:
                return create_method_stub(fn_name, fn_class, restored_signature, doc_string)
        return create_function_stub('unknown', '(*args, **kwargs)', doc_string) + '\nunknown'

    else:
        return ''


def get_docstring(obj):
    if obj is not None:
        try:
            if IS_JYTHON:
                # Jython
                doc = obj.__doc__
                if doc is not None:
                    return doc

                from _pydev_bundle import _pydev_jy_imports_tipper

                is_method, infos = _pydev_jy_imports_tipper.ismethod(obj)
                ret = ''
                if is_method:
                    for info in infos:
                        ret += info.get_as_doc()
                    return ret

            else:

                doc = inspect.getdoc(obj)
                if doc is not None:
                    return doc
        except:
            pass
    else:
        return ''
    try:
        # if no attempt succeeded, try to return repr()...
        return repr(obj)
    except:
        try:
            # otherwise the class
            return str(obj.__class__)
        except:
            # if all fails, go to an empty string
            return ''


def create_class_stub(class_name, contents):
    return "class %s(object):\n%s" % (class_name, contents)


def create_function_stub(fn_name, fn_argspec, fn_docstring, indent=0):
    def shift_right(string, prefix):
        return ''.join(prefix + line for line in string.splitlines(True))

    fn_docstring = shift_right(inspect.cleandoc(fn_docstring), "  " * (indent + 1))
    ret = '''
def %s%s:
    """%s"""
    pass
''' % (fn_name, fn_argspec, fn_docstring)
    ret = ret[1:]  # remove first /n
    ret = ret.replace('\t', "  ")
    if indent:
        prefix = "  " * indent
        ret = shift_right(ret, prefix)
    return ret
