"""	Event dispatcher """

from traceback import format_exc
from collections import defaultdict, OrderedDict

from aleph.utils.log import getlog
from aleph.utils.loader import EntryPointLoader

class BaseDispatcher (object):
	"""
	Dispatch events to listeners based on the tag and protocol
	
	For each event the dispatcher is called with, each function in `listeners`
	under the key ``(tag, protocol)`` or ``(tag, None)`` is called with the
	event object.
	"""
	
	def __init__ (self, listeners=None):
		self.log = getlog(self)
		self.listeners = listeners
	
	def __call__ (self, tag, protocol, event):
		"""	Dispatches events to the listeners """
		for key in [(tag, protocol), tag]:
			for function in self.listeners.get(key, ()):
				try:
					function(event)
				except Exception as exception:
					self.log_exception(function, exception)
	
	def log_exception (self, function, exception):
		""" Log the error, and send a reply if possible """
		kwargs = {
			'message': exception,
			'e_type': exception.__class__.__name__,
			'function': function.__name__,
			'trace': format_exc()[:-1]
		}
		
		self.log.warning("Function '{function}' failed [{e_type}]\n{trace}".format(**kwargs))
		try:
			event.message_reply("An error occured [{e_type}: {message}]".format(**kwargs))
		except Exception:
			pass

class Dispatcher (BaseDispatcher):
	"""
	Loads modules, resolving their requirements, in order to create
	listeners for the BaseDispatcher.
	"""
	
	def __init__ (self, module_names):
		""" Load modules and hand them to the dispatcher """
		super(Dispatcher, self).__init__()
		
		self.module_loader = EntryPointLoader('aleph.modules', log=self.log)

		# Load the initial set of modules
		self.module_loader.load(*module_names)
		
		# Resolve the dependencies of each module
		self.modules = OrderedDict()
		for name in module_names:
			self.resolve(name, list())
	
		self.log.info("Using modules: %s", ', '.join(self.modules.keys()))
		
		self.listeners = self.load_listeners()
		
	def resolve (self, name, unresolved):
		"""
		Resolve a modules requirements
		
		- The module is placed in `unresolved` during resolution.
		- If the module has a requirements list, for each requirement:
			- Load it if not loaded
			- If it has not already been resolved, resolve it
		- Once the module is resolved it is placed in `resolved`
		"""
		module = self.module_loader[name]
		unresolved.append(name)
		if hasattr(module, 'requirements') and bool(module.requirements):
			for requirement_name in module.requirements:
				# Ensure the module is loaded
				self.module_loader.load(requirement_name)
				# Resolve the required module if it has not already been resolved
				if requirement_name not in self.modules.keys():
					requirement = self.module_loader[requirement_name]
					if requirement_name in unresolved:
						raise Exception('Circular reference detected: {name} -> {requirement}'.format(name, requirement=requirement.__name__))
					else:
						self.resolve(requirement_name, unresolved)
		unresolved.remove(name)
		self.modules[name] = module
		
	def load_listeners (self):
		""" Creates a dictionary containing event types and the listeners attached to them """
		listeners = defaultdict(list)
		for module in self.modules.values():
			for event_type, function in module:
				listeners[event_type].append(function)
		return listeners
