from collections import defaultdict

import asyncio
import json

import websockets

from set import Game


def _generate_identifiers():
    current_identifier = 0
    while True:
        yield str(current_identifier)
        # There is no maximum integer in python3.
        current_identifier += 1


class Player():
    """Represents a player realized though a client websocket connection."""

    _identifiers = _generate_identifiers()

    def __init__(self):
        # TODO: implement names
        self._name = None
        self._connection = None
        self._server_queue = None
        self._identifier = next(Player._identifiers)

    def get_identifier(self):
        return self._identifier

    def set_connection(self, connection):
        self._connection = connection

    def set_server_queue(self, queue):
        self._server_queue = queue

    async def listen(self):
        """Listen to and handle messages sent from the client.

        Upon receiving a message, add the player's identifier to the message
        and forward it to the server for processing.

        """
        listening = True

        while listening:
            if self._connection is None or self._server_queue is None:
                return

            client_message = await self._connection.recv()

            # Check that the connection was not forcefully closed.
            # If it was then ensure that the server side is updated/synchronized.
            if client_message is None:
                client_message = json.dumps( { 'command': 'server.quit' } )

            # Decode the message, add the player identifier, encode the message.
            decoded_message = json.loads(client_message)
            decoded_message['player'] = self.get_identifier()
            encoded_message = json.dumps(decoded_message)

            await self._server_queue.put(encoded_message)

            # Stop listening for messages if a quit message has been sent.
            if decoded_message['command'] == 'server.quit':
                listening = False

    async def send(self, message):
        """Handle the sending of a message to the client."""
        # Check that the connection is valid.
        if self._connection is None or self._connection.open is not True:
            return

        await self._connection.send(message)


class Server():
    """Represents the websocket server & player management components."""

    _identifiers = _generate_identifiers()

    def __init__(self, host, port):
        self._players = {}  # identifier -> player object
        self._games = {}  # identifier -> (name, game object)

        self._lobby_players = [] # list of identifiers
        self._ready_players = [] # list of identifiers

        self._player_game_map = {} # identifier -> identifier
        self._game_player_map = defaultdict(list) # id -> list(id)

        self._server_queue = asyncio.Queue()

        self._loop = asyncio.get_event_loop()
        self._websocket_server = websockets.serve(
            self._websocket_handler, host, port)

    def start(self):
        """Run the main websocket server."""
        tasks = asyncio.gather(
            asyncio.ensure_future(self._websocket_server),
            asyncio.ensure_future(self._handle_commands())
        )

        # Note: 'Task was destroyed but it is pending!' error will occur if
        # the server is closed while clients are still connected.
        try:
            self._loop.run_until_complete(tasks)
        except KeyboardInterrupt as exception:
            print('Caught keyboard interrupt. Shutting down server..')
            tasks.cancel()
            self._loop.run_forever()
            tasks.exception()
        finally:
            self._loop.close()

    async def _broadcast(self, message, recipients=None):
        """Send a message to specified players (by unique identifier)."""
        if recipients is None:
            recipients = self._players.keys()

        for recipient in recipients:
            player = self._players[recipient]
            await player.send(message)

    async def _websocket_handler(self, websocket, path):
        """Handle new player connections."""
        player = Player()
        player.set_connection(websocket)
        player.set_server_queue(self._server_queue)

        self._players[player.get_identifier()] = player
        self._lobby_players.append(player.get_identifier())
        print('Player ', player.get_identifier(), ' joined.')
        print('Players: ', self._players.keys())
        await player.listen()

    def _get_game_list(self):
        """Construct the game list.

        A list of dictionaries is returned, each containing an 'identifier'
        that uniquely describes the game, the game's (display) 'name', and a
        'running' boolean indicating whether the game has started.

        """
        return {
            'command': 'server.games',
            'data': [
                {
                    'identifier': identifier,
                    'name': self._games[identifier][0],
                    'running': self._games[identifier][1].is_running()
                }
                for identifier in self._games.keys()
            ]
        }

    def _join_game(self, player, game):
        """Allow a player to join a game.

        The player and game parameters are the objects' identifiers.

        Returns a message indicating which game has been joined, with an empty
        string representing that no game was joined.

        """
        message = { 'command': 'server.joingame', 'data': '' }

        # Don't let a player join a game if they are already in one,
        # or if the game has already started.
        if (player not in self._player_game_map.keys() and
                not self._games[game][1].is_running()):

            # Add the player to the game itself.
            # Note: Remember it's a 2-tuple '(name, game)'.
            self._games[game][1].add_player(player)

            # Update the server-side structures used for message routing.
            if player in self._lobby_players:
                self._lobby_players.remove(player)
            self._player_game_map[player] = game
            self._game_player_map[game].append(player)

            # Return a message indicating that the join was successful.
            message['data'] = game

        return message

    def _leave_lobby(self, player, game):
        """Allow a player to leave a game.

        The player and game parameters are the objects' identifiers.

        Returns a message indicating which game was left, with an empty string
        representing that no game was left.

        """
        message = { 'command': 'lobby.leave', 'data': '' }

        # Only let the player leave the lobby if the game hasn't started.
        if (player in self._game_player_map[game] and
                not self._games[game][1].is_running()):

            # Remove the player from the game itself.
            # Note: Remember it's a 2-tuple '(name, game)'.
            self._games[game][1].remove_player(player)

            # Update the server-side structures used for message routing.
            if player not in self._lobby_players:
                self._lobby_players.append(player)
            del self._player_game_map[player]
            self._game_player_map[game].remove(player)

            # Return a message indicating that the leave/quit was a success.
            message['data'] = game

    def _create_game(self, display_name=None):
        """Create a new game.

        Returns the created game's identifier.

        """
        identifier = next(Server._identifiers)
        name = display_name or ' '.join(('Game', str(identifier)))
        self._games[identifier] = (name, Game())

        return identifier

    async def _handle_commands(self):
        """Handle commands retrieved from the message queue."""
        while True:
            message = await self._server_queue.get()
            print('Received message: ', message)

            # Load the message dictionary.
            decoded_message = json.loads(message)

            # Use the command type to determine what action to take.
            command = decoded_message['command']
            sender = decoded_message['player'] # identifier

            ## Server lobby commands.
            if command == 'server.quit':
                # The player has left the server, clean up their resources.
                del self._players[sender]

                if sender in self._lobby_players:
                    self._lobby_players.remove(sender)
                if sender in self._player_game_map.keys():
                    # TODO: Remove the player from the game object too.
                    game = self._player_game_map[sender]
                    self._game_player_map[game].remove(sender)
                    del self._player_game_map[sender]

                print('Player ', sender, ' quit.')
                print('Players: ', self._players.keys())

            elif command == 'server.chat':
                # The player has sent a message.
                # Default to sending the message to all players.
                recipients = None

                # Determine the recipients by where the player is.
                if sender in self._lobby_players:
                    recipients = self._lobby_players
                elif sender in self._player_game_map.keys():
                    identifier = self._player_game_map[sender]
                    recipients = self._game_player_map[identifier]

                print('Sending', message, 'to', recipients)
                await self._broadcast(message, recipients)

            elif command == 'server.games':
                # Map game objects to function to get name and players.
                message = self._get_game_list()
                print('Game list is', json.dumps(message), '([', sender, '])')
                await self._broadcast(json.dumps(message), [sender])

            elif command == 'server.joingame':
                # Join the game.
                identifier = decoded_message['data']
                print('Player', sender, 'wants to join game', identifier)
                message = self._join_game(sender, identifier)
                print('Player', sender, 'join game message is', message)
                await self._broadcast(json.dumps(message))

            elif command == 'server.creategame':
                # Create a new game and then have the sender join it.
                identifier = self._create_game()
                message = self._get_game_list()
                print('Player', sender, 'created game', identifier)
                print('Game list is', json.dumps(message), '(None)')
                await self._broadcast(json.dumps(message))

                # Join the game.
                print('Player', sender, 'wants to join game', identifier)
                message = self._join_game(sender, identifier)
                print('Player', sender, 'join game message is', message)
                await self._broadcast(json.dumps(message), [sender])

            ## Game lobby commands.
            elif command == 'lobby.leave':
                # Allow a player to leave the game if it has not started.
                identifier = self._player_game_map[sender]
                print('Player', sender, 'wants to leave game', identifier)
                message = self._leave_lobby(sender, identifier)
                print('Player', sender, 'leave game message is', message)
                players = self._game_player_map[identifier]
                await self._broadcast(json.dumps(message), players)

            elif command == 'lobby.ready':
                # Notify that the player is (not) ready to play.
                ready_message = { 'command': 'lobby.ready' }
                start_message = None

                identifier = self._player_game_map[sender]
                players = self._game_player_map[identifier]

                if (sender in self._ready_players and
                        not self._games[identifier][1].is_running()):
                    # Only allow a player to untoggle the ready state if the
                    # game hasn't started.
                    self._ready_players.remove(sender)
                    ready_message['data'] = False
                    print('Player', sender, 'is not ready for game', identifier)

                elif sender not in self._ready_players:
                    self._ready_players.append(sender)
                    ready_message['data'] = True
                    print('Player', sender, 'is ready for game', identifier)

                    # Whenever a player is ready check to see if all players in
                    # the game are ready.  If so then start the game.
                    if players <= self._ready_players:
                        self._games[identifier][1].run()
                        start_message = { 'command': 'game.run' }
                        print('All players ready, starting game', identifier)

                await self._broadcast(json.dumps(ready_message), players)

                # Send a message to start the game
                if start_message is not None:
                    await self._broadcast(json.dumps(start_message), players)

            ## Game commands.
            elif command == 'game.quit':
                # Allow a player to quit a running game.
                pass

            elif command == 'game.scores':
                pass

            elif command == 'game.foundset':
                # A player has notified that they have found a set.
                pass


if __name__ == '__main__':

    # Run the server.
    Server('localhost', 8765).start()
