import asyncio
import websockets
import json
import queue # refer to https://docs.python.org/3/library/queue.html for cross threading communication
import uuid
from typing import Dict, List
from threading import Thread
from tetri5.util import ConfigurationManager

class MultiplayerService():
    _thread = None
    _current_game_id = None
    _client_id = str(uuid.uuid4())

    """ QUEUES """
    _receive_piece_queue = queue.Queue()
    _send_piece_queue = queue.Queue()
    _receive_stack_queue = queue.Queue()
    _send_stack_queue = queue.Queue()
    _receive_stats_queue = queue.Queue()
    _send_stats_queue = queue.Queue()
    _receive_message_queue = queue.Queue()
    _send_message_queue = queue.Queue()

    WAIT_FOR_OPPONENT = "wait_for_opponent"
    START_GAME = "start_game"

    @classmethod
    def init(cls) -> None:
        thread = Thread(target=asyncio.get_event_loop().run_until_complete,\
                        args=(_NetworkConnectionService.init(),))
        thread.start()

    """ SEND """
    @classmethod
    def send_piece(cls, piece: "PieceDto") -> None:
        cls._send_piece_queue.put(piece)

    @classmethod
    def send_stack(cls, stack: "StackDto") -> None:
        cls._send_stack_queue.put(stack)

    @classmethod
    def send_stats(cls, stats: "StatsDto") -> None:
        cls._send_stats_queue.put(stats)

    @classmethod
    def send_message(cls, message: str) -> None:
        cls._send_message_queue.put(message)

    """ RECEIVE """
    @classmethod
    def try_receive_piece(cls) -> "PieceDto":
        if cls._receive_piece_queue.empty():
            return None

        result = cls._receive_piece_queue.get()
        cls._receive_piece_queue.task_done()
        return result

    @classmethod
    def try_receive_stack(cls) -> "StackDto":
        if cls._receive_stack_queue.empty():
            return None

        result = cls._receive_stack_queue.get()
        cls._receive_stack_queue.task_done()
        return result

    @classmethod
    def try_receive_stats(cls) -> "StatsDto":
        if cls._receive_stats_queue.empty():
            return None

        result = cls._receive_stats_queue.get()
        cls._receive_stats_queue.task_done()
        return result

    @classmethod
    def try_receive_message(cls) -> str:
        if cls._receive_message_queue.empty():
            return None

        result = cls._receive_message_queue.get()
        cls._receive_message_queue.task_done()
        return result

    """ MISC """
    @classmethod
    def enter_game(cls, game_id: str) -> None:
        cls._current_game_id = game_id
        _NetworkConnectionService._join_game = True # TODO: change this to a function

    @classmethod
    def quit(cls) -> None:
        _NetworkConnectionService.close_connection()

class _NetworkConnectionService():
    _websocket = None
    _is_closed = False
    _join_game = False
    _pending_receive_task = None

    @classmethod
    async def init(cls) -> None:
        await cls._connect_to_server()
        await cls._run_network_loop()

    """ NETWORK SEND """
    @classmethod
    async def _try_enter_game(cls) -> None:
        if not cls._join_game:
            return
        
        json_message = json.dumps({"action": "enter_game",\
                                   "clientId": MultiplayerService._client_id,\
                                   "gameId": MultiplayerService._current_game_id})
        await cls._websocket.send(json_message)
        cls._join_game = False

    @classmethod
    async def _try_send_piece(cls) -> None:
        # if no messages to proccess, return
        if MultiplayerService._send_piece_queue.empty():
            return
        
        # get next piece to send and send to server
        piece = MultiplayerService._send_piece_queue.get()

        # construct json message
        json_message = json.dumps({"action": "send_piece",\
                                   "clientId": MultiplayerService._client_id,\
                                   "gameId": MultiplayerService._current_game_id,\
                                   "piece": piece.__dict__})

        await cls._websocket.send(json_message)
        MultiplayerService._send_piece_queue.task_done()

    @classmethod
    async def _try_send_stack(cls) -> None:
        # if no messages to proccess, return
        if MultiplayerService._send_stack_queue.empty():
            return
        
        # get next stack to send and send to server
        stack = MultiplayerService._send_stack_queue.get()

        # construct json message
        json_message = json.dumps({"action": "send_stack",\
                                   "clientId": MultiplayerService._client_id,\
                                   "gameId": MultiplayerService._current_game_id,\
                                   "stack": stack.__dict__})

        await cls._websocket.send(json_message)
        MultiplayerService._send_stack_queue.task_done()

    @classmethod
    async def _try_send_stats(cls) -> None:
        # if no messages to proccess, return
        if MultiplayerService._send_stats_queue.empty():
            return
        
        # get next stats to send and send to server
        stats = MultiplayerService._send_stats_queue.get()

        # construct json message
        json_message = json.dumps({"action": "send_stats",\
                                   "clientId": MultiplayerService._client_id,\
                                   "gameId": MultiplayerService._current_game_id,\
                                   "stats": stats.__dict__})

        await cls._websocket.send(json_message)
        MultiplayerService._send_stats_queue.task_done()

    @classmethod
    async def _try_send_message(cls) -> None:
        # if no messages to proccess, return
        if MultiplayerService._send_message_queue.empty():
            return
        
        # get next message to send and send to server
        message = MultiplayerService._send_message_queue.get()

        await cls._websocket.send(message)
        MultiplayerService._send_message_queue.task_done()

    """ NETWORK RECEIVE """
    # todo refactor
    @classmethod
    async def _try_receive_message(cls) -> None:
        try:
            task = cls._pending_receive_task or asyncio.create_task(cls._websocket.recv())
            done, pending = await asyncio.wait({task}, timeout=2e-3) # TODO experiment with the timeout

            if task in done:
                json_str = await task
                if json_str == "pong":
                    print("pong")
                    cls._pending_receive_task = None
                    return

                data = json.loads(json_str)
                if data["type"] == "wait_for_opponent":
                    MultiplayerService._receive_message_queue.put(MultiplayerService.WAIT_FOR_OPPONENT)
                if data["type"] == "start_game":
                    MultiplayerService._receive_message_queue.put(MultiplayerService.START_GAME)
                if data["type"] == "receive_piece":
                    MultiplayerService._receive_piece_queue.put(PieceDto.create(data["piece"]))
                if data["type"] == "receive_stack":
                    MultiplayerService._receive_stack_queue.put(StackDto.create(data["stack"]))
                if data["type"] == "receive_stats":
                    MultiplayerService._receive_stats_queue.put(StatsDto.create(data["stats"]))
                    print(data["stats"])
                if data["type"] == "exit_game":
                    print("Exit the game!")
                    cls.close_connection()

                cls._pending_receive_task = None
            elif len(pending):
                cls._pending_receive_task = pending.pop()
        finally:
            pass # TODO handle connection closed exception and attempt to reconnect

    """ MISC """
    @classmethod
    async def _run_network_loop(cls) -> None:
        while True:
            await asyncio.sleep(2e-3) # TODO add clock tick instead 

            await cls._try_enter_game()
            await cls._try_send_piece()
            await cls._try_send_stack()
            await cls._try_send_stats()
            await cls._try_send_message()
            await cls._try_receive_message()

            # if conection is closed, exit loop
            if cls._is_closed:
                await cls._websocket.close()
                break

    # ping_interval=None is important, otherwise the server will disconnect us
    # https://stackoverflow.com/a/58993145/11512104
    @classmethod
    async def _connect_to_server(cls) -> None:
        print("Connecting to server...") # TODO replace with logging
        url = ConfigurationManager.get("online", "server-url")
        cls._websocket = await websockets.connect(url, ping_interval=None)
        print("Connected to server...") # TODO replace with logging

    @classmethod
    def close_connection(cls) -> None:
        cls._is_closed = True

# DTOs

class PieceDto():
    def __init__(self, points: List, center: List, base_color: str, inner_color: str, outer_color: str) -> None:
        self.points = points
        self.center = center
        self.base_color = base_color
        self.inner_color = inner_color
        self.outer_color = outer_color

    @staticmethod
    def create(data: Dict) -> "PieceDto": 
        return PieceDto(data["points"], data["center"], data["base_color"], data["inner_color"], data["outer_color"])

class StackDto():
    def __init__(self, points: List, square_colors: List[Dict]) -> None:
        self.points = points
        self.square_colors = square_colors

    @staticmethod
    def create(data: Dict) -> "StackDto": 
        return StackDto(data["points"], data["square_colors"])

class StatsDto():
    def __init__(self, score: int, lines: int, is_well_full: bool) -> None:
        self.score = score
        self.lines = lines
        self.is_well_full = is_well_full

    @staticmethod
    def create(data: Dict) -> "StatsDto": 
        return StatsDto(data["score"], data["lines"], data["is_well_full"])