import asyncio
import websockets
from threading import Thread
from queue import Queue
from time import sleep
import json
from ws.connection import IConnection


class WsClient:
	def __init__(self, listener: IConnection, loop=None):
		self.listener = listener
		self.loop = loop
		self.stop = None
		self.bg_thread = None

	def bg_run(self, uri):
		print("bg_run: started")
		asyncio.set_event_loop(self.loop)
		fut = asyncio.ensure_future(self.run_client(uri), loop=self.loop)
		self.loop.run_until_complete(fut)
		self.stop = None
		print("bg_run: terminated")

	def connect(self, uri):
		if self.bg_thread is not None and self.bg_thread.is_alive():
			return

		self.stop: asyncio.Future[None] = self.loop.create_future()
		self.bg_thread = Thread(target=self.bg_run, args=(uri,))
		self.bg_thread.setDaemon(True)
		self.bg_thread.start()

	def disconnect(self):
		if self.stop is not None:
			self.loop.call_soon_threadsafe(self.stop.set_result, None)
			self.bg_thread.join()

	async def run_client(self, uri):
		try:
			websocket = await websockets.connect(uri, loop=self.loop)
		except Exception as exc:
			print(f"Failed to connect to {uri}: {exc}.")
			return

		print("handler: got connection to {}".format(websocket.remote_address))
		await self.listener.on_connect()
		try:
			path = "/"
			consumer_task = asyncio.ensure_future(self.handler_recv(websocket, path), loop=self.loop)
			producer_task = asyncio.ensure_future(self.handler_send(websocket, path), loop=self.loop)
			done, pending = await asyncio.wait([consumer_task, producer_task, self.stop], return_when=asyncio.FIRST_COMPLETED,)
			for task in pending:
				task.cancel()
		finally:
			print("handler: lost connection from {}".format(websocket.remote_address))
			await websocket.close()
			await self.listener.on_disconnect()

	async def handler_recv(self, websocket, path):
		while True:
			try:
				data = await websocket.recv()
				await self.listener.on_recv(data)
			except:
				print("handler_recv: lost connection from {}".format(websocket.remote_address))
				return

	async def handler_send(self, websocket, path):
		while True:
			data = await self.listener.on_send()
			if data is not None:
				try:
					await websocket.send(json.dumps(data))
				except:
					print("handler_send: lost connection from {}".format(websocket.remote_address))
					break


class PeriodicUpdater(IConnection):
	def __init__(self):
		self.ws_client = WsClient(self)

		self.state = Queue()
		self.thread_cancel = True
		self.thread = None

	def connect(self, uri):
		self.ws_client.connect(uri)

	def disconnect(self):
		self.ws_client.disconnect()

	def on_connect(self):
		self.thread_cancel = False
		self.thread = Thread(target=self.run)
		self.thread.setDaemon(True)
		self.thread.start()

	def on_disconnect(self):
		self.thread_cancel = True
		self.thread.join(1)

	async def on_send(self):
		return await asyncio.get_event_loop().run_in_executor(None, self.state.get)

	async def on_recv(self, data):
		print("R: {}".format(data))

	def send(self, data):
		self.state.put_nowait(data)

	def run(self):
		count = 0
		while not self.thread_cancel:
			self.send({"Value": count})
			count += 1
			sleep(0.5)


if __name__ == '__main__':

	ws = PeriodicUpdater()
	ws.connect(uri="ws://localhost:8765")

	sleep(5)
	ws.disconnect()
	sleep(1)
	ws.connect(uri="ws://localhost:8765")

	while True:
		sleep(1)