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

package pp.battleship.game.server;

import pp.battleship.BattleshipConfig;
import pp.battleship.message.client.ClientInterpreter;
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 java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Controls the server-side game logic for Battleship.
 * Manages game states, player interactions, and message handling.
 */
public class ServerGameLogic implements ClientInterpreter {
    private static final Logger LOGGER = System.getLogger(ServerGameLogic.class.getName());

    private final BattleshipConfig config;
    private final List<Player> players = new ArrayList<>(2);
    private final Set<Player> readyPlayers = new HashSet<>();
    private final ServerSender serverSender;
    private Player activePlayer;
    private ServerState state = ServerState.WAIT;

    /**
     * Constructs a ServerGameLogic with the specified sender and configuration.
     *
     * @param serverSender the sender used to send messages to clients
     * @param config       the game configuration
     */
    public ServerGameLogic(ServerSender serverSender, BattleshipConfig config) {
        this.serverSender = serverSender;
        this.config = config;
    }

    /**
     * Returns the  state of the game.
     */
    ServerState getState() {
        return state;
    }

    /**
     * Sets the new state of the game and logs the state transition.
     *
     * @param newState the new state to set
     */
    void setState(ServerState newState) {
        LOGGER.log(Level.DEBUG, "state transition {0} --> {1}", state, newState); //NON-NLS
        state = newState;
    }

    /**
     * Returns the opponent of the specified player.
     *
     * @param p the player
     * @return the opponent of the player
     */
    Player getOpponent(Player p) {
        if (players.size() != 2)
            throw new RuntimeException("trying to find opponent without having 2 players");
        final int index = players.indexOf(p);
        if (index < 0)
            throw new RuntimeException("Nonexistent player " + p);
        return players.get(1 - index);
    }

    /**
     * Returns the player representing the client with the specified connection ID.
     *
     * @param id the ID of the client
     * @return the player associated with the client ID, or null if not found
     */
    public Player getPlayerById(int id) {
        for (Player player : players)
            if (player.getId() == id)
                return player;
        LOGGER.log(Level.ERROR, "no player found with connection {0}", id); //NON-NLS
        return null;
    }

    /**
     * Sends a message to the specified player.
     *
     * @param player the player to send the message to
     * @param msg    the message to send
     */
    void send(Player player, ServerMessage msg) {
        LOGGER.log(Level.INFO, "sending to {0}: {1}", player, msg); //NON-NLS
        serverSender.send(player.getId(), msg);
    }

    /**
     * Adds a new player to the game if there are less than two players.
     * Transitions the state to SET_UP if two players are present.
     *
     * @param id the connection ID of the new player
     * @return the player added to the game, or null if the game is not in the right state
     */
    public Player addPlayer(int id) {
        if (state != ServerState.WAIT) {
            LOGGER.log(Level.ERROR, "addPlayer not allowed in {0}", state); //NON-NLS
            return null;
        }
        final int n = players.size() + 1;
        final Player player = new Player(id, "player " + n, config); //NON-NLS
        LOGGER.log(Level.INFO, "adding {0}", player); //NON-NLS
        players.add(player);
        if (players.size() == 2) {
            activePlayer = players.get(0);
            for (Player p : players)
                send(p, new GameDetails(config));
            setState(ServerState.SET_UP);
        }
        return player;
    }

    /**
     * Handles the reception of a MapMessage.
     *
     * @param msg  the received MapMessage
     * @param from the ID of the sender client
     */
    @Override
    public void received(MapMessage msg, int from) {
        if (state != ServerState.SET_UP)
            LOGGER.log(Level.ERROR, "playerReady not allowed in {0}", state); //NON-NLS
        else
            playerReady(getPlayerById(from), msg.getShips());
    }

    /**
     * Handles the reception of a ShootMessage.
     *
     * @param msg  the received ShootMessage
     * @param from the ID of the sender client
     */
    @Override
    public void received(ShootMessage msg, int from) {
        if (state != ServerState.BATTLE)
            LOGGER.log(Level.ERROR, "shoot not allowed in {0}", state); //NON-NLS
        else
            shoot(getPlayerById(from), msg.getPosition());
    }

    /**
     * Marks the player as ready and sets their ships.
     * Transitions the state to PLAY if both players are ready.
     *
     * @param player the player who is ready
     * @param ships  the list of ships placed by the player
     */
    void playerReady(Player player, List<Battleship> ships) {
        if (!readyPlayers.add(player)) {
            LOGGER.log(Level.ERROR, "{0} was already ready", player); //NON-NLS
            return;
        }
        ships.forEach(player.getMap()::add);
        if (readyPlayers.size() == 2) {
            for (Player p : players)
                send(p, new StartBattleMessage(p == activePlayer));
            setState(ServerState.BATTLE);
        }
    }

    /**
     * Handles the shooting action by the player.
     *
     * @param p   the player who shot
     * @param pos the position of the shot
     */
    void shoot(Player p, IntPoint pos) {
        if (p != activePlayer) return;
        final Player otherPlayer = getOpponent(activePlayer);
        final Battleship selectedShip = otherPlayer.getMap().findShipAt(pos);
        if (selectedShip == null) {
            // shot missed
            send(activePlayer, EffectMessage.miss(true, pos));
            send(otherPlayer, EffectMessage.miss(false, pos));
            activePlayer = otherPlayer;
        }
        else {
            // shot hit a ship
            selectedShip.hit(pos);
            if (otherPlayer.getMap().getRemainingShips().isEmpty()) {
                // game is over
                send(activePlayer, EffectMessage.won(pos, selectedShip));
                send(otherPlayer, EffectMessage.lost(pos, selectedShip, activePlayer.getMap().getRemainingShips()));
                setState(ServerState.GAME_OVER);
            }
            else if (selectedShip.isDestroyed()) {
                // ship has been destroyed, but game is not yet over
                send(activePlayer, EffectMessage.shipDestroyed(true, pos, selectedShip));
                send(otherPlayer, EffectMessage.shipDestroyed(false, pos, selectedShip));
            }
            else {
                // ship has been hit, but it hasn't been destroyed
                send(activePlayer, EffectMessage.hit(true, pos));
                send(otherPlayer, EffectMessage.hit(false, pos));
            }
        }
    }
}
