package cpsc2150.extendedConnectX;

import cpsc2150.extendedConnectX.models.*;
import java.util.*;

/**
 * Class that contains the main method for running the ExtendedConnectX game, as well as
 * methods to start the game, clear the board, manipulate the current turn, play tokens, and
 * decide whether to play again (among others).
 *
 * @author Owen Sullivan
 * @version 1.0
 *
 * @invariant
 *      currentTurn != ' ' AND
 *      players.get() = [ uppercase character ]
 */
public class GameScreen {

    public static final int MIN_PLAYERS = 2;
    public static final int MAX_PLAYERS = 10;

    private static ArrayList<Character> players;
    private static char currentTurn;
    private static IGameBoard board;

    /**
     * Main function that runs the ExtendedConnectX game.
     *
     * @param args Arguments passed from command-line
     */
    public static void main(String[] args) {

        // Create Scanner object for getting user input
        Scanner input = new Scanner(System.in);

        // Assume we're playing again (to ensure the loop runs at least once)
        char playAgain = 'Y';
        // Loop while playAgain is Y (upper or lower case)
        while (playAgain == 'Y' || playAgain == 'y') {

            // Call helper function startGame to initialize
            startGame(input);
            // Print the empty board
            System.out.println(board.toString());

            // Get the column the user wants to play a token in by calling helper function inputColumn
            int column = inputColumn(input);
            // While true (loops until a break statement, which will always be triggered)
            while (true) {

                // Check to make sure the column has room
                while (!board.checkIfFree(column)) {

                    // If not, prompt again (validating input)
                    System.out.println("Column is full");
                    column = inputColumn(input);

                }

                // Place token in user-specified column
                board.placeToken(currentTurn, column);

                // Print the board with the new token placed
                System.out.println(board.toString());

                // If the latest token placed results in a win, print the winner and break the inner loop
                if (board.checkForWin(column)) {

                    System.out.println("Player " + currentTurn + " Won!");
                    break;

                    // If the latest token placed results in a tie, print a tie message and break the inner loop
                } else if (board.checkTie()) {

                    System.out.println("It's a tie!");
                    break;

                }

                // Call helper function nextTurn to advance the turn
                nextTurn();
                // Get the next column to place a token in from the user by calling helper function inputColumn
                column = inputColumn(input);

            }

            // Call helper function inputPlayAgain to prompt the user to play again
            playAgain = inputPlayAgain(input);

        }

    }

    /**
     * Starts the ExtendedConnectX game.
     *
     * @param input Scanner object used for getting user input
     *
     * @pre
     *      [ input must be initialized as a Scanner object ] AND [ input must not be closed ]
     *
     * @post
     *      currentTurn = [ the first player token in #players ] AND
     *      board = [ newly initialized GameBoard object ] AND players = inputPlayers
     */
    private static void startGame(Scanner input) {

        // Call helper function inputPlayers to initialize list of player tokens
        players = inputPlayers(input);

        // Set currentTurn to the first player token
        currentTurn = players.get(0);

        // Call helper functions to get number of rows, number of columns, number to win, and implementation
        int numRows = inputNumRows(input);
        int numColumns = inputNumColumns(input);
        int numToWin = inputNumToWin(input, numRows, numColumns);
        char implementation = inputImplementation(input);

        // If the user has chosen the fast implementation, call GameBoard constructor
        if (implementation == 'F') {

            // Create new game board, passing number of rows, number of columns, and number to win arguments
            board = new GameBoard(numRows, numColumns, numToWin);

            // Otherwise, call GameBoardMem constructor
        } else {

            // Create new game board, passing number of rows, number of columns, and number to win arguments
            board = new GameBoardMem(numRows, numColumns, numToWin);

        }

    }

    /**
     * Advances the game by switching the character representing whose turn it currently is to
     * the next player's token.
     *
     * @post
     *      currentTurn = [ character at index in #players following the index of #currentTurn in #players ] AND
     *      players = #players AND
     *      board = #board
     */
    private static void nextTurn() {

        // If the current player's token is the last in the players list, set currentTurn to
        // the first token in the list
        if (players.indexOf(currentTurn) == players.size() - 1) {

            currentTurn = players.get(0);

            // Otherwise, advance to the next token in the list
        } else {

            currentTurn = players.get(players.indexOf(currentTurn) + 1);

        }

    }

    /**
     * Prompts the user for input for the number of players and tokens for each player, then
     * returns an ArrayList of player tokens for use in the game.
     *
     * @param input Scanner object used for getting user input
     * @return ArrayList of player tokens
     *
     * @pre
     *      [ input must be initialized as a Scanner object ] AND [ input must not be closed ]
     *
     * @post
     *      inputPlayers = [ list of user-specified player tokens with size defined by user input ] AND
     *      currentTurn = #currentTurn AND players = #players AND board = #board
     */
    private static ArrayList<Character> inputPlayers(Scanner input) {

        // Prompt user for number of players and take in next integer
        System.out.println("How many players?");
        int numPlayers = Integer.parseInt(input.nextLine());

        // Validate user input
        while (numPlayers < MIN_PLAYERS || numPlayers > MAX_PLAYERS) {

            // If numPlayers is too low, print error
            if (numPlayers < MIN_PLAYERS) {

                System.out.println("Must be at least " + MIN_PLAYERS + " players");

                // If numPlayers is too high, print error
            } else {

                System.out.println("Must be " + MAX_PLAYERS + " players or fewer");

            }

            System.out.println("How many players?");
            numPlayers = Integer.parseInt(input.nextLine());

        }

        // Initialize players list
        players = new ArrayList<Character>();

        // Loop once for each player that requires a token
        for (int i = 0; i < numPlayers; i++) {

            // Prompt user to enter next player token and take in a character
            System.out.println("Enter the character to represent player " + (i + 1));
            char newToken = Character.toUpperCase(input.nextLine().charAt(0));

            // Validate input
            while (players.contains(newToken)) {

                // Print error message
                System.out.println(newToken + " is already taken as a player token!");

                System.out.println("Enter the character to represent player " + (i + 1));
                newToken = Character.toUpperCase(input.nextLine().charAt(0));

            }

            // Add token to players list
            players.add(newToken);

        }

        return players;

    }

    /**
     * Prompts the user for input for how many rows the game board should have.
     *
     * @param input Scanner object used for getting user input
     * @return Number of rows entered by the user
     *
     * @pre
     *      [ input must be initialized as a Scanner object ] AND [ input must not be closed ]
     *
     * @post
     *      inputNumRows = [ integer from user input ] AND
     *      currentTurn = #currentTurn AND players = #players AND board = #board
     */
    private static int inputNumRows(Scanner input) {

        // Prompt user for the number of rows and take in next integer
        System.out.println("How many rows should be on the board?");
        int numRows = Integer.parseInt(input.nextLine());

        // Validate user input
        while (numRows < IGameBoard.MIN_ROWS || numRows > IGameBoard.MAX_ROWS) {

            // If numRows is too low, print error
            if (numRows < IGameBoard.MIN_ROWS) {

                System.out.println("Must be at least " + IGameBoard.MIN_ROWS + " rows");

                // If numRows is too high, print error
            } else {

                System.out.println("Must be " + IGameBoard.MAX_ROWS + " rows or fewer");

            }

            System.out.println("How many rows should be on the board?");
            numRows = Integer.parseInt(input.nextLine());

        }

        return numRows;

    }

    /**
     * Prompts the user for input for how many columns the game board should have.
     *
     * @param input Scanner object used for getting user input
     * @return Number of columns entered by the user
     *
     * @pre
     *      [ input must be initialized as a Scanner object ] AND [ input must not be closed ]
     *
     * @post
     *      inputNumColumns = [ integer from user input ] AND
     *      currentTurn = #currentTurn AND players = #players AND board = #board
     */
    private static int inputNumColumns(Scanner input) {

        // Prompt user for the number of columns and take in next integer
        System.out.println("How many columns should be on the board?");
        int numColumns = Integer.parseInt(input.nextLine());

        // Validate user input
        while (numColumns < IGameBoard.MIN_COLUMNS || numColumns > IGameBoard.MAX_COLUMNS) {

            // If numColumns is too low, print error
            if (numColumns < IGameBoard.MIN_COLUMNS) {

                System.out.println("Must be at least " + IGameBoard.MIN_COLUMNS + " columns");

                // If numColumns is too high, print error
            } else {

                System.out.println("Must be " + IGameBoard.MAX_COLUMNS + " columns or fewer");

            }

            System.out.println("How many columns should be on the board?");
            numColumns = Integer.parseInt(input.nextLine());

        }

        return numColumns;

    }

    /**
     * Prompts the user for input for how many consecutive tokens should be required to win.
     *
     * @param input Scanner object used for getting user input
     * @param rows Number of rows in board
     * @param columns Number of columns in board
     * @return Number of consecutive tokens entered by the user
     *
     * @pre
     *      [ input must be initialized as a Scanner object ] AND [ input must not be closed ] AND
     *      MIN_ROWS {@code <=} rows {@code <} MAX_ROWS AND
     *      MIN_COLUMNS {@code <=} columns {@code <} MAX_COLUMNS
     *
     * @post
     *      inputNumToWin = [ integer from user input ] AND
     *      currentTurn = #currentTurn AND players = #players AND board = #board
     */
    private static int inputNumToWin(Scanner input, int rows, int columns) {

        // Prompt user for the number to win and take in next integer
        System.out.println("How many in a row to win?");
        int numToWin = Integer.parseInt(input.nextLine());

        // Validate user input
        while (numToWin < IGameBoard.MIN_NUM_TO_WIN || numToWin > IGameBoard.MAX_NUM_TO_WIN || numToWin > rows || numToWin > columns) {

            // If numToWin is too low, print error
            if (numToWin < IGameBoard.MIN_NUM_TO_WIN) {

                System.out.println("Must be at least " + IGameBoard.MIN_NUM_TO_WIN + " in a row");

                // If numToWin is too high, print error
            } else if (numToWin > IGameBoard.MAX_NUM_TO_WIN) {

                System.out.println("Must be " + IGameBoard.MAX_NUM_TO_WIN + " in a row or fewer");

                // If numToWin is greater than the number of rows, print error
            } else if (numToWin > rows) {

                System.out.println("Must be " + rows + " in a row or fewer");

                // If numToWin is greater than the number of columns, print error
            } else {

                System.out.println("Must be " + columns + " in a row or fewer");

            }

            System.out.println("How many in a row to win?");
            numToWin = Integer.parseInt(input.nextLine());

        }

        return numToWin;

    }

    /**
     * Prompts the user for input for which game implementation to use, then returns the input.
     *
     * @param input Scanner object used for getting user input
     * @return Character option entered by the user
     *
     * @pre
     *      [ input must be initialized as a Scanner object ] AND [ input must not be closed ]
     *
     * @post
     *      inputImplementation = [ character from user input ] AND
     *      currentTurn = #currentTurn AND players = #players AND board = #board
     */
    private static char inputImplementation(Scanner input) {

        // Prompt user for an implementation and take in char input
        System.out.println("Would you like a Fast Game (F/f) or a Memory Efficient Game (M/m)?");
        char implementation = input.nextLine().charAt(0);

        // Validate user input
        while (implementation != 'F' && implementation != 'f' && implementation != 'M' && implementation != 'm') {

            // Print error
            System.out.println("Please enter F or M");

            System.out.println("Would you like a Fast Game (F/f) or a Memory Efficient Game (M/m)?");
            implementation = input.nextLine().charAt(0);

        }

        // Return uppercase of user input
        return Character.toUpperCase(implementation);

    }

    /**
     * Prompts the user for input for what column to place a token in, then returns that column number.
     *
     * @param input Scanner object used for getting user input
     * @return Column number entered by the user
     *
     * @pre
     *      [ input must be initialized as a Scanner object ] AND [ input must not be closed ]
     *
     * @post
     *      inputColumn = [ integer from user input ] AND
     *      currentTurn = #currentTurn AND players = #players AND board = #board
     */
    private static int inputColumn(Scanner input) {

        // Prompt user for a column number and take in next integer
        System.out.println("Player "+ currentTurn + ", what column do you want to place your marker in?");
        int column = Integer.parseInt(input.nextLine());

        // Validate user input
        while (column < 0 || column > board.getNumColumns() - 1) {

            // If column is too low, print error
            if (column < 0) {

                System.out.println("Column cannot be less than 0");

                // If column is too high, print error
            } else {

                System.out.println("Column cannot be greater than " + (board.getNumColumns() - 1));

            }

            System.out.println("Player "+ currentTurn + ", what column do you want to place your marker in?");
            column = Integer.parseInt(input.nextLine());

        }

        return column;

    }

    /**
     * Prompts the user for input for whether to play again, then returns the input.
     *
     * @param input Scanner object used for getting user input
     * @return Character option entered by the user
     *
     * @pre
     *      [ input must be initialized as a Scanner object ] AND [ input must not be closed ]
     *
     * @post
     *      inputPlayAgain = [ character from user input ] AND
     *      currentTurn = #currentTurn AND players = #players AND board = #board
     */
    private static char inputPlayAgain(Scanner input) {

        // Prompt user to play again and take in char input
        System.out.println("Would you like to play again? Y/N");
        char playAgain = input.nextLine().charAt(0);

        // Validate user input
        while (playAgain != 'Y' && playAgain != 'y' && playAgain != 'N' && playAgain != 'n') {

            System.out.println("Would you like to play again? Y/N");
            playAgain = input.nextLine().charAt(0);

        }

        return playAgain;

    }

}
