package it.polimi.ingsw.gc14.Network.TCP.Server;

import it.polimi.ingsw.gc14.Controller.GameController;
import it.polimi.ingsw.gc14.LimitedList;
import it.polimi.ingsw.gc14.Model.Game;
import it.polimi.ingsw.gc14.Network.EventType;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.Network.NetworkEvents.AddPlayer;

import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;

/**
 * Server TCP. Accepts connections and manages all client handlers.
 */
public class TCPServer {

    /** TCP port */
    int port;

    /** Number of currently connected clients */
    int ConnectedPlayers;

    /** Socket TCP */
    ServerSocket socketTCP;

    /** Server game's controller */
    GameController controller;

    /** Queue containing the events to be applied to the game model */
    BlockingQueue<NetworkEvent> actionQueue;

    /**
     * List containing the usernames of joined players.
     * {@link LimitedList}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedList#setLimit(int)}.
     */
    private LimitedList<String> playerList;

    /** List containing all client's handlers */
    private List<ClientHandler> clientHandlers;


    /**
     * Class constructor that initializes the attributes.
     * @param controller The game controller
     * @param port The TCP port
     * @param actionQueue The action queue
     * @param playerList The player's usernames list
     */
    public TCPServer(GameController controller, int port, BlockingQueue<NetworkEvent> actionQueue, LimitedList<String> playerList){
        this.port = port;
        this.ConnectedPlayers = 0;
        this.socketTCP = null;
        this.controller = controller;
        this.actionQueue = actionQueue;
        this.playerList = playerList;
        this.clientHandlers = new ArrayList<>();
    }


    /**
     * Starts the TCP server.
     * If the first event is not AddPlayer, the request is rejected.
     * If the desired number of player is invalid, the request is rejected.
     *
     * If this is the first player to connect, a new game model is created and passed to the controller. Additionally, the playerList's limit is set.
     * If the controller successfully adds the player, the username is added to {@link #playerList} and the handler is added to {@link #clientHandlers}.
     *
     * If any error occurs, the server sends -1 back to the client. Otherwise, it sends 1.
     */
    public void start(){

        try{
            socketTCP = new ServerSocket(port);
        }
        catch (IOException e){
            System.out.println("Could not start the server TCP on port: " + port);
            e.printStackTrace();
            return;
        }
        System.out.println("Server TCP started on port: " +  port);
        Socket clientSocket;

        while(true){

            try{
                clientSocket = socketTCP.accept();
                ObjectInputStream clientSocketObj = new ObjectInputStream(clientSocket.getInputStream());
                NetworkEvent event = (NetworkEvent) clientSocketObj.readObject();

                if(!(event.getEventType() == EventType.ADD_PLAYER)){
                    clientSocket.getOutputStream().write((int)(-1));
                    clientSocket.close();
                    System.out.println("Invalid parameters. Connection terminated.\n");
                }
                else{
                    AddPlayer eventAddPlayer = (AddPlayer) event;
                    if (eventAddPlayer.getProposedNPlayer() < 2 || eventAddPlayer.getProposedNPlayer() > 5) {
                        clientSocket.getOutputStream().write((int) (-1));
                        clientSocket.close();
                        System.out.println("Invalid parameters. Connection terminated.\n");
                    }
                    synchronized (controller) {
                        if (playerList.isEmpty()){
                            Game model = new Game(eventAddPlayer.getProposedNPlayer());
                            controller.setModel(model);
                            playerList.setLimit(eventAddPlayer.getProposedNPlayer());
                        }
                        if (controller.addPlayer(eventAddPlayer.getUsername())) {
                            playerList.add(eventAddPlayer.getUsername());
                            clientSocket.getOutputStream().write((int) (1));

                            System.out.println("Accepted player: " + eventAddPlayer.getUsername());
                            ClientHandler clientHandler = new ClientHandler(clientSocket, clientHandlers, actionQueue);
                            clientHandlers.add(clientHandler);
                            ConnectedPlayers++;

                            Thread t = new Thread(clientHandler);
                            t.start();
                        } else {
                            clientSocket.getOutputStream().write((int) (-1));
                            clientSocket.close();
                            System.out.println("Player could not be added. Connection terminated.\n");
                        }
                    }
                }
            }
            catch(IOException e){
                e.printStackTrace();
            }
            catch(ClassNotFoundException e){
                throw new RuntimeException(e);
            }
        }
    }


    /** Sends an action to all TCP clients */
    public void notifyAll(NetworkEvent event){
        clientHandlers.forEach((x) -> x.notifyEvent(event));
    }


    /** Sends a game model to all TCP clients */
    public void notifyAll(Game model){
        clientHandlers.forEach((x) -> x.notifyModel(model));
    }
}
