package connectfour;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.io.IOException;
import java.rmi.RemoteException;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.ShutdownSignalException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.AMQP.BasicProperties.Builder;
import server.*;
import shared.*;

public class GameSession implements Runnable {
	
	private int m_id;
	private Calendar m_startTime;
	private String m_queueName;
	private String[] m_players;
	private String[] m_playerQueueNames;
	private int m_numberOfPlayers;
	private byte m_currentPlayer;
	private byte m_winner;
	private byte[] m_lastPieceLocation;
	private byte[][] m_grid;
	private String m_brokerHostName;
	private ConnectionFactory m_factory;
	private Connection m_connection;
	private Channel m_channel;
	private QueueingConsumer m_consumer;
	private boolean m_initialized;
	private boolean m_running;
	private boolean m_started;
	private boolean m_sessionAborted;
	private Thread m_sessionThread;
	public static int sessionIDCounter = 1;
	final public static int MAX_PLAYERS = 2;
	final public static int GRID_WIDTH = 7;
	final public static int GRID_HEIGHT = 6;
	final public static byte UNDETERMINED = 0;
	final public static byte PLAYER1 = 1;
	final public static byte PLAYER2 = 2;
	final public static byte DRAW = 3;
	final public static String[] TABLE_HEADERS = { "ID", "Player 1", "Player 2", "Time Started", "Status" };
	final public static DateFormat TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	public GameSession() {
		m_id = sessionIDCounter++;
		m_startTime = Calendar.getInstance();
		m_queueName = "Game Session " + sessionIDCounter + " Queue";
		m_grid = new byte[GRID_WIDTH][GRID_HEIGHT];
		clearGrid();
		m_players = new String[MAX_PLAYERS];
		m_lastPieceLocation = new byte[MAX_PLAYERS];
		m_playerQueueNames = new String[MAX_PLAYERS];
		clearLastPieceLocation();
		m_winner = UNDETERMINED;
		m_initialized = false;
		m_started = false;
		m_sessionAborted = false;
	}
	
	// initialize messaging and threads
	public boolean initialize(String brokerHostName) {
		if(m_initialized) { return false; }
		
		// initialize RabbitMQ connection and queues
		m_brokerHostName = brokerHostName;
		m_factory = new ConnectionFactory();
		m_factory.setHost(m_brokerHostName);
		try {
			m_connection = m_factory.newConnection();
			m_channel = m_connection.createChannel();
			m_channel.queueDeclare(m_queueName, false, false, false, null);
			m_consumer = new QueueingConsumer(m_channel);
			m_channel.basicConsume(m_queueName, true, m_consumer);
		}
		catch(IOException e) {
			System.err.println("Error initializing messaging service: " + e.getMessage());
			return false;
		}
		
		// set game session as initialized
		m_initialized = true;
		
		// create and start game session thread
		m_sessionThread = new Thread(this);
		m_sessionThread.start();
		
		return true;
	}
	
	// add a player to the game session
	public boolean addPlayer(String userName, String userQueueName) {
		if(userName == null || userQueueName == null) { return false; }
		if(m_numberOfPlayers == MAX_PLAYERS) { return false; }
		
		// store the player's name and queue name
		m_players[m_numberOfPlayers] = userName;
		m_playerQueueNames[m_numberOfPlayers] = userQueueName;
		
		// increment the number of players
		m_numberOfPlayers++;
		
		return true;
	}
	
	// get the session id
	public int getID() {
		return m_id;
	}
	
	// get the session's queue name
	public String getQueueName() {
		return m_queueName;
	}
	
	// get the player's queue name at the specified index
	public String getPlayerQueueName(int index) {
		if(index < 0 || index >= m_playerQueueNames.length) { return null; }
		
		return m_playerQueueNames[index];
	}
	
	// get the queue associated with the specified player name
	public String getPlayerQueueName(String playerName) {
		int playerIndex = getPlayerIndex(playerName);
		if(playerIndex < 0) { return null; }
		
		return m_playerQueueNames[playerIndex];
	}
	
	// get the opponent's player queue based on the specified player index
	public String getOpponentPlayerQueueName(int index) {
		if(index < 0 || index >= m_playerQueueNames.length) { return null; }
		
		return m_playerQueueNames[index == 0 ? 1 : 0];
	}
	
	// get the opponent's player queue based on the specified player name
	public String getOpponentPlayerQueueName(String playerName) {
		int playerIndex = getPlayerIndex(playerName);
		if(playerIndex < 0) { return null; }
		
		return m_playerQueueNames[playerIndex == 0 ? 1 : 0];
	}
	
	// get the number of players in the game session
	public int numberOfPlayers() {
		return m_numberOfPlayers;
	}
	
	// return if the game session has started
	public boolean isStarted() {
		return m_started;
	}
	
	// return if the game session is full
	public boolean isFull() {
		return m_numberOfPlayers == MAX_PLAYERS;
	}
	
	// return if the game session is finished
	public boolean isFinished() {
		return m_sessionAborted || (isStarted() && (m_winner == PLAYER1 || m_winner == PLAYER2 || m_winner == DRAW));
	}
	
	// return the name of the player at the specified index
	public String getPlayerName(int index) {
		if(index < 0 || index >= m_numberOfPlayers) { return null; }
		
		return m_players[index];
	}
	
	// return the index of the specified player based on their name
	public int getPlayerIndex(String playerName) {
		if(playerName == null) { return -1; }
		
		for(int i=0;i<m_numberOfPlayers;i++) {
			if(playerName.equalsIgnoreCase(m_players[i])) {
				return i;
			}
		}
		
		return -1;
	}
	
	// return if the game session contains the specified player
	public boolean contains(String playerName) {
		return getPlayerIndex(playerName) >= 0;
	}
	
	// remove the specified player from the game session
	public boolean playerLeft(String playerName) {
		int playerIndex = getPlayerIndex(playerName);
		if(playerIndex < 0) { return false; }
		
		m_sessionAborted = true;
		
		return true;
	}
	
	// return the time that the session started at
	public Calendar getStartTime() {
		return m_startTime;
	}
	
	// return the time the session started at as a string
	public String getStartTimeAsString() {
		return TIME_FORMAT.format(m_startTime.getTime());
	}
	
	// return the status of the game session as a string
	public String getStatusAsString() {
		if(isFinished()) {
			if(m_winner == PLAYER1) {
				return "Finished: Player 1 Won";
			}
			else if(m_winner == PLAYER2) {
				return "Finished: Player 2 Won";
			}
			else if(m_winner == DRAW) {
				return "Finished: Draw";
			}
		}
		else if(isStarted()) {
			if(m_currentPlayer == PLAYER1) {
				return "Playing: Player 1's Turn";
			}
			else if(m_currentPlayer == PLAYER2) {
				return "Playing: Player 2's Turn";
			}
		}
		else if(isFull()) {
			return "Starting Game";
		}
		else if(!isFull()) {
			return "Waiting for Opponent";
		}
		return "";
	}
	
	// return the session information as a String array
	public String[] getAsTableEntry() {
		return new String[] { Integer.toString(m_id), m_players[0], m_players[1], getStartTimeAsString(), getStatusAsString() }; 
	}
	
	// stat the game session
	public boolean startGame() {
		// verify that the game session can be started
		if(!isFull() || m_started || !m_initialized) { return false; }
		
		// set game session as started and randomly choose first player
		m_started = true;
		m_currentPlayer = (byte) ((Math.random() * 2) + 1);
		
		// generate the start game message and send it to each player
		Message startGame;
		
		startGame = new Message("Start Game");
		startGame.setAttribute("Current Player Name", m_players[m_currentPlayer - 1]);
		
		startGame.setAttribute("Player Number", Integer.toString(PLAYER1));
		startGame.setAttribute("Opponent Player Name", m_players[1]);
		sendMessageToPlayer(startGame, PLAYER1);
		
		startGame.setAttribute("Player Number", Integer.toString(PLAYER2));
		startGame.setAttribute("Opponent Player Name", m_players[0]);
		sendMessageToPlayer(startGame, PLAYER2);
		
		return true;
	}
	
	// switch to the next player
	public void nextPlayer() {
		m_currentPlayer = (byte) ((m_currentPlayer == PLAYER1) ? PLAYER2 : PLAYER1);
	}
	
	// clear the game grid of all pieces
	public void clearGrid() {
		for(int i=0;i<GRID_WIDTH;i++) {
			for(int j=0;j<GRID_HEIGHT;j++) {
				m_grid[i][j] = UNDETERMINED;
			}
		}
	}
	
	// clear the last placed piece's location
	public void clearLastPieceLocation() {
		m_lastPieceLocation[0] = -1;
		m_lastPieceLocation[1] = -1;
	}
	
	// place a piece in the specified column
	public boolean placePiece(int column) {
		if(column < 0 || column > GRID_WIDTH) { return false; }
		
		boolean placedPiece = false;
		
		// place piece at the next available location in this column (if any)
		for(int j=GRID_HEIGHT-1;j>=0;j--) {
			if(m_grid[column][j] == UNDETERMINED) {
				m_grid[column][j] = m_currentPlayer;
				m_lastPieceLocation[0] = (byte) column;
				m_lastPieceLocation[1] = (byte) j;
				placedPiece = true;
				break;
			}
		}
		
		// check for a winner
		checkGameFinished();
		
		return placedPiece;
	}
	
	public boolean checkGameFinished() {
		int x, y;
		byte[] count = new byte[2];
		
		// check horizontal
		for(int j=0;j<GRID_HEIGHT;j++) {
			for(int i=0;i<GRID_WIDTH;i++) {
				count = updateCount(count, i, j);
				if(checkWin(count, i, j)) {
					return true;
				}
			}
			
			// reset count
			count[0] = 0;
			count[1] = 0;
		}
		
		// check vertical
		for(int i=0;i<GRID_WIDTH;i++) {
			for(int j=0;j<GRID_HEIGHT;j++) {
				count = updateCount(count, i, j);
				if(checkWin(count, i, j)) {
					return true;
				}
			}
			
			// reset count
			count[0] = 0;
			count[1] = 0;
		}
		
		// check diagonals
		for(int i=0;i<GRID_WIDTH;i++) {
			x = i;
			y = 0;
			
			// check right diagonal (top)
			while(isValid(x, y)) {
				count = updateCount(count, x, y);
				if(checkWin(count, x, y)) {
					return true;
				}
				
				// move downwards, to the right
				x++;
				y++;
			}
			
			// reset variables
			x = i;
			y = 0;
			count[0] = 0;
			count[1] = 0;
			
			// check left diagonal (top)
			while(isValid(x, y)) {
				count = updateCount(count, x, y);
				if(checkWin(count, x, y)) {
					return true;
				}
				
				// move downwards, to the left
				x--;
				y++;
			}
			
			// reset variables
			x = i;
			y = GRID_HEIGHT - 1;
			count[0] = 0;
			count[1] = 0;
			
			// check right diagonal (bottom)
			while(isValid(x, y)) {
				count = updateCount(count, x, y);
				if(checkWin(count, x, y)) {
					return true;
				}
				
				// move upwards, to the right
				x++;
				y--;
			}
			
			// reset variables
			x = i;
			y = GRID_HEIGHT - 1;
			count[0] = 0;
			count[1] = 0;
			
			// check left diagonal (bottom)
			while(isValid(x, y)) {
				count = updateCount(count, x, y);
				if(checkWin(count, x, y)) {
					return true;
				}
				
				// move upwards, to the left
				x--;
				y--;
			}
			
			// reset count
			count[0] = 0;
			count[1] = 0;
		}
		
		// check if the game board is full (no more moves)
		boolean boardFull = true;
		for(int i=0;i<GRID_WIDTH;i++) {
			if(m_grid[i][0] == UNDETERMINED) {
				boardFull = false;
				break;
			}
		}
		
		// if the board is full, set the outcome as a draw
		if(boardFull) {
			m_winner = DRAW;
			return true;
		}
		
		return false;
	}
	
	// check if there is a winner based on a cumulative count
	private boolean checkWin(byte[] count, int x, int y) {
		if(m_grid[x][y] == UNDETERMINED) { return false; }
		
		// check if the player at the current position has won
		if(count[m_grid[x][y] - 1] == 4) {
			m_winner = m_grid[x][y];
			return true;
		}
		return false;
	}
	
	// updates a cumulative count which keeps track of how many pieces each player has in a row
	private byte[] updateCount(byte[] count, int x, int y) {
		// if there is a player piece in the current position
		if(m_grid[x][y] != UNDETERMINED) {
			// increment current player count, reset other player's count
			count[m_grid[x][y] - 1]++;
			count[m_grid[x][y] == PLAYER1 ? 1 : 0] = 0;
		}
		// if there is no piece in the current position
		else {
			// reset both player counts
			count[0] = 0;
			count[1] = 0;
		}
		
		// return the updated count
		return count;
	}
	
	// checks if the specified x and y are within the limits of the board size
	public static boolean isValid(int x, int y) {
		// verify that the position exists within the game board
		return x >= 0 && y >= 0 && x < GRID_WIDTH && y < GRID_HEIGHT;
	}
	
	// sends the message to the opposing player based on the specified player number
	public boolean sendMessageToOpposingPlayer(Message message, int playerNumber) {
		if(!(playerNumber == PLAYER1 || playerNumber == PLAYER2)) { return false; }
		
		return sendMessageToPlayer(message, m_playerQueueNames[(playerNumber == PLAYER1 ? PLAYER2 : PLAYER1) - 1]);
	}
	
	// sends the message to the opposing player based on the specified player queue name
	public boolean sendMessageToOpposingPlayer(Message message, String playerQueueName) {
		if(message == null || playerQueueName == null) { return false; }
		
		return sendMessageToPlayer(message, playerQueueName.equals(m_playerQueueNames[0]) ? m_playerQueueNames[1] : m_playerQueueNames[0]);
	}
	
	// sends the message to the player based on the specified player number
	public boolean sendMessageToPlayer(Message message, int playerNumber) {
		if(!(playerNumber == PLAYER1 || playerNumber == PLAYER2)) { return false; }
		
		return sendMessageToPlayer(message, m_playerQueueNames[playerNumber - 1]);
	}
	
	// sends the message to the player based on the specified player name
	public boolean sendMessageToPlayer(Message message, String playerQueueName) {
		if(message == null || playerQueueName == null) { return false; }
		
		// build the RabbitMQ message and send it to the player
		try {
			Builder builder = new AMQP.BasicProperties.Builder();
			BasicProperties properties = builder.contentType("text/plain").replyTo(m_queueName).build();
			m_channel.basicPublish("", playerQueueName, properties, Message.serializeMessage(message));
		}
		catch(Exception e) {
			Server.console.writeLine("Error sending message to player \"" + playerQueueName + "\": " + e.getMessage());
			return false;
		}
		
		return true;
	}
	
	// handle incoming messages from users
	public void handleMessage(QueueingConsumer.Delivery delivery) {
		// extract and verify the message stored in the delivery
		if(delivery == null) { return; }
		Message message = null;
		try { message = Message.deserializeMessage(delivery.getBody()); }
		catch(Exception e) { return; }
		if(message == null) { return; }
		
		// handle place piece messages
		if(message.getType().equalsIgnoreCase("Place Piece")) {
			// get user's name and column, and verify them
			String userName = (String) message.getAttribute("User Name");
			int columnIndex = -1;
			try {
				columnIndex = Integer.parseInt((String) message.getAttribute("Column Index"));
			}
			catch(NumberFormatException e) {
				return;
			}
			if(columnIndex < 0 || columnIndex >= GRID_WIDTH) { return; }
			
			// if the move was valid and a piece was placed
			if(placePiece(columnIndex)) {
				// generate and send a message to the player indicating that their move was valid
				Message reply = new Message("Move Valid");
				reply.setAttribute("User Name", userName);
				sendMessageToPlayer(reply, delivery.getProperties().getReplyTo());
				
				// generate and send a message to the opposing player that a piece was placed
				Message piecePlaced = new Message("Piece Placed");
				piecePlaced.setAttribute("User Name", userName);
				piecePlaced.setAttribute("Column Index", Integer.toString(columnIndex));
				sendMessageToPlayer(piecePlaced, m_currentPlayer == PLAYER1 ? PLAYER2 : PLAYER1);
				
				Server.console.writeLine("Player \"" + userName + "\"" + " placed piece at location: (" + m_lastPieceLocation[0] + ", " + m_lastPieceLocation[1] + ")");
				
				// if the game ended in a draw
				if(m_winner == DRAW) {
					// generate a message indicating that the game ended in a draw and send it to each player 
					Message draw = new Message("Draw");
					sendMessageToPlayer(draw, PLAYER1);
					sendMessageToPlayer(draw, PLAYER2);
					
					// update each player's stats and add a new match history to the database
					try { Server.database.addDraw(m_players[0]); } catch(RemoteException e) { }
					try { Server.database.addDraw(m_players[1]); } catch(RemoteException e) { }
					try { Server.database.addMatch(new MatchData(Calendar.getInstance(), m_players[0], m_players[1], Outcome.Draw)); } catch(RemoteException e) { }
					
					Server.console.writeLine("Session #" + m_id + " ended in a draw.");
					
					// remove the game session
					Server.instance.removeSession(this);
					
					return;
				}
				// otherwise, if a player won the game
				else if(m_winner == PLAYER1 || m_winner == PLAYER2) {
					// generate a message indicating that the game has ended ended and which player won, then send it to each player
					Message winner = new Message("Player Won");
					winner.setAttribute("User Name", m_players[m_winner - 1]);
					sendMessageToPlayer(winner, PLAYER1);
					sendMessageToPlayer(winner, PLAYER2);
					
					// update each player's stats and add a new match history to the database
					try { Server.database.addWin(m_players[m_winner - 1]); } catch(RemoteException e) { }
					try { Server.database.addLoss(m_players[(m_winner == PLAYER1 ? PLAYER2 : PLAYER1) - 1]); } catch(RemoteException e) { }
					try { Server.database.addMatch(new MatchData(Calendar.getInstance(), m_players[0], m_players[1], m_winner == PLAYER1 ? Outcome.Win : Outcome.Loss)); } catch(RemoteException e) { }
					
					Server.console.writeLine("Player \"" + m_players[m_winner - 1] + "\" won the game in session #" + m_id + "!");
					
					// remove the game session
					Server.instance.removeSession(this);
					
					return;
				}
				
				// switch to the next player
				nextPlayer();
			}
			// otherwise, if the move was invalid
			else {
				// generate and send a message to the user indicating that their move was invalid
				Message reply = new Message("Move Invalid");
				reply.setAttribute("User Name", userName);
				sendMessageToPlayer(reply, delivery.getProperties().getReplyTo());
				
				Server.console.writeLine("Player \"" + userName + "\" attempted an invalid move in column: " + columnIndex );
			}
		}
	}
	
	// stop the game session
	public void stop() {
		// reset all initialization variables
		m_initialized = false;
		m_running = false;
		m_sessionAborted = true;
		
		// stop all active threads
		try { m_sessionThread.interrupt(); } catch(Exception e) { }
		try { m_channel.close(); } catch(Exception e) { }
		try { m_connection.close(); } catch(Exception e) { }
	}
	
	// indefinitely listen for messages from players in the game session
	public void run() {
		if(!m_initialized) { return; }
		
		m_running = true;
		
		// listen for and handle messages from players
		while(m_running) {
			try {
				handleMessage(m_consumer.nextDelivery());
			}
			catch(InterruptedException e) {
				stop();
			}
			catch(ShutdownSignalException e) {
				stop();
			}
			catch(Exception e) {
				Server.console.writeLine("Critical error, server shutting down.");
				e.printStackTrace();
				stop();
			}
		}
	}
	
}
