package net.woodyfolsom.msproj;

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

public class GameState {
	private int blackPrisoners;
	private int whitePrisoners;
	private GameBoard gameBoard;
	private GameConfig gameConfig;
	private Player playerToMove;
	private List<Action> moveHistory;

	public GameState(GameConfig gameConfig) {
		this.blackPrisoners = 0;
		this.whitePrisoners = 0;
		this.gameConfig = gameConfig;
		int size = gameConfig.getSize();
		if (size < 1 || size > 19) {
			throw new IllegalArgumentException("Invalid board size: " + size);
		}
		gameBoard = new GameBoard(size);
		playerToMove = Player.BLACK;
		moveHistory = new ArrayList<Action>();
	}

	public GameState(GameState that) {
		this.blackPrisoners = that.blackPrisoners;
		this.whitePrisoners = that.whitePrisoners;
		this.playerToMove = that.playerToMove;
		gameBoard = new GameBoard(that.gameBoard);
		moveHistory = new ArrayList<Action>(that.moveHistory);
		this.gameConfig = new GameConfig(that.gameConfig);
	}

	public void clearBoard() {
		blackPrisoners = 0;
		whitePrisoners = 0;
		gameBoard.clear();
		playerToMove = Player.BLACK;
	}

	public int getBlackPrisoners() {
		return blackPrisoners;
	}

	public GameResult getResult() {
		GameBoard markedBoard;

		if (gameBoard.isTerritoryMarked()) {
			markedBoard = gameBoard;
		} else {
			markedBoard = new GameBoard(gameBoard);
			TerritoryMarker.markTerritory(markedBoard);
		}

		int gameLength = moveHistory.size();

		// This functionality duplicates isTerminal(),
		// however for the result to be reported, we need to know
		// how the game was terminated to differentiate between resignation,
		// scored complete
		// and (eventually) time out/forfeit.
		boolean isFinished = false;

		if (gameLength >= 1) {
			if (moveHistory.get(gameLength - 1).isResign()) {
				if (gameLength % 2 == 1) {
					return GameResult.WHITE_BY_RESIGNATION;
				} else {
					return GameResult.BLACK_BY_RESIGNATION;
				}
			}
			if (gameLength >= 2) {
				if (moveHistory.get(gameLength - 1).isPass()
						&& moveHistory.get(gameLength - 2).isPass()) {
					isFinished = true;
				}
			}
		}

		int blackScore = gameBoard.countSymbols(GameBoard.BLACK_STONE,
				GameBoard.BLACK_TERRITORY);
		int whiteScore = gameBoard.countSymbols(GameBoard.WHITE_STONE,
				GameBoard.WHITE_TERRITORY);

		return new GameResult(blackScore, whiteScore, markedBoard.getSize(),
				gameConfig.getKomi(), isFinished);
	}

	public List<String> getEmptyCoords() {
		List<String> emptyCoords = new ArrayList<String>();

		for (int colIndex = 0; colIndex < gameBoard.getSize(); colIndex++) {
			for (int rowIndex = 0; rowIndex < gameBoard.getSize(); rowIndex++) {
				if (GameBoard.EMPTY_INTERSECTION == gameBoard.getSymbolAt(
						colIndex, rowIndex))
					emptyCoords
							.add(GameBoard.getCoordinate(colIndex, rowIndex));
			}
		}

		return emptyCoords;
	}

	public GameBoard getGameBoard() {
		return gameBoard;
	}

	public GameConfig getGameConfig() {
		return gameConfig;
	}

	public Player getPlayerToMove() {
		return playerToMove;
	}

	public int getWhitePrisoners() {
		return whitePrisoners;
	}

	/**
	 * Used for setting up the board.  Places the player's stone at the specified coordinates.
	 * 
	 * Returns false if the requested intersection is occupied or the resulting position is illegal.
	 * Returns false if the requested action is PASS, RESIGN or NONE.
	 * Returns false if the moveHistory's size is already >0 (method should only be used to set up board).
	 * 
	 * Does NOT advance the playerToMove or add the action to the move history.
	 * 
	 * @param player
	 * @param action
	 * @return
	 */
	public boolean placeStone(Player player, Action action) {
		if (action.isPass() || action.isResign() || action.isNone()) {
			return false;
		}
		
		if (moveHistory.size() > 0) {
			return false;
		}
		
		Player actualPTM = playerToMove;
		
		playerToMove = player;
		
		boolean validMove = playStone(player,action);
		
		moveHistory.clear();
		playerToMove = actualPTM;
		
		return validMove;
	}
	
	public boolean playStone(Player player, String move) {
		return playStone(player, Action.getInstance(move));
	}

	/**
	 * Places a stone at the requested coordinate. Placement is legal if the
	 * coordinate is currently empty, has at least one liberty (empty neighbor),
	 * or at least one neighbor has this (recursive) property, or results in the
	 * capture and removal of at least one stone to produce a legal placement.
	 * Note also that placement is illegal if it would result in the capture of
	 * one's own group. Hence, placement must result in the capture of an
	 * adjacent stone or stones, if any.
	 * 
	 * @param colLabel
	 * @param row
	 * @param stone
	 * @return
	 */
	public boolean playStone(Player player, Action action) {
		if (player != playerToMove) {
			throw new IllegalArgumentException("Requested " + player
					+ " move, but it is " + playerToMove + "'s turn!");
		}

		if (player == Player.NONE) {
			throw new IllegalArgumentException("Cannot play as " + player);
		}

		if (action.isPass()) {
			playerToMove = GoGame.getNextPlayer(player);
			moveHistory.add(action);
			return true;
		}

		if (action.isResign()) {
			playerToMove = Player.NONE;
			moveHistory.add(action);
			return true;
		}

		if (action.isNone()) {
			return false;
		}

		char currentStone = gameBoard.getSymbolAt(action.getColumn(),
				action.getRow());

		if (currentStone != GameBoard.EMPTY_INTERSECTION) {
			return false;
		}

		// assertCorrectHash();

		gameBoard.pushHashHistory();

		gameBoard.clearCaptureList();

		// Place stone as requested, then check for (1) captured neighbors and
		// (2) illegal move due to 0 liberties.
		char stoneSymbol = player.getStoneSymbol();
		// Player opponent = GoGame.getNextPlayer(player);

		gameBoard.setSymbolAt(action.getColumn(), action.getRow(), stoneSymbol);

		// look for captured adjacent groups and increment the prisoner counter
		char opponentSymbol = GoGame.getNextPlayer(player).getStoneSymbol();

		int col = GameBoard.getColumnIndex(action.getColumn());
		int row = action.getRow() - 1;

		int prisonerCount = 0;
		if (col > 0 && gameBoard.getSymbolAt(col - 1, row) == opponentSymbol) {
			int liberties = LibertyCounter.countLiberties(gameBoard, col - 1,
					row, opponentSymbol, true);
			if (liberties == 0) {
				prisonerCount += gameBoard.captureMarkedGroup(opponentSymbol);
			} else {
				gameBoard.unmarkGroup(opponentSymbol);
			}
		}
		if (col < gameBoard.getSize() - 1
				&& gameBoard.getSymbolAt(col + 1, row) == opponentSymbol) {
			int liberties = LibertyCounter.countLiberties(gameBoard, col + 1,
					row, opponentSymbol, true);
			if (liberties == 0) {
				prisonerCount += gameBoard.captureMarkedGroup(opponentSymbol);
			} else {
				gameBoard.unmarkGroup(opponentSymbol);
			}
		}
		if (row > 0 && gameBoard.getSymbolAt(col, row - 1) == opponentSymbol) {
			int liberties = LibertyCounter.countLiberties(gameBoard, col,
					row - 1, opponentSymbol, true);
			if (liberties == 0) {
				prisonerCount += gameBoard.captureMarkedGroup(opponentSymbol);
			} else {
				gameBoard.unmarkGroup(opponentSymbol);
			}
		}
		if (row < gameBoard.getSize() - 1
				&& gameBoard.getSymbolAt(col, row + 1) == opponentSymbol) {
			int liberties = LibertyCounter.countLiberties(gameBoard, col,
					row + 1, opponentSymbol, true);
			if (liberties == 0) {
				prisonerCount += gameBoard.captureMarkedGroup(opponentSymbol);
			} else {
				gameBoard.unmarkGroup(opponentSymbol);
			}
		}

		if (stoneSymbol == GameBoard.BLACK_STONE) {
			blackPrisoners += prisonerCount;
		} else if (stoneSymbol == GameBoard.WHITE_STONE) {
			whitePrisoners += prisonerCount;
		}

		// Moved test for 0 liberties until after attempting to capture
		// neighboring groups.
		// This will only happen if no neighboring groups were capture, hence
		// there is nothing to undo.
		// So return now.
		if (0 == LibertyCounter.countLiberties(gameBoard, action.getColumn(),
				action.getRow(), stoneSymbol)) {
			gameBoard.removeStone(action.getColumn(), action.getRow());
			gameBoard.popHashHistory();

			return false;
		}

		// If this hashcode has already appeared, then probably Ko violation.
		// TODO change this to a Map<Long, List<GameBoard>> and check for
		// complete board equality
		if (gameBoard.isKoViolation()) {
			List<Integer> captureList = gameBoard.getCaptureList();
			for (int i : captureList) {
				gameBoard.setSymbolAt(i, opponentSymbol);
			}

			// And finally, remove the originally played stone, which was never
			// valid due to ko.
			gameBoard.removeStone(action.getColumn(), action.getRow());

			gameBoard.clearCaptureList();

			gameBoard.popHashHistory();

			// assertCorrectHash();
			return false;
		} else {
			// assertCorrectHash();
			playerToMove = GoGame.getNextPlayer(player);
			moveHistory.add(action);
			return true;
		}
	}

	public boolean isTerminal() {
		int nMoves = moveHistory.size();
		if (nMoves < 2) {
			return false; // Impossible for a game to be over in 1 move unless
							// the first player resigns
			// before the first player has played, the game is considered to be
			// 'not over'
		}
		return moveHistory.get(nMoves - 1).isPass()
				&& moveHistory.get(nMoves - 2).isPass();
	}

	public String toString() {
		int boardSize = gameBoard.getSize();
		StringBuilder sb = new StringBuilder("  ");
		for (int cIndex = 0; cIndex < boardSize; cIndex++) {
			sb.append(' ');
			if (cIndex < 'I' - 'A') {
				sb.append((char) ('A' + cIndex));
			} else {
				sb.append((char) ('A' + cIndex + 1));
			}
		}
		// note the extra space
		sb.append(System.lineSeparator());
		for (int rIndex = boardSize - 1; rIndex >= 0; rIndex--) {
			if (rIndex < 9) {
				sb.append(' ');
			}
			sb.append(rIndex + 1);
			for (int cIndex = 0; cIndex < boardSize; cIndex++) {
				sb.append(' ');
				sb.append(gameBoard.getSymbolAt(cIndex, rIndex));
			}
			sb.append(" " + (rIndex + 1));
			if (rIndex == boardSize / 2) {
				sb.append("     WHITE(O) has captured ");
				sb.append(getWhitePrisoners());
				sb.append(" stones");
			} else if (rIndex == boardSize / 2 - 1) {
				sb.append("     BLACK(X) has captured ");
				sb.append(getBlackPrisoners());
				sb.append(" stones");
			} else if (rIndex == boardSize / 2 - 2) {
				sb.append("     " + playerToMove + " to move");
			}
			sb.append(System.lineSeparator());
		}
		sb.append("  ");
		for (int cIndex = 0; cIndex < boardSize; cIndex++) {
			sb.append(' ');
			if (cIndex < 'I' - 'A') {
				sb.append((char) ('A' + cIndex));
			} else {
				sb.append((char) ('A' + cIndex + 1));
			}
		}
		return sb.toString();
	}
}