"""	Core components of the reactor """

import select

from aleph.utils.inheritdict import InheritDict
from aleph.utils.loader import EntryPointLoader
from aleph.utils.log import getlog
		
class BaseReactor (object):
	""" Watches file descriptors and manages the handlers associated with them. """
	
	def __init__ (self, callback=None):
		"""	Creates a polling object and a dictionary of handlers. """
		self.log = getlog(self)
		self.fd = dict()           #: A dictionary of handlers ``{fd: server}``
		self.poll = select.poll()  #: The polling object
		if callback is not None:
			self.set_callback(callback)
	
	def set_callback (self, callback):
		"""	The function to call with each event """
		self.callback = callback
	
	def connect (self, handler):
		"""
		Start listening for input
		
		:Parameters:
			- `handler` (:py:class:`~aleph.interfaces.Handler`): The handler to start watching
		"""
		try:
			handler.on_connect()
		except Exception as e:
			handler.on_connect_fail(exception=e)
		else:
			handler.on_connect_complete()
			self.fd[handler.fileno()] = handler
			self.poll.register(handler, select.POLLIN)
			self.log.info("Connected to {}:{}".format(handler.address, handler.port))
			self.callback('connect', handler.__protocol__, handler)
		
	def disconnect (self, handler):
		"""
		Stop listening for input
		
		:Parameters:
			- `handler` (EventHandler): The server to disconnect from
		"""
		self.callback('disconnect', handler.__protocol__, handler)
		self.poll.unregister(handler)
		del self.fd[handler.fileno()]
		handler.on_disconnect()
		self.log.warning("Disconnected from {}:{}".format(handler.address, handler.port))
	
	#: 60 second timeout for poll()
	_timeout = 1000 * 60
	
	#: Error indicating a socket has disconnected
	_error = (select.POLLERR | select.POLLHUP | select.POLLNVAL)
	
	def tick (self):
		"""	Check each file descriptor and notify the associated handler is required. """
		events = self.poll.poll(self._timeout)
		for fd, event in events:
			handler = self.fd[fd]
			if event & self._error:
				# Disconnect servers that  have a broken connection
				self.disconnect(handler)
			elif event & select.POLLIN:
				for event in handler.on_input(reactor=self):
					# Do not yield events that have returned None
					if not event[2] == None:
						self.callback(*event)

class Reactor (BaseReactor):
	""" Manages aleph's protocols and connections on top of the BaseReactor """
	def __init__ (self, config, connections, callback=None):
		super(Reactor, self).__init__(callback)
		self.config = config
		self.protocol_loader = EntryPointLoader('aleph.protocols', self.log)
		self.connections = [self.create_connection(c) for c in connections]
	
	def create_connection (self, c):
		"""	Creates a connection from it's configuration dictionary """
		Protocol = self.protocol_loader.load_entry_point(c.pop('protocol'))
		return Protocol(InheritDict(c, parent=self.config))
	
	def __enter__ (self):
		""" Connect to all handlers """
		for handler in self.connections:
			self.connect(handler)
	
	def loop (self):
		with self:
			while self.fd:
				try:
					self.tick()
				except KeyboardInterrupt:
					print
					break
	
	def __exit__ (self, *e):
		"""	Disconnect all connected handlers """
		for handler in self.fd.values():
			self.disconnect(handler)
