namespace Mapbox.Unity.Location
{
	using UnityEngine;
	using System.Collections;
	using System.Globalization;
	using System;
	using System.IO;
	using System.Text;
	using Mapbox.Utils;

	public class DeviceLocationProviderAndroidNative : AbstractLocationProvider, IDisposable
	{
		[SerializeField]
		[Tooltip("Using higher value like 500 usually does not require to turn GPS chip on and thus saves battery power. Values like 5-10 could be used for getting best accuracy.")]
		public float _desiredAccuracyInMeters = 5.0f;

		/// <summary>
		/// The minimum distance (measured in meters) a device must move laterally before location is updated.
		/// https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(java.lang.String,%20long,%20float,%20android.location.LocationListener)
		/// </summary>
		[SerializeField]
		[Tooltip("The minimum distance (measured in meters) a device must move laterally before location is updated. Higher values like 500 imply less overhead.")]
		float _updateDistanceInMeters = 0.0f;


		/// <summary>
		/// The minimum time interval between location updates, in milliseconds.
		/// https://developer.android.com/reference/android/location/LocationManager.html#requestLocationUpdates(java.lang.String,%20long,%20float,%20android.location.LocationListener)
		/// </summary>
		[SerializeField]
		[Tooltip("The minimum time interval between location updates, in milliseconds. It's reasonable to not go below 500ms.")]
		long _updateTimeInMilliSeconds = 1000;


		private WaitForSeconds _wait1sec;
		private WaitForSeconds _wait5sec;
		private WaitForSeconds _wait60sec;
		/// <summary>polls location provider only at the requested update intervall to reduce load</summary>
		private WaitForSeconds _waitUpdateTime;
		private bool _disposed;
		private static object _lock = new object();
		private Coroutine _pollLocation;

		private AndroidJavaObject _activityContext = null;
		private AndroidJavaObject _gpsInstance;
		private AndroidJavaObject _sensorInstance;


		~DeviceLocationProviderAndroidNative() { Dispose(false); }
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}


		protected virtual void Dispose(bool disposeManagedResources)
		{
			if (!_disposed)
			{
				if (disposeManagedResources)
				{
					shutdown();
				}
				_disposed = true;
			}
		}


		private void shutdown()
		{
			try
			{
				lock (_lock)
				{
					if (null != _gpsInstance)
					{
						_gpsInstance.Call("stopLocationListeners");
						_gpsInstance.Dispose();
						_gpsInstance = null;
					}
					if (null != _sensorInstance)
					{
						_sensorInstance.Call("stopSensorListeners");
						_sensorInstance.Dispose();
						_sensorInstance = null;
					}
				}
			}
			catch (Exception ex)
			{
				Debug.LogError(ex);
			}
		}


		protected virtual void OnDestroy() { shutdown(); }


		protected virtual void OnDisable() { shutdown(); }

		protected virtual void Awake()
		{
			// safe measures to not run when disabled or not selected as location provider
			if (!enabled) { return; }
			if (!transform.gameObject.activeInHierarchy) { return; }


			_wait1sec = new WaitForSeconds(1);
			_wait5sec = new WaitForSeconds(5);
			_wait60sec = new WaitForSeconds(60);
			// throttle if entered update intervall is unreasonably low
			_waitUpdateTime = _updateTimeInMilliSeconds < 500 ? new WaitForSeconds(0.5f) : new WaitForSeconds((float)_updateTimeInMilliSeconds / 1000.0f);

			_currentLocation.IsLocationServiceEnabled = false;
			_currentLocation.IsLocationServiceInitializing = true;

			if (Application.platform == RuntimePlatform.Android)
			{
				getActivityContext();
				getGpsInstance(true);
				getSensorInstance();

				if (_pollLocation == null)
				{
					_pollLocation = StartCoroutine(locationRoutine());
				}
			}
		}


		private void getActivityContext()
		{
			using (AndroidJavaClass activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
			{
				_activityContext = activityClass.GetStatic<AndroidJavaObject>("currentActivity");
			}

			if (null == _activityContext)
			{
				Debug.LogError("Could not get UnityPlayer activity");
				return;
			}
		}


		private void getGpsInstance(bool showToastMessages = false)
		{
			if (null == _activityContext) { return; }

			using (AndroidJavaClass androidGps = new AndroidJavaClass("com.mapbox.android.unity.AndroidGps"))
			{
				if (null == androidGps)
				{
					Debug.LogError("Could not get class 'AndroidGps'");
					return;
				}

				_gpsInstance = androidGps.CallStatic<AndroidJavaObject>("instance", _activityContext);
				if (null == _gpsInstance)
				{
					Debug.LogError("Could not get 'AndroidGps' instance");
					return;
				}

				_activityContext.Call("runOnUiThread", new AndroidJavaRunnable(() => { _gpsInstance.Call("showMessage", "starting location listeners"); }));

				_gpsInstance.Call("startLocationListeners", _updateDistanceInMeters, _updateTimeInMilliSeconds);
			}
		}


		private void getSensorInstance()
		{
			if (null == _activityContext) { return; }

			using (AndroidJavaClass androidSensors = new AndroidJavaClass("com.mapbox.android.unity.AndroidSensors"))
			{
				if (null == androidSensors)
				{
					Debug.LogError("Could not get class 'AndroidSensors'");
					return;
				}

				_sensorInstance = androidSensors.CallStatic<AndroidJavaObject>("instance", _activityContext);
				if (null == _sensorInstance)
				{
					Debug.LogError("Could not get 'AndroidSensors' instance");
					return;
				}

				_sensorInstance.Call("startSensorListeners");
			}
		}

		private IEnumerator locationRoutine()
		{

			while (true)
			{
				// couldn't get player activity, wait and retry
				if (null == _activityContext)
				{
					SendLocation(_currentLocation);
					yield return _wait60sec;
					getActivityContext();
					continue;
				}
				// couldn't get gps plugin instance, wait and retry
				if (null == _gpsInstance)
				{
					SendLocation(_currentLocation);
					yield return _wait60sec;
					getGpsInstance();
					continue;
				}

				// update device orientation
				if (null != _sensorInstance)
				{
					_currentLocation.DeviceOrientation = _sensorInstance.Call<float>("getOrientation");
				}

				bool locationServiceAvailable = _gpsInstance.Call<bool>("getIsLocationServiceAvailable");
				_currentLocation.IsLocationServiceEnabled = locationServiceAvailable;


				// app might have been started with location OFF but switched on after start
				// check from time to time
				if (!locationServiceAvailable)
				{
					_currentLocation.IsLocationServiceInitializing = true;
					_currentLocation.Accuracy = 0;
					_currentLocation.HasGpsFix = false;
					_currentLocation.SatellitesInView = 0;
					_currentLocation.SatellitesUsed = 0;

					SendLocation(_currentLocation);
					_gpsInstance.Call("stopLocationListeners");
					yield return _wait5sec;
					_gpsInstance.Call("startLocationListeners", _updateDistanceInMeters, _updateTimeInMilliSeconds);
					yield return _wait1sec;
					continue;
				}


				// if we got till here it means location services are running
				_currentLocation.IsLocationServiceInitializing = false;

				try
				{
					AndroidJavaObject locNetwork = _gpsInstance.Get<AndroidJavaObject>("lastKnownLocationNetwork");
					AndroidJavaObject locGps = _gpsInstance.Get<AndroidJavaObject>("lastKnownLocationGps");

					// easy cases: neither or either gps location or network location available
					if (null == locGps & null == locNetwork) { populateCurrentLocation(null); }
					if (null != locGps && null == locNetwork) { populateCurrentLocation(locGps); }
					if (null == locGps && null != locNetwork) { populateCurrentLocation(locNetwork); }

					// gps- and network location available: figure out which one to use
					if (null != locGps && null != locNetwork) { populateWithBetterLocation(locGps, locNetwork); }


					_currentLocation.TimestampDevice = UnixTimestampUtils.To(DateTime.UtcNow);
					SendLocation(_currentLocation);
				}
				catch (Exception ex)
				{
					Debug.LogErrorFormat("GPS plugin error: " + ex.ToString());
				}
				yield return _waitUpdateTime;
			}
		}


		private void populateCurrentLocation(AndroidJavaObject location)
		{
			if (null == location)
			{
				_currentLocation.IsLocationUpdated = false;
				_currentLocation.IsUserHeadingUpdated = false;
				return;
			}

			double lat = location.Call<double>("getLatitude");
			double lng = location.Call<double>("getLongitude");
			Utils.Vector2d newLatLng = new Utils.Vector2d(lat, lng);
			bool coordinatesUpdated = !newLatLng.Equals(_currentLocation.LatitudeLongitude);
			_currentLocation.LatitudeLongitude = newLatLng;

			float newAccuracy = location.Call<float>("getAccuracy");
			bool accuracyUpdated = newAccuracy != _currentLocation.Accuracy;
			_currentLocation.Accuracy = newAccuracy;

			// divide by 1000. Android returns milliseconds, we work with seconds
			long newTimestamp = location.Call<long>("getTime") / 1000;
			bool timestampUpdated = newTimestamp != _currentLocation.Timestamp;
			_currentLocation.Timestamp = newTimestamp;

			string newProvider = location.Call<string>("getProvider");
			bool providerUpdated = newProvider != _currentLocation.Provider;
			_currentLocation.Provider = newProvider;

			bool hasBearing = location.Call<bool>("hasBearing");
			// only evalute bearing when location object actually has a bearing
			// Android populates bearing (which is not equal to device orientation)
			// only when the device is moving.
			// Otherwise it is set to '0.0'
			// https://developer.android.com/reference/android/location/Location.html#getBearing()
			// We don't want that when we rotate a map according to the direction
			// thes user is moving, thus don't update 'heading' with '0.0'
			if (!hasBearing)
			{
				_currentLocation.IsUserHeadingUpdated = false;
			}
			else
			{
				float newHeading = location.Call<float>("getBearing");
				_currentLocation.IsUserHeadingUpdated = newHeading != _currentLocation.UserHeading;
				_currentLocation.UserHeading = newHeading;
			}

			float? newSpeed = location.Call<float>("getSpeed");
			bool speedUpdated = newSpeed != _currentLocation.SpeedMetersPerSecond;
			_currentLocation.SpeedMetersPerSecond = newSpeed;

			// flag location as updated if any of below conditions evaluates to true
			// Debug.LogFormat("coords:{0} acc:{1} time:{2} speed:{3}", coordinatesUpdated, accuracyUpdated, timestampUpdated, speedUpdated);
			_currentLocation.IsLocationUpdated =
				providerUpdated
				|| coordinatesUpdated
				|| accuracyUpdated
				|| timestampUpdated
				|| speedUpdated;

			// Un-comment if required. Throws a warning right now.
			//bool networkEnabled = _gpsInstance.Call<bool>("getIsNetworkEnabled");
			bool gpsEnabled = _gpsInstance.Call<bool>("getIsGpsEnabled");
			if (!gpsEnabled)
			{
				_currentLocation.HasGpsFix = null;
				_currentLocation.SatellitesInView = null;
				_currentLocation.SatellitesUsed = null;
			}
			else
			{
				_currentLocation.HasGpsFix = _gpsInstance.Get<bool>("hasGpsFix");
				_currentLocation.SatellitesInView = _gpsInstance.Get<int>("satellitesInView");
				_currentLocation.SatellitesUsed = _gpsInstance.Get<int>("satellitesUsedInFix");
			}

		}


		/// <summary>
		/// If GPS and network location are available use the newer/better one
		/// </summary>
		/// <param name="locGps"></param>
		/// <param name="locNetwork"></param>
		private void populateWithBetterLocation(AndroidJavaObject locGps, AndroidJavaObject locNetwork)
		{

			// check which location is fresher
			long timestampGps = locGps.Call<long>("getTime");
			long timestampNet = locNetwork.Call<long>("getTime");
			if (timestampGps > timestampNet)
			{
				populateCurrentLocation(locGps);
				return;
			}

			// check which location has better accuracy
			float accuracyGps = locGps.Call<float>("getAccuracy");
			float accuracyNet = locNetwork.Call<float>("getAccuracy");
			if (accuracyGps < accuracyNet)
			{
				populateCurrentLocation(locGps);
				return;
			}

			// default to network
			populateCurrentLocation(locNetwork);
		}

#if UNITY_ANDROID

		private string time2str(AndroidJavaObject loc)
		{
			long time = loc.Call<long>("getTime");
			DateTime dtPlugin = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Add(TimeSpan.FromMilliseconds(time));
			return dtPlugin.ToString("yyyyMMdd HHmmss");
		}



		private string loc2str(AndroidJavaObject loc)
		{

			if (null == loc) { return "loc: NULL"; }

			try
			{

				double lat = loc.Call<double>("getLatitude");
				double lng = loc.Call<double>("getLongitude");

				return string.Format(CultureInfo.InvariantCulture, "{0:0.00000000} / {1:0.00000000}", lat, lng);
			}
			catch (Exception ex)
			{
				return ex.ToString();
			}
		}

#endif



	}
}
