#!/usr/bin/env python3


"""
This file is part of the Tetris project.

Written 2023 by Maarten Tromp <maarten@geekabit.nl>


Website
-------
https://www.geekabit.nl/projects/tetris-window/


License
-------
CC0 (no copyright, public domain)
The person who associated a work with this deed has dedicated the work to the public domain by
waiving all of his or her rights to the work worldwide under copyright law, including all related
and neighbouring rights, to the extent allowed by law. You can copy, modify, distribute and perform
the work, even for commercial purposes, all without asking permission.
https://creativecommons.org/share-your-work/public-domain/cc0/
"""


# imports
import sys
import signal
import logging
import asyncio
from collections import deque
import websockets


# project imports
import config
from screen import Screen
from player import Player
from game_manager import GameManager


# init
logging.basicConfig(format = "%(asctime)s %(levelname)s %(message)s", level = logging.INFO)


# constants
SCREEN = Screen() # Screen object.
DISPLAYS = set() # List of all connected displays.
WAITING_PLAYERS = deque() # Queue of connected anc configures players that are ready to play.
OUTBOX = deque() # Queue of messages waiting to be sent.
GM = GameManager(SCREEN, WAITING_PLAYERS)


async def init():
	"""
	Initialize backend, start websocket servers and game manager.

	Parameters
	----------
	none

	Returns
	-------
	none
	This function will never return since the game manager never finishes.
	"""


	logging.debug("backend init")
	signal.signal(signal.SIGINT, handle_sigint)

	logging.debug("starting controller server")
	async with websockets.serve(register_player, port = config.CONTROLLER_PORT):
			#ping_interval = 1, ping_timeout = 1):
		logging.debug("starting display server")
		async with websockets.serve(register_display, port = config.DISPLAY_PORT):
			await main()


async def main():
	"""
	Main loop. This funciton update game manager and displays.

	Parameters
	----------
	none

	Returns
	-------
	none
		This function will never return since it's an endless loop.
	"""

	while True:
		# update display
		if SCREEN.dirty:
			websockets.broadcast(DISPLAYS, SCREEN.dump())

		# send messages
		if OUTBOX:
			await send_messages()

		# wait for next frame
		await asyncio.sleep(.1)

		GM.update()


async def register_player(websocket):
	"""
	This function is run when a player connects to the controller websocket.
	Add to / remove from list of players and handle incoming messages.

	Parameters
	----------
	websocket: websocket object

	Returns
	-------
	none
	"""

	player = Player(websocket, OUTBOX)
	try:
		async for message in websocket:
			player.handle_message(message)
			if player.state == "ready":
				GM.queue_player(player)
	except websockets.exceptions.ConnectionClosedError as exception:
		logging.debug("player %s, (%s) connection closed with error: %s", player.name, player.id,
			repr(exception))

	logging.info("player %s (%s) disconnected", player.name, player.id)
	# update player state
	player.state = "disconnected"
	if WAITING_PLAYERS.count(player):
		# remove from queue
		WAITING_PLAYERS.remove(player)
		GM.send_queue_status()


async def register_display(websocket):
	"""
	This function is run when a display connects to the display websocket.
	Add to / remove from list of connected displays and handle incoming messages.

	Parameters
	----------
	websocket: websocket object

	Returns
	-------
	none
	"""

	display_id = id(websocket)
	logging.info("display %s connected", display_id)
	DISPLAYS.add(websocket)
	await websocket.send(SCREEN.dump())
	try:
		await websocket.wait_closed()
	finally:
		logging.info("display %s disconnected", display_id)
		DISPLAYS.remove(websocket)


async def send_messages():
	"""
	Send all messages from OUTBOX.

	Parameters
	----------
	none

	Returns
	-------
	none
	"""

	while OUTBOX:
		(websocket, message) = OUTBOX.pop()
		try:
			await websocket.send(message)
		except websockets.exceptions.ConnectionClosedOK as exception:
			logging.debug("connection closed while sending message: %s", repr(exception))
		except websockets.exceptions.ConnectionClosedError as exception:
			logging.debug("connection closed with error while sending message: %s", repr(exception))


def handle_sigint(sig, frame):
	"""
	Clear screen and exit.

	Parameters
	----------
	none

	Returns
	-------
	none
	"""

	del sig, frame
	logging.debug("recieved SIGINT")

	logging.debug("clearing screen")
	SCREEN.clear()
	websockets.broadcast(DISPLAYS, SCREEN.dump())

	logging.debug("quitting")
	sys.exit(0)


if __name__ == "__main__":
	asyncio.run(init())
