package com.connectfour;

import java.io.IOException;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.AMQP.BasicProperties.Builder;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;

public class ConnectFourView extends View {
	
	private GameType m_gameType;
	private String m_userName;
	private int m_playerNumber;
	private String m_queueName;
	private String m_opponentPlayerName;
	private String m_sessionQueueName;
	private Channel m_channel;
	private int m_gridSize;
	private int m_boardWidth;
	private int m_boardHeight;
	private int m_xOffset;
	private int m_yOffset;
	private int m_selectedX;
	private int m_selectedY;
	private byte m_currentPlayer;
	private byte[][] m_grid;
	private byte m_winner;
	private Paint m_gridLinePaint;
	private Paint m_selectionPaint;
	private Paint[] m_playerPaint;
	private boolean m_gridIntialized;
	private boolean m_waitingForMoveValidation;
	private Handler m_handler;
	public static ConnectFourView instance;
	final private static boolean DEBUG = false;
	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;
	
	public ConnectFourView(Context context, AttributeSet attrs) {
		super(context, attrs);
		
		// store a global reference to the game view
		instance = this;
		
		// initialize a handler for running ui-related code
		m_handler = new Handler();
		
		// initialize variables
		m_gridIntialized = false;
		m_waitingForMoveValidation = false;
		
		// initialize the game
		initGame();
		requestFocus();
	}
	
	private void initGame() {
		m_grid = new byte[GRID_WIDTH][GRID_HEIGHT];
		
		// initialize grid paint
		m_gridLinePaint = new Paint();
		m_gridLinePaint.setColor(Color.WHITE);
		m_gridLinePaint.setStrokeWidth(1);
		m_gridLinePaint.setStyle(Style.STROKE);
		
		// initialize selection paint
		m_selectionPaint = new Paint();
		m_selectionPaint.setColor(Color.RED);
		m_selectionPaint.setStrokeWidth(1);
		m_selectionPaint.setStyle(Style.STROKE);
		
		// initialize player piece paint
		m_playerPaint = new Paint[3];
		for(int i=0;i<m_playerPaint.length;i++) {
			m_playerPaint[i] = new Paint();
			m_playerPaint[i].setStyle(Style.FILL);
		}
		m_playerPaint[0].setStrokeWidth(1);
		m_playerPaint[0].setStyle(Style.STROKE);
		m_playerPaint[0].setColor(Color.WHITE);
		m_playerPaint[1].setColor(Color.RED);
		m_playerPaint[2].setColor(Color.BLUE);
		
		// clear grid and selection
		clearSelection();
		clearGrid();
		
		m_winner = UNDETERMINED;
		m_currentPlayer = PLAYER1;
		
		m_gridIntialized = false;
		
		// retrieve game information from parent activity
		m_gameType = ConnectFour.instance.getGameType();
		if(m_gameType == GameType.Multiplayer) {
			m_userName = ConnectFour.instance.getUserName();
			m_playerNumber = ConnectFour.instance.getPlayerNumber();
			m_currentPlayer = (byte) (m_userName.equalsIgnoreCase(ConnectFour.instance.getCurrentPlayerName()) ? m_playerNumber : (m_playerNumber == PLAYER1 ? PLAYER2 : PLAYER1));
			m_queueName = ConnectFour.instance.getQueueName();
			m_opponentPlayerName = ConnectFour.instance.getOpponentPlayerName();
			m_sessionQueueName = ConnectFour.instance.getSessionQueueName();
			m_channel = ConnectFour.instance.getChannel();
			
			// display player's colour and opponent's name if multiplayer game
			Toast.makeText(ConnectFourGame.instance, ConnectFourGame.instance.getString(m_playerNumber == PLAYER1 ? R.string.player_red : R.string.player_blue), Toast.LENGTH_SHORT).show();
			Toast.makeText(ConnectFourGame.instance, ConnectFourGame.instance.getString(R.string.opponent_name) + " " + m_opponentPlayerName, Toast.LENGTH_SHORT).show();
		}
	}
	
	// switch to the next player
	public void nextPlayer() {
		m_currentPlayer = (byte) ((m_currentPlayer == PLAYER1) ? PLAYER2 : PLAYER1);
	}
	
	// clear the selected grid cell
	public void clearSelection() {
		m_selectedX = -1;
		m_selectedY = -1;
	}
	
	// clear the grid of all game 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;
			}
		}
	}
	
	// checks if the game is finished
	public boolean isFinished() {
		return m_winner == PLAYER1 || m_winner == PLAYER2 || m_winner == DRAW;
	}
	
	// handles touch events
	public boolean onTouchEvent(MotionEvent e) {
		// check if the game is finished
		// or if the game is multiplayer and it is waiting for a response from the server, or it is not the current player's turn
		if(isFinished()) { return true; }
		if(m_gameType == GameType.Multiplayer && (m_waitingForMoveValidation || m_currentPlayer != m_playerNumber)) { return true; }
		
		// if the user pressed on the screen
		if(e.getAction() == MotionEvent.ACTION_DOWN) {
			// clear the selected grid cell
			clearSelection();
			
			// get the position touched
			int x = (int) e.getX();
			int y = (int) e.getY();
			
			// verify that the position is within the grid of the game board
			if(x < m_xOffset || x > m_xOffset + m_boardWidth ||
			   y < m_yOffset || y > m_yOffset + m_boardHeight) {
				invalidate();
				return true;
			}
			
			// get the grid position corresponding to this point
			m_selectedX = (x - m_xOffset) / m_gridSize;
			m_selectedY = (y - m_yOffset) / m_gridSize;
			
			// verify the selected grid position
			if(m_selectedX < 0 || m_selectedX >= GRID_WIDTH ||
			   m_selectedY < 0 || m_selectedY >= GRID_HEIGHT) {
				clearSelection();
				invalidate();
				return true;
			}
			
			// if the game is being played locally
			if(m_gameType == GameType.Hotseat) {
				// place a piece at the current position
				placePiece(m_selectedX);
			}
			// otherwise if it is a multiplayer game
			else if(m_gameType == GameType.Multiplayer) {
				// send a message to the server indicating where you wish to place your piece
				shared.Message placePiece = new shared.Message("Place Piece");
				placePiece.setAttribute("User Name", m_userName);
				placePiece.setAttribute("Column Index", Integer.toString(m_selectedX));
				try { sendMessageToSession(placePiece); }
				catch(IOException e2) {
					Log.e("error", "Error sending message to session: " + placePiece);
				}
				
				// set waiting variable to true
				m_waitingForMoveValidation = true;
			}
			
			// redraw the game board
			invalidate();
			
			return true;
		}
		
		return false;
	}
	
	// places a piece in the specified column
	public void placePiece(int columnIndex) {
		if(columnIndex < 0 || columnIndex >= GRID_WIDTH) { return; }
		
		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[columnIndex][j] == UNDETERMINED) {
				m_grid[columnIndex][j] = m_currentPlayer;
				placedPiece = true;
				break;
			}
		}
		
		// check if a piece was placed
		if(placedPiece) {
			if(m_gameType == GameType.Hotseat) {
				// check for a winner
				if(checkGameFinished()) {
					return;
				}
				
				// switch to the next player
				nextPlayer();
			}
		}
		else {
			// if the game was not placed, and it is a hotseat game, display that the user made an invalid move
			if(m_gameType == GameType.Hotseat) {
				Toast.makeText(ConnectFourGame.instance, ConnectFourGame.instance.getString(R.string.invalid_move), Toast.LENGTH_SHORT).show();
			}
		}
	}
	
	// checks if the game is finished
	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 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
		if(boardFull) {
			// set the outcome as a draw
			m_winner = DRAW;
			
			// if the game is a local game, display that there was a draw
			if(m_gameType == GameType.Hotseat) {
				Toast.makeText(ConnectFourGame.instance, ConnectFourGame.instance.getString(R.string.draw), Toast.LENGTH_LONG).show();
			}
			
			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_gameType == GameType.Multiplayer) { return false; }
		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];
			
			// if the player has won and the game is local, display a message indicating which player won
			if(m_gameType == GameType.Hotseat) {
				Toast.makeText(ConnectFourGame.instance, m_winner == PLAYER1 ? ConnectFourGame.instance.getString(R.string.red_won) : ConnectFour.instance.getString(R.string.blue_won), Toast.LENGTH_LONG).show();
			}
			
			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;
	}
	
	// draw the game board
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		
		// initialize the game board grid
		initGrid(canvas);
		
		// draw debugging stuff
		if(DEBUG) {
			// draw grid
			drawGrid(canvas);
			
			// draw selected grid cell
			drawSelection(canvas);
		}
		
		// draw all game pieces
		drawPieces(canvas);
		
		// indicate which player's turn it is
		drawCurrentPlayer(canvas);
		
		if(DEBUG) {
			// indicate which player has won (if any)
			drawWinner(canvas);
		}
	}
	
	// initialize grid variables
	private void initGrid(Canvas canvas) {
		if(m_gridIntialized) { return; }
		
		// calculate the total width and height of the board
		int w = canvas.getClipBounds().width() / GRID_WIDTH;
		int h = canvas.getClipBounds().height() / GRID_HEIGHT;
		
		// calculate and initialize grid values
		m_gridSize = w < h ? w : h;
		m_boardWidth = m_gridSize * GRID_WIDTH;
		m_boardHeight = m_gridSize * GRID_HEIGHT;
		m_xOffset = (canvas.getClipBounds().width() - (m_gridSize * GRID_WIDTH)) / 2;
		m_yOffset = (canvas.getClipBounds().height() - (m_gridSize * GRID_HEIGHT)) / 2;
		
		// set grid as initialized
		m_gridIntialized = true;
	}
	
	// draw the grid
	private void drawGrid(Canvas canvas) {
		// draw vertical lines
		for(int i=0;i<GRID_WIDTH+1;i++) {
			canvas.drawLine((m_gridSize * i) + m_xOffset, m_yOffset, (m_gridSize * i) + m_xOffset, (m_gridSize * GRID_HEIGHT) + m_yOffset, m_gridLinePaint);
		}
		
		// draw horizontal lines
		for(int j=0;j<GRID_HEIGHT+1;j++) {
			canvas.drawLine(m_xOffset, (m_gridSize * j) + m_yOffset, (m_gridSize * GRID_WIDTH) + m_xOffset, (m_gridSize * j) + m_yOffset, m_gridLinePaint);
		}
	}
	
	// draw the selected grid cell
	private void drawSelection(Canvas canvas) {
		if(m_selectedX < 0 || m_selectedY < 0) { return; }
		
		// get position of selected grid block
		int x = m_xOffset + (m_gridSize * m_selectedX);
		int y = m_yOffset + (m_gridSize * m_selectedY);
		
		// draw selected grid block
		canvas.drawRect(x, y, x + m_gridSize, y + m_gridSize, m_selectionPaint);
	}
	
	// draw the game pieces
	private void drawPieces(Canvas canvas) {
		for(int i=0;i<GRID_WIDTH;i++) {
			for(int j=0;j<GRID_HEIGHT;j++) {
				if(m_grid[i][j] < 0 || m_grid[i][j] > 2) { continue; }
				
				// calculate circle location and size
				float r = m_gridSize / 2.0f;
				float x = m_xOffset + (m_gridSize * i) + r;
				float y = m_yOffset + (m_gridSize * j) + r;
				
				// draw circle with padding
				canvas.drawCircle(x + 1, y + 1, r - 2, m_playerPaint[m_grid[i][j]]);
			}
		}
	}
	
	// draw the current player as a game piece in the top left corner
	private void drawCurrentPlayer(Canvas canvas) {
		if(m_currentPlayer == UNDETERMINED) { return; }
		
		// calculate circle location and size
		float r = m_gridSize / 2.0f;
		float x = r;
		float y = r;
		
		// draw circle with padding
		canvas.drawCircle(x + 1, y + 1, r - 2, m_playerPaint[m_currentPlayer]);
	}
	
	// draw the winner as a game piece under the current player game piece
	private void drawWinner(Canvas canvas) {
		if(m_winner == UNDETERMINED) { return; }
		
		// calculate circle location and size
		float r = m_gridSize / 2.0f;
		float x = r;
		float y = r + m_gridSize;
		
		// draw circle with padding
		if(m_winner < m_playerPaint.length) {
			canvas.drawCircle(x + 1, y + 1, r - 2, m_playerPaint[m_winner]);
		}
	}
	
	// sends a message to the queue for the current session
	public void sendMessageToSession(shared.Message message) throws IOException {
		if(message == null) { return; }
		
		// build the RabbitMQ message and send it
		Builder builder = new AMQP.BasicProperties.Builder();
		BasicProperties properties = builder.contentType("text/plain").replyTo(m_queueName).build();
		m_channel.basicPublish("", m_sessionQueueName, properties, shared.Message.serializeMessage(message));
	}
	
	// handle incoming messages
	public void handleMessage(QueueingConsumer.Delivery delivery) {
		// extract and verify the message stored in the delivery
		if(delivery == null) { return; }
		shared.Message message = null;
		try { message = shared.Message.deserializeMessage(delivery.getBody()); }
		catch(Exception e) { return; }
		if(message == null) { return; }
		
		// handle messages for when other users place pieces in the game board
		if(message.getType().equalsIgnoreCase("Piece Placed")) {
			// obtain and verify the column index
			int columnIndex = -1;
			try {
				columnIndex = Integer.parseInt((String) message.getAttribute("Column Index"));
			}
			catch(NumberFormatException e) {
				return;
			}
			if(columnIndex < 0 || columnIndex >= GRID_WIDTH) { return; }
			
			// place a piece in the specified column
			placePiece(columnIndex);
			
			// switch to the next player
			nextPlayer();
			
			// update the view
			m_handler.post(new Runnable() {
				public void run() {
					invalidate();
				}
			});
		}
		// handle responses for valid moves
		else if(message.getType().equalsIgnoreCase("Move Valid")) {
			// verify the user name
			String userName = (String) message.getAttribute("User Name");
			if(!m_userName.equalsIgnoreCase(userName)) { return; }
			
			// reset the waiting variable
			m_waitingForMoveValidation = false;
			
			// place a piece in the selected column
			placePiece(m_selectedX);
			
			// switch to the next player
			nextPlayer();
			
			// update the view
			m_handler.post(new Runnable() {
				public void run() {
					invalidate();
				}
			});
		}
		// handle responses for invalid moves
		else if(message.getType().equalsIgnoreCase("Move Invalid")) {
			// verify the user name
			String userName = (String) message.getAttribute("User Name");
			if(!m_userName.equalsIgnoreCase(userName)) { return; }
			
			// reset the waiting variable
			m_waitingForMoveValidation = false;
			
			// display a message indicating that the move was invalid
			ConnectFourGame.instance.runOnUiThread(new Runnable() {
				public void run() {
					Toast.makeText(ConnectFourGame.instance, ConnectFourGame.instance.getString(R.string.invalid_move), Toast.LENGTH_SHORT).show();
				}
			});
			
			// update the view
			m_handler.post(new Runnable() {
				public void run() {
					invalidate();
				}
			});
		}
		// handle messages for when a player wins the game
		else if(message.getType().equalsIgnoreCase("Player Won")) {
			// verify the user name
			final String userName = (String) message.getAttribute("User Name");
			if(userName == null) { return; }
			
			// determine the winer by the user name
			m_winner = (byte) (m_userName.equalsIgnoreCase(userName) ? m_playerNumber : (m_playerNumber == PLAYER1 ? PLAYER2 : PLAYER1));
			
			// display a message indicating which player won the game
			ConnectFourGame.instance.runOnUiThread(new Runnable() {
				public void run() {
					Toast.makeText(ConnectFourGame.instance, userName + " " + ConnectFourGame.instance.getString(R.string.won), Toast.LENGTH_LONG).show();
				}
			});
			
			// update the view
			m_handler.post(new Runnable() {
				public void run() {
					invalidate();
				}
			});
		}
		// handle messages for when a draw is reached
		else if(message.getType().equalsIgnoreCase("Draw")) {
			// set the outcome to a draw
			m_winner = DRAW;
			
			// display a message indicating that the game ended in a draw
			ConnectFourGame.instance.runOnUiThread(new Runnable() {
				public void run() {
					Toast.makeText(ConnectFourGame.instance, ConnectFourGame.instance.getString(R.string.draw), Toast.LENGTH_LONG).show();
				}
			});
			
			// update the view
			m_handler.post(new Runnable() {
				public void run() {
					invalidate();
				}
			});
		}
	}
	
}
