import inspect

from parallels.core.utils.yaml_utils import write_yaml, read_yaml

_default_type_checker = None
_default_type_report = None


def get_default_type_checker():
    """Get default global instance of TypeChecker"""
    global _default_type_checker
    if _default_type_checker is None:
        _default_type_checker = TypeChecker()
    return _default_type_checker


def get_default_type_report():
    """Get default global instance of TypeCheckerReport"""
    global _default_type_report
    if _default_type_report is None:
        _default_type_report = TypeCheckerReport(
            type_checker=get_default_type_checker()
        )
    return _default_type_report


class TypeChecker(object):
    def __init__(self):
        self._modules_with_decorator = ['parallels.core.utils.type_checker.TypeChecker']
        self._functions_with_decorator = []

        self._arg_types_by_functions = {}
        self._arg_types_error_by_functions = {}
        self._return_types_by_functions = {}
        self._return_types_error_by_functions = {}

    def _check_types(self, func):
        def my_decorator(*args, **kwargs):
            arg_values = {}
            for key, val in inspect.getcallargs(func, *args, **kwargs).items():
                if key not in ['self', 'cls']:
                    arg_values[key] = self._get_item_type(val)

            function_id = (inspect.getsourcefile(func), inspect.getsourcelines(func)[1], func.__name__)
            if len(arg_values) != 0:
                if function_id in self._arg_types_error_by_functions:
                    if arg_values not in self._arg_types_error_by_functions[function_id]:
                        self._arg_types_error_by_functions[function_id].append(arg_values)
                elif function_id not in self._arg_types_by_functions:
                    self._arg_types_by_functions[function_id] = arg_values
                else:
                    new_arg_values = {}
                    old_arg_values = self._arg_types_by_functions[function_id]
                    for i in old_arg_values:
                        if self._is_equal_type(arg_values[i], old_arg_values[i]):
                            new_arg_values[i] = self._get_common_type(arg_values[i], old_arg_values[i])
                        else:
                            if function_id not in self._arg_types_error_by_functions:
                                self._arg_types_error_by_functions[function_id] = [arg_values, old_arg_values]

                            del self._arg_types_by_functions[function_id]
                            return self._get_return_types(func(*args, **kwargs), function_id)

                    self._arg_types_by_functions[function_id] = new_arg_values

            return self._get_return_types(func(*args, **kwargs), function_id)

        return my_decorator

    def _get_return_types(self, result, function_id):
        return_type = self._get_item_type(result)

        if function_id in self._return_types_error_by_functions:
            if return_type not in self._return_types_error_by_functions[function_id] and return_type != 'NoneType':
                self._return_types_error_by_functions[function_id].append(return_type)
        elif function_id not in self._return_types_by_functions:
            self._return_types_by_functions[function_id] = return_type
        else:
            if self._is_equal_type(return_type, self._return_types_by_functions[function_id]):
                new_type = self._get_common_type(return_type, self._return_types_by_functions[function_id])
            else:
                if function_id not in self._return_types_error_by_functions:
                    self._return_types_error_by_functions[function_id] = [
                        return_type, self._return_types_by_functions[function_id]
                    ]

                del self._return_types_by_functions[function_id]
                return result
    
            self._return_types_by_functions[function_id] = new_type
        return result

    @classmethod
    def _get_base_class(cls, item):
        if item.__base__.__name__ in ['object', 'tuple']:
            module = ''
            if '__module__' in dir(item):
                module = item.__module__ + '.'
            return module + item.__name__
        else:
            return cls._get_base_class(item.__base__)

    @classmethod
    def _get_item_type(cls, item):
        if '__class__' not in dir(item):
            module = ''
            if '__module__' in dir(item):
                module = item.__module__ + '.'
            return module + item.__class__.__name__
        else:
            return cls._get_base_class(item.__class__)

    @staticmethod
    def _is_equal_type(type1, type2):
        if type1 == 'NoneType' or type2 == 'NoneType':
            return True
        return type1 == type2

    @staticmethod
    def _get_common_type(type1, type2):
        if type1 == 'NoneType':
            return type2
        else:
            return type1

    def add_decorator_to_functions(self):
        import parallels
        self._add_decorator_to_functions(parallels)

    def _add_decorator_to_functions(self, item):
        for value in item.__dict__.values():
            mod = inspect.getmodule(value)
            if mod is not None:
                name = mod.__name__
                if name.startswith('parallels'):
                    if inspect.isfunction(value) and value.__name__ not in self._functions_with_decorator:
                        self._functions_with_decorator.append(value.__name__)
                        if hasattr(value, 'contextmanager'):
                            print(value)
                            delattr(value, 'contextmanager')
                        setattr(item, value.__name__, self._check_types(value))
                    if name not in self._modules_with_decorator:
                        self._modules_with_decorator.append(name)
                        self._add_decorator_to_functions(value)

    def get_arg_types_by_functions(self):
        return self._arg_types_by_functions

    def get_arg_types_error_by_functions(self):
        return self._arg_types_error_by_functions

    def get_return_types_by_functions(self):
        return self._return_types_by_functions

    def get_return_types_error_by_functions(self):
        return self._return_types_error_by_functions

    def set_arg_types_by_functions(self, _arg_types_by_functions):
        self._arg_types_by_functions = _arg_types_by_functions

    def set_arg_types_error_by_functions(self, _arg_types_error_by_functions):
        self._arg_types_error_by_functions = _arg_types_error_by_functions

    def set_return_types_by_functions(self, _return_types_by_functions):
        self._return_types_by_functions = _return_types_by_functions

    def set_return_types_error_by_functions(self, _return_types_error_by_functions):
        self._return_types_error_by_functions = _return_types_error_by_functions


class TypeCheckerReport(object):
    def __init__(self, type_checker):
        self._type_checker = type_checker
        self._path_to_arg_types = None
        self._path_to_arg_types_error = None
        self._path_to_return_type = None
        self._path_to_return_type_error = None

    def set_path_to_arg_types(self, report_filename):
        self._path_to_arg_types = report_filename

    def set_path_to_arg_types_error(self, report_filename):
        self._path_to_arg_types_error = report_filename

    def set_path_to_return_type(self, report_filename):
        self._path_to_return_type = report_filename

    def set_path_to_return_type_error(self, report_filename):
        self._path_to_return_type_error = report_filename

    def get_reports(self):
        if self._path_to_arg_types is not None:
            self._type_checker.set_arg_types_by_functions(read_yaml(self._path_to_arg_types))

        if self._path_to_arg_types_error is not None:
            self._type_checker.set_arg_types_error_by_functions(read_yaml(self._path_to_arg_types_error))

        if self._path_to_return_type is not None:
            self._type_checker.set_return_types_by_functions(read_yaml(self._path_to_return_type))

        if self._path_to_return_type_error is not None:
            self._type_checker.set_return_types_error_by_functions(read_yaml(self._path_to_return_type_error))

    def save_reports(self):
        if self._path_to_arg_types is not None:
            write_yaml(
                self._path_to_arg_types,
                self._type_checker.get_arg_types_by_functions()
            )

        if self._path_to_arg_types_error is not None:
            write_yaml(
                self._path_to_arg_types_error,
                self._type_checker.get_arg_types_error_by_functions()
            )

        if self._path_to_return_type is not None:
            write_yaml(
                self._path_to_return_type,
                self._type_checker.get_return_types_by_functions()
            )

        if self._path_to_return_type_error is not None:
            write_yaml(
                self._path_to_return_type_error,
                self._type_checker.get_return_types_error_by_functions()
            )


def configure_report_locations(type_checker_report, server):
    type_checker_report.set_path_to_arg_types(
        server.get_session_file_path(
            'arg-types-data.yaml'
        )
    )
    type_checker_report.set_path_to_arg_types_error(
        server.get_session_file_path(
            'arg-types-error-data.yaml'
        )
    )
    type_checker_report.set_path_to_return_type(
        server.get_session_file_path(
            'return-type-data.yaml'
        )
    )
    type_checker_report.set_path_to_return_type_error(
        server.get_session_file_path(
            'return-type-error-data.yaml'
        )
    )
