/**
 * 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 java.util.Arrays;

/**
 * Model for a Reversi game. <a href="http://en.wikipedia.org/wiki/Reversi">Reversi</a>
 * is a game played by two players ({@link #WHITE White} and {@link #BLACK Black}) on
 * an 8x8 board. Board positions are 0-63; starting at 0 in the upper-left corner then
 * travelling horizontally row by row until position 63 in the bottom-right corner of
 * the board.<p>
 *
 * This class maintains game state as individual turns are taken until the game is
 * {@link #isGameFinished() finished}. {@link #BLACK Black} always starts the game.
 * Two stones for each player are always placed in the center of the game board at
 * the start of each game (following standard Reversi rules).
 *
 * @author Matt Tucker
 */
public class ReversiModel {

    /**
     * Blank.
     */
    public static final int BLANK = 0;

    /**
     * White.
     */
    public static final int WHITE = 1;

    /**
     * Black.
     */
    public static final int BLACK = 2;

    private final int[] board;
    private int currentPlayer;
    private boolean gameFinished = false;
    private final int[] flipBuffer = new int[7];
    private int flipBufferSize = 0;

    /**
     * Constructs a new game instance and sets up initial board positions.
     */
    public ReversiModel() {
        board = new int[64];
        // Set all positions to blank.
        Arrays.fill(board, BLANK);
        // Setup initial positions.
        board[27] = WHITE;
        board[28] = BLACK;
        board[35] = BLACK;
        board[36] = WHITE;

        // Black always starts.
        currentPlayer = BLACK;
    }

    /**
     * Returns true if the game is finished (i.e., no more moves are possible.). The winner
     * is the player with the highest score when the game is finished.
     *
     * @return true if the game is finished.
     */
    public boolean isGameFinished() {
        return gameFinished;
    }

    /**
     * Returns true if the current player can place a stone at the specified position.
     *
     * @param position the position on the board (0-63).
     * @return true if the position is a valid move.
     */
    public boolean isValidMove(int position) {
        return !gameFinished && isValidMove(position, getCurrentPlayer());
    }

    /**
     * Returns the current value at a specific board position (between 0 and 63, inclusive). The value
     * is either {@link #BLANK blank}, {@link #WHITE white}, or {@link #BLACK black}.
     *
     * @param position the board position.
     * @return the value at the specified board position.
     */
    public int getBoardValue(int position) {
        if (position < 0 || position > 63) {
            throw new IllegalArgumentException("Invalid position: " + position + ". Valid board " +
                "positions are 0 through 63");
        }
        return board[position];
    }

    /**
     * Returns the current player: {@link #WHITE white} or {@link #BLACK black}. Note that
     * it's possible for a player to have several turns in a row, depending on whether
     * there's a valid move available for the other player. The {@link #BLACK black} player
     * always starts the game.
     *
     * @return the current player.
     */
    public int getCurrentPlayer() {
        return currentPlayer;
    }

    /**
     * Returns the black player's current score.
     *
     * @return the black player's current score.
     */
    public int getWhiteScore() {
        int score = 0;
        for (int aBoard : board) {
            if (aBoard == WHITE) {
                score++;
            }
        }
        return score;
    }

    /**
     * Returns the white player's current score.
     *
     * @return the white player's current score.
     */
    public int getBlackScore() {
        int score = 0;
        for (int aBoard : board) {
            if (aBoard == BLACK) {
                score++;
            }
        }
        return score;
    }

    /**
     * Causes the {@link #getCurrentPlayer() current player} to execute a move by placing a stone at
     * the specified position. Executing a move will flip one or more of the other player's stones.
     *
     * @param position the position to make the move at.
     * @return true if the move was successful; false otherwise.
     */
    public boolean makeMove(int position) {
        if (!isValidMove(position)) {
            return false;
        }
        board[position] = currentPlayer;
        // Execute the move
        getFlipCount(position, currentPlayer, true);

        // See if other user has a valid move. If so, switch to other player.
        int otherPlayer = currentPlayer == WHITE ? BLACK : WHITE;
        boolean hasMove = false;
        for (int i = 0; i < board.length; i++) {
            if (isValidMove(i, otherPlayer)) {
                hasMove = true;
                break;
            }
        }

        // If the other player has at least one valid move, switch players.
        if (hasMove) {
            currentPlayer = otherPlayer;
        }
        // Otherwise, stick with the current player.
        else {
            // Make sure the current player has a valid move. Otherwise, the game must be over.
            hasMove = false;
            for (int i = 0; i < board.length; i++) {
                if (isValidMove(i, currentPlayer)) {
                    hasMove = true;
                    break;
                }
            }
            if (!hasMove) {
                gameFinished = true;
            }
        }

        return true;
    }

    /**
     * Returns a String representation of the current game board (ASCII art). A fixed
     * width font is necessary to display the String properly.
     *
     * @return a String representation of the current game board.
     */
    public String printBoard() {
        StringBuilder buf = new StringBuilder();
        buf.append("+----------------+\n");
        for (int i = 0; i < 64; i++) {
            if (i % 8 == 0) {
                buf.append("|");
            }
            if (board[i] == BLANK) {
                buf.append("  ");
            } else if (board[i] == WHITE) {
                buf.append(" w");
            } else {
                buf.append(" b");
            }
            if (i % 8 == 7) {
                buf.append("|\n");
            }
        }
        buf.append("+----------------+");
        return buf.toString();
    }

    /**
     * Returns true if the specified player has a valid move at the specified position (ignoring
     * whose turn it is currently).
     *
     * @param position the position.
     * @param player the player.
     * @return return true if the player has a move at the position.
     */
    private boolean isValidMove(int position, int player) {
        if (position < 0 || position > 63) {
            return false;
        }
        if (board[position] != BLANK) {
            return false;
        } else {
            return getFlipCount(position, player, false) > 0;
        }
    }

    /**
     * Computes the number of stones that would be flipped by executing a move at the specified position.
     * If <tt>doFlip</tt> is true, the game board will be updated.
     *
     * @param position the position for the move.
     * @param player the player.
     * @param doFlips true if stones should be actually flipped; false if flips should only
     *      be calculated hypothetically.
     * @return the number of stones that were or that would be flipped by the move.
     */
    private synchronized int getFlipCount(int position, int player, boolean doFlips) {
        int flipCount = 0;

        // Traverse vertically, horizontally, and diagonally to flip pieces.

        // Going left horizontally.

        boolean edge;

        if (position % 8 > 1) {
            edge = false;
            for (int i = position - 1; !edge; i--) {
                if (i % 8 == 0) edge = true;
                if (board[i] == BLANK) {
                    break;
                } else if (board[i] == player) {
                    for (int j = 0; j < flipBufferSize; j++) {
                        flipCount++;
                        if (doFlips) {
                            board[flipBuffer[j]] = player;
                        }
                    }
                    break;
                }
                flipBuffer[flipBufferSize++] = i;
            }
            // Reset flip buffer.
            flipBufferSize = 0;
        }
        // Going right horizontally.
        if (position % 8 < 6) {
            edge = false;
            for (int i = position + 1; !edge; i++) {
                if (i % 8 == 7) edge = true;
                if (board[i] == BLANK) {
                    break;
                } else if (board[i] == player) {
                    for (int j = 0; j < flipBufferSize; j++) {
                        flipCount++;
                        if (doFlips) {
                            board[flipBuffer[j]] = player;
                        }
                    }
                    break;
                }
                flipBuffer[flipBufferSize++] = i;
            }
            // Reset flip buffer.
            flipBufferSize = 0;
        }
        // Going up vertically.
        if (position > 15) {
            for (int i = position - 8; i >= 0; i -= 8) {
                if (board[i] == BLANK) {
                    break;
                } else if (board[i] == player) {
                    for (int j = 0; j < flipBufferSize; j++) {
                        flipCount++;
                        if (doFlips) {
                            board[flipBuffer[j]] = player;
                        }
                    }
                    break;
                }
                flipBuffer[flipBufferSize++] = i;
            }
            // Reset flip buffer.
            flipBufferSize = 0;
        }
        // Going down vertically.
        if (position < 48) {
            for (int i = position + 8; i <= 64; i += 8) {
                if (board[i] == BLANK) {
                    break;
                } else if (board[i] == player) {
                    for (int j = 0; j < flipBufferSize; j++) {
                        flipCount++;
                        if (doFlips) {
                            board[flipBuffer[j]] = player;
                        }
                    }
                    break;
                }
                flipBuffer[flipBufferSize++] = i;
            }
            // Reset flip buffer.
            flipBufferSize = 0;
        }
        // Diagonal up-left
        if (position > 15 && position % 8 > 1) {
            edge = false;
            for (int i = position - 9; !edge; i -= 9) {
                if (i < 8 || i % 8 == 0) edge = true;
                if (board[i] == BLANK) {
                    break;
                } else if (board[i] == player) {
                    for (int j = 0; j < flipBufferSize; j++) {
                        flipCount++;
                        if (doFlips) {
                            board[flipBuffer[j]] = player;
                        }
                    }
                    break;
                }
                flipBuffer[flipBufferSize++] = i;
            }
            // Reset flip buffer.
            flipBufferSize = 0;
        }
        // Diagonal up-right
        if (position > 15 && position % 8 < 6) {
            edge = false;
            for (int i = position - 7; !edge; i -= 7) {
                if (i < 8 || i % 8 == 7) edge = true;
                if (board[i] == BLANK) {
                    break;
                } else if (board[i] == player) {
                    for (int j = 0; j < flipBufferSize; j++) {
                        flipCount++;
                        if (doFlips) {
                            board[flipBuffer[j]] = player;
                        }
                    }
                    break;
                }
                flipBuffer[flipBufferSize++] = i;
            }
            // Reset flip buffer.
            flipBufferSize = 0;
        }
        // Diagonal down-left
        if (position < 48 && position % 8 > 1) {
            edge = false;
            for (int i = position + 7; !edge; i += 7) {
                if (i > 55 || i % 8 == 0) edge = true;
                if (board[i] == BLANK) {
                    break;
                } else if (board[i] == player) {
                    for (int j = 0; j < flipBufferSize; j++) {
                        flipCount++;
                        if (doFlips) {
                            board[flipBuffer[j]] = player;
                        }
                    }
                    break;
                }
                flipBuffer[flipBufferSize++] = i;
            }
            // Reset flip buffer.
            flipBufferSize = 0;
        }
        // Diagonal down-right
        if (position < 48 && position % 8 < 6) {
            edge = false;
            for (int i = position + 9; !edge; i += 9) {
                if (i > 55 || i % 8 == 7) edge = true;
                if (board[i] == BLANK) {
                    break;
                } else if (board[i] == player) {
                    for (int j = 0; j < flipBufferSize; j++) {
                        flipCount++;
                        if (doFlips) {
                            board[flipBuffer[j]] = player;
                        }
                    }
                    break;
                }
                flipBuffer[flipBufferSize++] = i;
            }
            // Reset flip buffer.
            flipBufferSize = 0;
        }

        return flipCount;
    }
}
