/**
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.game.reversi;

import javax.swing.*;

import java.util.*;
import java.util.List;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.filter.StanzaExtensionFilter;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.StandardExtensionElement;
import org.jivesoftware.smack.packet.StanzaBuilder;
import org.jivesoftware.spark.util.log.Log;
import org.jxmpp.jid.Jid;

/**
 * The game UI, which is created after both players have accepted a new game.
 *
 * @author Bill Lynch
 */
public class ReversiPanel extends JPanel {

    private static final long serialVersionUID = 3591458286918924065L;
    private static final int BOARD_SIZE = 320;
    private static final int INFO_PANEL_HEIGHT = 50;
    private static final int BORDER_SIZE = 5;
    public static final int TOTAL_WIDTH = BOARD_SIZE + (BORDER_SIZE * 2);
    public static final int TOTAL_HEIGHT = TOTAL_WIDTH + INFO_PANEL_HEIGHT; // frame width + 50 for the info panel
    private static final int NUM_BLOCKS = 8;
    private static final int BLOCK_SIZE = BOARD_SIZE / NUM_BLOCKS;

    private static final int DISC_SIZE = (int) (BLOCK_SIZE * 0.8); // 80% of block size

    private final XMPPConnection connection;
    private final int otherPlayer;
    private final int gameID;
    private final Jid opponentJID;
    private StanzaListener gameMoveListener;

    private final List<ReversiBlock> blocks = new ArrayList<>();
    // Main game object
    private final ReversiModel reversi;

    // All images used by the game.

    private final Image imageBackground = ReversiRes.getImageIcon(ReversiRes.REVERSI_BOARD).getImage();
    private final Image imageScoreWhite = ReversiRes.getImageIcon(ReversiRes.REVERSI_SCORE_WHITE).getImage();
    private final Image imageScoreBlack = ReversiRes.getImageIcon(ReversiRes.REVERSI_SCORE_BLACK).getImage();
    private final Image imageTurnBlack = ReversiRes.getImageIcon(ReversiRes.REVERSI_LABEL_BLACK).getImage();
    private final Image imageTurnWhite = ReversiRes.getImageIcon(ReversiRes.REVERSI_LABEL_WHITE).getImage();
    private final Image imageButtonResign = ReversiRes.getImageIcon(ReversiRes.REVERSI_RESIGN).getImage();
    private final Image imageYou = ReversiRes.getImageIcon(ReversiRes.REVERSI_YOU).getImage();
    private final Image imageThem = ReversiRes.getImageIcon(ReversiRes.REVERSI_THEM).getImage();

    /**
     * Creates a new Reversi panel.
     *
     * @param connection     Connection associated.
     * @param gameID         Game ID number
     * @param startingPlayer Whether we are the starting player or not
     * @param opponentJID    JID of opponent
     */
    public ReversiPanel(XMPPConnection connection, final int gameID, boolean startingPlayer, Jid opponentJID) {
        this.connection = connection;
        this.gameID = gameID;
        this.opponentJID = opponentJID;
        otherPlayer = startingPlayer ? ReversiModel.WHITE : ReversiModel.BLACK;

        // Load all images.

        // Start the game
        reversi = new ReversiModel();

        if (connection != null) {
            gameMoveListener = stanza -> {
                GameMove move = stanza.getExtension(GameMove.class);
                // If this is a move for the current game.
                if (move.getGameID() == gameID) {
                    int position = move.getPosition();
                    // Make sure that the other player is allowed to make the move right now.
                    if (reversi.getCurrentPlayer() == otherPlayer && reversi.isValidMove(position)) {
                        reversi.makeMove(position);
                        // Redraw board.
                        ReversiPanel.this.repaint();
                    }
                    // TODO: other user automatically forfeits!
                    // else {
                    // }
                    // Execute move.
                }
            };

            connection.addAsyncStanzaListener(gameMoveListener,
                new StanzaExtensionFilter(GameMove.ELEMENT_NAME, GameMove.NAMESPACE));
            // TODO: at end of game, remove listener.
        }

        setOpaque(false);

        // Use absolute layout
        setLayout(null);
        // Set its size:
        setPreferredSize(new Dimension(TOTAL_WIDTH, TOTAL_HEIGHT));

        // Make a new panel which is the game board grid:
        JPanel reversiBoard = new JPanel(new GridLayout(NUM_BLOCKS, NUM_BLOCKS, 0, 0));
        reversiBoard.setOpaque(false);
        for (int i = 0; i < NUM_BLOCKS * NUM_BLOCKS; i++) {
            ReversiBlock block = new ReversiBlock(this, i);
            blocks.add(block);
            reversiBoard.add(block);
        }
        // Add the reversi board to the main panel:
        add(reversiBoard);
        // Position it:
        reversiBoard.setBounds(BORDER_SIZE, BORDER_SIZE, BOARD_SIZE, BOARD_SIZE);
        // TODO: listen for click on resign button!!
    }

    /**
     * Sends a forfeit message to the other player.
     */
    public void sendForfeit() throws SmackException.NotConnectedException {
        StandardExtensionElement forfeit = StandardExtensionElement.builder(GameForfeit.ELEMENT_NAME, GameForfeit.NAMESPACE)
            .addElement("gameID", Integer.toString(gameID))
            .build();
        Message message = connection.getStanzaFactory()
            .buildMessageStanza()
            .to(opponentJID)
            .addExtension(forfeit)
            .build();

        try {
            connection.sendStanza(message);
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        } finally {
            connection.removeAsyncStanzaListener(gameMoveListener);
        }
    }

    @Override
    public void paintComponent(Graphics g) {
        // Turn on anti-aliasing.
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON
        );

        // Background
        g.drawImage(imageBackground, 0, 0, null);

        // Draw info panel components.

        // Draw the score.
        g.drawImage(imageScoreWhite, 3, BOARD_SIZE + BORDER_SIZE * 2 + 7, null);
        g.drawImage(imageScoreBlack, 3, BOARD_SIZE + BORDER_SIZE * 2 + 27, null);
        g.setFont(new Font("SansSerif", Font.BOLD, 12));
        String whiteScore = String.valueOf(reversi.getWhiteScore());
        String blackScore = String.valueOf(reversi.getBlackScore());
        FontMetrics fm = g.getFontMetrics();
        int width = Math.max(fm.stringWidth(whiteScore), fm.stringWidth(blackScore));
        g.drawString(whiteScore, imageScoreBlack.getWidth(null) + 7 + width - fm.stringWidth(whiteScore),
            BOARD_SIZE + BORDER_SIZE * 2 + 22);
        g.drawString(blackScore, imageScoreWhite.getWidth(null) + 7 + width - fm.stringWidth(blackScore),
            BOARD_SIZE + BORDER_SIZE * 2 + 42);

        // Draw who's turn it is.
        if (!reversi.isGameFinished()) {
            if (reversi.getCurrentPlayer() == ReversiModel.BLACK) {
                g.drawImage(imageTurnBlack, 116, BOARD_SIZE + BORDER_SIZE * 2 + 11, null);
            } else {
                g.drawImage(imageTurnWhite, 116, BOARD_SIZE + BORDER_SIZE * 2 + 11, null);
            }
        } else {
            int me = otherPlayer == ReversiModel.BLACK ? ReversiModel.WHITE : ReversiModel.BLACK;
            String whoWins = "Draw";
            if (reversi.getBlackScore() > reversi.getWhiteScore()) {
                if (me == ReversiModel.BLACK)
                    whoWins = "YOU WIN!";
                else
                    whoWins = "YOU LOST!";
            } else if (reversi.getBlackScore() < reversi.getWhiteScore()) {
                if (me == ReversiModel.WHITE)
                    whoWins = "YOU WIN!";
                else
                    whoWins = "YOU LOST!";
            }
            g.drawString(whoWins, 130, BOARD_SIZE + BORDER_SIZE * 2 + 20);
        }
        if (reversi.getCurrentPlayer() == otherPlayer) {
            g.drawImage(imageThem, 163, BOARD_SIZE + BORDER_SIZE * 2 + 31, null);
        } else {
            g.drawImage(imageYou, 163, BOARD_SIZE + BORDER_SIZE * 2 + 31, null);
        }

        // The resign button.
        g.drawImage(imageButtonResign, 281, BOARD_SIZE + BORDER_SIZE * 2 + 17, null);
    }

    /**
     * A Reversi block (one of the squares of the grid).
     */
    public class ReversiBlock extends JPanel {

        private static final long serialVersionUID = -8504469339731900770L;
        private final ReversiPanel ui;
        private final int index;

        public ReversiBlock(ReversiPanel ui, int index) {
            super();
            this.ui = ui;
            this.index = index;
            setPreferredSize(new Dimension(BLOCK_SIZE, BLOCK_SIZE));
            addMouseListener(new ReversiBlockMouseListener(this));
            setOpaque(false);
        }

        /**
         * Returns a handle on the game UI.
         *
         * @return ReversiPanel of block.
         */
        public ReversiPanel getReversiUI() {
            return ui;
        }

        /**
         * This block's index (0->63).
         *
         * @return Index of block
         */
        public int getIndex() {
            return index;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            // Turn on anti-aliasing:
            ((Graphics2D) g).setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON
            );

            // Draw a disc in the block if the game says we should.
            int boardValue = reversi.getBoardValue(index);
            if (boardValue == org.jivesoftware.game.reversi.ReversiModel.BLACK) {
                drawDisc(g, Color.BLACK);
            } else if (reversi.getBoardValue(index) == org.jivesoftware.game.reversi.ReversiModel.WHITE) {
                drawDisc(g, Color.WHITE);
            }
        }

        /**
         * Draws the disc.
         *
         * @param g     Graphics to draw
         * @param color Color
         */
        private void drawDisc(Graphics g, Color color) {
            int position = BLOCK_SIZE - ((BLOCK_SIZE + DISC_SIZE) / 2);
            g.setColor(color);
            g.fillOval(position, position, DISC_SIZE, DISC_SIZE);
        }
    }

    /**
     * A mouse listener for a Reversi block.
     */
    public class ReversiBlockMouseListener extends MouseAdapter {

        private final ReversiBlock block;

        public ReversiBlockMouseListener(ReversiBlock block) {
            this.block = block;
        }

        /**
         * Highlight the block if this block is a valid move.
         */
        @Override
        public void mouseEntered(MouseEvent e) {
            super.mouseEntered(e);
            if (reversi.getCurrentPlayer() != otherPlayer && reversi.isValidMove(block.getIndex())) {
                block.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                block.setBorder(BorderFactory.createLineBorder(Color.WHITE));
            }
        }

        /**
         * Set the block color back to the default.
         */
        @Override
        public void mouseExited(MouseEvent e) {
            super.mouseExited(e);
            block.setCursor(Cursor.getDefaultCursor());
            block.setBorder(null);
        }

        /**
         * If the click box is a valid move, register a move in this box.
         */
        @Override
        public void mouseClicked(MouseEvent e) {
            super.mouseClicked(e);
            // Make sure that it's our turn and that it's a valid move.
            if (reversi.getCurrentPlayer() != otherPlayer && reversi.isValidMove(block.getIndex())) {
                // Update the game model.
                reversi.makeMove(block.getIndex());

                // Send the move to the other player.
                GameMove move = new GameMove();
                move.setGameID(gameID);
                move.setPosition(block.getIndex());
                Message message = StanzaBuilder.buildMessage()
                    .addExtension(move)
                    .to(opponentJID)
                    .build();
                try {
                    connection.sendStanza(message);
                } catch (SmackException.NotConnectedException | InterruptedException e1) {
                    Log.warning("Unable to send move to " + message.getTo(), e1);
                }
                // Repaint board.
                ReversiPanel.this.repaint();
                // Repaint all blocks.
//                for (Iterator it = block.getReversiUI().getBlocks().iterator(); it.hasNext();) {
//                    ReversiBlock component = (ReversiBlock)it.next();
//                    component.repaint();
//                }

            }
        }
    }

    public List<ReversiBlock> getBlocks() {
        return blocks;
    }
}
