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

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

Inspections performable on Python objects.

:copyright: (c) 2021 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",
    "%": "mod",
    "**": "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)
