package com.limelight;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;

import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.StreamSettings;
import com.limelight.utils.Dialog;
import com.limelight.utils.UiHelper;

import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;

public class PcView extends Activity {
	private Button settingsButton, addComputerButton;
	private ListView pcList;
	private ArrayAdapter<ComputerObject> pcListAdapter;
	private ComputerManagerService.ComputerManagerBinder managerBinder;
	private boolean freezeUpdates, runningPolling;
	private ServiceConnection serviceConnection = new ServiceConnection() {
		public void onServiceConnected(ComponentName className, IBinder binder) {
			final ComputerManagerService.ComputerManagerBinder localBinder =
					((ComputerManagerService.ComputerManagerBinder)binder);
			
			// Wait in a separate thread to avoid stalling the UI
			new Thread() {
				@Override
				public void run() {
					// Wait for the binder to be ready
					localBinder.waitForReady();
					
					// Now make the binder visible
					managerBinder = localBinder;
					
					// Start updates
					startComputerUpdates();
					
					// Force a keypair to be generated early to avoid discovery delays
					new AndroidCryptoProvider(PcView.this).getClientCertificate();
				}
			}.start();
		}

		public void onServiceDisconnected(ComponentName className) {
			managerBinder = null;
		}
	};
	
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
		
		// Reinitialize views just in case orientation changed
		initializeViews();
	}
	
	private final static int APP_LIST_ID = 1;
	private final static int PAIR_ID = 2;
	private final static int UNPAIR_ID = 3;
	private final static int WOL_ID = 4;
	private final static int DELETE_ID = 5;
	
	private void initializeViews() {
		setContentView(R.layout.activity_pc_view);

        UiHelper.notifyNewRootView(this);

        // Set default preferences if we've never been run
        PreferenceManager.setDefaultValues(this, R.xml.preferences, false);

		// Setup the list view
		settingsButton = (Button)findViewById(R.id.settingsButton);
		addComputerButton = (Button)findViewById(R.id.manuallyAddPc);

		pcList = (ListView)findViewById(R.id.pcListView);
		pcList.setAdapter(pcListAdapter);
		pcList.setItemsCanFocus(true);
		pcList.setOnItemClickListener(new OnItemClickListener() {
			@Override
			public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
					long id) {
				ComputerObject computer = pcListAdapter.getItem(pos);
				if (computer.details == null) {
					// Placeholder item; no context menu for it
					return;
				}
				else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
					// Open the context menu if a PC is offline
					openContextMenu(arg1);
				}
				else if (computer.details.pairState != PairState.PAIRED) {
					// Pair an unpaired machine by default
					doPair(computer.details);
				}
				else {
					doAppList(computer.details);
				}
			}
		});
		registerForContextMenu(pcList);
		settingsButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				startActivity(new Intent(PcView.this, StreamSettings.class));
			}
		});
		addComputerButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				Intent i = new Intent(PcView.this, AddComputerManually.class);
				startActivity(i);
			}
		});

		if (pcListAdapter.isEmpty()) {
			addListPlaceholder();
		}
		else {
			pcListAdapter.notifyDataSetChanged();
		}
	}
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		// Bind to the computer manager service
		bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
				Service.BIND_AUTO_CREATE);
		
		pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
		pcListAdapter.setNotifyOnChange(false);
		
		initializeViews();
	}
	
	private void startComputerUpdates() {
		if (managerBinder != null) {
			if (runningPolling) {
				return;
			}
			
			freezeUpdates = false;
			managerBinder.startPolling(new ComputerManagerListener() {
				@Override
				public void notifyComputerUpdated(final ComputerDetails details) {
					if (!freezeUpdates) {
						PcView.this.runOnUiThread(new Runnable() {
							@Override
							public void run() {
								updateListView(details);
							}
						});
					}
				}
			});
			runningPolling = true;
		}
	}
	
	private void stopComputerUpdates(boolean wait) {
		if (managerBinder != null) {
			if (!runningPolling) {
				return;
			}
			
			freezeUpdates = true;
			
			managerBinder.stopPolling();
			
			if (wait) {
				managerBinder.waitForPollingStopped();
			}
			
			runningPolling = false;
		}
	}
	
	@Override
	public void onDestroy() {
		super.onDestroy();
		
		if (managerBinder != null) {
			unbindService(serviceConnection);
		}
	}
	
	@Override
	protected void onResume() {
		super.onResume();
		
		startComputerUpdates();
	}
	
	@Override
	protected void onPause() {
		super.onPause();
		
		stopComputerUpdates(false);
	}
	
	@Override
	protected void onStop() {
		super.onStop();
		
		Dialog.closeDialogs();
	}
	
	@Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
		stopComputerUpdates(false);
		
		// Call superclass
		super.onCreateContextMenu(menu, v, menuInfo);
                
        AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
        ComputerObject computer = pcListAdapter.getItem(info.position);
        if (computer == null || computer.details == null) {
        	startComputerUpdates();
        	return;
        }
        
        // Inflate the context menu
        if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
            menu.add(Menu.NONE, WOL_ID, 1, "Send Wake-On-LAN request");
            menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
        }
        else if (computer.details.pairState != PairState.PAIRED) {
            menu.add(Menu.NONE, PAIR_ID, 1, "Pair with PC");
            if (computer.details.reachability == ComputerDetails.Reachability.REMOTE) {
                menu.add(Menu.NONE, DELETE_ID, 2, "Delete PC");
            }
        }
        else {
            menu.add(Menu.NONE, APP_LIST_ID, 1, "View Game List");
            menu.add(Menu.NONE, UNPAIR_ID, 2, "Unpair");
        }
    }
	
	@Override
	public void onContextMenuClosed(Menu menu) {
		startComputerUpdates();
	}
	
	private void doPair(final ComputerDetails computer) {
		if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
			Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
			return;
		}
		if (computer.runningGameId != 0) {
			Toast.makeText(PcView.this, "Computer is currently in a game. " +
					"You must close the game before pairing.", Toast.LENGTH_LONG).show();
			return;
		}
		if (managerBinder == null) {
			Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
					"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
			return;
		}
		
		Toast.makeText(PcView.this, "Pairing...", Toast.LENGTH_SHORT).show();
		new Thread(new Runnable() {
			@Override
			public void run() {
				NvHTTP httpConn;
				String message;
				try {
					// Stop updates and wait while pairing
					stopComputerUpdates(true);
					
					InetAddress addr = null;
					if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
						addr = computer.localIp;
					}
					else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
						addr = computer.remoteIp;
					}
					
					httpConn = new NvHTTP(addr,
							managerBinder.getUniqueId(),
							PlatformBinding.getDeviceName(), 
							PlatformBinding.getCryptoProvider(PcView.this));
					if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
						message = "Already paired";
					}
					else {
						final String pinStr = PairingManager.generatePinString();
						
						// Spin the dialog off in a thread because it blocks
						Dialog.displayDialog(PcView.this, "Pairing", "Please enter the following PIN on the target PC: "+pinStr, false);
						
						PairingManager.PairState pairState = httpConn.pair(pinStr);
						if (pairState == PairingManager.PairState.PIN_WRONG) {
							message = "Incorrect PIN";
						}
						else if (pairState == PairingManager.PairState.FAILED) {
							message = "Pairing failed";
						}
						else if (pairState == PairingManager.PairState.PAIRED) {
							message = "Paired successfully";
						}
						else {
							// Should be no other values
							message = null;
						}
					}
				} catch (UnknownHostException e) {
					message = "Failed to resolve host";
				} catch (FileNotFoundException e) {
					message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
							+ "Try rebooting your machine or reinstalling GFE.";
				} catch (Exception e) {
					message = e.getMessage();
				}
				
				Dialog.closeDialogs();
				
				final String toastMessage = message;
				runOnUiThread(new Runnable() {
					@Override
					public void run() {
						Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
					}
				});
				
				// Start polling again
				startComputerUpdates();
			}
		}).start();
	}
	
	private void doWakeOnLan(final ComputerDetails computer) {
		if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
			Toast.makeText(PcView.this, "Computer is online", Toast.LENGTH_SHORT).show();
			return;
		}
		
		if (computer.macAddress == null) {
			Toast.makeText(PcView.this, "Unable to wake PC because GFE didn't send a MAC address", Toast.LENGTH_SHORT).show();
			return;
		}
		
		Toast.makeText(PcView.this, "Waking PC...", Toast.LENGTH_SHORT).show();
		new Thread(new Runnable() {
			@Override
			public void run() {
				String message;
				try {
					WakeOnLanSender.sendWolPacket(computer);
					message = "It may take a few seconds for your PC to wake up. " +
							"If it doesn't, make sure it's configured properly for Wake-On-LAN.";
				} catch (IOException e) {
					message = "Failed to send Wake-On-LAN packets";
				}
				
				final String toastMessage = message;
				runOnUiThread(new Runnable() {
					@Override
					public void run() {
						Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
					}
				});
			}
		}).start();
	}
	
	private void doUnpair(final ComputerDetails computer) {
		if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
			Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
			return;
		}
		if (managerBinder == null) {
			Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
					"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
			return;
		}
		
		Toast.makeText(PcView.this, "Unpairing...", Toast.LENGTH_SHORT).show();
		new Thread(new Runnable() {
			@Override
			public void run() {
				NvHTTP httpConn;
				String message;
				try {
					InetAddress addr = null;
					if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
						addr = computer.localIp;
					}
					else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
						addr = computer.remoteIp;
					}
					
					httpConn = new NvHTTP(addr,
							managerBinder.getUniqueId(),
							PlatformBinding.getDeviceName(), 
							PlatformBinding.getCryptoProvider(PcView.this));
					if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
						httpConn.unpair();
						if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
							message = "Unpaired successfully";
						}
						else {
							message = "Failed to unpair";
						}
					}
					else {
						message = "Device was not paired";
					}
				} catch (UnknownHostException e) {
					message = "Failed to resolve host";
				} catch (FileNotFoundException e) {
					message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
							+ "Try rebooting your machine or reinstalling GFE.";
				} catch (Exception e) {
					message = e.getMessage();
				}
								
				final String toastMessage = message;
				runOnUiThread(new Runnable() {
					@Override
					public void run() {
						Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
					}
				});
			}
		}).start();
	}
	
	private void doAppList(ComputerDetails computer) {
		if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
			Toast.makeText(PcView.this, "Computer is offline", Toast.LENGTH_SHORT).show();
			return;
		}
		if (managerBinder == null) {
			Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
					"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
			return;
		}
		
		Intent i = new Intent(this, AppView.class);
		i.putExtra(AppView.NAME_EXTRA, computer.name);
		i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId());
		
		if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
			i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
			i.putExtra(AppView.REMOTE_EXTRA, false);
		}
		else {
			i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
			i.putExtra(AppView.REMOTE_EXTRA, true);
		}
		startActivity(i);
	}

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
        ComputerObject computer = pcListAdapter.getItem(info.position);
        switch (item.getItemId())
        {
        case PAIR_ID:
        	doPair(computer.details);
        	return true;
        	
        case UNPAIR_ID:
        	doUnpair(computer.details);
        	return true;
        	
        case WOL_ID:
        	doWakeOnLan(computer.details);
        	return true;
        	
        case DELETE_ID:
    		if (managerBinder == null) {
    			Toast.makeText(PcView.this, "The ComputerManager service is not running. " +
    					"Please wait a few seconds or restart the app.", Toast.LENGTH_LONG).show();
    			return true;
    		}
        	managerBinder.removeComputer(computer.details.name);
        	removeListView(computer.details);
        	return true;
        	
        case APP_LIST_ID:
        	doAppList(computer.details);
        	return true;
        	
        default:
          return super.onContextItemSelected(item);
        }
    }

    private static String generateString(ComputerDetails details) {
    	StringBuilder str = new StringBuilder();
    	str.append(details.name).append(" - ");
    	if (details.state == ComputerDetails.State.ONLINE) {
    		str.append("Online ");
    		if (details.reachability == ComputerDetails.Reachability.LOCAL) {
    			str.append("(Local) - ");
    		}
    		else {
    			str.append("(Remote) - ");
    		}
    		if (details.pairState == PairState.PAIRED) {
    			if (details.runningGameId == 0) {
    				str.append("Available");
    			}
    			else {
    				str.append("In Game");
    			}
    		}
    		else {
    			str.append("Not Paired");
    		}
    	}
    	else {
    		str.append("Offline");
    	}
    	return str.toString();
    }
    
    private void addListPlaceholder() {
        pcListAdapter.add(new ComputerObject("Discovery is running. No computers found yet. " +
        		"If your PC doesn't show up in about 15 seconds, " +
        		"make sure your computer is running GFE or add your PC manually using the button above.", null));
    }
    
    private void removeListView(ComputerDetails details) {
		for (int i = 0; i < pcListAdapter.getCount(); i++) {
			ComputerObject computer = pcListAdapter.getItem(i);
			
			if (details.equals(computer.details)) {
				pcListAdapter.remove(computer);
				break;
			}
		}
		
		if (pcListAdapter.getCount() == 0) {
			// Add the placeholder if we're down to 0 computers
			addListPlaceholder();
		}
    }
    
	private void updateListView(ComputerDetails details) {
		String computerString = generateString(details);
		ComputerObject existingEntry = null;
		boolean placeholderPresent = false;
		
		for (int i = 0; i < pcListAdapter.getCount(); i++) {
			ComputerObject computer = pcListAdapter.getItem(i);
			
			// If there's a placeholder, there's nothing else
			if (computer.details == null) {
				placeholderPresent = true;
				break;
			}
			
			// Check if this is the same computer
			if (details.equals(computer.details)) {
				existingEntry = computer;
				break;
			}
		}
		
		if (existingEntry != null) {
			// Replace the information in the existing entry
			existingEntry.text = computerString;
			existingEntry.details = details;
		}
		else {
			// If the placeholder is the only object, remove it
			if (placeholderPresent) {
				pcListAdapter.remove(pcListAdapter.getItem(0));
			}
			
			// Add a new entry
			pcListAdapter.add(new ComputerObject(computerString, details));
		}
		
		// Notify the view that the data has changed
		pcListAdapter.notifyDataSetChanged();
	}
	
	public class ComputerObject {
		public String text;
		public ComputerDetails details;
		
		public ComputerObject(String text, ComputerDetails details) {
			this.text = text;
			this.details = details;
		}
		
		@Override
		public String toString() {
			return text;
		}
	}
}
