package com.cgutman.androidremotedebugger.service;


import java.util.HashMap;

import com.cgutman.adblib.AdbCrypto;
import com.cgutman.androidremotedebugger.console.ConsoleBuffer;
import com.cgutman.androidremotedebugger.devconn.DeviceConnection;
import com.cgutman.androidremotedebugger.devconn.DeviceConnectionListener;
import com.cgutman.androidremotedebugger.R;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import android.os.Binder;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.support.v4.app.NotificationCompat;

public class ShellService extends Service implements DeviceConnectionListener {
	
	private ShellServiceBinder binder = new ShellServiceBinder();
	private ShellListener listener = new ShellListener(this);
	
	private HashMap<String, DeviceConnection> currentConnectionMap =
			new HashMap<String, DeviceConnection>();
	
	private WifiLock wlanLock;
	private WakeLock wakeLock;

	private final static int CONN_BASE = 12131;
	private final static int FAILED_BASE = 12111;
	
	private int foregroundId;
	
	public class ShellServiceBinder extends Binder {
		public DeviceConnection createConnection(String host, int port) {
			DeviceConnection conn = new DeviceConnection(listener, host, port);
			listener.addListener(conn, ShellService.this);
			return conn;
		}
		
		public DeviceConnection findConnection(String host, int port) {
			String connStr = host+":"+port;
			return currentConnectionMap.get(connStr);
		}
		
		public void notifyPausingActivity(DeviceConnection devConn) {
		}
		
		public void notifyResumingActivity(DeviceConnection devConn) {
		}
		
		public void notifyDestroyingActivity(DeviceConnection devConn) {
			/* If we're pausing before destruction after the connection is closed, remove the failure
			 * notification */
			if (devConn.isClosed()) {
				NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
				nm.cancel(getFailedNotificationId(devConn));
			}
			
			/* Stop the the service if no connections remain */
			if (currentConnectionMap.isEmpty()) {
				stopSelf();
			}
		}
		
		public void addListener(DeviceConnection conn, DeviceConnectionListener listener) {
			ShellService.this.listener.addListener(conn, listener);
		}
		
		public void removeListener(DeviceConnection conn, DeviceConnectionListener listener) {
			ShellService.this.listener.removeListener(conn, listener);
		}
	}
	
	private synchronized void acquireWakeLocks() {
		if (wlanLock == null) {
			WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
			wlanLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL, "Remote ADB Shell");
		}
		if (wakeLock == null) {
			PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
			wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Remote ADB Shell");
		}
		wakeLock.acquire();
		wlanLock.acquire();
	}
	
	private synchronized void releaseWakeLocks() {
		wlanLock.release();
		wakeLock.release();
	}

	@Override
	public IBinder onBind(Intent arg0) {
		return binder;
	}
	
	private int getFailedNotificationId(DeviceConnection devConn) {
		return FAILED_BASE + getConnectionString(devConn).hashCode();
	}
	
	private int getConnectedNotificationId(DeviceConnection devConn) {
		return CONN_BASE + getConnectionString(devConn).hashCode();
	}
	
	private PendingIntent createPendingIntentForConnection(DeviceConnection devConn) {
		Context appContext = getApplicationContext();
		
		Intent i = new Intent(appContext, com.cgutman.androidremotedebugger.AdbShell.class);
		i.putExtra("IP", devConn.getHost());
		i.putExtra("Port", devConn.getPort());
		i.setAction(getConnectionString(devConn));
		
		return PendingIntent.getActivity(appContext, 0, i,
				PendingIntent.FLAG_UPDATE_CURRENT);
	}

	private Notification createNotification(DeviceConnection devConn, boolean connected) {
		String ticker;
		String message;
		
		if (connected) {
			ticker = "Connection Established";
			message = "Connected to "+getConnectionString(devConn);
		}
		else {
			ticker = "Connection Terminated";
			message = "Connection to "+getConnectionString(devConn)+" failed";
		}

		return new NotificationCompat.Builder(getApplicationContext())
				.setTicker("Remote ADB Shell - "+ticker)
				.setSmallIcon(R.drawable.notificationicon)
				.setOnlyAlertOnce(true)
				.setOngoing(connected)
				.setAutoCancel(!connected)
				.setContentTitle("Remote ADB Shell")
				.setContentText(message)
				.setContentIntent(createPendingIntentForConnection(devConn))
				.build();
	}
	
	private void updateNotification(DeviceConnection devConn, boolean connected) {
		NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
		
		removeNotification(devConn);

		if (connected) {
			if (foregroundId != 0) {
				/* There's already a foreground notification, so use the normal notification framework */
				nm.notify(getConnectedNotificationId(devConn), createNotification(devConn, connected));
			}
			else {
				/* This is the first notification so make it the foreground one */
				foregroundId = getConnectedNotificationId(devConn);
				startForeground(foregroundId, createNotification(devConn, connected));
			}
		}
		else {
			nm.notify(getFailedNotificationId(devConn), createNotification(devConn, connected));
		}
	}
	
	private void removeNotification(DeviceConnection devConn) {
		NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
		
		/* Removing failure notifications is easy */
		nm.cancel(getFailedNotificationId(devConn));
		
		/* Connected notifications is a bit more complex */
		if (getConnectedNotificationId(devConn) == foregroundId) {
			/* We're the foreground notification, so we need to switch in another
			 * notification to take our place */
			
			/* Search for a new device connection to promote */
			DeviceConnection newConn = null;
			for (DeviceConnection conn : currentConnectionMap.values()) {
				if (devConn == conn) {
					continue;
				}
				else {
					newConn = conn;
					break;
				}
			}
			
			if (newConn == null) {
				/* None found, so we're done in foreground */
				stopForeground(true);
				foregroundId = 0;
			}
			else {
				/* Found one, so cancel this guy's original notification
				 * and start it as foreground */
				foregroundId = getConnectedNotificationId(newConn);
				nm.cancel(foregroundId);
				startForeground(foregroundId, createNotification(newConn, true));
			}
		}
		else {
			/* This just a normal connected notification */
			nm.cancel(getConnectedNotificationId(devConn));
		}
	}
	
	private String getConnectionString(DeviceConnection devConn) {
		return devConn.getHost()+":"+devConn.getPort();
	}
	
	private void addNewConnection(DeviceConnection devConn) {
		currentConnectionMap.put(getConnectionString(devConn), devConn);
		acquireWakeLocks();
	}
	
	private void removeConnection(DeviceConnection devConn) {
		currentConnectionMap.remove(getConnectionString(devConn));
		releaseWakeLocks();
		
		/* Stop the the service if no connections remain */
		if (currentConnectionMap.isEmpty()) {
			stopSelf();
		}
	}

	@Override
	public void notifyConnectionEstablished(DeviceConnection devConn) {
		addNewConnection(devConn);
		updateNotification(devConn, true);
	}

	@Override
	public void notifyConnectionFailed(DeviceConnection devConn, Exception e) {
		/* No notification is displaying here */
	}
	
	@Override
	public void notifyStreamFailed(DeviceConnection devConn, Exception e) {
		updateNotification(devConn, false);
		removeConnection(devConn);
	}

	@Override
	public void notifyStreamClosed(DeviceConnection devConn) {
		removeNotification(devConn);
		removeConnection(devConn);
	}

	@Override
	public AdbCrypto loadAdbCrypto(DeviceConnection devConn) {
		return null;
	}

	@Override
	public void receivedData(DeviceConnection devConn, byte[] data, int offset,
			int length) {
	}

	@Override
	public boolean canReceiveData() {
		return false;
	}

	@Override
	public boolean isConsole() {
		return false;
	}

	@Override
	public void consoleUpdated(DeviceConnection devConn,
			ConsoleBuffer console) {
	}
}