using System;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Jovian.Utilities {
    /// <summary>
    /// <b>Editor use-only.</b><br/>
    /// Allows easy access between different instances. Supports 1 instance per type. <br/>
    /// Add instances and retrieve elsewhere.<br/><br/>
    /// Read more: https://en.wikipedia.org/wiki/Service_locator_pattern
    /// </summary>
    public static class EditorServiceLocator {
        private sealed class ServiceNotFoundException : Exception {
            public ServiceNotFoundException(Type type) : base($"ServiceNotFoundException. Type={type}") { }
        }

        private static Dictionary<Type, object> serviceContainer = new();
        public static bool IsDirty { get; private set; }

#if UNITY_EDITOR
        public static Dictionary<Type, object> ServiceContainer => serviceContainer;
#endif

        [RuntimeInitializeOnLoadMethod]
        public static void Init() {
            serviceContainer = new();
        }

#if UNITY_EDITOR
        [InitializeOnEnterPlayMode]
        private static void Reset(EnterPlayModeOptions options) {
            serviceContainer?.Clear();
            IsDirty = true;
        }
#endif

        /// <summary>
        /// Add an instance to the locator. Will throw an exception if a type already exists in the locator.
        /// <seealso cref="AddOrReplace{T}"/>
        /// </summary>
        [Conditional("UNITY_EDITOR")]
        public static void Add<T>(T service) {
#if UNITY_EDITOR
            if (service == null) {
                throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
            }

            serviceContainer.Add(typeof(T), service);
            IsDirty = true;
#else
            UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
#endif
        }

        /// <summary>
        /// Add an instance to the locator. Will replace any existing instance without an exception.
        /// An alias for <see cref="Set{T}"/>
        /// </summary>
        [Conditional("UNITY_EDITOR")]
        public static void AddOrReplace<T>(T service) {
            Set(service);
        }

        /// <summary>
        /// Add an instance to the locator. Will replace any existing instance without an exception.
        /// An alias for <see cref="AddOrReplace{T}"/>
        /// </summary>
        [Conditional("UNITY_EDITOR")]
        public static void Set<T>(T service) {
#if UNITY_EDITOR
            if (service == null) {
                throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
            }

            serviceContainer[typeof(T)] = service;
            IsDirty = true;
#else
            UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
#endif
        }

        /// <summary>
        /// Removes any type matching the instance passed in from the locator. This is good practice.
        /// </summary>
        /// <param name="service"></param>
        /// <typeparam name="T"></typeparam>
        /// <exception cref="NullReferenceException"></exception>
        [Conditional("UNITY_EDITOR")]
        public static void Remove<T>(T service) {
#if UNITY_EDITOR
            if (service == null) {
                throw new NullReferenceException($"Service is null. Expected instance of type '{typeof(T)}'");
            }

            serviceContainer.Remove(typeof(T));
            IsDirty = true;
#else
            UnityEngine.Debug.LogWarning($"EditorServiceLocator should not be used outside of the Editor");
#endif
        }

        /// <summary>
        /// Retrieves an instance from the locator matching the type. Will throw an exception if nothing is found.
        /// <seealso cref="TryGet{T}(out T)"/>
        /// </summary>
        public static T Get<T>() {
#if UNITY_EDITOR
            if (serviceContainer.TryGetValue(typeof(T), out object service)) {
                return (T)service;
            }
#endif
            throw new ServiceNotFoundException(typeof(T));
        }

        [Obsolete("Use Get<T> or TryGet<T>(out T) since they follow the C# conventions for TryGet")]
        public static T TryGet<T>() {
#if UNITY_EDITOR
            if (serviceContainer.TryGetValue(typeof(T), out object service)) {
                return (T)service;
            }
#endif
            return default;
        }

        /// <summary>
        /// Retrieves an instance from the locator matching the type. Returns true/false based on success.
        /// </summary>
        public static bool TryGet<T>(out T instance) {
#if UNITY_EDITOR
            if (serviceContainer.TryGetValue(typeof(T), out object service)) {
                instance = (T)service;
                return instance != null;
            }
#endif
            instance = default;
            return false;
        }

        [Conditional("UNITY_EDITOR")]
        public static void Clear() {
            serviceContainer.Clear();
            IsDirty = true;
        }

        [Conditional("UNITY_EDITOR")]
        public static void Clean() {
            IsDirty = false;
        }
    }
}