package com.jens.automation2.location;

import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.telephony.TelephonyManager;
import android.util.Log;

import com.jens.automation2.ActivityMainScreen;
import com.jens.automation2.AutomationService;
import com.jens.automation2.Miscellaneous;
import com.jens.automation2.PointOfInterest;
import com.jens.automation2.R;
import com.jens.automation2.Rule;
import com.jens.automation2.Settings;
import com.jens.automation2.Trigger.Trigger_Enum;
import com.jens.automation2.receivers.ConnectivityReceiver;
import com.jens.automation2.receivers.PhoneStatusListener;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;

public class LocationProvider
{
	protected static boolean passiveLocationListenerActive = false;
	protected static LocationListener passiveLocationListener;
	protected static LocationProvider locationProviderInstance = null;
	protected AutomationService parentService;
	public AutomationService getParentService()
	{
		return parentService;
	}

	protected Location currentLocation;
	protected static Location currentLocationStaticCopy;
	protected static double speed;
	protected ArrayList<Location> locationList = new ArrayList<Location>();
	protected static Handler speedHandler = null;
	protected static boolean speedTimerActive = false;
	protected static Calendar etaAtNextPoi = null;

	public static Calendar getEtaAtNextPoi()
	{
		return etaAtNextPoi;
	}

	public LocationProvider(AutomationService parent)
	{
		parentService = parent;
		locationProviderInstance = this;

		startLocationService();
	}

	public static LocationProvider getInstance()
	{
		return locationProviderInstance;
	}

	public Location getCurrentLocation()
	{
		return currentLocation;
	}
	public static Location getLastKnownLocation()
	{
		return currentLocationStaticCopy;
	}

	public static double getSpeed()
	{
		return speed;
	}

	public static void setSpeed(double speed)
	{
		LocationProvider.speed = speed;

		/*
			Check if the last location update may be to old.
			It could be that for whatever reason we didn't get a recent location, but the current speed
			indicates we have moved quite a bit.
		 */

		Calendar now = Calendar.getInstance();

		float distanceToClosestPoi = PointOfInterest.getClosestPOI(getLastKnownLocation()).getLocation().distanceTo(getLastKnownLocation());
		long timeInSecondsPassedSinceLastLocationUpdate = (now.getTimeInMillis() - getLastKnownLocation().getTime()) / 1000;

		// Could be we were driving towards it instead of away, but we'll ignore that for the moment.

		long secondsRequiredForArrival = now.getTimeInMillis()/1000 / (1000 / 60 * 60);
		now.add(Calendar.SECOND, (int)secondsRequiredForArrival);

		etaAtNextPoi = now;

		if(speedTimerActive)
			resetSpeedTimer(etaAtNextPoi);
		else
			startSpeedTimer(etaAtNextPoi);
	}

	public void setCurrentLocation(Location newLocation, boolean skipVerification)
	{
		if(newLocation != null)
		{
			Miscellaneous.logEvent("i", "Location", "Setting location.", 4);

			currentLocation = newLocation;
			currentLocationStaticCopy = newLocation;

			Miscellaneous.logEvent("i", "LocationListener", "Giving update to POI class", 4);
			PointOfInterest.positionUpdate(newLocation, parentService, false, skipVerification);

			try
			{
				if (
						locationList.size() >= 1
								&&
						locationList.get(locationList.size() - 1).getTime() == newLocation.getTime()
								&&
						locationList.get(locationList.size() - 1).getProvider().equals(newLocation.getProvider())
				)
				{
					// This is a duplicate update, do not store it
					Miscellaneous.logEvent("i", "LocationListener", "Duplicate location, ignoring.", 4);
				}
				else
				{
					Miscellaneous.logEvent("i", "Speed", "Commencing speed calculation.", 4);
					// This part keeps the last two location entries to determine the current speed.

					locationList.add(newLocation);

					if (newLocation.hasSpeed())
					{
						Miscellaneous.logEvent("i", "Speed", "Location has speed, taking that: " + String.valueOf(newLocation.getSpeed()) + " km/h", 4);
						setSpeed(newLocation.getSpeed());    // Take the value that came with the location, that should be more precise
					}
					else
					{
						speedCalculation:
						if (locationList.size() >= 2)
						{
							while (locationList.size() > 2)
							{
								// Remove all entries except for the last 2
								Miscellaneous.logEvent("i", "Speed", "About to delete oldest position record until only 2 left. Currently have " + String.valueOf(locationList.size()) + " records.", 4);
								locationList.remove(0);
							}

						/*
							The two most recent locations in the list must have a usable accuracy.
						 */
							for (int i = 0; i < 2; i++)
							{
								if
								(
										(locationList.get(i).getProvider().equals(LocationManager.GPS_PROVIDER) && locationList.get(i).getAccuracy() > Settings.satisfactoryAccuracyGps)
												||
										(locationList.get(i).getProvider().equals(LocationManager.NETWORK_PROVIDER) && locationList.get(i).getAccuracy() > Settings.satisfactoryAccuracyNetwork)
								)
								{
									Miscellaneous.logEvent("i", "Speed", "Not using 2 most recent locations for speed calculation because at least one does not have a satisfactory accuracy: " + locationList.get(i).toString(), 4);
									break speedCalculation;
								}
							}

							Miscellaneous.logEvent("i", "Speed", "Trying to calculate speed based on the last locations.", 4);

							double currentSpeed;
							long timeDifferenceInSeconds = (Math.abs(locationList.get(locationList.size() - 2).getTime() - locationList.get(locationList.size() - 1).getTime())) / 1000; //milliseconds
							if (timeDifferenceInSeconds <= Settings.speedMaximumTimeBetweenLocations * 60)
							{
								double distanceTraveled = locationList.get(locationList.size() - 2).distanceTo(locationList.get(locationList.size() - 1)); //results in meters

								if (timeDifferenceInSeconds == 0)
								{
									Miscellaneous.logEvent("w", "Speed", "No time passed since last position. Can't calculate speed here.", 4);
									return;
								}

								currentSpeed = distanceTraveled / timeDifferenceInSeconds * 3.6;    // convert m/s --> km/h

                            /*
                                Due to strange factors the time difference might be 0 resulting in mathematical error.
                             */
								if (Double.isInfinite(currentSpeed) || Double.isNaN(currentSpeed))
									Miscellaneous.logEvent("i", "Speed", "Error while calculating speed.", 4);
								else
								{
									Miscellaneous.logEvent("i", "Speed", "Current speed: " + String.valueOf(currentSpeed) + " km/h", 2);

									setSpeed(currentSpeed);

									// execute matching rules containing speed
									ArrayList<Rule> ruleCandidates = Rule.findRuleCandidates(Trigger_Enum.speed);
									for (Rule oneRule : ruleCandidates)
									{
										if(oneRule.getsGreenLight(this.getParentService()))
											oneRule.activate(getParentService(), false);
									}
								}
							}
							else
								Miscellaneous.logEvent("i", "Speed", "Last two locations are too far apart in terms of time. Cannot use them for speed calculation.", 4);
						}
						else
						{
							Miscellaneous.logEvent("w", "Speed", "Don't have enough values for speed calculation, yet.", 3);
						}
					}
				}
			}
			catch (Exception e)
			{
				Miscellaneous.logEvent("e", "Speed", "Error during speed calculation: " + Log.getStackTraceString(e), 3);
			}

			AutomationService.updateNotification();

			if (AutomationService.isMainActivityRunning(parentService))
				ActivityMainScreen.updateMainScreen();
		}
		else
			Miscellaneous.logEvent("w", "Location", "New location given is null. Ignoring.", 5);
	}

	public void startLocationService()
	{
		// startPhoneStateListener
			PhoneStatusListener.startPhoneStatusListener(parentService);			// also used to mute anouncements during calls

		// startConnectivityReceiver
			ConnectivityReceiver.startConnectivityReceiver(parentService);

		if(Settings.positioningEngine == 0)
		{
			if(Rule.isAnyRuleUsing(Trigger_Enum.pointOfInterest) || Rule.isAnyRuleUsing(Trigger_Enum.speed))
			{
				// startCellLocationChangedReceiver
				if (CellLocationChangedReceiver.isCellLocationChangedReceiverPossible())
				{
					if (WifiBroadcastReceiver.mayCellLocationReceiverBeActivated())
						CellLocationChangedReceiver.startCellLocationChangedReceiver();
				}
				else
				{
				/*
					Reasons why we may end up here:
					- Airplane mode is active
					- No phone module present (pure wifi device)
					- No SIM card is inserted or it's not unlocked

					We'd have to try GPS now to get an initial position.
					For permanent use there is no way we could know when it
					would make sense to check the position again.
				 */

					// Trigger a one-time-position-search
					Location loc = CellLocationChangedReceiver.getInstance().getLocation("fine");
					LocationProvider.getInstance().setCurrentLocation(loc, true);
				}

				// startPassiveLocationListener
				startPassiveLocationListener();
			}
		}
		else
		{
//			if(Rule.isAnyRuleUsing(Trigger_Enum.pointOfInterest))
//				GeofenceIntentService.startService();
		}
	}

	public void stopLocationService()
	{
		try
		{
			PhoneStatusListener.stopPhoneStatusListener(parentService);
			CellLocationChangedReceiver.stopCellLocationChangedReceiver();
			SensorActivity.stopAccelerometerReceiver();
			WifiBroadcastReceiver.stopWifiReceiver();
			SensorActivity.stopAccelerometerReceiver();
			stopPassiveLocationListener();
		}
		catch(Exception e)
		{
			Miscellaneous.logEvent("e", "cellReceiver", "Error stopping LocationReceiver: " + Log.getStackTraceString(e), 3);
		}
	}

	public void startPassiveLocationListener()
	{
		if(!passiveLocationListenerActive)
		{
			Miscellaneous.logEvent("i", "LocationListener", "Arming passive location listener.", 4);
			LocationManager myLocationManager = (LocationManager) parentService.getSystemService(Context.LOCATION_SERVICE);
			passiveLocationListener = new MyPassiveLocationListener();
			try
			{
				myLocationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, Settings.minimumTimeBetweenUpdate, Settings.minimumDistanceChangeForNetworkUpdate, passiveLocationListener);
			}
			catch(SecurityException e)
			{}
			passiveLocationListenerActive = true;
		}
	}
	public void stopPassiveLocationListener()
	{
		if(passiveLocationListenerActive)
		{
			Miscellaneous.logEvent("i", "LocationListener", "Disarming passive location listener.", 4);
			LocationManager myLocationManager = (LocationManager) parentService.getSystemService(Context.LOCATION_SERVICE);
			myLocationManager.removeUpdates(passiveLocationListener);
			passiveLocationListenerActive = false;
		}
	}

	public class MyPassiveLocationListener implements LocationListener
	{
		@Override
		public void onLocationChanged(Location up2DateLocation)
		{
			Miscellaneous.logEvent("i", "Location", "Got passive location update, provider: " + up2DateLocation.getProvider(), 3);
			setCurrentLocation(up2DateLocation, true);
		}

		@Override
		public void onProviderDisabled(String provider)
		{
			// TODO Auto-generated method stub

		}

		@Override
		public void onProviderEnabled(String provider)
		{
			// TODO Auto-generated method stub

		}

		@Override
		public void onStatusChanged(String provider, int status, Bundle extras)
		{
			// TODO Auto-generated method stub

		}
	}

	public void handleAirplaneMode(boolean state)
	{
		if(state)
		{
			Miscellaneous.logEvent("i", "Airplane mode", "CellLocationChangedReceiver will be deactivated due to Airplane mode.", 2);
			CellLocationChangedReceiver.stopCellLocationChangedReceiver();
		}
		else
		{
			Miscellaneous.logEvent("i", "Airplane mode", "CellLocationChangedReceiver will be activated due to end of Airplane mode.", 2);
			CellLocationChangedReceiver.startCellLocationChangedReceiver();
		}
	}

	public void handleRoaming(Boolean roaming)
	{
		if(roaming)
		{
			Miscellaneous.logEvent("i", "Roaming", "We're on roaming.", 4);
			if(CellLocationChangedReceiver.isCellLocationListenerActive())
			{
				Miscellaneous.logEvent("i", "Roaming", "Disabling CellLocationChangedReceiver because we're on roaming.", 3);
				CellLocationChangedReceiver.stopCellLocationChangedReceiver();
			}
		}
		else
		{
			Miscellaneous.logEvent("i", "Roaming", "We're not on roaming.", 4);
			if(!CellLocationChangedReceiver.isCellLocationListenerActive())
			{
				Miscellaneous.logEvent("i", "Roaming", "Enabling CellLocationChangedReceiver because we're not on roaming.", 3);
				CellLocationChangedReceiver.startCellLocationChangedReceiver();
			}
		}
	}

	public void applySettingsAndRules()
	{
		/*
		 * This method's purpose is to check settings and rules and determine
		 * if changes in them require monitors to be started or stopped.
		 * It takes care only of those which are more expensive.
		 */

		// TextToSpeech is handled in AutomationService class

		Miscellaneous.logEvent("i", "LocationProvider", this.getParentService().getResources().getString(R.string.applyingSettingsAndRules), 3);

		// *********** SETTING CHANGES ***********
		if(Settings.useWifiForPositioning && !WifiBroadcastReceiver.isWifiListenerActive())
		{
			Miscellaneous.logEvent("i", "LocationProvider", "Starting WifiReceiver because settings now allow to.", 4);
			WifiBroadcastReceiver.startWifiReceiver(this);
		}
		else if(!Settings.useWifiForPositioning && WifiBroadcastReceiver.isWifiListenerActive())
		{
			Miscellaneous.logEvent("i", "LocationProvider", "Shutting down WifiReceiver because settings forbid to.", 4);
			WifiBroadcastReceiver.stopWifiReceiver();
		}

		if(Settings.useAccelerometerForPositioning && !SensorActivity.isAccelerometerReceiverActive())
		{
			Miscellaneous.logEvent("i", "LocationProvider", "Starting accelerometerReceiver because settings now allow to.", 4);
			SensorActivity.startAccelerometerReceiver();
		}
		else if(!Settings.useAccelerometerForPositioning && SensorActivity.isAccelerometerReceiverActive())
		{
			Miscellaneous.logEvent("i", "LocationProvider", "Shutting down accelerometerReceiver because settings forbid to.", 4);
			SensorActivity.stopAccelerometerReceiver();
		}

		// *********** RULE CHANGES ***********
		if(!CellLocationChangedReceiver.isCellLocationListenerActive() && (Rule.isAnyRuleUsing(Trigger_Enum.pointOfInterest) || Rule.isAnyRuleUsing(Trigger_Enum.speed)))
		{
			Miscellaneous.logEvent("i", "LocationProvider", "Starting NoiseListener CellLocationChangedReceiver because used in a new/changed rule.", 4);
			if(CellLocationChangedReceiver.haveAllPermission())
				CellLocationChangedReceiver.startCellLocationChangedReceiver();
		}
		else
		{
			Miscellaneous.logEvent("i", "LocationProvider", "Shutting down CellLocationChangedReceiver because not used in any rule.", 4);
			CellLocationChangedReceiver.stopCellLocationChangedReceiver();
		}

		AutomationService.updateNotification();
	}

	public static void startSpeedTimer(Calendar timeOfForcedLocationCheck)
	{
		if(!speedTimerActive)
		{
			if(timeOfForcedLocationCheck == null)
			{
				Miscellaneous.logEvent("i", "SpeedTimer", "Have no value for speed timer. Using 5 minutes in the future.", 4);
				timeOfForcedLocationCheck = Calendar.getInstance();
				timeOfForcedLocationCheck.add(Calendar.MINUTE, 5);
			}

            Calendar calendar = Calendar.getInstance();
            SimpleDateFormat sdf = new SimpleDateFormat(Settings.dateFormat);

			Miscellaneous.logEvent("i", "SpeedTimer", "Starting SpeedTimer. Next forced location check would be at " + sdf.format(calendar.getTime()), 4);

			Message msg = new Message();
			msg.what = 1;

			if(speedHandler == null)
				speedHandler = new SpeedHandler();

			speedHandler.sendMessageAtTime(msg, timeOfForcedLocationCheck.getTimeInMillis());
//				speedHandler.sendMessageDelayed(msg, delayTime);
			speedTimerActive = true;
		}
	}

	public static void stopSpeedTimer()
	{
		if(speedTimerActive)
		{
			Miscellaneous.logEvent("i", "SpeedTimer", "Stopping SpeedTimer.", 4);

// 			Message msg = new Message();
// 			msg.what = 0;

			if(speedHandler == null)
				speedHandler = new SpeedHandler();
			else
				speedHandler.removeMessages(1);

			speedTimerActive = false;
		}
	}


	public static void resetSpeedTimer(Calendar timeOfForcedLocationCheck)
	{
		if(speedTimerActive)
		{
			if(timeOfForcedLocationCheck == null)
			{
				Miscellaneous.logEvent("i", "SpeedTimer", "Have no value for speed timer. Using 5 minutes in the future.", 4);
				timeOfForcedLocationCheck = Calendar.getInstance();
				timeOfForcedLocationCheck.add(Calendar.MINUTE, 5);
			}

			Calendar calendar = Calendar.getInstance();
			SimpleDateFormat sdf = new SimpleDateFormat(Settings.dateFormat);

			Miscellaneous.logEvent("i", "SpeedTimer", "Resetting SpeedTimer. Next forced location check would be at " + sdf.format(calendar.getTime()), 5);
			speedHandler.removeMessages(1);

			Message msg = new Message();
			msg.what = 1;
			speedHandler.sendMessageAtTime(msg, timeOfForcedLocationCheck.getTimeInMillis());
			speedTimerActive = true;
		}
		else
			startSpeedTimer(timeOfForcedLocationCheck);
	}

	static class SpeedHandler extends Handler
	{
		@Override
		public void handleMessage(Message msg)
		{
			super.handleMessage(msg);

			if(msg.what == 1)
			{
				// time is up, no cell location updates since x minutes, start accelerometer
				Miscellaneous.logEvent("i", "LocationProvider", "Timer triggered. Based on the last location and speed we may be at a POI. Forcing location update in case CellLocationChangedReceiver didn\'t fire.", 5);

				Location currentLocation = CellLocationChangedReceiver.getInstance().getLocation("coarse");
				AutomationService.getInstance().getLocationProvider().setCurrentLocation(currentLocation, false);
			}
		}
	}
}