package com.connectfour;

import java.io.IOException;
import shared.Message;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.DialogInterface.OnCancelListener;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.provider.Settings.Secure;
import android.text.InputType;
import android.text.method.PasswordTransformationMethod;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
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.AMQP.BasicProperties;
import com.rabbitmq.client.AMQP.BasicProperties.Builder;

public class ConnectFour extends Activity implements Runnable {
	
	private int m_playerNumber;
	private String m_userName;
	private int[] m_stats;
	private String m_currentPlayerName;
	private String m_opponentPlayerName;
	private String m_sessionQueueName;
	private boolean m_initialized;
	private boolean m_running;
	private boolean m_awaitingResponse;
	private boolean m_lostConnection;
	private boolean m_disconnectHandlerRunning;
	private boolean m_loggedIn;
	private GameType m_gameType = GameType.Hotseat;
	private String m_deviceID; 
	private String m_queueName;
	private ConnectionFactory m_connectionFactory;
	private Connection m_connection;
	private Channel m_channel;
	private QueueingConsumer m_consumer;
	private Thread m_clientThread;
	private Handler m_handler;
	private long m_lastTime;
	private PowerManager.WakeLock m_wakeLock;
	private ProgressDialog m_matchMakingDialog;
	private EditText m_loginUserNameTextField;
	private EditText m_loginPasswordTextField;
	private EditText m_createAccountUserNameTextField;
	private EditText m_createAccountPasswordTextField;
	private Thread m_disconnectHandlerThread;
	public static ConnectFour instance;
	final public static long PING_INTERVAL = 5000;
	final public static long CONNECTION_TIMEOUT = 7500;
	final public static String DEFAULT_SERVER_QUEUE_NAME = "Matchmaking Server Queue";
	final public static String DEFAULT_BROKER_HOSTNAME = "nitro404.dyndns.org";
	final public static boolean CHECK_CONNECTION = false;
	
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // store a global reference to the main activity
        instance = this;
        
        // initialize a handler for running ui-related code
        m_handler = new Handler();
        
        // obtain the unique device id and use it to name the local message queue
        m_deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID);
        m_queueName = m_deviceID + " Queue";
        
        // set application view to main layout
        setContentView(R.layout.main);
        
        // create listeners for the main gui elements
        createListeners();
        
        // initialize local variables
        m_playerNumber = 0;
        m_stats = new int[3];
        m_initialized = false;
    	m_running = false;
    	m_awaitingResponse = false;
    	m_lostConnection = false;
    	m_disconnectHandlerRunning = false;
    	m_loggedIn = false;
    	
    	// initialize the messaging system
        if(!initialize()) {
        	finish();
        }
        
        // acquire a wake lock to prevent the phone from sleeping
        PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
        m_wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getName());
        m_wakeLock.acquire();
        
        // initialize the ping time variable to the current time
        m_lastTime = System.currentTimeMillis();
    }
    
    // initialize the messaging system
    public boolean initialize() {
    	if(m_initialized) { return false; }
    	
    	// initialize the RabbitMQ connection and queues
		try {
			m_connectionFactory = new ConnectionFactory();
	    	m_connectionFactory.setHost(DEFAULT_BROKER_HOSTNAME);
			m_connection = m_connectionFactory.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) {
			Log.e("init", "Error initializing clinet messaging service: " + e.getMessage());
			return false;
		}
    	
		// set the application as initialized
		m_initialized = true;
		
		// create and initialize the incoming message listener thread
		m_clientThread = new Thread(this);
		m_clientThread.start();
		
		// create and initialize the server connectivity monitor thread
		m_disconnectHandlerThread = new Thread(new Runnable() {
			public void run() {
				if(!m_initialized) { return; }
				
				m_disconnectHandlerRunning = true;
				
				// check the server connection indefinitely
				while(m_disconnectHandlerRunning) {
					checkServerConnection();
					
					try { Thread.sleep(500L); }
					catch(InterruptedException e) {
						stop();
						break;
					}
				}
			}
		});
		m_disconnectHandlerThread.start();
		
    	return true;
    }
    
    // get the player's user name
    public String getUserName() {
    	return m_userName;
    }
    
    // get the player's number
    public int getPlayerNumber() {
    	return m_playerNumber;
    }
    
    // get the current player's name
    public String getCurrentPlayerName() {
    	return m_currentPlayerName;
    }
    
    // get the opponent player's name
    public String getOpponentPlayerName() {
    	return m_opponentPlayerName;
    }
    
    // get the local queue name
    public String getQueueName() {
    	return m_queueName;
    }
    
    // get the session queue's name
    public String getSessionQueueName() {
    	return m_sessionQueueName;
    }
    
    // get the RabbitMQ channel
    public Channel getChannel() {
    	return m_channel;
    }
    
    // get the game type
    public GameType getGameType() {
    	return m_gameType;
    }
    
    // create listeners for main layout gui elements
    public void createListeners() {
        // add listener to login button
        findViewById(R.id.loginButton).setOnClickListener(new View.OnClickListener() {
    		public void onClick(View v) {
    			// if the user is already logged in, return
    			if(m_loggedIn) { return; }
    			
    			// generate a log in prompt dialog, and a main layout for it
    			AlertDialog.Builder loginDialogBuilder = new AlertDialog.Builder(ConnectFour.this);
    			loginDialogBuilder.setTitle(R.string.log_in);
    			LinearLayout loginLayout = new LinearLayout(ConnectFour.this);
    			loginLayout.setOrientation(LinearLayout.VERTICAL);
    			
    			// add a user name label and text field to a new layout
    			LinearLayout userNameLayout = new LinearLayout(ConnectFour.this);
    			userNameLayout.setOrientation(LinearLayout.VERTICAL);
    			TextView userNameLabel = new TextView(ConnectFour.this);
    			userNameLabel.setText(R.string.user_name);
    			m_loginUserNameTextField = new EditText(ConnectFour.this);
    			userNameLayout.addView(userNameLabel);
    			userNameLayout.addView(m_loginUserNameTextField);
    			
    			// add a password label and text field to the new layout
    			LinearLayout passwordLayout = new LinearLayout(ConnectFour.this);
    			passwordLayout.setOrientation(LinearLayout.VERTICAL);
    			TextView passwordLabel = new TextView(ConnectFour.this);
    			passwordLabel.setText(R.string.password);
    			m_loginPasswordTextField = new EditText(ConnectFour.this);
    			m_loginPasswordTextField.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
    			m_loginPasswordTextField.setTransformationMethod(PasswordTransformationMethod.getInstance());
    			userNameLayout.addView(passwordLabel);
    			userNameLayout.addView(m_loginPasswordTextField);
    			
    			// add the new layouts to the main dialog layout
    			loginLayout.addView(userNameLayout);
    			loginLayout.addView(passwordLayout);
    			
    			// set the dialog to the layout view and add ok and cancel buttons to it
    			loginDialogBuilder.setView(loginLayout);
    			loginDialogBuilder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int button) {
						// get the username and password from the text fields
						String userName = m_loginUserNameTextField.getText().toString();
						String password = m_loginPasswordTextField.getText().toString();
						
						// verify that they are not empty
						if(userName.length() == 0 || password.length() == 0) {
							return;
						}
						
						// set the username as the user's current username
						m_userName = userName;
						
						// generate a login message with the username and password
	    				shared.Message message = new shared.Message("Login");
	    				message.setAttribute("User Name", userName);
	    				message.setAttribute("Password", password);
	    				
	    				// send the message to the server
	    				sendMessageToServer(message);
					}
    			});
    			loginDialogBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int which) { }
    			});
    			
    			// display the login dialog
    			loginDialogBuilder.show();
    		}
    	});
        
        // add listener to create account button
        findViewById(R.id.createAccountButton).setOnClickListener(new View.OnClickListener() {
			public void onClick(View v) {
				// generate a create account prompt dialog, and a main layout for it
				AlertDialog.Builder createAccountDialogBuilder = new AlertDialog.Builder(ConnectFour.this);
    			createAccountDialogBuilder.setTitle(R.string.create_account);
    			LinearLayout createAccountLayout = new LinearLayout(ConnectFour.this);
    			createAccountLayout.setOrientation(LinearLayout.VERTICAL);
    			
    			// add a user name label and text field to a new layout
    			LinearLayout userNameLayout = new LinearLayout(ConnectFour.this);
    			userNameLayout.setOrientation(LinearLayout.VERTICAL);
    			TextView userNameLabel = new TextView(ConnectFour.this);
    			userNameLabel.setText(R.string.user_name);
    			m_createAccountUserNameTextField = new EditText(ConnectFour.this);
    			userNameLayout.addView(userNameLabel);
    			userNameLayout.addView(m_createAccountUserNameTextField);
    			
    			// add a password label and text field to the new layout
    			LinearLayout passwordLayout = new LinearLayout(ConnectFour.this);
    			passwordLayout.setOrientation(LinearLayout.VERTICAL);
    			TextView passwordLabel = new TextView(ConnectFour.this);
    			passwordLabel.setText(R.string.password);
    			m_createAccountPasswordTextField = new EditText(ConnectFour.this);
    			m_createAccountPasswordTextField.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
    			m_createAccountPasswordTextField.setTransformationMethod(PasswordTransformationMethod.getInstance());
    			userNameLayout.addView(passwordLabel);
    			userNameLayout.addView(m_createAccountPasswordTextField);
    			
    			// add the new layouts to the main dialog layout
    			createAccountLayout.addView(userNameLayout);
    			createAccountLayout.addView(passwordLayout);
    			
    			// set the dialog to the layout view and add ok and cancel buttons to it
    			createAccountDialogBuilder.setView(createAccountLayout);
    			createAccountDialogBuilder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int button) {
						// get the username and password from the text fields
						String userName = m_createAccountUserNameTextField.getText().toString();
						String password = m_createAccountPasswordTextField.getText().toString();
						
						// verify that they are not empty
						if(userName.length() == 0 || password.length() == 0) {
							return;
						}
						
						// generate a create account message with the username and password
						shared.Message message = new shared.Message("Create Account");
						message.setAttribute("User Name", userName);
						message.setAttribute("Password", password);
						
						// send to the message to the server
						sendMessageToServer(message);
					}
    			});
    			createAccountDialogBuilder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
					public void onClick(DialogInterface dialog, int which) { }
    			});
    			
    			// display the create account dialog
    			createAccountDialogBuilder.show();
			}
        });
        
        // add listener to start game button
        findViewById(R.id.startGameButton).setOnClickListener(new View.OnClickListener() {
			public void onClick(View v) {
				// generate a dialog to prompt for whether the user wishes to play hotseat or online multiplayer
				final CharSequence[] items = { getString(R.string.hotseat), getString(R.string.multiplayer) };
				AlertDialog.Builder builder = new AlertDialog.Builder(ConnectFour.this);
				builder.setTitle(R.string.choose_game_type);
				builder.setItems(items, new DialogInterface.OnClickListener() {
					public synchronized void onClick(DialogInterface dialog, int index) {
						// start single player
						if(index == 0) { 
							startGame(GameType.Hotseat);
						}
						// start multi-player
						else if(index == 1) {
							// if the user is not logged in, display a message indicating that they must log in first
							if(!m_loggedIn) {
								Toast.makeText(ConnectFour.this, R.string.login_required, Toast.LENGTH_SHORT).show();
								
								return;
							}
							
							// get the handler to display a waiting gui with a message saying that it is looking for a game session
							m_handler.post(new Runnable() {
								public void run() {
									// if a window is already displayed for any reason, dismiss it
									if(m_matchMakingDialog != null) {
										m_matchMakingDialog.dismiss();
										m_matchMakingDialog = null;
									}
									
									// create the dialog and display it
									m_matchMakingDialog = ProgressDialog.show(ConnectFour.this, "", getString(R.string.finding_match), true);
									m_matchMakingDialog.setCancelable(true);
									m_matchMakingDialog.setOnCancelListener(new OnCancelListener() {
										public void onCancel(DialogInterface dialog) {
											// if the dialog is cancelled (ie. user presses the back button)
											if(dialog != null && dialog == m_matchMakingDialog) {
												// generate and send a message to the server indicating that the user has left the game session
												Message leftSession = new Message("Left Session");
												leftSession.setAttribute("User Name", m_userName);
												ConnectFour.instance.sendMessageToServer(leftSession);
												
												// dismiss the waiting dialog
												m_matchMakingDialog.dismiss();
												m_matchMakingDialog = null;
											}
										}
									});
								}
							});
							
							// generate a message indicating that the user is looking for a game session
							shared.Message message = new shared.Message("Find Game");
							message.setAttribute("User Name", m_userName);
							
							// send the message to the server
							sendMessageToServer(message);
						}
					}
				});
				
				// create and show the waiting dialog
				AlertDialog alert = builder.create();
				alert.show();
			}
        });
        
        // add listener to quit button
        findViewById(R.id.quitGameButton).setOnClickListener(new View.OnClickListener() {
			public void onClick(View v) {
				stop();
	    		finish();
			}
        });
    }
    
    // sets the current game mode, creates the game activity and starts it
    public void startGame(GameType type) {
    	m_gameType = type;
    	
    	// start the connect four game
    	Intent i = new Intent(this, ConnectFourGame.class);
        startActivity(i);
    }
    
    // update ping-related variables when a pong is received from the server
    public boolean ping() {
		if(!m_awaitingResponse && System.currentTimeMillis() > m_lastTime + PING_INTERVAL) { 
			m_lastTime = System.currentTimeMillis();
			m_awaitingResponse = true;
			return true;
		}
		return false;
	}
    
    // update the ping-related variables after a ping is sent to the server
    public void pong() {
    	m_lastTime = System.currentTimeMillis();
    	m_awaitingResponse = false;
    }
    
    // check if the connection to the server has timed out
    public boolean checkTimeout() {
		return m_awaitingResponse && System.currentTimeMillis() > m_lastTime + CONNECTION_TIMEOUT;
	}
    
    // check the connection to the server
    public void checkServerConnection() {
    	if(!m_initialized) { return; }
    	
    	// if a ping should be sent, send a ping to the server
    	if(ping()) {
			sendMessageToServer(new Message("Ping"));
		}
		
    	// if the connection has timed out
		if(checkTimeout()) {
			// if the connection should be checked
			if(CHECK_CONNECTION) {
				// if the connection was not already lost
				if(!m_lostConnection) {
					// display a message indicating that the connection to the server was lost
					ConnectFour.this.runOnUiThread(new Runnable() {
						public void run() {
							Toast.makeText(ConnectFour.this, getString(R.string.lost_connection), Toast.LENGTH_LONG).show();
						}
					});
					
					stop();
					finish();
				}
				
				m_lostConnection = true;
			}
		}
    }
	
    // sends a message to the server
	public void sendMessageToServer(shared.Message message)  {
		if(message == null) { return; }
		
		// build the RabbitMQ message, and send it to the server
		try {
			Builder builder = new AMQP.BasicProperties.Builder();
			BasicProperties properties = builder.contentType("text/plain").replyTo(m_queueName).build();
			m_channel.basicPublish("", DEFAULT_SERVER_QUEUE_NAME, properties, shared.Message.serializeMessage(message));
		}
		catch(Exception e) {
			Log.e("error", "Error sending message to server: " + e.getMessage());
		}
	}
	
	// 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 ping messages
		if(message.getType().equalsIgnoreCase("Ping")) {
			// send a pong response to the server
			sendMessageToServer(new Message("Pong"));
		}
		// handle pong messages
		else if(message.getType().equalsIgnoreCase("Pong")) {
			// update the ping-related variables after a pong is received from the server
			pong();
		}
		// handle account creates messages
		else if(message.getType().equalsIgnoreCase("Account Created")) {
			// get the user name
			final String userName = (String) message.getAttribute("User Name");
			
			// display a message indicating that the account was created
			ConnectFour.this.runOnUiThread(new Runnable() {
				public void run() {
					Toast.makeText(ConnectFour.this, getString(R.string.account_created1) + userName + getString(R.string.account_created2), Toast.LENGTH_SHORT).show();
				}
			});
		}
		// handle account not created messages
		else if(message.getType().equalsIgnoreCase("Account Not Created")) {
			// get the user name
			final String userName = (String) message.getAttribute("User Name");
			
			// display a message indicating that the account was not created
			ConnectFour.this.runOnUiThread(new Runnable() {
				public void run() {
					Toast.makeText(ConnectFour.this, getString(R.string.account_not_created1) + userName + getString(R.string.account_not_created2), Toast.LENGTH_SHORT).show();
				}
			});
		}
		// handle logged in response messages
		else if(message.getType().equalsIgnoreCase("Logged In")) {
			// get the user name
			final String userName = (String) message.getAttribute("User Name");
			
			// handle threading issues (ie. user name can be null on some occasions)
			while(m_userName == null) {
				try { Thread.sleep(100L); }
				catch(InterruptedException e) { }
			}
			
			// verify the user name against the local user name
			if(!m_userName.equalsIgnoreCase(userName)) { return; }
			
			// set the user as logged in
			m_loggedIn = true;
			
			// update the status label to indicate that the user is now logged in
			m_handler.post(new Runnable() {
				public void run() {
					((TextView) findViewById(R.id.loginStatusLabel)).setText(getString(R.string.logged_in) + ": " + m_userName);
				}
			});
			
			// display a message indicating the user is now logged in
			ConnectFour.this.runOnUiThread(new Runnable() {
				public void run() {
					Toast.makeText(ConnectFour.this, getString(R.string.logged_in1) + userName + getString(R.string.logged_in2), Toast.LENGTH_SHORT).show();
				}
			});
		}
		// handle log in failed response messages
		else if(message.getType().equalsIgnoreCase("Not Logged In")) {
			// get the user name
			final String userName = (String) message.getAttribute("User Name");
			
			// handle threading issues (ie. user name can be null on some occasions)
			while(m_userName == null) {
				try { Thread.sleep(100L); }
				catch(InterruptedException e) { }
			}
			
			// verify the user name against the local user name
			if(!m_userName.equalsIgnoreCase(userName)) { return; }
			
			// set the user as not logged in, and reset their local user name
			m_loggedIn = false;
			m_userName = null;
			
			// update the status label to indicate that the user's login attempt failed
			m_handler.post(new Runnable() {
				public void run() {
					((TextView) findViewById(R.id.loginStatusLabel)).setText(R.string.login_failed);
				}
			});
			
			// display a message indicating the user failed to log in
			ConnectFour.this.runOnUiThread(new Runnable() {
				public void run() {
					Toast.makeText(ConnectFour.this, getString(R.string.not_logged_in1) + userName + getString(R.string.not_logged_in2), Toast.LENGTH_SHORT).show();
				}
			});
			
		}
		// handle player stats messages
		else if(message.getType().equalsIgnoreCase("Player Stats")) {
			// get the user name
			final String userName = (String) message.getAttribute("User Name");
			
			// verify the user name against the local user name
			if(!m_userName.equalsIgnoreCase(userName)) { return; }
			
			// parse the stats out of the message attributes
			try {
				m_stats[0] = Integer.parseInt((String) message.getAttribute("Wins"));
				m_stats[1] = Integer.parseInt((String) message.getAttribute("Losses"));
				m_stats[2] = Integer.parseInt((String) message.getAttribute("Draws"));
			}
			catch(NumberFormatException e) {
				return;
			}
			
			// update the stats label with the user's stats
			m_handler.post(new Runnable() {
				public void run() {
					((TextView) findViewById(R.id.statsLabel)).setText(getString(R.string.stats_wins) + m_stats[0] + " " + getString(R.string.stats_losses) + m_stats[1] + " " + getString(R.string.stats_draws) + m_stats[2]);
				}
			});
		}
		// hangle start game messages
		else if(message.getType().equalsIgnoreCase("Start Game")) {
			// dismiss the waiting dialog
			m_handler.post(new Runnable() {
				public void run() {
					while(m_matchMakingDialog == null) {
						try { Thread.sleep(100L); }
						catch(InterruptedException e) { }
					}
					
					m_matchMakingDialog.dismiss();
					m_matchMakingDialog = null;
				}
			});
			
			// parse the game information
			try { m_playerNumber = Integer.parseInt((String) message.getAttribute("Player Number")); }
			catch(NumberFormatException e) { return; }
			m_currentPlayerName = (String) message.getAttribute("Current Player Name");
			m_opponentPlayerName = (String) message.getAttribute("Opponent Player Name");
			m_sessionQueueName = delivery.getProperties().getReplyTo();
			
			// start the game
			startGame(GameType.Multiplayer);
		}
		// handle player left messages
		else if(message.getType().equalsIgnoreCase("Player Left")) {
			// get the user name
			String userName = (String) message.getAttribute("User Name");
			
			// verify that the user name matches the opponent's player name
			if(userName == null || userName.length() == 0 || !userName.equals(m_opponentPlayerName)) { return; }
			
			// close the game activity
			ConnectFourGame.instance.finish();
			
			// display a message indicating that the opponent left the game
			ConnectFour.this.runOnUiThread(new Runnable() {
				public void run() {
					Toast.makeText(ConnectFour.this, getString(R.string.player) + " " + m_opponentPlayerName + " " + getString(R.string.left_session), Toast.LENGTH_SHORT).show();
				}
			});
		}
		// forward other messages to the game activity
		else {
			if(ConnectFourView.instance != null) {
				ConnectFourView.instance.handleMessage(delivery);
			}
		}
		
	}
	
	// handle key press events
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if(keyCode == KeyEvent.KEYCODE_BACK) {
			stop();
			finish();
		}
		
		return super.onKeyDown(keyCode, event);
	}
	
	// stop the client
	public void stop() {
		// reset all intialization variables
		m_initialized = false;
		m_running = false;
		m_awaitingResponse = false;
		m_lostConnection = true;
		m_disconnectHandlerRunning = false;
		
		// stop all threads and close all connections
		try { m_wakeLock.release(); } catch(Exception e) { }
		try { m_disconnectHandlerThread.interrupt(); } catch(Exception e) { }
		try { m_clientThread.interrupt(); } catch(Exception e) { }
		try { m_channel.close(); } catch(Exception e) { }
		try { m_connection.close(); } catch(Exception e) { }
	}
	
	// listen for incoming messages
	public void run() {
		if(!m_initialized) { return; }
		
		m_running = true;
		
		// receive messages indefinitely and handle them
		while(m_running) {
			try {
				handleMessage(m_consumer.nextDelivery());
			}
			catch(Exception e) {
				stop();
				break;
			}
		}
		
	}
    
}