import socket
from Message import Out as msg_out
from Message import In as msg_in
from Logger import logger

from time_service import TimeService
from clock import GameClock
import services
from sims.sim import Sim
from distributor import ops
import camera

from struct import unpack
from time import sleep, time
from random import choice

SIGNATURE		= b"RTBP"
VERSION_MAJOR	= 0
VERSION_MINOR	= 2

#GREETING_STRING = "Reading you loud and clear! (Communication established between The Sims 4 and RTBP)"

GREETINGS_STRING_PREFIXES = \
[
	"Reading you loud and clear.",
	"Hailing frequencies open.",
	"We're in the pipe, five by five.",
	"Giddyup!"
]

GREETINGS_STRING_SUFFIX = "Communication established between The Sims 4 and RTBP"

CONNECTION_ATTEMPTS			= 10
CONNECTION_FAILURE_TIMEOUT	= 10
BUFFER_SIZE					= 1024

_hooked = set()

def addClassMethodPreHook(Cls,method):
	global _hooked
	
	key = (Cls,method)
	
	if key in _hooked:
		return method
	
	def decorator(hook):
		def call(*args,**kargs):
			hook(*args,**kargs)
			
			return method(*args,**kargs)
		
		setattr(Cls,method.__name__,call)
		
		return hook
		
	_hooked.add(key)
	
	return decorator

def addClassMethodPostHook(Cls,method):
	global _hooked
	
	key = (Cls,method)
	
	if key in _hooked:
		return method
	
	def decorator(hook):
		def call(*args,**kargs):
			ret = method(*args,**kargs)
			
			hook(*args,**kargs)
			
			return ret
		
		setattr(Cls,method.__name__,call)
		
		return hook
		
	_hooked.add(key)
	
	return decorator

class SimulationClient(object):
	def __init__(self):
		self._socket		= None
		self._currentSpeed	= None
	
	def connect(self,host,port):
		for i in range(CONNECTION_ATTEMPTS):
			try:
				self._socket = socket.create_connection((host,port))
				
				break
				
			except Exception as error:
				self._socket = None
				
				logger.error("Unable to connect to RTBP's simulation server: {}".format(error))
				
				if i<CONNECTION_ATTEMPTS:
					logger.info("Attempt {}/{}. Retrying in {} seconds.".format((i+1),CONNECTION_ATTEMPTS,CONNECTION_FAILURE_TIMEOUT))
					
					sleep(CONNECTION_FAILURE_TIMEOUT)
				
		else:
			return False
			
		header = self.read()
		
		(signature,versionMajor,versionMinor) = unpack("<4sBB",header)
		
		if signature!=SIGNATURE:
			raise Exception("Error communicating with RTBP simulation server: invalid protocol signature: \"{}\"".
				format(signature))
		
		if versionMajor!=VERSION_MAJOR or versionMinor!=VERSION_MINOR:
			raise Exception("Error communicating with RTBP simulation server: wrong protocol version: \"{}.{}\"".
				format(versionMajor,versionMinor))
		
		self.log("{} ({})".format(choice(GREETINGS_STRING_PREFIXES),GREETINGS_STRING_SUFFIX))
		
		@addClassMethodPreHook(GameClock,GameClock._sync_clock_and_broadcast_gameclock)
		def onSetClockSpeed(clock,*args,**kargs):
			speed = clock.clock_speed
			
			if self._currentSpeed==speed:
				return
			
			self._currentSpeed = speed
			
			self.setClockSpeed(self._currentSpeed)
			
		#@addClassMethodHook(GameClock,GameClock.request_pause)
		#def onRequestPause(self,source,*args,**kargs):
			#global _client
			
			#_client.setClockSpeed(0)
			
		@addClassMethodPreHook(Sim,Sim.on_add)
		def onSimAdded(sim,*args,**kargs):
			# try:
			logger.info("Sim added: {}, \"{}\", {}, {}".format(sim.sim_id,sim.full_name,sim.transform.translation,
				sim.transform.orientation))
		
			self.onSimAdded(sim.sim_id,sim.full_name,sim.transform.translation,sim.transform.orientation)
				
			# except Exception as exception:
			# 	logger.error("{}: {}".format(exception.__class__.__name__,str(exception)))
			
		@addClassMethodPreHook(Sim,Sim.on_remove)
		def onSimRemove(sim,*args,**kargs):
			self.onSimRemoved(sim.sim_id)
			
		@addClassMethodPreHook(Sim,Sim.on_outfit_changed)
		def onSimOutfitChanged(sim,category_and_index,*args,**kargs):
			#_client.log("Sim Outfit Changed: {}".format(self.sim_info.get_current_outfit())) # <- Works.
			#_client.log("Sim Outfit Changed: {}".format(self.sim_info)) # <- Works.
			
			self.onSimOutfitChanged(sim.sim_id)
			
			# for i in self.sim_info.get_part_ids_for_current_outfit():
			# 	_client.log("Part ID: {}".format(i))
			
		# @addClassMethodPreHook(camera,camera.update) #This will work for a module, too.
		# def onCameraUpdate(sim_id,target_position,camera_position,follow_mode):
		# 	self.sendCameraPosition(camera_position)
		
		@addClassMethodPreHook(TimeService,TimeService.update)
		def onTimeServiceUpdate(service,*args,**kargs):
			self.update()
			
		return True
		
	def close(self):
		self._socket.close()
			
	def send(self,data):
		self._socket.send(bytes(data))
		
	def sendString(self,string):
		self.send(string.encode("ascii"))
		
	def read(self):
		return self._socket.recv(BUFFER_SIZE)
		
		# try:
			# buffer = self._socket.recv(BUFFER_SIZE,socket.MSG_DONTWAIT)
		# except BlockingIOError:
		# 	pass
		
	def update(self):
		try:
			self._socket.setblocking(False)
			
			message = msg_in.parse(self._socket)
			
			if type(message)==msg_in.RequestCameraPosition:
				logger.info("Request camera position message received")
				
				if camera._camera_position:
					self.sendCameraPosition(camera._camera_position)
				else:
					logger.error("Camera position is null!")
				
			else:
				logger.error("Unknown message type")
			
		except BlockingIOError:
			pass
			
		self._socket.setblocking(True)
	
	def log(self,string):
		self.send(msg_out.Log(string))
		
	def setClockSpeed(self,speed):
		self.send(msg_out.SetSpeed(speed))
		
	def onSimAdded(self,id,name,translation,rotation):
		self.send(msg_out.SimAdded(id,name,translation,rotation))
		
	def onSimRemoved(self,id):
		self.send(msg_out.SimRemoved(id))
		
	def onSimOutfitChanged(self,id):
		self.send(msg_out.SimOutfitChanged(id))
		
	def sendCameraPosition(self,position):
		self.send(msg_out.CameraPosition(position))
	
