package cpsc2150.extendedConnectX.models;

/**
 * Interface that contains methods for implementing an entire game board for the ExtendedConnectX game.
 * <p>This interface includes methods to place tokens, check token locations,
 * and determine whether or not the game has been won or is over.</p>
 *
 * @author Owen Sullivan
 * @version 1.0
 *
 * Initialization ensures:
 *      Game board contains no tokens, i.e., no spaces are populated or all spaces are a single space character.
 *
 * Defines:
 *      number_of_rows = Z
 *      number_of_columns = Z
 *      num_to_win = Z
 *
 * Constraints:
 *      MIN_ROWS {@code <=} number_of_rows {@code <} MAX_ROWS
 *      MIN_COLUMNS {@code <=} number_of_columns {@code <} MAX_COLUMNS
 *      MIN_NUM_TO_WIN {@code <=} num_to_win {@code <=} MAX_NUM_TO_WIN
 *
 */
public interface IGameBoard {

    int MIN_ROWS = 3;
    int MAX_ROWS = 100;
    int MIN_COLUMNS = 3;
    int MAX_COLUMNS = 100;
    int MIN_NUM_TO_WIN = 3;
    int MAX_NUM_TO_WIN = 25;

    /**
     * Function that returns true if the column can accept another
     * token; false otherwise.
     *
     * @param c Column to be checked
     * @return Whether the column can accept another token
     *
     * @pre
     *      0 {@code <=} c {@code <} number_of_columns
     *
     * @post
     *      checkIfFree = true iff ([ #self at column c and row number_of_rows - 1 ] = 'X' OR [ #self at column c and row number_of_rows - 1 ] = 'O') AND self = #self
     *      checkIfFree = false iff ([ #self at column c and row number_of_rows - 1 ] = ' ') AND self = #self
     */
    default boolean checkIfFree(int c) {

        // Create a BoardPosition object where the row is the top possible row and the column is the parameter c
        BoardPosition pos = new BoardPosition(getNumRows() - 1, c);

        // Call primary method whatsAtPos and check to see if return value is a space, indicating an empty position
        if (whatsAtPos(pos) == ' ') {

            return true;

        } else {

            return false;

        }

    }

    /**
     * Function that places the character p in column c. The token will be placed in
     * the lowest available row in column c.
     *
     * @param p Character to be placed (representing player token)
     * @param c Column to place token in
     *
     * @pre
     *      (p = 'X' OR p = 'O') AND 0 {@code <= } c {@code <} number_of_columns
     *
     * @post
     *      [ #self at column c and lowest available row ] = 'X' OR [ #self at column c and lowest available row ] = 'O' AND
     *      [ token is placed at the lowest available row ] AND
     *      [ self = #self plus the new token ]
     */
    void placeToken(char p, int c);

    /**
     * This function will check to see if the last token placed in
     * column c resulted in the player winning the game. If so it will return
     * true, otherwise false.
     *
     * @param c Column number to be checked
     * @return Whether placing a token results in a player winning the game
     *
     * @pre
     *      0 {@code <= } c {@code <} number_of_columns AND
     *      [ c is the column where the latest token was placed ]
     *
     * @post
     *      checkForWin = true iff (checkTie = false AND (checkHorizWin = true OR checkVertWin = true OR checkDiagWin = true) WHERE [ c is the column with the last placed token ] AND self = #self
     *      checkForWin = false iff (checkTie = true OR (checkHorizWin = false AND checkVertWin = false AND checkDiagWin = false) WHERE [ c is the column with the last placed token ] AND self = #self
     */
    default boolean checkForWin(int c) {

        // Call primary method checkTie to determine if there is a tie
        if (checkTie()) {

            // If so, return false immediately
            return false;

        }

        // Create a new BoardPosition object to start looking for a player token from the top of the supplied column
        BoardPosition pos = new BoardPosition(getNumRows() - 1, c);
        // Variable to track row subtraction
        int i = 0;
        // Loop until a player token is found in the column, or until we're at the last row
        while (whatsAtPos(pos) == ' ' && getNumRows() - 1 - i >= 1) {

            i++;
            // Re-assign a new BoardPosition one row down (after incrementing i)
            pos = new BoardPosition((getNumRows() - 1 - i), c);

        }

        // If any of the win check methods return true, the game has a winner
        // Use primary method whatsAtPos to stand in for the last placed token character
        if (checkHorizWin(pos, whatsAtPos(pos)) || checkVertWin(pos, whatsAtPos(pos)) || checkDiagWin(pos, whatsAtPos(pos))) {

            return true;

            // If none of the win check methods return true, the game does not have a winner
        } else {

            return false;

        }

    }

    /**
     * This function will check to see if the game has resulted in a
     * tie. A game is tied if there are no free board positions remaining.
     * It will return true if the game is tied and false otherwise.
     *
     * @return Whether the game has resulted in a tie
     *
     * @pre
     *      [ the game has not ended with a win yet ]
     *
     * @post
     *      checkTie = true iff ([all board positions are not ' ']) AND self = #self
     *      checkTie = false iff ([not all board positions are not ' ']) AND self = #self
     */
    default boolean checkTie() {

        // Loop through rows in game board
        for (int i = 0; i < getNumColumns(); i++) {

            // Loop through columns in game board
            for (int j = 0; j < getNumColumns(); j++) {

                // Create a new BoardPosition object with the current row and column
                BoardPosition pos = new BoardPosition(i, j);

                // Call primary method whatsAtPos to check if position is empty
                if (whatsAtPos(pos) == ' ') {

                    // Return false if so
                    return false;

                }

            }

        }

        // If the program has executed to this point, return true because all spaces have a token
        return true;

    }

    /**
     * This function checks to see if the last token placed (which was placed in
     * position pos by player p) resulted in 5 in a row horizontally. Returns
     * true if it does, otherwise false.
     *
     * @param pos BoardPosition pair of row and column
     * @param p Player to check with
     * @return Whether the last token placed resulted in 5 in a row horizontally
     *
     * @pre
     *      (0 {@code >=} pos.row {@code <} number_of_rows AND 0 {@code >=} pos.column {@code <} number_of_columns) AND
     *      p != ' ' AND [ pos is the game board position on the latest play ] AND
     *      [ p is the token located at position pos ]
     *
     * @post
     *      checkHorizWin = true iff ([num_to_win horizontal positions are p]) AND self = #self
     *      checkHorizWin = true iff ([num_to_win horizontal positions are not p]) AND self = #self
     */
    default boolean checkHorizWin(BoardPosition pos, char p) {

        // Create a counter for the number of matches (starting at 0)
        int numMatches = 0;
        // Create a bool that holds the status of whether matches are unbroken in sequence
        boolean unbroken = true;

        // Create a new BoardPosition object starting on the right side of the same row
        BoardPosition newPos = new BoardPosition(pos.getRow(), getNumColumns() - 1);

        // Loop while the column is valid and we haven't found enough matches yet
        while (newPos.getColumn() >= 0 && numMatches < getNumToWin()) {

            // If the position contains the player token and the line is unbroken
            if (whatsAtPos(newPos) == p && unbroken) {

                numMatches++;

                // If the position does not contain the player token, the line is broken
            } else {

                unbroken = false;

            }

            // If the line is broken, set the number of matches to zero and assume
            // the next line will be unbroken
            if (!unbroken) {

                numMatches = 0;
                unbroken = true;

            }

            // If we've found enough matches, return true
            if (numMatches == getNumToWin()) {

                return true;

            }

            // Set the new position one left of the current column
            newPos = new BoardPosition(pos.getRow(), newPos.getColumn() - 1);

        }

        return false;

    }

    /**
     * This function checks to see if the last token placed (which was placed in
     * position pos by player p) resulted in 5 in a row vertically. Returns
     * true if it does, otherwise false.
     *
     * @param pos BoardPosition pair of row and column
     * @param p Player to check with
     * @return Whether the last token placed resulted in 5 in a row vertically
     *
     * @pre
     *      (0 {@code >=} pos.row {@code <} number_of_rows AND 0 {@code >=} pos.column {@code <} number_of_columns) AND
     *      p != ' ' AND [ pos is the game board position on the latest play ] AND
     *      [ p is the token located at position pos ]
     *
     * @post
     *      checkVertWin = true iff ([num_to_win vertical positions are p]) AND self = #self
     *      checkVertWin = true iff ([num_to_win vertical positions are not p]) AND self = #self
     */
    default boolean checkVertWin(BoardPosition pos, char p) {

        // Create a counter for the number of matches (starting at 0)
        int numMatches = 0;
        // Create a bool that holds the status of whether matches are unbroken in sequence
        boolean unbroken = true;

        // Create a new BoardPosition object starting on the top of the same column
        BoardPosition newPos = new BoardPosition(getNumRows() - 1, pos.getColumn());

        // Loop while the row is valid and we haven't found enough matches yet
        while (newPos.getRow() >= 0 && numMatches < getNumToWin()) {

            // If the position contains the player token and the line is unbroken
            if (whatsAtPos(newPos) == p && unbroken) {

                numMatches++;

                // If the position does not contain the player token, the line is broken
            } else {

                unbroken = false;

            }

            // If the line is broken, set the number of matches to zero and assume
            // the next line will be unbroken
            if (!unbroken) {

                numMatches = 0;
                unbroken = true;

            }

            // If we've found enough matches, return true
            if (numMatches == getNumToWin()) {

                return true;

            }

            // Set the new position one left of the current column
            newPos = new BoardPosition(newPos.getRow() - 1, pos.getColumn());

        }

        return false;

    }

    /**
     * This function checks to see if the last token placed (which was placed in
     * position pos by player p) resulted in 5 in a row diagonally. Returns
     * true if it does, otherwise false.
     *
     * @param pos BoardPosition pair of row and column
     * @param p Player to check with
     * @return Whether the last token placed resulted in 5 in a row diagonally
     *
     * @pre
     *      (0 {@code >=} pos.row {@code <} number_of_rows AND 0 {@code >=} pos.column {@code <} number_of_columns) AND
     *      p != ' ' AND [ pos is the game board position on the latest play ] AND
     *      [ p is the token located at position pos ]
     *
     * @post
     *      checkVertWin = true iff ([num_to_win diagonal positions are p]) AND self = #self
     *      checkVertWin = true iff ([num_to_win diagonal positions are not p]) AND self = #self
     */
    default boolean checkDiagWin(BoardPosition pos, char p) {

        // Create a counter for the number of matches (including initial)
        int numMatches = 1;
        // Create a bool that holds the status of whether matches are unbroken in sequence
        boolean unbroken = true;

        // Create a new BoardPosition object starting on the adjacent bottom left space
        BoardPosition newPos = new BoardPosition(pos.getRow() - 1, pos.getColumn() - 1);
        // Loop while the row and column are valid and we haven't found enough matches yet
        while (newPos.getRow() >= 0 && newPos.getColumn() >= 0 && numMatches < getNumToWin()) {

            // If the position contains the player token and the line is unbroken
            if (whatsAtPos(newPos) == p && unbroken) {

                numMatches++;

                // If the position does not contain the player token, the line is broken
            } else {

                unbroken = false;

            }

            // If the line is broken, set the number of matches to zero and assume
            // the next line will be unbroken
            if (!unbroken) {

                numMatches = 0;
                unbroken = true;

            }

            // If we've found enough matches, return true
            if (numMatches == getNumToWin()) {

                return true;

            }

            // Set the new position one left of and one below the current position
            newPos = new BoardPosition(newPos.getRow() - 1, newPos.getColumn() - 1);

        }

        // If numMatches is 0 (line was broken at last check), reset numMatches (including initial)
        if (numMatches == 0) {

            numMatches = 1;

        }

        // Update newPos to start on the adjacent top right space
        newPos = new BoardPosition(pos.getRow() + 1, pos.getColumn() + 1);
        // Loop while the row and column are valid and we haven't found enough matches yet
        while (newPos.getRow() < getNumRows() && newPos.getColumn() < getNumColumns() && numMatches < getNumToWin()) {

            // If the position contains the player token and the line is unbroken
            if (whatsAtPos(newPos) == p && unbroken) {

                numMatches++;

                // If the position does not contain the player token, the line is broken
            } else {

                unbroken = false;

            }

            // If the line is broken, set the number of matches to zero and assume
            // the next line will be unbroken
            if (!unbroken) {

                numMatches = 0;
                unbroken = true;

            }

            // If we've found enough matches, return true
            if (numMatches == getNumToWin()) {

                return true;

            }

            // Set the new position one right of and one above the current position
            newPos = new BoardPosition(newPos.getRow() + 1, newPos.getColumn() + 1);

        }

        // Reset numMatches (including initial)
        numMatches = 1;

        // Update newPos to start on the adjacent bottom right space
        newPos = new BoardPosition(pos.getRow() - 1, pos.getColumn() + 1);
        // Loop while the row and column are valid and we haven't found enough matches yet
        while (newPos.getRow() >= 0 && newPos.getColumn() < getNumColumns() && numMatches < getNumToWin()) {

            // If the position contains the player token and the line is unbroken
            if (whatsAtPos(newPos) == p && unbroken) {

                numMatches++;

                // If the position does not contain the player token, the line is broken
            } else {

                unbroken = false;

            }

            // If the line is broken, set the number of matches to zero and assume
            // the next line will be unbroken
            if (!unbroken) {

                numMatches = 0;
                unbroken = true;

            }

            // If we've found enough matches, return true
            if (numMatches == getNumToWin()) {

                return true;

            }

            // Set the new position one right of and one below the current position
            newPos = new BoardPosition(newPos.getRow() - 1, newPos.getColumn() + 1);

        }

        // If numMatches is 0 (line was broken at last check), reset numMatches (including initial)
        if (numMatches == 0) {

            numMatches = 1;

        }

        // Update newPos to start on the adjacent top left space
        newPos = new BoardPosition(pos.getRow() + 1, pos.getColumn() - 1);
        // Loop while the row and column are valid and we haven't found enough matches yet
        while (newPos.getRow() < getNumRows() && newPos.getColumn() >= 0 && numMatches < getNumToWin()) {

            // If the position contains the player token and the line is unbroken
            if (whatsAtPos(newPos) == p && unbroken) {

                numMatches++;

                // If the position does not contain the player token, the line is broken
            } else {

                unbroken = false;

            }

            // If the line is broken, set the number of matches to zero and assume
            // the next line will be unbroken
            if (!unbroken) {

                numMatches = 0;
                unbroken = true;

            }

            // If we've found enough matches, return true
            if (numMatches == getNumToWin()) {

                return true;

            }

            // Set the new position one left of and one above the current position
            newPos = new BoardPosition(newPos.getRow() + 1, newPos.getColumn() - 1);

        }

        return false;

    }

    /**
     * Function that returns what is in the GameBoard at position pos.
     * If no marker is there, it returns a blank space char.
     *
     * @param pos BoardPosition pair of row and column
     * @return Character representing player token or space
     *
     * @pre
     *      (0 {@code >=} pos.row {@code <} number_of_rows AND 0 {@code >=} pos.column {@code <} number_of_columns)
     *
     * @post
     *      whatsAtPos = [ board at row pos.row and column pos.col ] AND self = #self
     */
    char whatsAtPos(BoardPosition pos);

    /**
     * This function returns true if the player is at pos; otherwise, it returns
     * false.
     *
     * @param pos BoardPosition pair of row and column
     * @param player Player to check with
     * @return Whether a player's token is at a position
     *
     * @pre
     *      (0 {@code >=} pos.row {@code <} number_of_rows AND 0 {@code >=} pos.column {@code <} number_of_columns) AND
     *      player != ' '
     *
     * @post
     *      isPlayerAtPos = true iff ([ board at row pos.row and column pos.col ] = player) AND self = #self
     *      isPlayerAtPos = false iff ([ board at row pos.row and column pos.col ] != player) AND self = #self
     */
    default boolean isPlayerAtPos(BoardPosition pos, char player) {

        // Call primary method whatsAtPos and check the result against player parameter
        if (whatsAtPos(pos) == player) {

            return true;

        } else {

            return false;

        }

    }

    /**
     * Returns the number of rows in the GameBoard.
     *
     * @return The number of rows
     *
     * @post
     *      getNumRows = number_of_rows AND self = #self
     */
    int getNumRows();

    /**
     * Returns the number of columns in the GameBoard.
     *
     * @return The number of columns
     *
     * @post
     *      getNumColumns = number_of_columns AND self = #self
     */
    int getNumColumns();

    /**
     * Returns the number of tokens in a row needed to win the game.
     *
     * @return The number of tokens
     *
     * @post
     *      getNumToWin = num_to_win AND self = #self
     */
    int getNumToWin();
    
}
