package game.logic;

import game.training.FeatureCount;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class GameBoard {
	/*************
	 * Constants
	 *************/
	/* Board size */
	public static final int NumOfRows = 6;
	public static final int NumOfColumns = 8;
	
	/* Cell values */
	public static final int EmptyCell = 0;
	public static final int RedPlayer = 1;
	public static final int BlackPlayer = 2;
	public static final int TieGame = 0;
	
	/* The state features as defined in the specification document. */
	
	/* ******************
	 * Player1 Features
	 * ******************
	 */
	/* Row features */
	public static final int TwoSideThreeInARow = 0;
	public static final int OneSideThreeInARow = 1;
	public static final int AllRowThrees = 2;
	public static final int ThreeInARowHole1 = 3; 
	public static final int ThreeInARowHole2 = 4; 
	public static final int TwoInARow = 5;
	
	/* Column features */
	public static final int ThreeInACol = 6;
	
	/* Diagonal features */
	public static final int TwoSideThreeInADiagonal = 7;
	public static final int OneSideThreeInADiagonal = 8;
	public static final int AllDiagonalThrees = 9;
	public static final int ThreeInADiagonalHole1 = 10;
	public static final int ThreeInADiagonalHole2 = 11; 
	public static final int TwoInADiagonal = 12;
	
	/* Additional features */
	public static final int OneMoveToWin = 13;
	public static final int ProhibitedMove = 14;
	public static final int PotentialFour = 15;
	public static final int WinColumns = 16;
	public static final int OneMoveToWinHole = 17;
	
	
	/* ******************
	 * Player2 Features
	 * ******************
	 */
	/* Row features */
	public static final int OpTwoSideThreeInARow = 18;
	public static final int OpOneSideThreeInARow = 19;
	public static final int OpAllRowThrees = 20;
	public static final int OpThreeInARowHole1 = 21;
	public static final int OpThreeInARowHole2 = 22;
	public static final int OpTwoInARow = 23;
	
	/* Column features */
	public static final int OpThreeInACol = 24;

	
	/* Diagonal features */
	public static final int OpTwoSideThreeInADiagonal = 25;
	public static final int OpOneSideThreeInADiagonal = 26;
	public static final int OpAllDiagonalThrees = 27;
	public static final int OpThreeInADiagonalHole1 = 28;
	public static final int OpThreeInADiagonalHole2 = 29;
	public static final int OpTwoInADiagonal = 30;

	
	/* Additional features */
	public static final int OpOneMoveToWin = 31;
	public static final int OpProhibitedMove = 32;
	public static final int OpPotentialFour = 33;
	public static final int OpWinColumns = 34;
	public static final int OpOneMoveToWinHole = 35;
	
	/* Vector sizes */
	public static final int VectorSize = 36;
	public static final int PlayerVectorSize = VectorSize / 2;
	
	/* Forced Win - all the possibilities to win in one move.
	 * Prohibited Moves - all the moves that if the player will make them,
	 * it will lose. */
	public List<Integer> Player1ForcedWin;
	public List<Integer> Player1ProhibitedMoves;
	public List<Integer> Player2ForcedWin;
	public List<Integer> Player2ProhibitedMoves;
	
	/****************
	 * Class fields
	 ****************/
	/* A 2D-array representation of the board */
	private int[][] board;
	private int totalDiscs;
	private int[] discsPerColumn;
	private int highestDisc;
	
	/* The state vector of the game. Will be updated throughout the game. */
	private Vector stateVector;
	
	/* A list of possible moves */
	private List<Integer> possibleMoves;
	
	private int winnerPlayer;
	private int currentPlayer;
	
	public int player1Four;
	public int player2Four;
	
	/**
	 * This constructor initializes all relevant data structures of the board.
	 * @param currentPlayer the player that starts the game.
	 */
	public GameBoard(int currentPlayer){
		this.board = new int[NumOfColumns][NumOfRows];
		this.stateVector = new Vector(VectorSize);
		this.stateVector.setZero();
		
		this.totalDiscs = 0;
		this.highestDisc = 0;
		this.discsPerColumn = new int[NumOfColumns];
		this.winnerPlayer = 0;
		this.currentPlayer = currentPlayer;
		
		this.possibleMoves = new ArrayList<Integer>();
		
		/* At first, all moves are possible */
		for (int i = 0; i < NumOfColumns; i++){
			this.possibleMoves.add(i);
		}
		
		player1Four = 0;
		player2Four = 0;
		
		Player1ForcedWin = new ArrayList<Integer>();
		Player1ProhibitedMoves = new ArrayList<Integer>();
		Player2ForcedWin = new ArrayList<Integer>();
		Player2ProhibitedMoves = new ArrayList<Integer>();
	}
	
	/* Getters */
	public Vector getStateVector(){
		return this.stateVector;
	}
	
	public int getTotalDiscs(){
		return this.totalDiscs;
	}
	
	public List<Integer> getPossibleMoves(){
		return this.possibleMoves;
	}
	
	public int getWinnerPlayer(){
		return this.winnerPlayer;
	}
	
	public int[][] getBoard(){
		return this.board;
	}
	
	public int getCurrentPlayer(){
		return this.currentPlayer;
	}
	
	public int[] getDiscsPerColumn(){
		return this.discsPerColumn;
	}
	
	public int getHighestDisc(){
		return this.highestDisc;
	}
	
	/* Setters */
	public void setTotalDiscs(int totalDiscs){
		this.totalDiscs = totalDiscs;
	}
	
	public void setHighestDisc(int highestDisc){
		this.highestDisc = highestDisc;
	}
	
	public void setDiscsPetColumn(int[] array){
		for (int i = 0; i < GameBoard.NumOfColumns; i++){
			this.discsPerColumn[i] = array[i];
		}
	}
	
	public void setBoard(int[][] b1){
		for (int i = 0; i < GameBoard.NumOfColumns; i++){
			for (int j = 0; j < GameBoard.NumOfRows; j++){
				this.board[i][j] = b1[i][j];
			}
		}
	}
	
	public void setPossibleMoves(List<Integer> list){
		this.possibleMoves.clear();
		for (int i = 0; i < list.size(); i++){
			this.possibleMoves.add(list.get(i));
		}
	}
	
	public void setStateVector(Vector state){
		for (int i = 0; i < state.getSize(); i++){
			this.stateVector.setValue(i, state.getValue(i));
		}
	}
	
	public void setWinnerPlayer(int winner){
		this.winnerPlayer = winner;
	}
	
	public void setCurrentPlayer(int player){
		this.currentPlayer = player;
	}
	
	/**
	 * Insert a disc to the specified column, and update the representation of the board.
	 * 
	 * @param columnIndex the column to insert the disc into.
	 * @param player the player that the disc belongs to.
	 */
	public void insertDisc(int columnIndex, int player){
		
		if (isColumnFull(columnIndex)){
			System.out.println("Cannot insert to column with index = " + columnIndex);
			return;
		}
		
		/* Update 2D-array of the board, discs per column and total discs. */
		this.board[columnIndex][discsPerColumn[columnIndex]] = player;
		this.discsPerColumn[columnIndex]++;
		this.totalDiscs++;
		
		/* Update highest disc if necessary. */
		if (this.discsPerColumn[columnIndex] > this.highestDisc){
			this.highestDisc = this.discsPerColumn[columnIndex];
		}
		
		/* If the column is full, remove it from list of allowed moves. */
		if (this.discsPerColumn[columnIndex] == NumOfRows){
			this.possibleMoves.remove(this.possibleMoves.indexOf(columnIndex));
		}
		
		this.player1Four = 0;
		this.player2Four = 0;
		
		/* Update the state vector after the play */
		this.updateStateVector();
	}
	
	/**
	 * Removes the disc on the top of the given column from the board. The action is needed 
	 * for the MiniMax algorithm.
	 * 
	 * @param columnIndex the column to remove the top disc from.
	 */
	public void removeDisc(int columnIndex){
		
		/* If the column was full, then it was removed from the possible moves list.
		 * Need to insert it back. */
		if (isColumnFull(columnIndex)){
			this.possibleMoves.add(columnIndex);
		}
		
		this.board[columnIndex][discsPerColumn[columnIndex] - 1] = EmptyCell;
		this.totalDiscs--;
		discsPerColumn[columnIndex]--;
		this.highestDisc = searchHighestDisc();
		
		this.player1Four = 0;
		this.player2Four = 0;
		
		/* Update the state vector after the removal play */
		this.updateStateVector();
	}
	
	/**
	 * Update the state vector so that it will hold features for [current player, opponent player].
	 */
	public void updateStateVector(){
		Vector player1Features = FeatureCount.CountFeatures(this, RedPlayer);
		Vector player2Features = FeatureCount.CountFeatures(this, BlackPlayer);
		
		for (int i = 0; i < VectorSize; i++){
			if (i < PlayerVectorSize){
				this.stateVector.setValue(i, player1Features.getValue(i));
			} else {
				this.stateVector.setValue(i, player2Features.getValue(i - PlayerVectorSize));
			}
		}
		
	}
	
	/**
	 * This procedure searches the highest disc in the board and returns this height.
	 * The height is measured by the number of discs in that column.
	 * 
	 * @return the height of the highest disc.
	 */
	public int searchHighestDisc(){
		int highest = 0;
		
		for (int i = 0; i < NumOfColumns; i++){
			if (highest < this.discsPerColumn[i]){
				highest = this.discsPerColumn[i];
			}
		}
		
		return highest;
	}
	
	/**
	 * This procedure checks whether a given column is full.
	 * 
	 * @param columnIndex the column we want to check.
	 */
	public boolean isColumnFull(int columnIndex){
		return (this.discsPerColumn[columnIndex] == NumOfRows);
	}
	
	/**
	 * This procedure tells us whether the game is over or not.
	 * The game is over once one of the players has a four in a row,
	 * or the game board is full (a tie game).
	 * 
	 * @return true iff the game is over.
	 */
	public boolean isOver(){
		if (this.player1Four > 0 || this.player2Four > 0){
			return true;
		} else if (this.totalDiscs == NumOfRows * NumOfColumns){
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * This procedure decides who is the winner, and updates the winner representation accordingly.
	 */
	public void decideWinner(){
		if (this.player1Four > 0){
			this.setWinnerPlayer(GameBoard.RedPlayer);
		} else if (this.player2Four > 0){
			this.setWinnerPlayer(GameBoard.BlackPlayer);
		} else if (this.totalDiscs == NumOfRows * NumOfColumns){
			this.setWinnerPlayer(TieGame);
		}
	}
	
	/**
	 * This function sets the four-in-a-row representation variables. It will be updated
	 * throughout the feature counts, and will be used to decide the winner.
	 * @param player The player that should be given the four-in-a-row.
	 * @param fours The number of four-in-a-row counted.
	 */
	public void setFour(int player, int fours){
		if (player == GameBoard.RedPlayer){
			player1Four += fours;
		} else {
			player2Four += fours;
		}
	}
	
	/**
	 * Switch players. The next player in a two player game can be denoted by
	 * 3-currentPlayer.
	 */
	public void switchPlayer(){
		this.currentPlayer = 3 - this.currentPlayer;
	}
	
	/**
	 * Create a copy of the given board in order to pass it to the MiniMax algorithm.
	 * @param b1 the board to be copied.
	 * @return a new game board holding the same values as the given board.
	 */
	public static GameBoard CopyBoard(GameBoard b1){
		GameBoard b2 = new GameBoard(b1.getCurrentPlayer());
		b2.setBoard(b1.getBoard());
		b2.setTotalDiscs(b1.getTotalDiscs());
		b2.setDiscsPetColumn(b1.getDiscsPerColumn());
		b2.setHighestDisc(b1.getHighestDisc());
		b2.setWinnerPlayer(b1.getWinnerPlayer());
		b2.setCurrentPlayer(b1.getCurrentPlayer());
		b2.setStateVector(b1.getStateVector());
		b2.setPossibleMoves(b1.getPossibleMoves());
		
		return b2;
	}
	
	/**
	 * Make a fine print of the given board, for debugging purposes.
	 * @param board the board to be printed to screen.
	 */
	public static void printBoard(int[][] board){
		System.out.println("Board status:");
		
		for (int i = 0; i < NumOfColumns; i++){
			System.out.print(" _");
		}
		System.out.println();
		
		for (int i = NumOfRows - 1; i >= 0; i--){
			for (int j = 0; j < NumOfColumns; j++){
				if (j == NumOfColumns - 1){
					System.out.println("|" + board[j][i] + "|");
				} else {
					System.out.print("|" + board[j][i]);
				}
			}
		}
		
		for (int i = 0; i < NumOfColumns; i++){
			System.out.print(" -");
		}
		System.out.println();
	}
	
	/**
	 * Generate a random array of sizes, that can be used to create a random board
	 * (mainly for debugging purposes).
	 * 
	 * @return an array of random sizes.
	 */
	public static int[] generateRandomSizes(){
		int[] d = new int[NumOfColumns];
		Random r = new Random();
		
		for (int i = 0; i < NumOfColumns; i++){
			d[i] = r.nextInt(NumOfRows + 1);
		}
		
		return d;
	}
	
	/**
	 * Generate a random yet plausible board (by plausible we mean that no disc
	 * is hanging in thin air - cannot have a disc in a cell that some cell beneath it is empty).
	 * This is done mainly for debugging reasons, and checking the feature counts.
	 * 
	 * @param sizes random size of the columns.
	 * 
	 * @return a 2D-array representing the board.
	 */
	public static int[][] generateRandomBoard(int[] sizes){
		int[][] b = new int[NumOfColumns][NumOfRows];
		Random r = new Random();
		
		for (int i = 0; i < NumOfColumns; i++){
			int limit = sizes[i];
			for (int j = 0; j < limit; j++){
				b[i][j] = r.nextInt(2) + 1;
			}
		}
		
		return b;
	}
}
