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

package pp.battleship.client;

import com.jme3.app.DebugKeysAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.system.AppSettings;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.style.BaseStyles;
import pp.battleship.client.gui.BattleAppState;
import pp.battleship.client.gui.EditorAppState;
import pp.battleship.client.gui.GameMusic;
import pp.battleship.client.gui.SeaAppState;
import pp.battleship.game.client.BattleshipClient;
import pp.battleship.game.client.ClientGameLogic;
import pp.battleship.game.client.ServerConnection;
import pp.battleship.game.singlemode.BattleshipClientConfig;
import pp.battleship.game.singlemode.ServerConnectionMockup;
import pp.battleship.notification.ClientStateEvent;
import pp.battleship.notification.GameEventListener;
import pp.battleship.notification.InfoTextEvent;
import pp.dialog.DialogBuilder;
import pp.dialog.DialogManager;
import pp.graphics.Draw;

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.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.LogManager;

import static pp.battleship.Resources.lookup;

/**
 * The main class for the Battleship client application.
 * It manages the initialization, input setup, GUI setup, and game states for the client.
 */
public class BattleshipApp extends SimpleApplication implements BattleshipClient, GameEventListener {

    /**
     * Logger for logging messages within the application.
     */
    private static final Logger LOGGER = System.getLogger(BattleshipApp.class.getName());

    /**
     * Path to the styles script for GUI elements.
     */
    private static final String STYLES_SCRIPT = "Interface/Lemur/pp-styles.groovy"; //NON-NLS

    /**
     * Path to the font resource used in the GUI.
     */
    private static final String FONT = "Interface/Fonts/Default.fnt"; //NON-NLS

    /**
     * Path to the client configuration file, if one exists.
     */
    private static final File CONFIG_FILE = new File("client.properties");

    /**
     * Input mapping name for mouse clicks.
     */
    public static final String CLICK = "CLICK";

    /**
     * Input mapping name for the Escape key.
     */
    private static final String ESC = "ESC";

    /**
     * Manager for handling dialogs within the application.
     */
    private final DialogManager dialogManager = new DialogManager(this);

    /**
     * The server connection instance, used for communicating with the game server.
     */
    private final ServerConnection serverConnection;

    /**
     * Instance of the {@link Draw} class for rendering graphics.
     */
    private Draw draw;

    /**
     * Text display at the top of the GUI for showing information to the user.
     */
    private BitmapText topText;

    /**
     * Executor service for handling asynchronous tasks within the application.
     */
    private ExecutorService executor;

    /**
     * Handler for managing the client's game logic.
     */
    private final ClientGameLogic logic;

    /**
     * Configuration settings for the Battleship client application.
     */
    private final BattleshipAppConfig config;

    /**
     * Listener for handling actions triggered by the Escape key.
     */
    private final ActionListener escapeListener = (name, isPressed, tpf) -> escape(isPressed);

    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 Battleship application.
     *
     * @param args Command-line arguments for launching the application.
     */
    public static void main(String[] args) {
        new BattleshipApp().start();
    }

    /**
     * Constructs a new {@code BattleshipApp} instance.
     * Initializes the configuration, server connection, and game logic listeners.
     */
    private BattleshipApp() {
        config = new BattleshipAppConfig();
        config.readFromIfExists(CONFIG_FILE);
        serverConnection = makeServerConnection();
        logic = new ClientGameLogic(serverConnection);
        logic.addListener(this);
        setShowSettings(config.getShowSettings());
        setSettings(makeSettings());
    }

    /**
     * Creates and configures application settings from the client configuration.
     *
     * @return A configured {@link AppSettings} object.
     */
    private AppSettings makeSettings() {
        final AppSettings settings = new AppSettings(true);
        settings.setTitle(lookup("battleship.name"));
        settings.setResolution(config.getResolutionWidth(), config.getResolutionHeight());
        settings.setFullscreen(config.fullScreen());
        settings.setUseRetinaFrameBuffer(config.useRetinaFrameBuffer());
        settings.setGammaCorrection(config.useGammaCorrection());
        return settings;
    }

    /**
     * Factory method for creating a server connection based on the current
     * client configuration.
     *
     * @return A {@link ServerConnection} instance, which could be a real or mock server.
     */
    private ServerConnection makeServerConnection() {
        if (config.isSingleMode())
            return new ServerConnectionMockup(this);
        return new NetworkSupport(this);
    }

    /**
     * Returns the dialog manager responsible for managing in-game dialogs.
     *
     * @return The {@link DialogManager} instance.
     */
    DialogManager getDialogManager() {
        return dialogManager;
    }

    /**
     * Returns the game logic handler for the client.
     *
     * @return The {@link ClientGameLogic} instance.
     */
    @Override
    public ClientGameLogic getGameLogic() {
        return logic;
    }

    /**
     * Returns the current configuration settings for the Battleship client.
     *
     * @return The {@link BattleshipClientConfig} instance.
     */
    @Override
    public BattleshipAppConfig getConfig() {
        return config;
    }

    /**
     * Initializes the application.
     * Sets up input mappings, GUI, game states, and connects to the server.
     */
    @Override
    public void simpleInitApp() {
        setPauseOnLostFocus(false);
        draw = new Draw(assetManager);
        setupInput();
        setupStates();
        setupGui();
        serverConnection.connect();
    }

    /**
     * Sets up the graphical user interface (GUI) for the application.
     */
    private void setupGui() {
        GuiGlobals.initialize(this);
        BaseStyles.loadStyleResources(STYLES_SCRIPT);
        GuiGlobals.getInstance().getStyles().setDefaultStyle("pp"); //NON-NLS
        final BitmapFont normalFont = assetManager.loadFont(FONT); //NON-NLS
        topText = new BitmapText(normalFont);
        final int height = context.getSettings().getHeight();
        topText.setLocalTranslation(10f, height - 10f, 0f);
        topText.setColor(config.getTopColor());
        guiNode.attachChild(topText);
    }

    /**
     * Configures input mappings and sets up listeners for user interactions.
     */
    private void setupInput() {
        inputManager.deleteMapping(INPUT_MAPPING_EXIT);
        inputManager.setCursorVisible(false);
        inputManager.addMapping(ESC, new KeyTrigger(KeyInput.KEY_ESCAPE));
        inputManager.addMapping(CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
        inputManager.addListener(escapeListener, ESC);
    }

    /**
     * Initializes and attaches the necessary application states for the game.
     */
    private void setupStates() {
        if (config.getShowStatistics()) {
            final BitmapFont normalFont = assetManager.loadFont(FONT); //NON-NLS
            final StatsAppState stats = new StatsAppState(guiNode, normalFont);
            stateManager.attach(stats);
        }
        flyCam.setEnabled(false);
        stateManager.detach(stateManager.getState(StatsAppState.class));
        stateManager.detach(stateManager.getState(DebugKeysAppState.class));

        attachGameSound();
        attachGameMusic();
        stateManager.attachAll(new EditorAppState(), new BattleAppState(), new SeaAppState());
    }

    /**
     * Attaches the game sound state and sets its initial enabled state.
     */
    private void attachGameSound() {
        final GameSound gameSound = new GameSound();
        logic.addListener(gameSound);
        gameSound.setEnabled(GameSound.enabledInPreferences());
        stateManager.attach(gameSound);
    }

    /**
     * Attaches the background music state and sets its initial enabled state.
     */
    private void attachGameMusic() {
        final GameMusic gameSound = new GameMusic();
        gameSound.setEnabled(GameMusic.enabledInPreferences());
        stateManager.attach(gameSound);
    }

    /**
     * Updates the application state every frame.
     * This method is called once per frame during the game loop.
     *
     * @param tpf Time per frame in seconds.
     */
    @Override
    public void simpleUpdate(float tpf) {
        super.simpleUpdate(tpf);
        dialogManager.update(tpf);
        logic.update(tpf);
    }

    /**
     * Handles the Escape key action to either close the top dialog or show the main menu.
     *
     * @param isPressed Indicates whether the Escape key is pressed.
     */
    private void escape(boolean isPressed) {
        if (!isPressed) return;
        if (dialogManager.showsDialog())
            dialogManager.escape();
        else
            new Menu(this).open();
    }

    /**
     * Returns the {@link Draw} instance used for rendering graphical elements in the game.
     *
     * @return The {@link Draw} instance.
     */
    public Draw getDraw() {
        return draw;
    }

    /**
     * Handles a request to close the application.
     * If the request is initiated by pressing ESC, this parameter is true.
     *
     * @param esc If true, the request is due to the ESC key being pressed.
     */
    @Override
    public void requestClose(boolean esc) { /* do nothing */ }

    /**
     * Closes the application, displaying a confirmation dialog if the client is connected to a server.
     */
    public void closeApp() {
        if (serverConnection.isConnected())
            confirmDialog(lookup("confirm.leaving"), this::close);
        else
            close();
    }

    /**
     * Closes the application, disconnecting from the server and stopping the application.
     */
    private void close() {
        serverConnection.disconnect();
        stop();
    }

    /**
     * Updates the informational text displayed in the GUI.
     *
     * @param text The information text to display.
     */
    void setInfoText(String text) {
        LOGGER.log(Level.DEBUG, "setInfoText {0}", text); //NON-NLS
        topText.setText(text);
    }

    /**
     * Updates the informational text in the GUI based on the key received in an {@link InfoTextEvent}.
     *
     * @param event The {@link InfoTextEvent} containing the key for the text to display.
     */
    @Override
    public void receivedEvent(InfoTextEvent event) {
        LOGGER.log(Level.DEBUG, "received info text {0}", event.key()); //NON-NLS
        setInfoText(lookup(event.key()));
    }

    /**
     * Handles client state events to update the game states accordingly.
     *
     * @param event The {@link ClientStateEvent} representing the state change.
     */
    @Override
    public void receivedEvent(ClientStateEvent event) {
        stateManager.getState(EditorAppState.class).setEnabled(logic.showEditor());
        stateManager.getState(BattleAppState.class).setEnabled(logic.showBattle());
        stateManager.getState(SeaAppState.class).setEnabled(logic.showBattle());
    }

    /**
     * Returns the executor service used for handling multithreaded tasks.
     *
     * @return The {@link ExecutorService} instance.
     */
    public ExecutorService getExecutor() {
        if (executor == null)
            executor = Executors.newCachedThreadPool();
        return executor;
    }

    /**
     * Stops the application, shutting down the executor service and halting execution.
     *
     * @param waitFor If true, waits for the application to stop before returning.
     */
    @Override
    public void stop(boolean waitFor) {
        if (executor != null) executor.shutdownNow();
        super.stop(waitFor);
    }

    /**
     * Displays a confirmation dialog with a specified question and action for the "Yes" button.
     *
     * @param question  The question to display in the dialog.
     * @param yesAction The action to perform if "Yes" is selected.
     */
    void confirmDialog(String question, Runnable yesAction) {
        DialogBuilder.simple(dialogManager)
                     .setTitle(lookup("dialog.question"))
                     .setText(question)
                     .setOkButton(lookup("button.yes"), yesAction)
                     .setNoButton(lookup("button.no"))
                     .build()
                     .open();
    }

    /**
     * Displays an error dialog with the specified error message.
     *
     * @param errorMessage The error message to display in the dialog.
     */
    void errorDialog(String errorMessage) {
        DialogBuilder.simple(dialogManager)
                     .setTitle(lookup("dialog.error"))
                     .setText(errorMessage)
                     .setOkButton(lookup("button.ok"))
                     .build()
                     .open();
    }
}
