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).iteritems():
				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'
		)
	)
