using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
// ReSharper disable UnusedMember.Global


namespace ARLocation
{
    using Utils;

    [AddComponentMenu("AR+GPS/AR Location Provider")]
    [HelpURL("https://http://docs.unity-ar-gps-location.com/guide/#arlocationprovider")]
    [DisallowMultipleComponent]
    public class ARLocationProvider : Singleton<ARLocationProvider>
    {
        [Serializable]
        public class LocationEnabledUnityEvent : UnityEvent<Location> {}
        [Serializable]
        public class LocationUpdatedUnityEvent : UnityEvent<Location> {}

        [Serializable]
        public class CompassUpdatedUnityEvent: UnityEvent<HeadingReading> {}

        [FormerlySerializedAs("LocationUpdateSettings")]
        [Tooltip("The options for the Location Provider.")]
        [Header("Update Settings")]
        public LocationProviderOptions LocationProviderSettings = new LocationProviderOptions();

        [Tooltip("The data of mock location. If present, overrides the Mock Location above.")]
        [Header("Mock Data")]
        public LocationData MockLocationData;

        [Tooltip("The maximum wait time to wait for location initialization.")]
        [Header("Initialization")]
        public uint MaxWaitTime = 200;

        [Tooltip("Wait this many seconds before starting location services. Useful when using Unity Remote.")]
        public uint StartUpDelay;

        [Header("Debug")]
        [Tooltip("When debug mode is enabled, this component will print relevant messages to the console. Filter by 'ARLocationProvider' in the log output to see the messages.")]
        public bool DebugMode;

        [Header("Events")]

        [Tooltip("Called after the first location is read.")]
        public LocationEnabledUnityEvent OnEnabled = new LocationEnabledUnityEvent();

        [Tooltip("Called after each new location update.")]
        public LocationUpdatedUnityEvent OnLocationUpdated = new LocationUpdatedUnityEvent();

        [Tooltip("Called after each new raw device GPS data is obtained.")]
        public LocationUpdatedUnityEvent OnRawLocationUpdated = new LocationUpdatedUnityEvent();

        [Tooltip("Called after each new compass update.")]
        public CompassUpdatedUnityEvent OnCompassUpdated = new CompassUpdatedUnityEvent();

        /// <summary>
        /// Returns the current location provider.
        /// </summary>
        public ILocationProvider Provider { get; private set; }

        /// <summary>
        /// If true, the location provider has received the first location data.
        /// </summary>
        public bool IsEnabled => Provider.IsEnabled;

        /// <summary>
        /// If true, the location provider has started, but no location data has been read.
        /// </summary>
        public bool HasStarted => Provider.HasStarted;

        /// <summary>
        /// The number of location updates so far.
        /// </summary>
        public int LocationUpdateCount => Provider.LocationUpdateCount;

        /// <summary>
        /// If true, updates are paused.
        /// </summary>
        public bool IsPaused => Provider.Paused;

        /// <summary>
        /// The latest location data.
        /// </summary>
        public LocationReading CurrentLocation => Provider.CurrentLocation;

        /// <summary>
        /// The previous location data.
        /// </summary>
        public LocationReading LastLocation => Provider.LastLocation;

        /// <summary>
        /// The current heading data.
        /// </summary>
        public HeadingReading CurrentHeading => Provider.CurrentHeading;


        /// <summary>
        /// Time since the location provider has started.
        /// </summary>
        public float TimeSinceStart => Time.time - Provider.StartTime;

        /// <summary>
        /// The distance from the initial measured position.
        /// </summary>
        public double DistanceFromStartPoint => Provider.DistanceFromStartPoint;

        private int measurementCount;
        private bool mute;

        public event LocationUpdatedDelegate OnLocationUpdatedDelegate;
        public event CompassUpdateDelegate OnCompassUpdateDelegate;
        public event Action OnRestartDelegate;

        public override void Awake()
        {
            base.Awake();

#if UNITY_EDITOR
            Provider = new MockLocationProvider();

            if (MockLocationData != null)
            {
                Logger.LogFromMethod("ARLocationProvider", "Awake", $"Using mock location {MockLocationData}", DebugMode);
                ((MockLocationProvider) Provider).mockLocation = MockLocationData.Location;
            }
#elif ARGPS_CUSTOM_PROVIDER
        // If you want to use a custom location provider, add 'ARGPS_CUSTOM_PROVIDER' to the define symbols in the Player
        // settings, create a implementation of ILocationProvider, and instantiate it in the line below.
        Provider = new ARGpsCustomLocationProvider();
#else
        Provider = new UnityLocationProvider();
#endif
            Logger.LogFromMethod("ARLocationProvider", "Awake",": Using provider " + Provider.Name, DebugMode);
        }



        private void InitProviderEventListeners()
        {
            Logger.LogFromMethod("ARLocationProvider", "InitProviderEventListeners","Initializing location provider listeners.", DebugMode);

            Provider.LocationUpdated += Provider_LocationUpdated;
            Provider.CompassUpdated += Provider_CompassUpdated;
            Provider.LocationUpdatedRaw += ProviderOnLocationUpdatedRaw;

            Provider.OnEnabled(OnProviderEnabledDelegate);

            if (Provider.IsEnabled)
            {
                ForceLocationUpdate();
            }
        }

        private void ProviderOnLocationUpdatedRaw(LocationReading currentLocation, LocationReading lastLocation)
        {
            OnRawLocationUpdated?.Invoke(currentLocation.ToLocation());
        }

        private void OnProviderEnabledDelegate()
        {
            Logger.LogFromMethod("ARLocationProvider", "OnProviderEnabledDelegate","Provider enabled; emitting 'OnEnabled' event.", DebugMode);
            OnEnabled?.Invoke(CurrentLocation.ToLocation());
        }

        IEnumerator Start()
        {
            InitProviderEventListeners();

            Provider.Options = LocationProviderSettings;

            Logger.LogFromMethod("ARLocationProvider", "Start","Starting the location provider", DebugMode);
            yield return StartCoroutine(Provider.Start(MaxWaitTime, StartUpDelay));
        }

        public void Mute()
        {
            Logger.LogFromMethod("ARLocationProvider", "Mute","Muting ARLocationProvider.", DebugMode);
            mute = true;
        }

        public void Unmute(bool emit = true)
        {
            Logger.LogFromMethod("ARLocationProvider", "Mute","Un-muting ARLocationProvider.", DebugMode);
            mute = false;
            if (Provider.IsEnabled && emit) ForceLocationUpdate();
        }

        private void Provider_CompassUpdated(HeadingReading heading, HeadingReading lastReading)
        {
            if (mute) return;

            OnCompassUpdateDelegate?.Invoke(heading, lastReading);
            OnCompassUpdated?.Invoke(heading);
        }

        private void Provider_LocationUpdated(LocationReading currentLocation, LocationReading lastLocation)
        {
            if (mute) return;

            measurementCount++;

            if ((LocationProviderSettings.MaxNumberOfUpdates > 0) && (measurementCount >= LocationProviderSettings.MaxNumberOfUpdates))
            {
                Provider.Pause();
            }

            Logger.LogFromMethod("ARLocationProvider", "Provider_LocationUpdated",$"New location {currentLocation}.", DebugMode);

            OnLocationUpdatedDelegate?.Invoke(currentLocation, lastLocation);
            OnLocationUpdated?.Invoke(currentLocation.ToLocation());
        }

        /// <summary>
        /// Force the provider to emit a location update event. This wont force a new read of location, just emit
        /// the last available measurement.
        /// </summary>
        public void ForceLocationUpdate()
        {
            Logger.LogFromMethod("ARLocationProvider", "ForceLocationUpdate","Emitting a forced location update", DebugMode);
            Provider.ForceLocationUpdate();
        }

        void Update()
        {
            if (Provider == null || !Provider.HasStarted)
            {
                return;
            }

            Provider.Update();
        }

        /// <summary>
        /// Pauses location updates
        /// </summary>
        public void Pause()
        {
            Logger.LogFromMethod("ARLocationProvider", "Pause","Pausing the location provider.", DebugMode);
            Provider?.Pause();
        }

        /// <summary>
        /// Resumes location updates
        /// </summary>
        public void Resume()
        {
            Logger.LogFromMethod("ARLocationProvider", "Resume","Resuming the location provider.", DebugMode);
            Provider?.Resume();
        }

        /// <summary>
        /// Resets the location provider.
        /// </summary>
        public void Restart()
        {
            Logger.LogFromMethod("ARLocationProvider", "Restart","Restarting the location provider.", DebugMode);
            Provider?.Restart();
            OnRestartDelegate?.Invoke();
        }

        /// <summary>
        /// Register a delegate to location updates.
        ///
        /// The `useRawIfEnabled` method if for situations where we want the latest data,
        /// like when we are adding objects at runtime.
        ///
        /// </summary>
        /// <param name="locationUpdatedDelegate"></param>
        /// <param name="useRawIfEnabled"></param>
        public void OnLocationUpdatedEvent(LocationUpdatedDelegate locationUpdatedDelegate, bool useRawIfEnabled = false)
        {
            if (IsEnabled)
            {
                locationUpdatedDelegate(CurrentLocation, useRawIfEnabled ? Provider.LastLocationRaw : LastLocation);
            }

            OnLocationUpdatedDelegate += locationUpdatedDelegate;
        }

        public void OnProviderRestartEvent(Action del)
        {
            OnRestartDelegate += del;
        }

        /// <summary>
        /// Register a delegate to compass/heading updates.
        /// </summary>
        /// <param name="compassUpdateDelegate"></param>
        public void OnCompassUpdatedEvent(CompassUpdateDelegate compassUpdateDelegate)
        {
            OnCompassUpdateDelegate += compassUpdateDelegate;
        }

        /// <summary>
        /// RegisterRegister delegate for when the provider enables location updates.
        /// </summary>
        /// <param name="del">Del.</param>
        public void OnEnabledEvent(LocationEnabledDelegate del)
        {
            Provider.OnEnabled(del);
        }

        /// <summary>
        /// Register a delegate for when the provider fails to initialize location services.
        /// </summary>
        /// <param name="del">Del.</param>
        public void OnFailedEvent(LocationFailedDelegate del)
        {
            Provider.OnFail(del);
        }
    }
}