package gr.uoc.csd.hy439.positioning.fingerprint;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationProvider;
import android.net.ConnectivityManager;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;

/**
 * This service calculates the device's location using 802.11 RSSI fingerprint positioning algorithms.
 * <p>This class should be implemented based on the {@link android.location.LocationManager LocationManager} class.</p>
 */
public class FingerprintService extends Service {	
	/**
	 * The provider value for Location fixes generated by this Service.
	 * <p>Listeneres can access it using the {@link Location#getProvider()} method.</p> 
	 */
	public static final String PROVIDER="fingerPrintProvider";
	
	/**
	 * After some successive {@link OutOfTrainingAreaException OutOfTrainingAreaExceptions}
	 * are thrown, the status of this provider will be set to
	 * {@link LocationProvider#OUT_OF_SERVICE OUT_OF_SERVICE}
	 */
	public static final int MAX_SUCCESSIVE_OUT_OF_TRAINING_AREA_EXCEPTIONS=5;
	
	public static final String APP_PREFERENCE_FILENAME="appPrefs";
	
	/**
	 * Minimum time between WiFi scans (and location estimations).
	 */
	private static final int MIN_TIME_BETWEEN_SCANS= 2000; // in milliseconds
	
	private final IBinder fingerprintServiceBinder = new FingerprintBinder();
	
	/**
	 * The instance of the fingerprinting algorithm that we will use for our estimation.
	 * */
	private PercentilesAlgorithm fingerprintAlgorithm;
	
	private Timer timer;
	private long minMinTime;
	private WifiManager wifi;
	private ConnectivityManager connectivityManager;
	private ArrayList<ScanResult> scanResultList;
	private Location location;
	/** The list of the registered {@link LocationListener LocationListeners} and their parameters. */
	private CopyOnWriteArrayList<Client> clientList;
	/** The list of the {@link LocationListener LocationListeners} that have requested only one location fix. */
	private CopyOnWriteArrayList<LocationListener> singleUpdateListeners;
	/** The currently working {@link RequestWifiScanTask}. */
	private TimerTask updateTask;
	private int nSuccessiveOutOfTrainingAreaExceptions;
	private boolean wifiEnabled;
	/** Keeps the preference about using the FingerprintService for location updates. */
	private boolean fingerprinting;
	private boolean providerState;
	
	/**
	 * When a {@link LocationListener} is registered for updates from this service, it is stored
	 * in a Client object, together with its preferences and its last notification time and location.  
	 */
	private class Client{
		LocationListener locationListener;
		long minTime;
		long lastNotificationTime;
		double minDist;
		Location previousLocation;
		Client(LocationListener locationListener,long minTime,double minDist){
			this.locationListener=locationListener;
			this.minTime=minTime;
			this.minDist=minDist;
			this.lastNotificationTime=0;
		}
	}
	
	/**
	 * Receives SCAN_RESULTS_AVAILABLE_ACTION intents
	 * Its method onReceive(...) is called each time new ScanResults are available.
	 */
	//class WifiReceiver extends BroadcastReceiver {
	BroadcastReceiver wifiReceiver= new BroadcastReceiver() {		
		@Override
		public void onReceive(Context context, Intent intent) {
			scanResultList = (ArrayList<ScanResult>) wifi.getScanResults();
			if (scanResultList == null)
				return;
			try {
				location= fingerprintAlgorithm.testPerformance(scanResultList);
				notifyListeners(location);
				nSuccessiveOutOfTrainingAreaExceptions= 0;
			} catch (OutOfTrainingAreaException e) {
				Log.w(this.getClass().getName(), e.getMessage());
				nSuccessiveOutOfTrainingAreaExceptions++;
				for (LocationListener locationListener : singleUpdateListeners) {
					locationListener.onStatusChanged(PROVIDER, LocationProvider.TEMPORARILY_UNAVAILABLE, null);
				}
				singleUpdateListeners.clear();
				if (clientList.isEmpty()) {
					Log.d(this.getClass().getName(), "onReceive() @ catch{}: clientList is empty. Unregistering wifiReceiver");
					listenForScanResults(false);
				}
				if (nSuccessiveOutOfTrainingAreaExceptions >= MAX_SUCCESSIVE_OUT_OF_TRAINING_AREA_EXCEPTIONS) {
					// Device is probably far from a known area. Better notify all listeners about that.
					Log.d(this.getClass().getName(), "Too many OutOfTrainingAreaExceptions. "+PROVIDER+" out of service");
					for (Client client : clientList) {
						client.locationListener.onStatusChanged(PROVIDER, LocationProvider.OUT_OF_SERVICE, null);
					}
				}
			} catch (NullPointerException npe) {
				// fingerprintAlgorithm is null
				npe.printStackTrace();
			}
		}
	};
	/**
	 * This task is executed periodically to probe the WifiManger to get new ScanResults 
	 */
	class RequestWifiScanTask extends TimerTask {
		@Override
	    public void run() {
	        Log.i(this.getClass().getName(), "Timer task doing work");
	        wifi.startScan();
	    }		
	}
	
	/**
	 * Receives Broadcasts about changes in the WiFi connection of the device.
	 * It notifies the clients by calling the
	 * {@link FingerprintService#onProviderEnabled() onProviderEnabled()} and
	 * {@link FingerprintService#onProviderDisabled() onProviderDisabled()}
	 */
	private BroadcastReceiver wifiConnectionReceiver= new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			final String action = intent.getAction();
		    if (action.equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION)) {
		        /*if (intent.getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false)) {
		        	//do stuff
		        	wifiEnabled=true;
		        	onProviderEnabled();
		        } else {
		            // wifi connection was lost
		        	wifiEnabled=false;
		        	onProviderDisabled();
		        }*/
		    	wifiEnabled=wifi.isWifiEnabled();
		    	Log.d(this.getClass().getName(), "wifiEnabled= "+wifiEnabled);
		    	propagateProviderState();
		    }			
		}		
	};
	
	/**
	 * @return the current enabled/disabled status of the fingerprint location provider.
	 */
	/*
	public boolean isProviderEnabled() {
		return wifi.isWifiEnabled();
	}
	*/
	/**
	 * It disables all the listeners when the provider (wifi) is unavailable.
	 * TODO: this service must be notified when the WiFi is disabled 
	 */
	/*
	public void onProviderDisabled(){
		if(!isProviderEnabled()){
			for(Client client : clientList){
				client.locationListener.onProviderDisabled(PROVIDER);
			}
		}
	}
	*/
	/**
	 * It enables all the listeners when the provider(wifi) is available.
	 * TODO: this service must be notified when the WiFi is disabled 
	 */
	/*
	public void onProviderEnabled(){
		if(isProviderEnabled()){
			for(Client client : clientList){
				client.locationListener.onProviderEnabled(PROVIDER);
			}
		}
	}
	*/
	
	/**
	 * Registers the current activity to be notified periodically for location updates
	 * calculated by 802.11 fingerprinting algorithms.
	 * 
	 * @param minTime minimum time interval between location updates, in milliseconds
	 * @param minDistance minimum distance between location updates, in meters
	 * @param listener a {@link LocationListener} whose onLocationChanged(FingerprintLocation) method will be called for each location update
	 */
	public void requestLocationUpdates(long minTime, float minDistance, LocationListener listener) {
		if (clientList.isEmpty() && singleUpdateListeners.isEmpty() && providerState)
			listenForScanResults(true);
		if (minTime < MIN_TIME_BETWEEN_SCANS)
			minTime= MIN_TIME_BETWEEN_SCANS;
		if (minDistance < 0)
			minDistance= 0;
		/*
		 * Is this listener already registered? If yes, update its minTime and minDist.
		 */
		boolean alreadyRegistered= false;
		for (Client client :clientList) {
			if (client.locationListener.equals(listener)) {
				alreadyRegistered= true;
				client.minTime= minTime;
				client.minDist=minDistance;
			}
		}
		/*
		 * If no, register it.
		 */
		if (alreadyRegistered == false) {
			clientList.add(new Client(listener,minTime,minDistance));
		}
		/*
		 * Update the WiFi scan triggering rate. 
		 */
		for (Client client :clientList) {
			if (client.minTime < minMinTime) {
				minMinTime= client.minTime;
			}
		}
		if (providerState)
			startTriggeringWifiMangerForScans(minMinTime);
		else
			listener.onProviderDisabled(PROVIDER);
	}
	/**
	 * Requests a single location update based on the specified criteria.
	 * <p>It may take a while to receive the most recent location. If an immediate location is required, applications may use the getLastKnownLocation(String) method.</p>
	 * <p>In case the provider is disabled by the user, the update will not be received, and the onProviderDisabled(String) method will be called. As soon as the provider is enabled again, the onProviderEnabled(String) method will be called and location updates will start again.</p>
	 * <p>The supplied Looper is used to implement the callback mechanism.</p>
	 * 
	 * @param listener a {@link LocationListener} whose onLocationChanged(FingerprintLocation) method will be called when the location update is available
	 * @param looper a Looper object whose message queue will be used to implement the callback mechanism, or null to make callbacks on the main thread
	 */
	public void requestSingleUpdate(LocationListener listener, Looper looper) {
		if (!providerState) {
			listener.onProviderDisabled(PROVIDER);
			return;
		}
		if (clientList.isEmpty() && singleUpdateListeners.isEmpty())
			listenForScanResults(true);
		singleUpdateListeners.add(listener);
		wifi.startScan();
	}
	
	/**
	 * Removes any current registration for location updates of the current activity with the given LocationListener.
	 * @param listener
	 */
	public void removeUpdates(LocationListener listener) {
		if (clientList.isEmpty())
			return;
		// Find the client and remove it
		for(Client client:clientList){
			if(client.locationListener.equals(listener)) {
				clientList.remove(client);
			}
		}
		//Log.d(this.getClass().getName(), "removeUpdates(): clientList.size()= "+clientList.size()+", singleUpdateListeners.size()= "+singleUpdateListeners.size());
		if (clientList.isEmpty()) {
			if (singleUpdateListeners.isEmpty()) {
				stopTriggeringWifiMangerForScans();
				Log.d(this.getClass().getName(), "removeUpdates(): clientList & singleUpdateListeners are empty. Unregistering wifiReceiver");
				listenForScanResults(false);
			}
		} else {
			// slow down the WiFi scan rate
			long tempMin=Long.MAX_VALUE;
			for(Client client:clientList) {
				if(client.minTime<tempMin){
					tempMin=client.minTime;
				}
			}
			startTriggeringWifiMangerForScans(tempMin);
		}
		/*
		long tempMin=Long.MAX_VALUE;
		for(Client client:clientList){
			if((client.locationListener.equals(listener))&&(client.minTime==minMinTime)){
				for(Client client_:clientList){
					if(!client_.equals(client)){
						if(client_.minDist<tempMin){
							tempMin=client_.minTime;
						}
					}
					
				}
				clientList.remove(client);
			}
			minMinTime=tempMin;
		}
		//Log.d(this.getClass().getName(), "removeUpdates(): clientList.size()= "+clientList.size()+", singleUpdateListeners.size()= "+singleUpdateListeners.size());
		if (clientList.isEmpty()) {
			stopTriggeringWifiMangerForScans();
			if (singleUpdateListeners.isEmpty()) {
				Log.d(this.getClass().getName(), "removeUpdates(): clientList & singleUpdateListeners are empty. Unregistering wifiReceiver");
				unregisterReceiver(wifiReceiver);
			}
		}
		*/			
	}
	
	/**
	 * This method is called when a new location is calculated.
	 * It notifies all registered {@link LocationListener LocationListeners} whose preferences are fullfilled
	 */
	private void notifyListeners(Location location){
		long currTime=System.currentTimeMillis();
		for(Client client:clientList){
			if((currTime-client.lastNotificationTime>client.minTime) && ((client.previousLocation==null)||(client.previousLocation.distanceTo(location)>client.minDist))){
				client.locationListener.onLocationChanged(location);
			}
		} 
		for (LocationListener listener : singleUpdateListeners) {
			listener.onLocationChanged(location);
		}
		int singleUpdateListenersSize= singleUpdateListeners.size();
		singleUpdateListeners.clear();
		//Log.d(this.getClass().getName(), "removeUpdates(): clientList.size()= "+clientList.size()+", singleUpdateListeners.size()= "+singleUpdateListeners.size());
		if (clientList.isEmpty() && singleUpdateListenersSize > 0) {
			Log.d(this.getClass().getName(), "notifyListeners(): clientList is empty. Unregistering wifiReceiver");
			listenForScanResults(false);
		}
	}
	/**
	 * Cancels the previous {@link Timer} object, if any, and creates new timer and {@link TimerTask} objects to
	 * periodically trigger the {@link WifiManager} for {@link ScanResult ScanResults}.
	 * @param period amount of time in milliseconds between subsequent executions of the timerTask. 
	 */
	private void startTriggeringWifiMangerForScans(long period) {
		if (timer != null)
			stopTriggeringWifiMangerForScans();
		updateTask= new RequestWifiScanTask();
		timer=new Timer();
		timer.schedule(updateTask, 0, period);		
	}
	/**
	 * Cancels the {@link Timer} and {@link TimerTask} objects, to stop triggering the {@link WifiManager} for
	 * {@link ScanResult ScanResults}.
	 */
	private void stopTriggeringWifiMangerForScans() {
		if (timer != null) {
			timer.cancel();
			timer.purge();
			timer= null;
			updateTask.cancel();
		}
	}
	
	/**
	 * Registers/unregisters the {@link FingerprintService#wifiReceiver wifiReceiver},
	 * supressing any exceptions because it was already registered/unregistered.
	 * @param listen
	 */
	private void listenForScanResults(boolean listen) {
		try {
			if (listen) {
				registerReceiver(wifiReceiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
			} else {
				unregisterReceiver(wifiReceiver);
			}
		} catch (IllegalArgumentException e) {
			// do nothing
		}
	}
	
	@Override
	public void onCreate() {
		super.onCreate();
		Log.i(this.getClass().getName(), "I am being created!");
		/*
		try {
			fingerprintAlgorithm= new PercentilesAlgorithm();
		} catch (NumberFormatException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		*/
		clientList=new CopyOnWriteArrayList<Client>();
		singleUpdateListeners= new CopyOnWriteArrayList<LocationListener>();
		minMinTime=Long.MAX_VALUE;	
		nSuccessiveOutOfTrainingAreaExceptions= 0;
		wifi= (WifiManager) getSystemService(Context.WIFI_SERVICE);
		connectivityManager= (ConnectivityManager) this.getSystemService(CONNECTIVITY_SERVICE);
		
		//this.wifiEnabled= connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnected();
		this.wifiEnabled= wifi.isWifiEnabled();
		SharedPreferences appPrefs = getSharedPreferences(APP_PREFERENCE_FILENAME, MODE_PRIVATE);
		fingerprinting= appPrefs.getBoolean("fingerprinting", true);
		providerState= wifiEnabled && fingerprinting;
		if (fingerprinting)
			fingerprintAlgorithm= new PercentilesAlgorithm(this);
		
		registerReceiver(this.wifiConnectionReceiver, new IntentFilter(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION));
	}
	@Override
	public void onDestroy() {
		fingerprinting=false;
		propagateProviderState();
		unregisterReceiver(this.wifiConnectionReceiver);
		Log.i(this.getClass().getName(), "I am being destroyed...");
		super.onDestroy();
	}
	
	/* 
	 * (non-Javadoc)
	 * @see android.app.Service#onBind(android.content.Intent)
	 */
	@Override
	public IBinder onBind(Intent arg0) {
		return fingerprintServiceBinder;
	}
	
	public class FingerprintBinder extends Binder {
		public FingerprintService getService() {
			// Return this instance of FingerprintService so clients can call public methods
			return FingerprintService.this;
		}
	}
	
	/**
	 * Enables/Disables the Fingerprint location provider.
	 * @param enable true to enable it, false to disable it.
	 * @return always true. Allows the caller to know when the method is done.
	 */
	public boolean enableProvider(boolean enable) {
		if (enable) {
			fingerprintAlgorithm= new PercentilesAlgorithm(FingerprintService.this);
		} else {
			fingerprintAlgorithm= null;
			System.gc();
		}
		fingerprinting= enable;
		propagateProviderState();
		return true;
	}
	
	/**
	 * If the WiFi interface is turned off, or the user explicitly deactivates
	 * the fingerprint positioning mechanism, this location provider is disabled.
	 * When the state of this provider is changed, all locationListeners are notified.
	 */
	private void propagateProviderState() {
		boolean oldProviderState= providerState;
		providerState= wifiEnabled && fingerprinting;
		if (oldProviderState != providerState) {
			if (providerState) {
				Log.i(this.getClass().getName(), "Provider enabled.");
				long tempMin=Long.MAX_VALUE;
				for(Client client : clientList){
					client.locationListener.onProviderEnabled(PROVIDER);
					if(client.minTime<tempMin){
						tempMin=client.minTime;
					}
				}
				if (!clientList.isEmpty()) {
					listenForScanResults(true);
					startTriggeringWifiMangerForScans(tempMin);
				}
			} else {
				Log.i(this.getClass().getName(), "Provider disabled.");
				stopTriggeringWifiMangerForScans();
				listenForScanResults(false);
				for(Client client : clientList){
					client.locationListener.onProviderDisabled(PROVIDER);
				}
			}
		}
	}
	
}