package player.gamer.statemachine.sadna;

/**
 * Represent Connect Four game board
 */
public class Board {

	// consts
	public static final int ATTR_VEC_SIZE = 27;
	public static final int HEIGHT = 6;
	public static final int WIDTH = 8;
	private static final double MIDDLE_COLUMN = 3.5;
	private static final double MIDDLE_ROW = 2.5;
	// indexes for attributes vector
	public static final int FOUR_COINS_INDEX = 0; //0-1
	public static final int THREE_EVEN_INDEX = 2; //2-5
	public static final int THREE_ODD_INDEX = 6; //6-9
	public static final int THREE_COLUMN_INDEX = 10; //10-11
	public static final int TWO_COINS_INDEX = 12; //12-17
	public static final int ONE_COIN_INDEX = 18; //18-21
	public static final int DISTANCE_FROM_CENTER = 22; //22
	public static final int IMMEDIATE_THREAT = 23; //23,24
	public static final int DOUBLE_THREAT = 25; //25, 26
	
	private Coin[][] gameBoard = new Coin [WIDTH][HEIGHT];
	private int heights[] = new int[WIDTH];
	private boolean didWin;
	private Coin lastPlayedCoin;
	
	private int immidiateThreatPlayerCounter;
	private int immidiateThreatOpponentCounter;
	
	private Coin[][] doubleThreat;
	
    public enum LineType {
        ROW(0),
        DIAGONAL(1),
        COLUMN(2);
        
        private int typeNum;
        
        private LineType(int typeNum) {
            this.typeNum = typeNum;
        }
        
        public int getLineNum() {
            return typeNum;
        }
    }
    
	/*--- Public Methods --*/

	/**
	 * @param column The column to drop the coin in
	 * @param coin Player coin or Opponent coin
	 */
	public void setCoin(int column, Coin coin) {
		gameBoard[column][heights[column]] = coin;
		heights[column]++;
		
		lastPlayedCoin = coin;
	}
	
	/**
	 * @param column The column the highest coin should be removed from
	 */
	public void removeCoin(int column) {
		heights[column]--;
		gameBoard[column][heights[column]] = null;
	}
	
	public Coin getCoin(int column, int row) {
		return gameBoard[column][row];
	}

	/**
	 * @return The row of the first available square in the given column 
	 */
	public int getHeight(int column) {
		return heights[column];
	}
	
	/**
	 * Calculate if forced moved exists on the board.
	 * Force move exists if putting the given coin in the given column will win the game -
	 * the coin will complete 4 in a row, column or diagonal.
	 * @param column The column the coin is played on
	 * @param coin The next coin to play
	 * @return True if force move exists, otherwise false
	 */
	public boolean forcedMoveExists(int column, Coin coin) {
		int row = getHeight(column);

		int count = 0;
		// check row
		for (int i = 1; column-i >= 0 && count < 3 ; i++) {
			if (getCoin(column-i, row) == coin) {
				count++;
			} else {
				break;
			}
		}
		
		for (int i = 1; column+i < WIDTH && count < 3 ; i++) {
			if (getCoin(column+i, row) == coin) {
				count++;
			} else {
				break;
			}
		}

		if (count >= 3) {
			return true;
		}
		
		count = 0;
		
		// check column
		for (int i = 1; row-i >= 0 && count < 3 ; i++) {
			if (getCoin(column, row-i) == coin) {
				count++;
			} else { 
				break;
			}
		}
		
		if (count >= 3) {
			return true;
		}
		
		count = 0;
		
		// check diagonal asc
		for (int i = 1; column-i >= 0 && row-i >= 0 && count < 3 ; i++) {
			if (getCoin(column-i, row-i) == coin) {
				count++;
			} else {
				break;
			}
		}
		
		for (int i = 1; column+i < WIDTH && row+i < HEIGHT && count < 3 ; i++) {
			if (getCoin(column+i, row+i) == coin) {
				count++;
			} else {
				break;
			}
		}

		if (count >= 3) {
			return true;
		}
		
		count = 0;
		
		// check diagonal desc
		for (int i = 1; column-i >= 0 && row+i < HEIGHT && count < 3 ; i++) {
			if (getCoin(column-i, row+i) == coin) {
				count++;
			} else {
				break;
			}
		}
		
		for (int i = 1; column+i < WIDTH && row-i >= 0 && count < 3 ; i++) {
			if (getCoin(column+i, row-i) == coin) {
				count++;
			} else {
				break;
			}
		}

		if (count >= 3) {
			return true;
		}
		
		return false;
	}
	
	public void setDidWin(boolean didWin) {
		this.didWin = didWin;
	}
	
	public boolean didWin() {
		return didWin;
	}

	/**
	 * Go over the board and calculate the attributes vector. Every 4 adjacent squares
	 * are tested to see if they hold one of the attributes.
	 * For example, this situation: _ _ O O O _ _ _ will be counted as: 2 coins in a row
	 * (for the first 4 squares), 3 coins in a row (for the second 4 squares), 3 coins in a
	 * row, 2 coins in a row and one coin in a row. Also, three times one coin in a column.
	 * This situation: X O O O X (where X is Red coin and O is black coin) however will not
	 * be counted as three in a row - since the black coins are blocked.   
	 * @return Attributes vector, every index holds the number of times the attribute
	 * appears on the board
	 */
	public int[] calclateAttributeVector() {
		int[] attVec = new int[ATTR_VEC_SIZE];
		int[] squares = new int[8];
		Coin nextCoin = getOtherCoin(lastPlayedCoin);
		
		int playerCenterDist = 0;
		int opponentCenterDist = 0;
		
		immidiateThreatPlayerCounter = 0;
		immidiateThreatOpponentCounter = 0;
		doubleThreat = new Coin[WIDTH][HEIGHT];
		
		// go over every 4 adjacent squares in the board
		for (int column = 0; column < WIDTH; column++) {
			for (int row = 0; row < HEIGHT; row++) {
				squares[0] = column;
				squares[1] = row;
				// row
				if (column < 5) {
					squares[2] = column + 1;
					squares[3] = row;
					squares[4] = column + 2;
					squares[5] = row;
					squares[6] = column + 3;
					squares[7] = row;
					checkFour(LineType.ROW, squares, attVec, nextCoin);
				}
				// column
				if (row < 3) {
					squares[2] = column;
					squares[3] = row + 1;
					squares[4] = column;
					squares[5] = row + 2;
					squares[6] = column;
					squares[7] = row + 3;
					checkFour(LineType.COLUMN, squares, attVec, nextCoin);
				}
				// diagonal asc
				if (row < 3 && column < 5) {
					squares[2] = column + 1;
					squares[3] = row + 1;
					squares[4] = column + 2;
					squares[5] = row + 2;
					squares[6] = column + 3;
					squares[7] = row + 3;
					checkFour(LineType.DIAGONAL, squares, attVec, nextCoin);
				}
				// diagonal desc
				if (row < 3 && column > 2) {
					squares[2] = column - 1;
					squares[3] = row + 1;
					squares[4] = column - 2;
					squares[5] = row + 2;
					squares[6] = column - 3;
					squares[7] = row + 3;
					checkFour(LineType.DIAGONAL, squares, attVec, nextCoin);
				}
				
				// calculate distance from vector 
				if (getCoin(column, row) == Coin.PLAYER) {
					playerCenterDist += (int)Math.abs(column - MIDDLE_COLUMN);
					playerCenterDist += (int)Math.abs(row - MIDDLE_ROW);
				}
				else if (getCoin(column, row) == Coin.OPPONENT) {
					opponentCenterDist += (int)Math.abs(column - MIDDLE_COLUMN);
					opponentCenterDist += (int)Math.abs(row - MIDDLE_ROW);
				}
			}
		}
		// update immediate attribute
		if (immidiateThreatPlayerCounter > 1 && immidiateThreatOpponentCounter == 0) {
			attVec[IMMEDIATE_THREAT]++;
		}
		if (immidiateThreatOpponentCounter > 1 && immidiateThreatPlayerCounter == 0) {
			attVec[IMMEDIATE_THREAT + 1]++;
		}
		// update distance from center
		attVec[DISTANCE_FROM_CENTER] = opponentCenterDist - playerCenterDist;
		return attVec;
	}
	
	/*--- Private Methods ---*/

	private Coin getOtherCoin(Coin coin) {
		if (coin == Coin.PLAYER) {
			return Coin.OPPONENT;
		}
		return Coin.PLAYER;
	}

	/**
	 * Translate single coin to number
	 * @param coin The coin
	 * @return 1 for empty square, 2 for player coin and 3 for opponent coin 
	 */
	private int CoinToNum(Coin coin) {
		if (coin == null) {
			return 1;
		}
		if (coin == Coin.PLAYER) {
			return 2;
		}
		if (coin == Coin.OPPONENT) {
			return 3;
		}
		return -1;
	}
	
	private int getAttrIndex(LineType type) {
		return type.getLineNum()*2;
	}

	/**
	 * Translate the given 4 squares to a number and test if they hold any attribute
	 * @param type Row, Column or Diagonal
	 * @param fourSquare the given 4 adjacent squares indexes
	 * @param attVec The attributes vector to update
	 * @param nextCoin The type of the coin that will be played on the next round, used in order to check even and odd threats
	 */
	private void checkFour(LineType type, int[] fourSquare, int[] attVec, Coin nextCoin) {
		int firstCoinCol = fourSquare[0];
		int firstCoinRow = fourSquare[1];
		int lastCoinCol = fourSquare[6];
		int lastCoinRow = fourSquare[7];
		Coin coin1 = getCoin(fourSquare[0], fourSquare[1]);
		Coin coin2 = getCoin(fourSquare[2], fourSquare[3]);
		Coin coin3 = getCoin(fourSquare[4], fourSquare[5]);
		Coin coin4 = getCoin(fourSquare[6], fourSquare[7]);
		// Translate the given 4 squares to a number
		int coinsRep = 1000 * CoinToNum(coin1) + 100 * CoinToNum(coin2) + 10 * CoinToNum(coin3) + CoinToNum(coin4);
		
		// update the attributes vector by the 4 coins
		// 2 = square with player coin, 3 = square with opponent coin, 1 = empty square
		switch (coinsRep) {
			case 2222: 
				attVec[FOUR_COINS_INDEX]++;
				break;
			case 3333: 
				attVec[FOUR_COINS_INDEX + 1]++;
				break;
			case 2221: 
				countParityAtt(type, Coin.PLAYER, nextCoin, lastCoinRow, lastCoinCol, attVec);
				updateImmediateThreat(lastCoinCol, lastCoinRow, Coin.PLAYER);
				updateDoubleThreat(lastCoinCol, lastCoinRow, Coin.PLAYER, attVec);
				break;
			case 1222:
				countParityAtt(type, Coin.PLAYER, nextCoin, firstCoinRow, firstCoinCol, attVec);
				updateImmediateThreat(firstCoinCol, firstCoinRow, Coin.PLAYER);
				updateDoubleThreat(firstCoinCol, firstCoinRow, Coin.PLAYER, attVec);
				break;
			case 3331:
				countParityAtt(type, Coin.OPPONENT, nextCoin, lastCoinRow, lastCoinCol, attVec);
				updateImmediateThreat(lastCoinCol, lastCoinRow, Coin.OPPONENT);
				updateDoubleThreat(lastCoinCol, lastCoinRow, Coin.OPPONENT, attVec);
				break;
			case 1333:
				countParityAtt(type, Coin.OPPONENT, nextCoin, firstCoinRow, firstCoinCol, attVec);
				updateImmediateThreat(firstCoinCol, firstCoinRow, Coin.OPPONENT);
				updateDoubleThreat(firstCoinCol, firstCoinRow, Coin.OPPONENT, attVec);
				break;
			case 2211:
			case 1221:
			case 1122:
				attVec[TWO_COINS_INDEX + getAttrIndex(type)]++;
				break;
			case 3311:
			case 1331:
			case 1133:
				attVec[TWO_COINS_INDEX + 1 + getAttrIndex(type)]++;
				break;
			case 2212:
				countParityAtt(type, Coin.PLAYER, nextCoin, fourSquare[5], fourSquare[4], attVec);
				updateImmediateThreat(fourSquare[4], fourSquare[5], Coin.PLAYER);
				updateDoubleThreat(fourSquare[4], fourSquare[5], Coin.PLAYER, attVec);
				break;
			case 2122:
				countParityAtt(type, Coin.PLAYER, nextCoin, fourSquare[3], fourSquare[2], attVec);
				updateImmediateThreat(fourSquare[2], fourSquare[3], Coin.PLAYER);
				updateDoubleThreat(fourSquare[2], fourSquare[3], Coin.PLAYER, attVec);
				break;
			case 3313:
				countParityAtt(type, Coin.OPPONENT, nextCoin, fourSquare[5], fourSquare[4], attVec);
				updateImmediateThreat(fourSquare[4], fourSquare[5], Coin.OPPONENT);
				updateDoubleThreat(fourSquare[4], fourSquare[5], Coin.OPPONENT, attVec);
				break;
			case 3133:
				countParityAtt(type, Coin.OPPONENT, nextCoin, fourSquare[3], fourSquare[2], attVec);
				updateImmediateThreat(fourSquare[2], fourSquare[3], Coin.OPPONENT);
				updateDoubleThreat(fourSquare[2], fourSquare[3], Coin.OPPONENT, attVec);
				break;
			case 2111:
			case 1211:
			case 1121:
			case 1112:
				if (type != LineType.DIAGONAL) { 
					attVec[ONE_COIN_INDEX + type.getLineNum()]++;
				}
				break;
			case 3111:
			case 1311:
			case 1131:
			case 1113:
				if (type != LineType.DIAGONAL) {
					attVec[ONE_COIN_INDEX + 1 + type.getLineNum()]++;
				}
				break;
			case 2121:
			case 2112:
			case 1212:
				attVec[TWO_COINS_INDEX + getAttrIndex(type)]++;
				break;
			case 3131:
			case 3113:
			case 1313:
				attVec[TWO_COINS_INDEX + 1 + getAttrIndex(type)]++;
				break;
			default:
				break;
		}
	}
	
	/**
	 * Test for even and odd threats
	 */
	private void countParityAtt(LineType type, Coin attOwner, Coin nextCoin, int edgeCoinRow, int edgeCoinCol, int[] attVec){
		// even and odd threats are not relevant for column (only for row and diagonal)
		if (type == LineType.COLUMN) {
			if (attOwner == Coin.PLAYER) {
				attVec[THREE_COLUMN_INDEX]++;
			}
			else {
				attVec[THREE_COLUMN_INDEX + 1]++;
			}
			return;
		}
		
		int distance = edgeCoinRow - getHeight(edgeCoinCol) + 1; // the distance between the empty square to the last coin in the square's column
		if (attOwner == Coin.PLAYER) {
			if((distance % 2 == 0 && nextCoin == Coin.PLAYER) || (distance % 2 == 1 && nextCoin == Coin.OPPONENT)) {
				//update even threat
				attVec[THREE_EVEN_INDEX + getAttrIndex(type)]++;
			}
			else {
				//update odd threat
				attVec[THREE_ODD_INDEX + getAttrIndex(type)]++;
			}
		}
		else {
			if((distance % 2 == 0 && nextCoin == Coin.OPPONENT) || (distance % 2 == 1 && nextCoin == Coin.PLAYER)) {
				//update even threat	
				attVec[THREE_EVEN_INDEX + 1 + getAttrIndex(type)]++;
			}
			else {
				//update odd threat
				attVec[THREE_ODD_INDEX + 1 + getAttrIndex(type)]++;
			}
		}
	}
	
	/**
	 * Test immediate threat - 3 adjacent coins so the next step will end the game
	 */
	private void updateImmediateThreat(int column, int row, Coin coin) {
		if (getHeight(column) == row) {
			if (coin == Coin.PLAYER) {
				immidiateThreatPlayerCounter++;
			}
			else {
				immidiateThreatOpponentCounter++;
			}
		}
	}
	
	/**
	 * Update replica of the board with every 3 adjacent coins. If the same threat exists below or above the coins -
	 * update double threat attribute in the attributes vector.  
	 */
	private void updateDoubleThreat(int column, int row, Coin coin, int[] attVec) {
		doubleThreat[column][row] = coin;
		
		// double threat exists
		if ((row > 0 && doubleThreat[column][row - 1] == coin) || (row < 5 && doubleThreat[column][row + 1] == coin)) {
			if (coin == Coin.PLAYER) {
				attVec[DOUBLE_THREAT]++;
			}
			else if (coin == Coin.OPPONENT) {
				attVec[DOUBLE_THREAT + 1]++;
			}
		}
	}
}
