# -*- coding: utf-8 -*-
"""
UTc! module: src.networking.communication

Purpose
=======
 Provides routines for synchronously and asynchronously communicating with a
 UTc! server.
 
Legal
=====
 All code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv3, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import select
import socket
import threading
import time

_PACKET_SIZE = 8192 #: The maximum amount of data to try to read from the server.
_TIMEOUT = 10.0 #: The number of seconds to wait before considering the connection broken.

class Socket(threading.Thread):
	"""
	A connection to a UTc! server.
	"""
	_alive = True #: False when this thread should die.
	_data_box = None #: The container that will store read data.
	_game_variables = None #: A data structure used to share variables between this thread and game-space.
	_lock = None #: A lock used to prevent queue access violations.
	_queue = None #: The list of messages waiting to be sent.
	_server = None #: The socket that will be used to communicate with the server.
	
	def __init__(self, host, port, data_box, game_variables):
		"""
		Initialises the variables required to maintain this connection.
		
		@type host: basestring
		@param host: The address of the host to which a connection will be
		    established.
		@type port: int
		@param post: The port on which the host is running a UTc! server.
		@type data_box: L{DataBox}
		@param data_box: The DataBox into which all read data will be placed.
		@type game_variables: L{game.GameVariables}
		@param game_variables: A primitive data container that provides references
		    to data that needs to be shared between this thread and the gameplay
		    thread.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		self._queue = []
		self._lock = threading.Lock()
		
		self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self._server.connect((host, port))
		self._data_box = data_box
		self._game_variables = game_variables
		
	def forceData(self, data):
		"""
		Sends data to the server.
		
		@type data: str
		@param data: The line of data to be sent, not newline-terminated.
		
		@raise L{ConnectionError}: If the connection to the server was
		    interrupted.
		"""
		try:
			self._server.sendall(data + "\n")
		except:
			raise ConnectionError("Server disconnected")
			
	def kill(self):
		"""
		Indicates that this thread should terminate gracefully.
		"""
		self._alive = False
		
	def readData(self, block=False):
		"""
		Reads data from the connection, storing anything received in
		C{self._data_box}.
		
		@type block: bool
		@param block: If True, this function will raise a TimeoutError if
		    C{_TIMEOUT} seconds pass without any data being received. If False,
		    then this function returns immediately, regardless of whether data
		    was received or not.
		
		@raise L{ConnectionError}: If the connection to the server was
		    interrupted.
		@raise L{TimeoutError}: If no data was received.
		"""
		timeout_counter = 0.0
		
		server = self._server
		data_box = self._data_box
		while True:
			(input_events, output_events, error_events) = select.select([server], [], [server], 0.01)
			if server in input_events:
				try:
					data = server.recv(_PACKET_SIZE)
					if len(data) == 0:
						raise ConnectionError("Server disconnected")
					data_box.feedData(data)
					if data_box.hasLines():
						return
				except:
					raise ConnectionError("Server disconnected")
			elif server in error_events:
				raise ConnectionError("Server disconnected")
			else:
				if not block or data_box.hasLines():
					return
				if timeout_counter >= _TIMEOUT:
					raise TimeoutError("Timed out while waiting for data")
				timeout_counter += 0.05
				time.sleep(0.05)
				
	def run(self):
		"""
		Continually sends data describing the state of this client's ship and any
		missiles it controls to the server, as well as any event data that has
		been queued.
		"""
		lock = self._lock
		server = self._server
		queue = self._queue
		player = self._game_variables.player
		player_name = self._game_variables.player_name
		hostile_missiles = self._game_variables.hostile_missiles
		game_lock = self._game_variables.lock
		throttle = self._game_variables.throttle
		
		try:
			while self._alive:
				data = None
				
				game_lock.acquire()
				if player.isAlive():
					current_time = self._game_variables.current_time
					data = '\n'.join([player.describe(current_time)] + [missile.describe(current_time) for missile in hostile_missiles]) + '\n'
				game_lock.release()
				
				lock.acquire()
				if queue:
					queue_string = '\n'.join(queue) + '\n'
					if data:
						data += queue_string
					else:
						data = queue_string
					self._queue = queue = []
				lock.release()
				
				if data:
					try:
						server.sendall(data)
					except:
						break
				time.sleep(throttle)
		except:
			pass
			
		try:
			server.shutdown(socket.SHUT_RDWR)
		except:
			pass
		try:
			server.close()
		except:
			pass
			
	def sendData(self, data):
		"""
		Queues data to be sent to the server.
		
		@type data: str
		@param data: The line of data to be sent, not newline-terminated.
		"""
		self._lock.acquire()
		self._queue.append(data)
		self._lock.release()
		
		
class Error(Exception):
	"""
	The base exception from which all exceptions native to this module inherit.
	"""
	def __init__(self, message):
		"""
		Sets the message for a generic exception.
		"""
		Exception.__init__(self, message)
		
class ConnectionError(Error):
	"""
	Indicates that a problem occurred while communicating with the server.
	"""
	pass
	
class TimeoutError(Error):
	"""
	Indicates that the server failed to provide data within the expected
	timeframe.
	"""
	pass
	