package net.woodyfolsom.msproj.policy;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import net.woodyfolsom.msproj.Action;
import net.woodyfolsom.msproj.GameConfig;
import net.woodyfolsom.msproj.GameResult;
import net.woodyfolsom.msproj.GameState;
import net.woodyfolsom.msproj.GoGame;
import net.woodyfolsom.msproj.Player;
import net.woodyfolsom.msproj.tree.GameTreeNode;
import net.woodyfolsom.msproj.tree.MonteCarloProperties;

public class MonteCarloUCT extends MonteCarlo {
	public static final double TUNING_CONSTANT = 0.50;

	public MonteCarloUCT(Policy movePolicy, long searchTimeLimit) {
		super(movePolicy, searchTimeLimit);
	}

	@Override
	public List<GameTreeNode<MonteCarloProperties>> descend(
			GameTreeNode<MonteCarloProperties> node) {
		double bestScore = Double.NEGATIVE_INFINITY;
		GameTreeNode<MonteCarloProperties> bestNode = node;

		// What if the optimum leaf node is actually a terminal node?
		// From Kocsis and Szepesvari, the value of an actual terminal node is
		// 0, so it will never be grown.

		double nodeVisits = node.getProperties().getVisits();
		Set<Action> actionsExplored = node.getActions();
		GameState gameState = node.getGameState();

		Action randomNewChildAction = new RandomMovePolicy().getAction(node
				.getGameState().getGameConfig(), gameState, actionsExplored,
				gameState.getPlayerToMove());

		// only descend to the best child if no new actions are available at
		// this node
		if (randomNewChildAction == Action.NONE) {
			for (Action action : actionsExplored) {
				GameTreeNode<MonteCarloProperties> childNode = node
						.getChild(action);

				double childScore;
				if (childNode.getGameState().isTerminal()) {
					childScore = 0.0;
				} else {
					MonteCarloProperties properties = childNode.getProperties();
					childScore = (double) (properties.getWins() / properties
							.getVisits())
							+ (TUNING_CONSTANT * Math.sqrt(Math.log(nodeVisits)
									/ childNode.getProperties().getVisits()));
				}
				// TODO add random tie breaker?
				// otherwise the child that is selected first will be biased
				if (childScore >= bestScore) {
					bestScore = childScore;
					bestNode = childNode;
				}
			}
		}

		if (bestNode == node) {
			List<GameTreeNode<MonteCarloProperties>> bestNodeList = new ArrayList<GameTreeNode<MonteCarloProperties>>();
			bestNodeList.add(bestNode);
			return bestNodeList;
		} else {
			return descend(bestNode);
		}
	}

	@Override
	public Action getBestAction(GameTreeNode<MonteCarloProperties> node) {
		Action bestAction = Action.NONE;
		double bestScore = Double.NEGATIVE_INFINITY;
		GameTreeNode<MonteCarloProperties> bestChild = null;

		for (Action action : node.getActions()) {
			GameTreeNode<MonteCarloProperties> childNode = node
					.getChild(action);

			MonteCarloProperties properties = childNode.getProperties();
			double childScore = (double) properties.getWins()
					/ properties.getVisits();

			if (childScore >= bestScore) {
				bestScore = childScore;
				bestAction = action;
				bestChild = childNode;
			}
		}

		if (bestAction == Action.NONE) {
			System.out
					.println("MonteCarloUCT failed - no actions were found for the current game staet (not even PASS).");
		} else {
			System.out.println("Action " + bestAction + " selected for "
					+ node.getGameState().getPlayerToMove()
					+ " with simulated win ratio of "
					+ (bestScore * 100.0 + "%"));
			System.out.println("It was visited "
					+ bestChild.getProperties().getVisits() + " times out of "
					+ node.getProperties().getVisits() + " rollouts among "
					+ node.getNumChildren()
					+ " valid actions from the current state.");
		}
		return bestAction;
	}

	@Override
	public List<GameTreeNode<MonteCarloProperties>> grow(GameConfig gameConfig,
			GameTreeNode<MonteCarloProperties> node, Player player) {
		Policy randomMovePolicy = new RandomMovePolicy();
		Set<Action> exploredActions = node.getActions();
		Action action = randomMovePolicy.getAction(gameConfig,
				node.getGameState(), exploredActions, player);
		if (Action.NONE == action) {
			throw new RuntimeException(
					"Unable to grow node - are all actions already explored?  Board state: "
							+ node.getGameState() + "\nExplored actions: "
							+ exploredActions);
		}
		GameState nextGameState = new GameState(node.getGameState());
		nextGameState.playStone(player, action);
		List<GameTreeNode<MonteCarloProperties>> newChildren = new ArrayList<GameTreeNode<MonteCarloProperties>>();
		GameTreeNode<MonteCarloProperties> newChild = new GameTreeNode<MonteCarloProperties>(
				nextGameState, new MonteCarloProperties());

		newChildren.add(newChild);
		node.addChild(action, newChild);

		return newChildren;
	}

	@Override
	/**
	 * Rollout currently depends on the hardcoded ROLLOUT_DEPTH_LIMIT superclass parameter,
	 * Even with super-ko detection, a rollout might take an unrealistically long time due to unlikely playouts.
	 */
	public int rollout(GameConfig gameConfig,
			GameTreeNode<MonteCarloProperties> node, Player player) {
		Policy randomMovePolicy = new RandomMovePolicy();

		Action action;
		int rolloutDepth = 0;
		GameState rolloutGameState = new GameState(node.getGameState());
		Player currentPlayer = rolloutGameState.getPlayerToMove();
		do {
			rolloutDepth++;
			action = randomMovePolicy.getAction(gameConfig, rolloutGameState,
					currentPlayer);
			if (action != Action.NONE) {
				if (!rolloutGameState.playStone(currentPlayer, action)) {
					throw new RuntimeException(
							"Failed to play move selected by RandomMovePolicy");
				}
				currentPlayer = GoGame.getNextPlayer(currentPlayer);
			}
		} while (action != Action.NONE && rolloutDepth < ROLLOUT_DEPTH_LIMIT);

		numStateEvaluations++;

		GameResult gameScore = rolloutGameState.getResult();

		if (gameScore.isWinner(player)) {
			return 1;
		} else {
			return 0;
		}
	}

	@Override
	public void update(GameTreeNode<MonteCarloProperties> node, int reward) {
		GameTreeNode<MonteCarloProperties> currentNode = node;
		while (currentNode != null) {
			MonteCarloProperties nodeProperties = currentNode.getProperties();
			nodeProperties.setWins(nodeProperties.getWins() + reward);
			nodeProperties.setVisits(nodeProperties.getVisits() + 1);
			currentNode = currentNode.getParent();
		}
	}

	@Override
	public Action getAction(GameConfig gameConfig, GameState gameState,
			Collection<Action> prohibitedActions, Player player) {
		throw new UnsupportedOperationException(
				"Prohibited actions not supported by this class.");
	}

	@Override
	public String toString() {
		return "MonteCarloUCT";
	}

	@Override
	public void setState(GameState gameState) {
		// TODO Auto-generated method stub
		
	}
}