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

"""
jishaku.repl.inspections
~~~~~~~~~~~~~~~~~~~~~~~~~

Inspections performable on Python objects.

:copyright: (c) 2019 Devon (Gorialis) R
:license: MIT, see LICENSE for more details.

"""

import collections
import functools
import inspect
import os

INSPECTIONS = []
MethodWrapperType = type((1).__le__)
WrapperDescriptorType = type(int.__le__)


def add_inspection(name):
    """
    Add a Jishaku object inspection
    """

    # create the real decorator
    def inspection_inner(func):
        """
        Jishaku inspection decorator
        """

        # pylint: disable=inconsistent-return-statements

        # create an encapsulated version of the inspection that swallows exceptions
        @functools.wraps(func)
        def encapsulated(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except (TypeError, AttributeError, ValueError, OSError):
                return

        INSPECTIONS.append((name, encapsulated))
        return func
    return inspection_inner


def all_inspections(obj):
    """
    Generator to iterate all current Jishaku inspections.
    """

    for name, callback in INSPECTIONS:
        result = callback(obj)
        if result:
            yield name, result


def class_name(obj):
    """
    Get the name of an object, including the module name if available.
    """

    name = obj.__name__
    module = getattr(obj, '__module__')

    if module:
        name = f'{module}.{name}'
    return name


# pylint: disable=missing-docstring
# pylint: disable=inconsistent-return-statements


@add_inspection("Type")
def type_inspection(obj):
    return type(obj).__name__


@add_inspection("Object ID")
def id_inspection(obj):
    return hex(id(obj))


@add_inspection("Length")
def len_inspection(obj):
    return len(obj)


@add_inspection("MRO")
def mro_inspection(obj):
    if not inspect.isclass(obj):
        return

    return ', '.join(class_name(x) for x in inspect.getmro(obj))


@add_inspection("Type MRO")
def type_mro_inspection(obj):
    obj_type = type(obj)
    if obj_type in (type, object):
        return

    return ', '.join(class_name(x) for x in inspect.getmro(obj_type))


@add_inspection("Subclasses")
def subclass_inspection(obj):
    if isinstance(obj, type) and hasattr(obj, "__subclasses__"):
        subclasses = type.__subclasses__(obj)
    else:
        return

    output = ', '.join(class_name(x) for x in subclasses[0:5])

    if len(subclasses) > 5:
        output += ', ...'

    return output


@add_inspection("Module Name")
def module_inspection(obj):
    return inspect.getmodule(obj).__name__


@add_inspection("File Location")
def file_loc_inspection(obj):
    file_loc = inspect.getfile(obj)
    cwd = os.getcwd()
    if file_loc.startswith(cwd):
        file_loc = "." + file_loc[len(cwd):]
    return file_loc


@add_inspection("Line Span")
def line_span_inspection(obj):
    source_lines, source_offset = inspect.getsourcelines(obj)
    return f"{source_offset}-{source_offset + len(source_lines)}"


@add_inspection("Signature")
def sig_inspection(obj):
    return inspect.signature(obj)


@add_inspection("Content Types")
def content_type_inspection(obj):
    if not isinstance(obj, (tuple, list, set)):
        return

    total = len(obj)
    types = collections.Counter(type(x) for x in obj)

    output = ', '.join(f'{x.__name__} ({y*100/total:.1f}\uFF05)' for x, y in types.most_common(3))
    if len(types) > 3:
        output += ', ...'

    return output


POSSIBLE_OPS = {
    '<': 'lt',
    '<=': 'le',
    '==': 'eq',
    '!=': 'ne',
    '>': 'gt',
    '>=': 'ge',
    '+': 'add',
    '-': 'sub',
    '*': 'mul',
    '@': 'matmul',
    '/': 'truediv',
    '//': 'floordiv',
    '\uFF05': 'mod',  # fake percent to avoid prolog comment
    '**': 'pow',
    '<<': 'lshift',
    '>>': 'rshift',
    '&': 'and',
    '^': 'xor',
    '|': 'or'
}


def check_not_slot(obj, attr):
    """
    Check that a given attribute isn't just an open slot for subclasses
    """

    if isinstance(getattr(obj, attr, None), MethodWrapperType):
        return not isinstance(getattr(type(obj), attr, None), WrapperDescriptorType)

    return not isinstance(getattr(obj, attr, None), WrapperDescriptorType)


@add_inspection("Operations")
def compat_operation_inspection(obj):
    this_dict = dir(obj)
    operations = []

    for operation, member in POSSIBLE_OPS.items():
        if f'__{member}__' in this_dict and check_not_slot(obj, f'__{member}__'):
            operations.append(operation)
        elif f'__r{member}__' in this_dict and check_not_slot(obj, f'r__{member}__'):
            operations.append(operation)

        if f'__i{member}__' in this_dict and check_not_slot(obj, f'i__{member}__'):
            operations.append(f'{operation}=')

    return ' '.join(operations)
