package battleship.ui.gamewindow;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import battleship.Constants;
import battleship.ui.playgroundmatrix.PlaygroundMatrix;
import battleship.utils.Utils;

/**
 * Das Spiele-Fenster, wo das eigentliche Spiel stattfindet. Die wichtigsten
 * Dinge sind dabei zwei grpahische Spielfelder (welche in GameSession mit den
 * logischen Spielfeldern verbunden sind) und einem Chat.
 */
public final class GameWindow {
    /*
     * Hauptfenster
     */
    private final JFrame window;

    /*
     * Button, um dem Computer eine Zug ausführen zu lassen. Des Weiteren lässt sich
     * an diesem Button erkennen, wann man am Zug ist. Kann man einen Computer-Zug
     * ausführen ist man dran, sonst nicht.
     */
    private final JButton computerMoveButton;

    /*
     * Button, um aufzugeben
     */
    private final JButton withdrawButton;

    /*
     * Chat-Eingabe (einzeilig). Dort werden die Chatnachrichten, welche man senden
     * möchte, eingegeben.
     */
    private final JTextField chatInputField;

    /*
     * Die Chat-Ausgabe. Dort werden System-Nachrichten angezeigt sowie die
     * Nachrichten beider Spieler. Wenn ein Spieler einem Spiel beitritt werden dort
     * beispielsweise als Systemnachricht die Namen angezeigt.
     */
    private final JTextPane chatOutputPane;

    /*
     * Button, um die eingegebene Chatnachricht abzusenden
     */
    private final JButton sendButton;

    /*
     * Die verschiedenen Styls
     */
    private final Style chatSystemStyle;
    private final Style chatUserStyle;
    private final Style chatPeerStyle;

    /*
     * Die eigentlichen Spielfelder
     */
    private final PlaygroundMatrix playersField;
    private final PlaygroundMatrix opponentField;

    /*
     * Verschiedene Handler, welche aufgerufen werden, wenn der Benutzer bestimmte
     * GUI-Element betätigt.
     */
    private MessageHandler messagehandler;
    private ClickHandler withdrawhandler;
    private ClickHandler computermovehandler;

    /*
     * Höhe und Breite des Spielfeldes
     */
    private final int rows;
    private final int cols;

    private final Logger logger;

    /**
     * Erstellt ein neues Spielfenster
     *
     * @param rows Anzahl der Zeilen (Höhe) der Spielfelder
     * @param cols Anzahl der Spalten (Breite) der Spielfelder
     */
    public GameWindow(final int rows, final int cols) {
        this.logger = Logger.getLogger(GameWindow.class.getName());
        this.logger.setLevel(Constants.LOG_LEVEL);

        this.rows = rows;
        this.cols = cols;
        this.messagehandler = null;
        this.withdrawhandler = null;
        this.computermovehandler = null;

        this.window = new JFrame("Battleship");

        // Fensteroptionen
        this.window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // Haupt-Panel, welches sowohl die Spielbrett-Panels als auch die Chat-Panels
        // beinhaltet
        final JPanel mainPanel = new JPanel(new BorderLayout());

        // Panel, welches die zwei Spielfelder beinhaltet; 1 Zeile, 2 Spalten, Abstand
        // 10px
        final JPanel playingFieldsPanel = new JPanel(new GridLayout(1, 2, 10, 10));

        // Spielfeld des Spielers
        this.opponentField = new PlaygroundMatrix(this.rows, this.cols);
        playingFieldsPanel.add(this.opponentField);

        // Spielfeld des Gegners
        this.playersField = new PlaygroundMatrix(this.rows, this.cols);
        playingFieldsPanel.add(this.playersField);

        // Panel für Chat und Buttons
        final JPanel controlPanel = new JPanel();
        controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.Y_AXIS));

        this.chatOutputPane = new JTextPane();
        this.chatOutputPane.setPreferredSize(new Dimension(300, 100));
        // Der Nuter kann selbst keine eigenen Chatnachrichten direkt in den Chatverlauf
        // schreiben, er kann aber Nachrichten darauf kopieren
        this.chatOutputPane.setEditable(false);

        this.chatSystemStyle = this.chatOutputPane.addStyle("system style", null);
        StyleConstants.setForeground(this.chatSystemStyle, new Color(179, 107, 0));

        this.chatUserStyle = this.chatOutputPane.addStyle("user style", null);
        StyleConstants.setForeground(this.chatUserStyle, Color.GRAY);

        this.chatPeerStyle = this.chatOutputPane.addStyle("peer style", null);
        StyleConstants.setForeground(this.chatPeerStyle, Color.BLACK);

        final JScrollPane chatScrollPane = new JScrollPane(this.chatOutputPane);
        controlPanel.add(chatScrollPane);

        // Panel, in welchem die Chatnachrichten geschrieben und abgesendet werden
        // können
        final JPanel chatInputPanel = new JPanel();
        chatInputPanel.setLayout(new BoxLayout(chatInputPanel, BoxLayout.X_AXIS));

        this.chatInputField = new JTextField();
        this.chatInputField.setMaximumSize(new Dimension(200, 30));
        chatInputPanel.add(this.chatInputField);

        // Send-Button
        this.sendButton = new JButton("Send");
        chatInputPanel.add(this.sendButton);

        // InputPanel hinzufügen
        controlPanel.add(chatInputPanel);

        // Abstand schaffen
        controlPanel.add(Box.createRigidArea(new Dimension(0, 20)));

        // Panel für Withdraw-Button und Computer-Button
        final JPanel actionPanel = new JPanel();
        actionPanel.setLayout(new BoxLayout(actionPanel, BoxLayout.X_AXIS));

        // Computer-Button
        this.computerMoveButton = new JButton("Computer");
        actionPanel.add(this.computerMoveButton);

        // Abstand zwischen den Buttons
        actionPanel.add(Box.createRigidArea(new Dimension(10, 0)));

        // Withdraw-Button
        this.withdrawButton = new JButton("Withdraw");
        actionPanel.add(this.withdrawButton);

        // buttonPanel zu controlPanel hinzufügen
        controlPanel.add(actionPanel);

        // Panels hinzufügen
        mainPanel.add(playingFieldsPanel, BorderLayout.CENTER); // Beide Spielfelder
        mainPanel.add(controlPanel, BorderLayout.EAST); // Chat und Buttons

        this.sendButton.addActionListener(e -> this.messageEvent());
        this.chatInputField.addActionListener(e -> this.messageEvent());
        this.withdrawButton.addActionListener(e -> this.withdrawEvent());
        this.computerMoveButton.addActionListener(e -> this.computerMoveEvent());

        // Haupt-Panel zum Fenster hinzufügen
        this.window.add(mainPanel);
    }

    /**
     * Schließt das Spiele-Fenster
     */
    public void close() {
        this.window.dispose();
    }

    /**
     * Deaktiviert die Option aufzugeben
     */
    public void disableWithdraw() {
        this.withdrawButton.setEnabled(false);
    }

    /**
     * Legt fest, ob der Computer ein Zug machen darf
     *
     * @param isEnabled true, falls man den Computer anweisen darf einen Zug zu
     *                  unternehmen, ansonsten false.
     */
    public void enableComputerMove(final boolean isEnabled) {
        this.computerMoveButton.setEnabled(isEnabled);
    }

    /**
     * Gibt das Spielfeld des Gegners zurück. Auf dieses kann dann beispielsweise
     * gezeichnet werden.
     *
     * @return Das Spielfeld des Gegners
     */
    public PlaygroundMatrix getOpponentField() {
        return this.opponentField;
    }

    /**
     * Gibt das Spielfeld des Spielers zurück. Auf dieses kann dann beispielsweise
     * gezeichnet werden.
     *
     * @return Das Spielfeld des Spielers
     */
    public PlaygroundMatrix getPlayersField() {
        return this.playersField;
    }

    /**
     * Legt fest, ob der aktuelle Spieler gerade an der Reihe ist oder der Gegner
     * und aktiviert bzw. deaktkviert die entsprechenden GUI-Elemente.
     *
     * @param isPlayersTurn true, falls der Spieler am Zug ist, sonst, falls der
     *                      Gegner am Zug ist, false.
     */
    public void playersTurn(final boolean isPlayersTurn) {
        this.enableComputerMove(isPlayersTurn);
    }

    /**
     * Legt einen Handler fest, der aufgerufen werden soll, wenn der Computer einen
     * Zug machen soll.
     *
     * @param computermovehandler Der Handler
     */
    public void setComputerMoveHandler(final ClickHandler computermovehandler) {
        this.computermovehandler = computermovehandler;
    }

    /**
     * Legt einen Handler fest, welcher aufgerufen werden soll, wenn der Spieler
     * eine Chatnachricht senden möchte.
     *
     * @param messagehandler Der Handler
     */
    public void setMessageHandler(final MessageHandler messagehandler) {
        this.messagehandler = messagehandler;
    }

    /**
     * Legt einen Handler fest, welcher aufgerufen werden soll, wenn der Spieler
     * aufgibt.
     *
     * @param withdrawhandler Der Handler
     */
    public void setWithdrawHandler(final ClickHandler withdrawhandler) {
        this.withdrawhandler = withdrawhandler;
    }

    /**
     * Zeigt das Spiele-Fenster an.
     */
    public void show() {
        this.window.pack();
        this.window.setVisible(true);
    }

    /**
     * Schreibt eine Nachricht in einem bestimmten Style in den Chat-Verlauf
     *
     * @param text  Die Nachricht
     * @param style Der Style
     */
    public void writeMessage(final String text, final Style style) {
        try {
            final StyledDocument sd = this.chatOutputPane.getStyledDocument();
            sd.insertString(sd.getLength(), text + "\n", style);
        } catch (final BadLocationException e) {
            this.logger.log(Level.SEVERE, "Error when displaying the text message", e);
        }
    }

    /**
     * Schreibt eine Chatnachricht vom Gegner in den Chat-Verlauf
     *
     * @param text Die Chatnachricht
     */
    public void writeMessageFromPeer(final String text) {
        this.writeMessage(text, this.chatPeerStyle);
    }

    /**
     * Schreibt eine Chatnachricht vom System in den Chat-Verlauf
     *
     * @param text Die Chatnachricht
     */
    public void writeMessageFromSystem(final String text) {
        this.writeMessage(text, this.chatSystemStyle);
    }

    /**
     * Schreibt eine Chatnachricht vom Spieler in den Chat-Verlauf
     *
     * @param text Die Chatnachricht
     */
    public void writeMessageFromUser(final String text) {
        this.writeMessage(text, this.chatUserStyle);
    }

    /**
     * Löst ein ComputerMoveEvent aus. Der Computer soll einen Zug machen.
     */
    private void computerMoveEvent() {
        this.playersTurn(false);
        if (this.computermovehandler != null) {
            try {
                new Thread() {
                    @Override
                    public void run() {
                        try {
                            GameWindow.this.computermovehandler.click();
                        } catch (final Exception e) {
                            GameWindow.this.logger.log(Level.SEVERE, "Error computer moving.", e);
                            SwingUtilities.invokeLater(
                                    () -> JOptionPane.showMessageDialog(
                                            GameWindow.this.window, "Error computer moving: " + e.getMessage(), "Error",
                                            JOptionPane.ERROR_MESSAGE
                                    )
                            );
                        }
                    }
                }.start();
            } catch (final Exception e) {
                this.logger.log(Level.SEVERE, "Error", e);
                JOptionPane
                        .showMessageDialog(this.window, "Error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    /**
     * Löst ein messageEvent aus. Der Nutzer hat eine Chatnachricht abgesendet.
     */
    private void messageEvent() {
        this.chatInputField.setEnabled(false);
        this.sendButton.setEnabled(false);

        final String text = this.chatInputField.getText();
        this.chatInputField.setText("");
        this.chatInputField.setEnabled(true);
        this.sendButton.setEnabled(true);

        if (this.messagehandler != null && !text.isBlank()) {
            try {
                new Thread() {
                    @Override
                    public void run() {
                        try {
                            GameWindow.this.messagehandler.message(Utils.sanitizeString(text));
                        } catch (final Exception e) {
                            GameWindow.this.logger.log(Level.SEVERE, "Error connecting to the server.", e);
                            SwingUtilities.invokeLater(
                                    () -> JOptionPane.showMessageDialog(
                                            GameWindow.this.window, "Error connecting to the server: " + e.getMessage(),
                                            "Error", JOptionPane.ERROR_MESSAGE
                                    )
                            );
                        }
                    }
                }.start();
            } catch (final Exception e) {
                this.logger.log(Level.SEVERE, "Error", e);
                JOptionPane
                        .showMessageDialog(this.window, "Error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    /**
     * Löst ein withdrawEvent aus. Der Benutzer hat aufgegeben.
     */
    private void withdrawEvent() {
        this.disableWithdraw();
        this.enableComputerMove(false);
        if (this.withdrawhandler != null) {
            try {
                new Thread() {
                    @Override
                    public void run() {
                        try {
                            GameWindow.this.withdrawhandler.click();
                        } catch (final Exception e) {
                            GameWindow.this.logger.log(Level.SEVERE, "Error withdrawing.", e);
                            SwingUtilities.invokeLater(
                                    () -> JOptionPane.showMessageDialog(
                                            GameWindow.this.window, "Error withdrawing: " + e.getMessage(), "Error",
                                            JOptionPane.ERROR_MESSAGE
                                    )
                            );
                        }
                    }
                }.start();
            } catch (final Exception e) {
                this.logger.log(Level.SEVERE, "Error", e);
                JOptionPane
                        .showMessageDialog(this.window, "Error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
    }
}
