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

package pp.battleship.game.client;

import pp.battleship.message.client.ClientMessage;
import pp.battleship.message.server.EffectMessage;
import pp.battleship.message.server.GameDetails;
import pp.battleship.message.server.ServerInterpreter;
import pp.battleship.message.server.StartBattleMessage;
import pp.battleship.model.IntPoint;
import pp.battleship.model.ShipMap;
import pp.battleship.model.dto.ShipMapDTO;
import pp.battleship.notification.ClientStateEvent;
import pp.battleship.notification.GameEvent;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.battleship.notification.Sound;
import pp.battleship.notification.SoundEvent;

import java.io.File;
import java.io.IOException;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.ArrayList;
import java.util.List;

import static java.lang.Math.max;

/**
 * Controls the client-side game logic for Battleship.
 * Manages the player's ship placement, interactions with the map, and response to server messages.
 */
public class ClientGameLogic implements ServerInterpreter, GameEventBroker {
    static final Logger LOGGER = System.getLogger(ClientGameLogic.class.getName());
    private final ClientSender clientSender;
    private final List<GameEventListener> listeners = new ArrayList<>();
    private GameDetails details;
    private ShipMap ownMap;
    private ShipMap harbor;
    private ShipMap opponentMap;
    private ClientState state = new InitialState(this);

    /**
     * Constructs a ClientGameLogic with the specified sender object.
     *
     * @param clientSender the object used to send messages to the server
     */
    public ClientGameLogic(ClientSender clientSender) {
        this.clientSender = clientSender;
    }

    /**
     * Returns the current state of the game logic.
     */
    ClientState getState() {
        return state;
    }

    /**
     * Sets the current state of the game logic.
     *
     * @param newState the new state to be set
     */
    void setState(ClientState newState) {
        LOGGER.log(Level.DEBUG, "state transition {0} --> {1}", state.getName(), newState.getName()); //NON-NLS
        state = newState;
        notifyListeners(new ClientStateEvent());
        state.entry();
    }

    /**
     * Returns the game details.
     *
     * @return the game details
     */
    GameDetails getDetails() {
        return details;
    }

    /**
     * Returns the player's own map.
     *
     * @return the player's own map
     */
    public ShipMap getOwnMap() {
        return ownMap;
    }

    /**
     * Returns the opponent's map.
     *
     * @return the opponent's map
     */
    public ShipMap getOpponentMap() {
        return opponentMap;
    }

    /**
     * Returns the harbor map.
     *
     * @return the harbor map
     */
    public ShipMap getHarbor() {
        return harbor;
    }

    /**
     * Checks if the editor should be shown.
     *
     * @return true if the editor should be shown, false otherwise
     */
    public boolean showEditor() {
        return state.showEditor();
    }

    /**
     * Checks if the battle state should be shown.
     *
     * @return true if the battle state should be shown, false otherwise
     */
    public boolean showBattle() {
        return state.showBattle();
    }

    /**
     * Sets the game details provided by the server.
     *
     * @param details the game details including map size and ships
     */
    @Override
    public void received(GameDetails details) {
        state.receivedGameDetails(details);
    }

    /**
     * Moves the preview ship to the specified position.
     *
     * @param pos the new position for the preview ship
     */
    public void movePreview(IntPoint pos) {
        state.movePreview(pos);
    }

    /**
     * Handles a click on the player's own map.
     *
     * @param pos the position where the click occurred
     */
    public void clickOwnMap(IntPoint pos) {
        state.clickOwnMap(pos);
    }

    /**
     * Handles a click on the harbor map.
     *
     * @param pos the position where the click occurred
     */
    public void clickHarbor(IntPoint pos) {
        state.clickHarbor(pos);
    }

    /**
     * Handles a click on the opponent's map.
     *
     * @param pos the position where the click occurred
     */
    public void clickOpponentMap(IntPoint pos) {
        state.clickOpponentMap(pos);
    }

    /**
     * Rotates the preview ship.
     */
    public void rotateShip() {
        state.rotateShip();
    }

    /**
     * Marks the player's map as finished.
     */
    public void mapFinished() {
        state.mapFinished();
    }

    /**
     * Checks if the player's map is complete (i.e., all ships are placed).
     *
     * @return true if all ships are placed, false otherwise
     */
    public boolean isMapComplete() {
        return state.isMapComplete();
    }

    /**
     * Checks if there is currently a preview ship.
     *
     * @return true if there is currently a preview ship, false otherwise
     */
    public boolean movingShip() {
        return state.movingShip();
    }

    /**
     * Starts the battle based on the server message.
     *
     * @param msg the message indicating whose turn it is to shoot
     */
    @Override
    public void received(StartBattleMessage msg) {
        state.receivedStartBattle(msg);
    }

    /**
     * Reports the effect of a shot based on the server message.
     *
     * @param msg the message containing the effect of the shot
     */
    @Override
    public void received(EffectMessage msg) {
        state.receivedEffect(msg);
    }

    /**
     * Initializes the player's own map, opponent's map, and harbor based on the game details.
     *
     * @param details the game details including map size and ships
     */
    void initializeMaps(GameDetails details) {
        this.details = details;
        final int numShips = details.getShipNums().values().stream().mapToInt(Integer::intValue).sum();
        final int maxLength = details.getShipNums().keySet().stream().mapToInt(Integer::intValue).max().orElse(2);
        ownMap = new ShipMap(details.getWidth(), details.getHeight(), this);
        opponentMap = new ShipMap(details.getWidth(), details.getHeight(), this);
        harbor = new ShipMap(max(maxLength, 2), max(numShips, details.getHeight()), this);
    }

    /**
     * Sets the informational text to be displayed to the player.
     *
     * @param key the key for the info text
     */
    void setInfoText(String key) {
        notifyListeners(new InfoTextEvent(key));
    }

    /**
     * Emits an event to play the specified sound.
     *
     * @param sound the sound to be played.
     */
    public void playSound(Sound sound) {
        notifyListeners(new SoundEvent(sound));
    }

    /**
     * Loads a map from the specified file.
     *
     * @param file the file to load the map from
     * @throws IOException if an I/O error occurs
     */
    public void loadMap(File file) throws IOException {
        state.loadMap(file);
    }

    /**
     * Checks if the player's own map may be loaded from a file.
     *
     * @return true if the own map may be loaded from file, false otherwise
     */
    public boolean mayLoadMap() {
        return state.mayLoadMap();
    }

    /**
     * Checks if the player's own map may be saved to a file.
     *
     * @return true if the own map may be saved to file, false otherwise
     */
    public boolean maySaveMap() {
        return state.maySaveMap();
    }

    /**
     * Saves the player's own map to the specified file.
     *
     * @param file the file to save the map to
     * @throws IOException if the map cannot be saved in the current state
     */
    public void saveMap(File file) throws IOException {
        if (ownMap != null && maySaveMap())
            new ShipMapDTO(ownMap).saveTo(file);
        else
            throw new IOException("You are not allowed to save the map in this state of the game");
    }

    /**
     * Sends a message to the server.
     *
     * @param msg the message to be sent
     */
    void send(ClientMessage msg) {
        if (clientSender == null)
            LOGGER.log(Level.ERROR, "trying to send {0} with sender==null", msg); //NON-NLS
        else
            clientSender.send(msg);
    }

    /**
     * Adds a listener to receive game events.
     *
     * @param listener the listener to add
     */
    public synchronized void addListener(GameEventListener listener) {
        listeners.add(listener);
    }

    /**
     * Removes a listener from receiving game events.
     *
     * @param listener the listener to remove
     */
    public synchronized void removeListener(GameEventListener listener) {
        listeners.remove(listener);
    }

    /**
     * Notifies all listeners of a game event.
     *
     * @param event the game event to notify listeners of
     */
    @Override
    public void notifyListeners(GameEvent event) {
        final List<GameEventListener> copy;
        synchronized (this) {
            copy = new ArrayList<>(listeners);
        }
        for (GameEventListener listener : copy)
            event.notifyListener(listener);
    }

    /**
     * Called once per frame by the update loop.
     *
     * @param delta time in seconds since the last update call
     */
    public void update(float delta) {
        state.update(delta);
    }
}
