// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;

namespace Microsoft.MixedReality.Toolkit
{
    /// <summary>
    /// Static class that represents the Mixed Reality Toolkit service registry.
    /// </summary>
    /// <remarks>
    /// <para>The service registry is used to enable discovery of and access to active Mixed Reality Toolkit services at
    /// runtime without requiring direct code reference to a singleton style component.</para>
    /// </remarks>
    public static class MixedRealityServiceRegistry
    {
        /// <summary>
        /// The service registry store where the key is the Type of the service interface and the value is
        /// a pair in which they key is the service instance and the value is the registrar instance.
        /// </summary>
        private static Dictionary<Type, List<KeyValuePair<IMixedRealityService, IMixedRealityServiceRegistrar>>> registry =
            new Dictionary<Type, List<KeyValuePair<IMixedRealityService, IMixedRealityServiceRegistrar>>>();

        /// <summary>
        /// A cache used to power <seealso cref="GetAllServices(IMixedRealityServiceRegistrar)"/>
        /// </summary>
        /// <remarks>
        /// <para>Lists are sorted in ascending priority order (i.e. services with a smaller priority
        /// value are first in the list).</para>
        /// </remarks>
        private static Dictionary<IMixedRealityServiceRegistrar, List<IMixedRealityService>> allServicesByRegistrar =
            new Dictionary<IMixedRealityServiceRegistrar, List<IMixedRealityService>>();

        /// <summary>
        /// A cache used to power <seealso cref="GetAllServices"/>
        /// </summary>
        /// <remarks>
        /// <para>The list is sorted in ascending priority order (i.e. services with a smaller priority
        /// value are first in the list).</para>
        /// </remarks>
        private static List<IMixedRealityService> allServices = new List<IMixedRealityService>();

        /// <summary>
        /// A comparer used to sort the allServices and allServiceByRegistrar lists in-place.
        /// </summary>
        private static readonly Comparer<IMixedRealityService> ascendingOrderComparer =
            Comparer<IMixedRealityService>.Create((i1, i2) => i1.Priority.CompareTo(i2.Priority));

        /// <summary>
        /// Static constructor.
        /// </summary>
        static MixedRealityServiceRegistry()
        { }

        /// <summary>
        /// Adds an <see cref="IMixedRealityService"/> instance to the registry.
        /// </summary>
        /// <typeparam name="T">The interface type of the service being added.</typeparam>
        /// <param name="serviceInstance">Instance of the service to add.</param>
        /// <param name="registrar">Instance of the registrar manages the service.</param>
        /// <returns>
        /// True if the service was successfully added, false otherwise.
        /// </returns>
        public static bool AddService<T>(T serviceInstance, IMixedRealityServiceRegistrar registrar) where T : IMixedRealityService
        {
            if (serviceInstance == null)
            {
                // Adding a null service instance is not supported.
                return false;
            }

            if (serviceInstance is IMixedRealityDataProvider)
            {
                // Data providers are generally not used by application code. Services that intend for clients to
                // directly communicate with their data providers will expose a GetDataProvider or similarly named
                // method.
                return false;
            }

            if (TryGetService<T>(out _, serviceInstance.Name))
            {
                return false;
            }

            Type interfaceType = typeof(T);

            // Ensure we have a place to put our newly registered service.
            if (!registry.ContainsKey(interfaceType))
            {
                registry.Add(interfaceType, new List<KeyValuePair<IMixedRealityService, IMixedRealityServiceRegistrar>>());
            }

            List<KeyValuePair<IMixedRealityService, IMixedRealityServiceRegistrar>> services = registry[interfaceType];
            services.Add(new KeyValuePair<IMixedRealityService, IMixedRealityServiceRegistrar>(serviceInstance, registrar));
            AddServiceToCache(serviceInstance, registrar);
            return true;
        }

        /// <summary>
        /// Removes an <see cref="IMixedRealityService"/> instance from the registry.
        /// </summary>
        /// <typeparam name="T">The interface type of the service being removed.</typeparam>
        /// <param name="serviceInstance">Instance of the service to remove.</param>
        /// <param name="registrar">Instance of the registrar manages the service.</param>
        /// <returns>
        /// True if the service was successfully removed, false otherwise.
        /// </returns>
        public static bool RemoveService<T>(T serviceInstance, IMixedRealityServiceRegistrar registrar) where T : IMixedRealityService
        {
            return RemoveServiceInternal(typeof(T), serviceInstance, registrar);
        }

        /// <summary>
        /// Removes an <see cref="IMixedRealityService"/> instance from the registry.
        /// </summary>
        /// <typeparam name="T">The interface type of the service being removed.</typeparam>
        /// <param name="serviceInstance">Instance of the service to remove.</param>
        /// <returns>
        /// True if the service was successfully removed, false otherwise.
        /// </returns>
        public static bool RemoveService<T>(T serviceInstance) where T : IMixedRealityService
        {
            T tempService;
            IMixedRealityServiceRegistrar registrar;

            if (!TryGetService<T>(out tempService, out registrar))
            {
                return false;
            }

            if (!object.ReferenceEquals(serviceInstance, tempService))
            {
                return false;
            }

            return RemoveServiceInternal(typeof(T), serviceInstance, registrar);
        }

        /// <summary>
        /// Removes an <see cref="IMixedRealityService"/> instance from the registry.
        /// </summary>
        /// <typeparam name="T">The interface type of the service being removed.</typeparam>
        /// <param name="name">The friendly name of the service to remove.</param>
        /// <returns>
        /// True if the service was successfully removed, false otherwise.
        /// </returns>
        public static bool RemoveService<T>(string name) where T : IMixedRealityService
        {
            T tempService;
            IMixedRealityServiceRegistrar registrar;

            if (!TryGetService<T>(out tempService, out registrar, name))
            {
                return false;
            }

            return RemoveServiceInternal(typeof(T), tempService, registrar);
        }

        /// <summary>
        /// Removes an <see cref="IMixedRealityService"/> instance from the registry.
        /// </summary>
        /// <param name="interfaceType">The interface type of the service being removed.</param>
        /// <param name="serviceInstance">Instance of the service to remove.</param>
        /// <param name="registrar">Instance of the registrar manages the service.</param>
        /// <returns>
        /// True if the service was successfully removed, false otherwise.
        /// </returns>
        private static bool RemoveServiceInternal(
            Type interfaceType,
            IMixedRealityService serviceInstance,
            IMixedRealityServiceRegistrar registrar)
        {
            if (!registry.ContainsKey(interfaceType)) { return false; }

            List<KeyValuePair<IMixedRealityService, IMixedRealityServiceRegistrar>> services = registry[interfaceType];

            bool removed = services.Remove(new KeyValuePair<IMixedRealityService, IMixedRealityServiceRegistrar>(serviceInstance, registrar));

            if (services.Count == 0)
            {
                // If the last service was removed, the key can be removed.
                registry.Remove(interfaceType);
            }

            RemoveServiceFromCache(serviceInstance, registrar);

            return removed;
        }

        /// <summary>
        /// Adds the given service/registrar combination to the GetAllServices cache
        /// </summary>
        private static void AddServiceToCache(
            IMixedRealityService service,
            IMixedRealityServiceRegistrar registrar)
        {
            // Services are stored in ascending priority order - adding them to the
            // list requires that we re-enforce that order. This must happen
            // in both the allServices and allServicesByRegistrar data structures.
            allServices.Add(service);
            allServices.Sort(ascendingOrderComparer);

            if (!allServicesByRegistrar.ContainsKey(registrar))
            {
                allServicesByRegistrar.Add(registrar, new List<IMixedRealityService>());
            }

            allServicesByRegistrar[registrar].Add(service);
            allServicesByRegistrar[registrar].Sort(ascendingOrderComparer);
        }

        /// <summary>
        /// Removes the given service/registrar combination from the GetAllServices cache
        /// </summary>
        private static void RemoveServiceFromCache(
            IMixedRealityService service,
            IMixedRealityServiceRegistrar registrar)
        {
            // Removing from the sorted list keeps sort order, so re-sorting isn't necessary
            allServices.Remove(service);
            if (allServicesByRegistrar.ContainsKey(registrar))
            {
                allServicesByRegistrar[registrar].Remove(service);
                if (allServicesByRegistrar[registrar].Count == 0)
                {
                    allServicesByRegistrar.Remove(registrar);
                }
            }
        }

        /// <summary>
        /// Gets the first instance of the requested service from the registry that matches the given query.
        /// </summary>
        /// <typeparam name="T">The interface type of the service being requested.</typeparam>
        /// <param name="serviceInstance">Output parameter to receive the requested service instance.</param>
        /// <param name="name">Optional name of the service.</param>
        /// <returns>
        /// True if the requested service is being returned, false otherwise.
        /// </returns>
        public static bool TryGetService<T>(
            out T serviceInstance,
            string name = null) where T : IMixedRealityService
        {
            return TryGetService<T>(
                out serviceInstance,
                out _,                  // The registrar out param is not used, it can be discarded.
                name);
        }

        /// <summary>
        /// Gets the first instance of the requested service from the registry that matches the given query.
        /// </summary>
        /// <typeparam name="T">The interface type of the service being requested.</typeparam>
        /// <param name="serviceInstance">Output parameter to receive the requested service instance.</param>
        /// <param name="registrar">Output parameter to receive the registrar that loaded the service instance.</param>
        /// <param name="name">Optional name of the service.</param>
        /// <returns>
        /// True if the requested service is being returned, false otherwise.
        /// </returns>
        public static bool TryGetService<T>(
            out T serviceInstance,
            out IMixedRealityServiceRegistrar registrar,
            string name = null) where T : IMixedRealityService
        {
            Type interfaceType = typeof(T);

            if (TryGetServiceInternal(interfaceType, out IMixedRealityService tempService, out registrar, name))
            {
                Debug.Assert(tempService is T, "The service in the registry does not match the expected type.");
                serviceInstance = (T)tempService;
                return true;
            }

            serviceInstance = default(T);
            registrar = null;
            return false;
        }

        /// <summary>
        /// Gets the first instance of the requested service from the registry that matches the given query.
        /// </summary>
        /// <param name="interfaceType">The interface type of the service being requested.</param>
        /// <param name="serviceInstance">Output parameter to receive the requested service instance.</param>
        /// <param name="registrar">Output parameter to receive the registrar that loaded the service instance.</param>
        /// <param name="name">Optional name of the service.</param>
        /// <returns>
        /// True if the requested service is being returned, false otherwise.
        /// </returns>
        public static bool TryGetService(Type interfaceType,
            out IMixedRealityService serviceInstance,
            out IMixedRealityServiceRegistrar registrar,
            string name = null)
        {
            if (!typeof(IMixedRealityService).IsAssignableFrom(interfaceType))
            {
                Debug.LogWarning($"Cannot find type {interfaceType.Name} since it does not extend IMixedRealityService");
                serviceInstance = null;
                registrar = null;
                return false;
            }

            return TryGetServiceInternal(interfaceType, out serviceInstance, out registrar, name);
        }

        private static readonly ProfilerMarker TryGetServiceInternalPerfMarker = new ProfilerMarker("[MRTK] MixedRealityServiceRegistry.TryGetServiceInternal");

        private static bool TryGetServiceInternal(Type interfaceType,
            out IMixedRealityService serviceInstance,
            out IMixedRealityServiceRegistrar registrar,
            string name = null)
        {
            using (TryGetServiceInternalPerfMarker.Auto())
            {
                // Assume failed and return null unless proven otherwise
                serviceInstance = null;
                registrar = null;

                // If there is an entry for the interface key provided, search that small list first
                if (registry.ContainsKey(interfaceType))
                {
                    if (FindEntry(registry[interfaceType], interfaceType, name, out serviceInstance, out registrar))
                    {
                        return true;
                    }
                }

                // Either there is no entry for the interface type, or it was not placed in that list. 
                // Services can have multiple supported interfaces thus they may match the requested query but be placed in a different registry bin
                // Thus, search all bins until a match is found
                foreach (var list in registry.Values)
                {
                    if (FindEntry(list, interfaceType, name, out serviceInstance, out registrar))
                    {
                        return true;
                    }
                }

                return false;
            }
        }

        private static readonly ProfilerMarker FindEntryPerfMarker = new ProfilerMarker("[MRTK] MixedRealityServiceRegistry.FindEntry");

        /// <summary>
        /// Helper method to search list of IMixedRealityService/IMixedRealityServiceRegistrar pairs to find first service that matches name and interface type query
        /// </summary>
        /// <param name="serviceList">list of IMixedRealityService/IMixedRealityServiceRegistrar pairs to search</param>
        /// <param name="interfaceType">type of interface to check</param>
        /// <param name="name">name of service to check. Wildcard if null or empty</param>
        /// <param name="serviceInstance">reference to IMixedRealityService matching query, null otherwise</param>
        /// <param name="registrar">reference to IMixedRealityServiceRegistrar matching query, null otherwise</param>
        /// <returns>true if found first entry to match query, false otherwise</returns>
        private static bool FindEntry(List<KeyValuePair<IMixedRealityService, IMixedRealityServiceRegistrar>> serviceList,
            Type interfaceType,
            string name,
            out IMixedRealityService serviceInstance,
            out IMixedRealityServiceRegistrar registrar)
        {
            using (FindEntryPerfMarker.Auto())
            {
                // Assume failed and return null unless proven otherwise
                serviceInstance = null;
                registrar = null;

                for (int i = 0; i < serviceList.Count; ++i)
                {
                    var svc = serviceList[i].Key;
                    if ((string.IsNullOrEmpty(name) || svc.Name == name) && interfaceType.IsAssignableFrom(svc.GetType()))
                    {
                        serviceInstance = svc;
                        registrar = serviceList[i].Value;

                        return true;
                    }
                }

                return false;
            }
        }

        /// <summary>
        /// Clears the registry cache of all services
        /// </summary>
        public static void ClearAllServices()
        {
            if (registry != null)
            {
                registry.Clear();
                allServices.Clear();
                allServicesByRegistrar.Clear();
            }
        }

        /// <summary>
        /// Returns readonly list of all services registered
        /// </summary>
        /// <remarks>
        /// The list is sorted in ascending priority order.
        /// </remarks>
        public static IReadOnlyList<IMixedRealityService> GetAllServices()
        {
            return allServices;
        }

        /// <summary>
        /// Returns readonly list of all services registered for given registrar
        /// </summary>
        /// <param name="registrar">Registrar object to filter services by</param>
        /// <remarks>
        /// The list is sorted in ascending priority order.
        /// </remarks>
        /// <returns>Readonly list of all services registered for given registrar, all services if parameter null.
        /// If given a registrar that the registry is not aware of, returns null.
        /// </returns>
        public static IReadOnlyCollection<IMixedRealityService> GetAllServices(IMixedRealityServiceRegistrar registrar)
        {
            if (registrar == null)
            {
                return GetAllServices();
            }
            if (allServicesByRegistrar.TryGetValue(registrar, out List<IMixedRealityService> services))
            {
                return services;
            }
            return null;
        }
    }
}
