////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////

package pp.battleship.server;

import com.jme3.network.ConnectionListener;
import com.jme3.network.HostedConnection;
import com.jme3.network.Message;
import com.jme3.network.MessageListener;
import com.jme3.network.Network;
import com.jme3.network.Server;
import com.jme3.network.serializing.Serializer;
import pp.battleship.BattleshipConfig;
import pp.battleship.game.server.Player;
import pp.battleship.game.server.ServerGameLogic;
import pp.battleship.game.server.ServerSender;
import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.client.MapMessage;
import pp.battleship.message.client.ShootMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerMessage;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.Battleship;
import pp.battleship.model.IntPoint;
import pp.battleship.model.Shot;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.LogManager;

/**
 * Server implementing the visitor pattern as MessageReceiver for ClientMessages
 */
public class BattleshipServer implements MessageListener<HostedConnection>, ConnectionListener, ServerSender {
    private static final Logger LOGGER = System.getLogger(BattleshipServer.class.getName());
    private static final File CONFIG_FILE = new File("server.properties");

    private final BattleshipConfig config = new BattleshipConfig();
    private Server myServer;
    private final ServerGameLogic logic;
    private final BlockingQueue<ReceivedMessage> pendingMessages = new LinkedBlockingQueue<>();

    static {
        // Configure logging
        LogManager manager = LogManager.getLogManager();
        try {
            manager.readConfiguration(new FileInputStream("logging.properties"));
            LOGGER.log(Level.INFO, "Successfully read logging properties"); //NON-NLS
        }
        catch (IOException e) {
            LOGGER.log(Level.INFO, e.getMessage());
        }
    }

    /**
     * Starts the Battleships server.
     */
    public static void main(String[] args) {
        new BattleshipServer().run();
    }

    /**
     * Creates the server.
     */
    BattleshipServer() {
        config.readFromIfExists(CONFIG_FILE);
        LOGGER.log(Level.INFO, "Configuration: {0}", config); //NON-NLS
        logic = new ServerGameLogic(this, config);
    }

    public void run() {
        startServer();
        while (true)
            processNextMessage();
    }

    private void startServer() {
        try {
            LOGGER.log(Level.INFO, "Starting server..."); //NON-NLS
            myServer = Network.createServer(config.getPort());
            initializeSerializables();
            myServer.start();
            registerListeners();
            LOGGER.log(Level.INFO, "Server started: {0}", myServer.isRunning()); //NON-NLS
        }
        catch (IOException e) {
            LOGGER.log(Level.ERROR, "Couldn't start server: {0}", e.getMessage()); //NON-NLS
            exit(1);
        }
    }

    private void processNextMessage() {
        try {
            pendingMessages.take().process(logic);
        }
        catch (InterruptedException ex) {
            LOGGER.log(Level.INFO, "Interrupted while waiting for messages"); //NON-NLS
            Thread.currentThread().interrupt();
        }
    }

    private void initializeSerializables() {
        Serializer.registerClass(GameDetails.class);
        Serializer.registerClass(StartBattleMessage.class);
        Serializer.registerClass(MapMessage.class);
        Serializer.registerClass(ShootMessage.class);
        Serializer.registerClass(EffectMessage.class);
        Serializer.registerClass(Battleship.class);
        Serializer.registerClass(IntPoint.class);
        Serializer.registerClass(Shot.class);
    }

    private void registerListeners() {
        myServer.addMessageListener(this, MapMessage.class);
        myServer.addMessageListener(this, ShootMessage.class);
        myServer.addConnectionListener(this);
    }

    @Override
    public void messageReceived(HostedConnection source, Message message) {
        LOGGER.log(Level.INFO, "message received from {0}: {1}", source.getId(), message); //NON-NLS
        if (message instanceof ClientMessage clientMessage)
            pendingMessages.add(new ReceivedMessage(clientMessage, source.getId()));
    }

    @Override
    public void connectionAdded(Server server, HostedConnection hostedConnection) {
        LOGGER.log(Level.INFO, "new connection {0}", hostedConnection); //NON-NLS
        logic.addPlayer(hostedConnection.getId());
    }

    @Override
    public void connectionRemoved(Server server, HostedConnection hostedConnection) {
        LOGGER.log(Level.INFO, "connection closed: {0}", hostedConnection); //NON-NLS
        final Player player = logic.getPlayerById(hostedConnection.getId());
        if (player == null)
            LOGGER.log(Level.INFO, "closed connection does not belong to an active player"); //NON-NLS
        else { //NON-NLS
            LOGGER.log(Level.INFO, "closed connection belongs to {0}", player); //NON-NLS
            exit(0);
        }
    }

    private void exit(int exitValue) { //NON-NLS
        LOGGER.log(Level.INFO, "close request"); //NON-NLS
        if (myServer != null)
            for (HostedConnection client : myServer.getConnections()) //NON-NLS
                if (client != null) client.close("Game over"); //NON-NLS
        System.exit(exitValue);
    }

    /**
     * Send the specified message to the specified connection.
     *
     * @param id      the connection id
     * @param message the message
     */
    public void send(int id, ServerMessage message) {
        if (myServer == null || !myServer.isRunning()) {
            LOGGER.log(Level.ERROR, "no server running when trying to send {0}", message); //NON-NLS
            return;
        }
        final HostedConnection connection = myServer.getConnection(id);
        if (connection != null)
            connection.send(message);
        else
            LOGGER.log(Level.ERROR, "there is no connection with id={0}", id); //NON-NLS
    }
}