package game.training;

import java.util.Arrays;

import game.logic.GameBoard;
import game.logic.Vector;

public class GameSimulation {
	
	/****************
	 * Class fields
	 ****************/
	/* The players of the game */
	private TrainPlayer player1;
	private TrainPlayer player2;
	
	/* The game board */
	private GameBoard board;
	
	/* Other game features */
	private int currentPlayer;
	
	/* For debugging purposes */
	private boolean watchGame;
	
	/* Counts how many turns have passed (as part of alpha updating). A turn
	 * is defined as player1's move and player2's move. */
	public static double TurnsPassed = 0.0;
	
	/**
	 * Constructor - start simulate a new game with random players!
	 */
	public GameSimulation(){
		this.currentPlayer = GameBoard.RedPlayer;
		this.board = new GameBoard(this.currentPlayer);
		
		this.player1 = new TrainPlayer(GameBoard.RedPlayer);
		this.player2 = new TrainPlayer(GameBoard.BlackPlayer);
		this.watchGame = false;
	}
	
	/** Start a new game with the given players */
	public GameSimulation(TrainPlayer player1, TrainPlayer player2){
		this.currentPlayer = GameBoard.RedPlayer;
		this.board = new GameBoard(this.currentPlayer);
		
		this.player1 = player1;
		this.player2 = player2;
		this.watchGame = false;
	}
	
	/** Start a new game with the given players and with the option to view a whole
	 * game. */
	public GameSimulation(TrainPlayer player1, TrainPlayer player2, boolean watch){
		this.currentPlayer = GameBoard.RedPlayer;
		this.board = new GameBoard(this.currentPlayer);
		
		this.player1 = player1;
		this.player2 = player2;
		this.watchGame = watch;
	}
	
	/** The main procedure of this class. Simulate a whole game and return the winner!
	 * The simulation runs as long as the game is not over, and as follows:
	 * 1. Player1 chooses a move, according to its type (random, agent, trainer).
	 * 2. The move is played, and the state feature vector is updated.
	 * 3. If this move caused player1 to win, exit loop.
	 * 4. Else, switch player.
	 * 5. Player2 chooses a move.
	 * 6. The move is played, and state feature vector is updated.
	 * 7. If this move caused player2 to win, exit loop.
	 * 8. Else, switch player.
	 * 9. Go to step 1. 
	 * 
	 * In addition, we want to update the players' weight vectors throughout the simulation.
	 * The update is done in the following way:
	 * 1. At the beginning of the first loop, the "old" state of the board is saved in
	 * player1St and its score is under player1OldExpectedReturn.
	 * 2. Suppose that at the start of the k'th loop this holds. We will show that
	 * on the start of the k+1'th loop it holds as well.
	 * 3. The first step of the loop is player1's choice of a move (epsilon-greedy or coerced move).
	 * After this choice, we wait for player2 to make its move.
	 * 4. Now we can update the weight vector: Reaching the end of the loop indicates that no one
	 * has won. So we calculate the new expected return and update weights.
	 * 5. If anywhere along the way some player has won, we exit the loop and update the weights
	 * afterwards. */
	public int playGame(){
		
		int player1Action = 0;
		int player2Action = 0;
		
		/* Player1 weight-updating variables (will only be updated if it is an Agent) */
		double player1OldExpectedReturn = 0.0;
		double player1NewExpectedReturn = 0.0;
		double player1Reward = 0.0;
		Vector player1St = new Vector(GameBoard.VectorSize);
		Vector player1Stplus1 = new Vector(GameBoard.VectorSize);
		player1St.setZero();
		player1Stplus1.setZero();
		
		/* Player2 weight-updating variables (will only be updated if it is an Agent) */
		double player2OldExpectedReturn = 0.0;
		double player2NewExpectedReturn = 0.0;
		double player2Reward = 0.0;
		Vector player2St = new Vector(GameBoard.VectorSize);
		Vector player2Stplus1 = new Vector(GameBoard.VectorSize);
		player2St.setZero();
		player2Stplus1.setZero();
		
		/* Current State */
		Vector CurrentState = new Vector(GameBoard.VectorSize);
		CurrentState.setZero();
		
		/* Continue playing as long as the game is not over.
		 * Player1 gets to start. */
		while (!board.isOver()){
			
			/* Update Alpha according to the turn */
			SarsaLearning.Alpha = 1.0/(TurnsPassed+1);
			
			/* Save St+1 for player1 - state before player1 action */
			player1St.copyVector(CurrentState);
			player1OldExpectedReturn = player1NewExpectedReturn;
			
			/* Choose player1's move, at, and make it. Use the coerced moves we've collected
			 * as a guidance - If there is a winning move for player1, then take it.
			 * Else, if there is a winning move of player2, take it so we won't loose (unless
			 * player2 has two winning moves or had some sort of a strategy for us). Else,
			 * choose the best move according to the MiniMax algorithm. */
			if (board.Player1ForcedWin != null && !board.Player1ForcedWin.isEmpty()){
				player1Action = board.Player1ForcedWin.get(0);
			} else if (board.Player2ForcedWin != null && !board.Player2ForcedWin.isEmpty()){
				player1Action = board.Player2ForcedWin.get(0);
			} else {
				player1Action = player1.chooseMove(board, watchGame);
			}
			
			/* Play the move and update the current state */
			board.insertDisc(player1Action, this.currentPlayer);
			CurrentState.copyVector(board.getStateVector());
			
			/* Check if game is over, and if so find who the winner is and break */
			if (board.isOver()){
				board.decideWinner();
				break;
			}
			
			/* Calculate St+1 expected return and update weights for player2 - we are after player1
			 * move, so this is the new state for player2. */
			player2NewExpectedReturn = Vector.dotProduct(CurrentState, player2.getWeights());
			player2.setWeights(SarsaLearning.UpdateWeightVector(player2.getWeights(), player2Reward,
					player2St, player2OldExpectedReturn, player2NewExpectedReturn, watchGame, player2.getRole()));
			
			/* Switch Players */
			switchPlayer();
			board.switchPlayer();
			
			/* Save St for player2 - state before its move */
			player2St.copyVector(CurrentState);
			player2OldExpectedReturn = player2NewExpectedReturn;
			
			/* Choose player2's move, at, and make it. Use the coerced moves we've collected
			 * as a guidance - If there is a winning move for player2, then take it.
			 * Else, if there is a winning move of player1, take it so we won't loose (unless
			 * player1 has two winning moves or had some sort of a strategy for us). Else,
			 * choose the best move according to the MiniMax algorithm. */
			if (board.Player2ForcedWin != null && !board.Player2ForcedWin.isEmpty()){
				player2Action = board.Player2ForcedWin.get(0);
			} else if (board.Player1ForcedWin != null && !board.Player1ForcedWin.isEmpty()){
				player2Action = board.Player1ForcedWin.get(0);
			} else {
				player2Action = player2.chooseMove(board, watchGame);
			}
			
			/* Play the move */
			board.insertDisc(player2Action, this.currentPlayer);
			CurrentState.copyVector(board.getStateVector());

			if (board.isOver()){
				board.decideWinner();
				break;
			}
			
			/* Calculate St+1 expected return and update weights for player1 */
			player1NewExpectedReturn = Vector.dotProduct(CurrentState, player1.getWeights());
			player1.setWeights(SarsaLearning.UpdateWeightVector(player1.getWeights(), player1Reward, player1St,
					player1OldExpectedReturn, player1NewExpectedReturn, watchGame, player1.getRole()));
			
			/* Switch Players and start all over again */
			switchPlayer();
			board.switchPlayer();
			
			/* A turn has passed */
			TurnsPassed++;
		}
		
		/* Game is over, get players' immediate rewards:
		 * 1.0 for winning
		 * 0.0 for tie
		 * -1.0 for loosing */
		player1Reward = getReward(board.getWinnerPlayer(), GameBoard.RedPlayer);
		player2Reward = getReward(board.getWinnerPlayer(), GameBoard.BlackPlayer);
		
		if (player1.getType() == TrainPlayer.AGENT){
			player1NewExpectedReturn = 0.0;
			player1.setWeights(SarsaLearning.UpdateWeightVector(player1.getWeights(), player1Reward,
					player1St, player1OldExpectedReturn, player1NewExpectedReturn, 
					watchGame, player1.getRole()));
		}
		
		if (player2.getType() == TrainPlayer.AGENT){
			player2NewExpectedReturn = 0.0;
			player2.setWeights(SarsaLearning.UpdateWeightVector(player2.getWeights(), player2Reward,
					player2St, player2OldExpectedReturn, player2NewExpectedReturn,
					watchGame, player2.getRole()));
		}
		
		/* Print the board statistics at the end of the game, for debugging purposes */
		if (watchGame){
			GameSimulation.PrintStats(board);
		}

		return board.getWinnerPlayer();
	}
	
	/**
	 * Switch the current player. 
	 */
	public void switchPlayer(){
		this.currentPlayer = 3 - this.currentPlayer;
	}
	
	/**
	 * Get the game board.
	 * @return the game board of the current simulation.
	 */
	public GameBoard getGameBoard(){
		return this.board;
	}
	
	/**
	 * Get the immediate reward of the player.
	 * @param winner The winner of the current game.
	 * @param player The player that we want to decide its reward.
	 * 
	 * @return 1 for winning, -1 for loosing, 0 for a tie.
	 */
	public double getReward(int winner, int player){
		double reward = 0.0;
		if (winner == player){
			reward = 1.0;
		} else if (winner == 3 - player){
			reward = -1.0;
		}
		
		return reward;
	}
	
	/**
	 * Print board-related statistics of the given game board for debugging purposes.
	 * @param g the game board.
	 */
	public static void PrintStats(GameBoard g){
		GameBoard.printBoard(g.getBoard());
		System.out.println("Winner Player: " + g.getWinnerPlayer());
		System.out.println("Current Player: " + g.getCurrentPlayer());
		System.out.println("Total Moves: " + g.getTotalDiscs());
		System.out.println("Highest Disc: " + g.getHighestDisc());
		System.out.println("Discs per Column: " + Arrays.toString(g.getDiscsPerColumn()));
		System.out.println("Possible Moves: " + g.getPossibleMoves());
		System.out.println("Game Vector:    " + g.getStateVector().indexVector());
	}
}