import java.util.ArrayList;
import java.util.Random;

/**
 * AmazonState - a state representation for a game position in Amazons.  Additionally, this implements the AmazonsPlayer interface, 
 * so that play directly integrates with the state object.  See the Wikipedia "Game of the Amazons" page for rule details.
 * 
 * @author Todd W. Neller
 *
 * Note: In the code below, a grid "position" is represented as a single integer in zero-based row-major form, 
 * i.e. for row r and column c on a SIZE-by-SIZE board, the position p is (r * SIZE + c).  
 * For position p, r = p / SIZE; c = p % SIZE; 
 */
public class AmazonsState implements AmazonsPlayer {
	/**
	 * a constant indicating no player/piece.  Note: <code>NONE</code>, <code>WHITE</code>, <code>BLACK</code>, <code>ARROW</code> will each have unique integer values.
	 */
	public static final int NONE = 0;

	/**
	 * a constant indicating a white Amazon (queen).  Note: <code>NONE</code>, <code>WHITE</code>, <code>BLACK</code>, <code>ARROW</code> will each have unique integer values.
	 */
	public static final int WHITE = 1;

	/**
	 * a constant indicating a black Amazon (queen).  Note: <code>NONE</code>, <code>WHITE</code>, <code>BLACK</code>, <code>ARROW</code> will each have unique integer values.
	 */
	public static final int BLACK = 2;

	/**
	 * a constant indicating an arrow (blocked position).  Note: <code>NONE</code>, <code>WHITE</code>, <code>BLACK</code>, <code>ARROW</code> will each have unique integer values.
	 */
	public static final int ARROW = 3;
	
	/**
	 * dimensions of grid
	 */
	public static final int SIZE = 10;

	protected static final int[] dRows = {0, -1, -1, -1, 0, 1, 1, 1};
	protected static final int[] dCols = {1, 1, 0, -1, -1, -1, 0, 1};	
	protected static int[][][] lines;
	protected static int[][] gridDistance = new int[2][SIZE * SIZE];
	protected static final int UNREACHABLE = SIZE * SIZE;
	protected static int[] whiteSearchQueue = new int[SIZE * SIZE], blackSearchQueue = new int[SIZE * SIZE];
	protected static final boolean DISTANCE_TIE_GOES_TO_CURRENT_PLAYER = false;
	protected static Random random = new Random();
	protected static int[] play = new int[3];

	/**
	 * grid pieces indexed by row * columns + column (row-major ordering)
	 */
	protected int[] grid = new int[SIZE * SIZE]; 
	
	protected int[][] amazonPositions = new int[2][0]; // indexed by WHITE(0)/BLACK(1), zero-based amazon piece number
		
	public int legalMoveCount;
	public int legalShotCount;
	
	public int[][] legalMoves = new int[2][136]; // first dimension: srcPos [0], destPos[0], second dimension: legal move number
	public int[] legalShots = new int[35]; // shot positions indexed by legal shot number
	
	/**
	 * Current player, initially <code>AmazonsState.WHITE</code>
	 */
	protected int currentPlayer = WHITE; 

	/**
	 * Whether or not Amazon was just moved.  If true, an arrow must be fired next.
	 */
	protected boolean amazonMoved = false;

	/**
	 * The current position of the previous Amazon just moved.
	 */
	protected int moveDestPos;
	
	protected int turnsTaken = 0;
	
	public AmazonsState() {
	}
	
	/**
	 * Copy constructor.
	 * @param state - original state
	 */
	public AmazonsState(AmazonsState state) {
		grid = state.grid.clone();
		amazonPositions = new int[2][];
		amazonPositions[0] = state.amazonPositions[0].clone();
		amazonPositions[1] = state.amazonPositions[1].clone();
		legalMoves = new int[2][136]; // don't copy contents, just reallocate
		legalShots = new int[35]; // don't copy contents, just reallocate
		currentPlayer = state.currentPlayer;
		amazonMoved = state.amazonMoved;
		moveDestPos = state.moveDestPos;
		this.turnsTaken = state.turnsTaken;
	}
	
	static {
		// Precompute all lines of play from each grid position
		lines = new int[SIZE * SIZE][][];
		for (int i = 0; i < SIZE * SIZE; i++) {
			int row = i / SIZE;
			int col = i % SIZE;
			ArrayList<ArrayList<Integer>> lineLists = new ArrayList<ArrayList<Integer>>();
			for (int dir = 0; dir < 8; dir++) {
				int currRow = row + dRows[dir];
				int currCol = col + dCols[dir];
				ArrayList<Integer> line = new ArrayList<Integer>();
				while (currRow >= 0 && currRow < SIZE && currCol >= 0 && currCol < SIZE) {
					line.add(currRow * SIZE + currCol);
					currRow += dRows[dir];
					currCol += dCols[dir];
				}
				if (!line.isEmpty())
					lineLists.add(line);
			}
			lines[i] = new int[lineLists.size()][];
			for (int j = 0; j < lineLists.size(); j++) {
				ArrayList<Integer> line = lineLists.get(j);
				int lineLength = line.size();
				lines[i][j] = new int[lineLength];
				for (int k = 0; k < lineLength; k++)
					lines[i][j][k] = line.get(k);
			}
		}
	}

	/**
	 * Clears the game board, removing all Amazons and arrows.
	 */
	protected void clearBoard() {
		for (int i = 0; i < grid.length; i++) {
			grid[i] = NONE;
		}
		amazonPositions = new int[2][0];
	}
	
	/**
	 * Adds an Amazon to the board at the given position row and column.
	 * @param color - color of Amazon to be added, WHITE or BLACK.
	 * @param row - position row
	 * @param col - position column
	 */
	protected void addAmazon(int color, int row, int col) {
		int i = (color == WHITE) ? 0 : 1;
		int pos = row * SIZE + col;
		int prevAmazons = amazonPositions[i].length;
		int[] newAmazonPosList = new int[prevAmazons + 1];
		System.arraycopy(amazonPositions[i], 0, newAmazonPosList, 0, prevAmazons);
		newAmazonPosList[prevAmazons] = pos;
		amazonPositions[i] = newAmazonPosList;
		grid[pos] = color;
	}
	
	/**
	 * init - initializes the game state to the standard start configuration (see Wikipedia "Game of the Amazons")
	 */
	public void init() {
		clearBoard();
		addAmazon(WHITE, 0, 3);
		addAmazon(WHITE, 0, 6);
		addAmazon(WHITE, 3, 0);
		addAmazon(WHITE, 3, 9);
		addAmazon(BLACK, 6, 0);
		addAmazon(BLACK, 6, 9);
		addAmazon(BLACK, 9, 3);
		addAmazon(BLACK, 9, 6);
	}
	
	/**
	 * Get the contents of the given grid position row and column.
	 * @param row - position row.
	 * @param col - position column
	 * @return the contents of the given grid position row and column.
	 */
	int	get(int row, int col) {
		return get(row * SIZE + col);
	}
	
	/**
	 * Get the contents of the given grid position.
	 * @param pos - grid position
	 * Each position is encoded in zero-based row-major form, i.e. for row r and column c on a SIZE-by-SIZE board, the position p is
	 * (r * SIZE + c).  For position p, r = p / SIZE; c = p % SIZE;
	 * @return contents of the given grid position.
	 */
	int get(int pos) {
		return grid[pos];
	}
	
//	boolean takeTurnIfLegal(int srcRow, int srcCol, int destRow, int destCol, int shotRow, int shotCol) {
//		if (isLegalTurn(srcRow, srcCol, destRow, destCol, shotRow, shotCol)) {
//			takeTurn(srcRow, srcCol, destRow, destCol, shotRow, shotCol);
//			return true;
//		}
//		else {
//			return false;
//		}
//	}
//	
//	boolean takeTurnIfLegal(int srcPos, int destPos, int shotPos) {
//		if (isLegalTurn(srcPos, destPos, shotPos)) {
//			takeTurn(srcPos, destPos, shotPos);
//			return true;
//		}
//		else {
//			return false;
//		}
//	}
//	
//	boolean isLegalTurn(int srcRow, int srcCol, int destRow, int destCol, int shotRow, int shotCol) {
//		if (grid[srcRow * SIZE + srcCol] != currentPlayer)
//			return false;
//		// Check if legal move.
//		int rowDiff = destRow - srcRow;
//		int colDiff = destCol - srcCol;
//		if (rowDiff != 0 && colDiff != 0 && Math.abs(rowDiff) != Math.abs(colDiff))
//			return false;
//		int dRow = (int) Math.signum(rowDiff); 
//		int dCol = (int) Math.signum(colDiff); 
//		int row = srcRow + dRow;
//		int col = srcCol + dCol;
//		while (row >= 0 && row < SIZE && col >= 0 && col < SIZE && (row != destRow || col != destCol) && grid[row * SIZE + col] == NONE) {
//			row += dRow;
//			col += dCol;
//		}
//		if (row == destRow && col == destCol && grid[row * SIZE + col] == NONE) {
//			// Legal move.  Now check if legal shot.
//			rowDiff = shotRow - destRow;
//			colDiff = shotCol - destCol;
//			dRow = (int) Math.signum(rowDiff); 
//			dCol = (int) Math.signum(colDiff);
//			row = destRow + dRow;
//			col = destCol + dCol;
//			while (row >= 0 && row < SIZE && col >= 0 && col < SIZE && (row != shotRow || col != shotCol) 
//					&& (grid[row * SIZE + col] == NONE || (shotRow == srcRow && shotCol == srcCol))) {
//				row += dRow;
//				col += dCol;
//			}
//			return row == shotRow && col == shotCol && (grid[row * SIZE + col] == NONE || (shotRow == srcRow && shotCol == srcCol));
//		}
//		else
//			return false;
//	}
//	
//	boolean isLegalTurn(int srcPos, int destPos, int shotPos) {		
//		return isLegalTurn(srcPos / SIZE, srcPos % SIZE, destPos / SIZE, destPos % SIZE, shotPos / SIZE, shotPos % SIZE);
//	}
	
	
	/**
	 * takeTurn - make the given play. 
	 * @param srcPos - Amazon source position
	 * @param destPos - Amazon destination position
	 * @param shotPos - Amazon shot position
	 * Each position is encoded in zero-based row-major form, i.e. for row r and column c on a SIZE-by-SIZE board, the position p is
	 * (r * SIZE + c).  For position p, r = p / SIZE; c = p % SIZE;
	 */
	public void takeTurn(int srcPos, int destPos, int shotPos) {  // does not check legality
		int[] amazonPos = (currentPlayer == WHITE) ? amazonPositions[0] : amazonPositions[1];
		int amazonIndex = 0;
		while (amazonIndex < amazonPos.length && amazonPos[amazonIndex] != srcPos)
			amazonIndex++;
		if (amazonIndex == amazonPos.length) { // no amazon to move at srcPos; (not checking that whole move is legal here)
			System.err.println("Illegal move - no current player amazon at source position " + srcPos);
			throw new RuntimeException("Illegal move - no current player amazon at source position " + srcPos);
		}
		amazonPos[amazonIndex] = destPos;
		grid[destPos] = grid[srcPos];
		grid[srcPos] = NONE;
		grid[shotPos] = ARROW;
		currentPlayer = (currentPlayer == WHITE) ? BLACK : WHITE;
		turnsTaken++;
	}
	
	/**
	 * @return whether or not the current player has a legal move.
	 */
	boolean hasLegalMove() {
		int[] amazonPos = (currentPlayer == WHITE) ? amazonPositions[0] : amazonPositions[1];
		for (int pos : amazonPos) 
			for (int[] line : lines[pos])
				if (grid[line[0]] == NONE)
					return true;
		return false;
	}
	
	/**
	 * Compute all legal moves.  The number of legal moves will be in legalMoveCount, and the move positions themselves will be
	 * int the 2D array legalMoves.  The moves will be in the first dimension indices 0 - (legalMoveCount - 1).
	 * Each move is a length two array of source position and destination position.
	 */
	void computeLegalMoves() { // compute legal moves for current player (amazonMoved should be false)
		legalMoveCount = 0;
		int[] amazonPos = (currentPlayer == WHITE) ? amazonPositions[0] : amazonPositions[1];
		for (int srcPos : amazonPos)
			for (int[] line : lines[srcPos]) {
				int i = 0;
				while (i < line.length && grid[line[i]] == NONE) {
					legalMoves[0][legalMoveCount] = srcPos;
					legalMoves[1][legalMoveCount++] = line[i++]; // empty destination position along line of empty positions
				}
			}
	}
	
	/**
	 * Compute all legal shots.  The number of legal shots will be in legalShotCount, and the shot positions themselves will be 
	 * from indices 0 - (legalShotCount - 1) in the int array legalShots.
	 */
	void computeLegalShots() { // compute legal shots for amazon just moved (amazonMoved should be true)
		legalShotCount = 0;
		for (int[] line : lines[moveDestPos]) {
			int i = 0;
			while (i < line.length && grid[line[i]] == NONE) 
				legalShots[legalShotCount++] = line[i++]; // empty shot position along line of empty positions
		}
	}
	
	/**
	 * move - Move a current player Amazon from source position srcPos to destination position destPos.
	 * @param srcPos - source position
	 * @param destPos - destination position
	 * 
	 */
	void move(int srcPos, int destPos) {  // does not check legality
		int[] amazonPos = (currentPlayer == WHITE) ? amazonPositions[0] : amazonPositions[1];
		int amazonIndex = 0;
		while (amazonIndex < amazonPos.length && amazonPos[amazonIndex] != srcPos)
			amazonIndex++;
		if (amazonIndex == amazonPos.length) { // no amazon to move at srcPos; (not checking that whole move is legal here)
			System.err.println("Illegal move - no current player amazon at source position " + srcPos);
			throw new RuntimeException("Illegal move - no current player amazon at source position " + srcPos);
		}
		amazonPos[amazonIndex] = destPos;
		grid[destPos] = grid[srcPos];
		grid[srcPos] = NONE;
		amazonMoved = true;
		moveDestPos = destPos;
	}
	
	/**
	 * shoot - make shot to shotPos from Amazon just moved to destPos. 
	 * @param destPos - destination position for Amazon just moved.
	 * @param shotPos - shot position for Amazon.
	 */
	void shoot(int destPos, int shotPos) { // does not check legality
		grid[shotPos] = ARROW;
		amazonMoved = false;
		currentPlayer = (currentPlayer == WHITE) ? BLACK : WHITE;
	}
	
	/**
	 * Convert a position row and column to a String in Lorentz's Amazons notation.
	 * @param row - position row.
	 * @param col - position column.
	 * @return String representation of position.
	 */
	String rowColToString(int row, int col) {
		return String.format("%s%d", "" + (char) ('a' + col), row + 1);
	}
	
	/**
	 * Convert a position to a String in Lorentz's Amazons notation.
	 * @param pos - a position is encoded in zero-based row-major form, i.e. for row r and column c on a SIZE-by-SIZE board, the position p is
	 * (r * SIZE + c).  For position p, r = p / SIZE; c = p % SIZE;
	 * @return String representation of position.
	 */
	String posToString(int pos) {
		return rowColToString(pos / SIZE, pos % SIZE);
	}
	
	
	/**
	 * Convert an entire turn move to a String in Lorentz's Amazons notation.
	 * @param srcPos - Amazon source position
	 * @param destPos - Amazon destination position
	 * @param shotPos - Amazon shot position
	 * Each position is encoded in zero-based row-major form, i.e. for row r and column c on a SIZE-by-SIZE board, the position p is
	 * (r * SIZE + c).  For position p, r = p / SIZE; c = p % SIZE;
	 * @return String representation of move.
	 */
	String moveToString(int srcPos, int destPos, int shotPos) {
		return String.format("%s - %s (%s)", posToString(srcPos), posToString(destPos), posToString(shotPos));
	}
	
//	void printEval() {
//		computeBoardDistances();
//		System.out.println("White Distances");
//		for (int row = SIZE - 1; row >= 0; row--) {
//			for (int col = 0; col < SIZE; col++) {
//				if (gridDistance[0][row * SIZE + col] == UNREACHABLE)
//					System.out.print("-- ");
//				else
//					System.out.printf("%2d ", gridDistance[0][row * SIZE + col]);
//			}
//			System.out.println();
//		}
//		System.out.println("Black Distances");
//		for (int row = SIZE - 1; row >= 0; row--) {
//			for (int col = 0; col < SIZE; col++) {
//				if (gridDistance[1][row * SIZE + col] == UNREACHABLE)
//					System.out.print("-- ");
//				else
//					System.out.printf("%2d ", gridDistance[1][row * SIZE + col]);
//			}
//			System.out.println();
//		}
//	}
	
	/**
	 * Compute board evaluation according to Lorentz's scheme.  All values less than UNREACHABLE, indicate the minimum 
	 * number of moves it would take a player to reach the position. To go beyond Lorentz's scheme and award ties to the 
	 * current player, set the flag DISTANCE_TIE_GOES_TO_CURRENT_PLAYER to true.

	 * @param currentPlayer - current player
	 * @param grid - contents of board positions
	 * @return simple board evaluation according to Lorentz's scheme 
	 */
	int simpleEval() {
		computeBoardDistances();
		int count = 0;
		for (int i = 0; i < SIZE * SIZE; i++)
			if (gridDistance[0][i] != UNREACHABLE || gridDistance[1][i] != UNREACHABLE) // reachable by some player
				if (gridDistance[0][i] != 0 && gridDistance[1][i] != 0) { // not currently occupied by a player
					if (gridDistance[0][i] == gridDistance[1][i]) { // same distance
						if (DISTANCE_TIE_GOES_TO_CURRENT_PLAYER)
							count += (currentPlayer == WHITE ^ amazonMoved) ? 1 : -1; // if Amazon already moved, tie goes to next player.
					}
					else
						count += (gridDistance[0][i] < gridDistance[1][i]) ? 1 : -1;
				}
		return (currentPlayer == WHITE) ? count : -count;
	}
	
	/**
	 * Compute board distances from each player.  All values less than UNREACHABLE, indicate the minimum 
	 * number of moves it would take a player to reach the position.
	 * 
	 * @param grid - contents of board positions
	 */
	void computeBoardDistances() {
		int whiteSearchQueueCount = 0, blackSearchQueueCount = 0;
		int whiteSearchQueueIndex = 0, blackSearchQueueIndex = 0;
		for (int i = 0; i < SIZE * SIZE; i++) {
			gridDistance[0][i] = UNREACHABLE;
			gridDistance[1][i] = UNREACHABLE;
			if (grid[i] == WHITE) {
				gridDistance[0][i] = 0;
				whiteSearchQueue[whiteSearchQueueCount++] = i;
			}
			else if (grid[i] == BLACK) {
				gridDistance[1][i] = 0;
				blackSearchQueue[blackSearchQueueCount++] = i;
			}
		}
		// Propagate white moves via breadth first search
		while (whiteSearchQueueIndex < whiteSearchQueueCount) {
			int pos = whiteSearchQueue[whiteSearchQueueIndex++];
			int nextDist = gridDistance[0][pos] + 1;
			for (int[] line : lines[pos]) {
				for (int i = 0; i < line.length && grid[line[i]] == NONE; i++)
					if (gridDistance[0][line[i]] > nextDist) {
						gridDistance[0][line[i]] = nextDist;
						whiteSearchQueue[whiteSearchQueueCount++] = line[i];
					}
			}
		}
		// Propagate black moves via breadth first search
		while (blackSearchQueueIndex < blackSearchQueueCount) {
			int pos = blackSearchQueue[blackSearchQueueIndex++];
			int nextDist = gridDistance[1][pos] + 1;
			for (int[] line : lines[pos]) {
				for (int i = 0; i < line.length && grid[line[i]] == NONE; i++)
					if (gridDistance[1][line[i]] > nextDist) {
						gridDistance[1][line[i]] = nextDist;
						blackSearchQueue[blackSearchQueueCount++] = line[i];
					}
			}
		}		
	}
	
	/**
	 * getPlay - get the chosen play
	 * @param millisRemaining - player decision-making milliseconds remaining in the game. 
	 * @return an int array of length 3 containing the Amazon source position, Amazon destination position, and Amazon shot position.
	 * Each position is encoded in zero-based row-major form, i.e. for row r and column c on a SIZE-by-SIZE board, the position p is
	 * (r * SIZE + c).  For position p, r = p / SIZE; c = p % SIZE;
	 */
	public int[] getPlay(long millisRemaining) {
		return getRandomPlay();
	}
	
	/**
	 * @return a random play, an int array of length 3 containing the Amazon source position, Amazon destination position, and Amazon shot position.
	 * Each position is encoded in zero-based row-major form, i.e. for row r and column c on a SIZE-by-SIZE board, the position p is
	 * (r * SIZE + c).  For position p, r = p / SIZE; c = p % SIZE;
	 */
	public int[] getRandomPlay() {
		// get random move
		computeLegalMoves();
		int moveIndex = random.nextInt(legalMoveCount);
		int srcPos = legalMoves[0][moveIndex];
		int destPos = legalMoves[1][moveIndex];
		// make random move
		grid[destPos] = grid[srcPos];
		grid[srcPos] = NONE;
		int oldMoveDestPos = moveDestPos;
		moveDestPos = destPos;
		// get random shot
		computeLegalShots();
		int shotPos = legalShots[random.nextInt(legalShotCount)];
		// undo random move
		moveDestPos = oldMoveDestPos;
		grid[srcPos] = grid[destPos];
		grid[destPos] = NONE;
		// return play
		play[0] = srcPos;
		play[1] = destPos;
		play[2] = shotPos;
		return play;
	}
	
	/**
	 * @return the winner of the game (WHITE or BLACK), or NONE if the game has not ended.
	 */
	public int getWinner() {
		if (amazonMoved || hasLegalMove())
			return NONE;
		return currentPlayer == WHITE ? BLACK : WHITE;
	}
	
	/**
	 * @return a String respresentation of the board.
	 */
	public String boardToString() {
		StringBuilder sb = new StringBuilder("  ");
		for (int col = 0; col < SIZE; col++)
			sb.append(" " + (char) ('a' + col));
		sb.append("\n");
		for (int row = SIZE - 1; row >= 0; row --) {
			sb.append(String.format("%2d", row + 1));
			for (int col = 0; col < SIZE; col++) {
				char symbol = '.';
				int contents = get(row, col);
				if (contents == ARROW)
					symbol = '#';
				else if (contents == WHITE)
					symbol = 'W';
				else if (contents == BLACK)
					symbol = 'B';
				sb.append(" " + symbol);
			}
			sb.append(String.format(" %2d\n", row + 1));
		}
		sb.append("  ");
		for (int col = 0; col < SIZE; col++)
			sb.append(" " + (char) ('a' + col));
		sb.append("\n");
		sb.append(String.format("%s to play.\n", currentPlayer == WHITE ? "White" : "Black"));
		return sb.toString();
	}

	@Override
	/**
	 * getName - get the name of the player
	 * @return the name of the player
	 */
	public String getName() {
		return "RandomPlayer";
	}
	
	/**
	 * @return current player (WHITE or BLACK)
	 */
	public int getPlayer() {
		return currentPlayer;
	}

	public static void main(String[] args) {
		// Random game demo:
		AmazonsState state = new AmazonsState();
		state.init();
		while (state.getWinner() == NONE) {
			System.out.println(state.boardToString());
			int[] play = state.getPlay(100000);
			System.out.println(state.moveToString(play[0], play[1], play[2]));
			state.takeTurn(play[0], play[1], play[2]);
		}
		System.out.println(state.boardToString());
		System.out.printf("%s wins.", state.getWinner() == WHITE ? "White" : "Black");
	}
}
