using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Voile.UI;
using Voile.Utils;
using Voile.VFS;

namespace Voile.Resources
{
    /// <summary>
    /// Manages resource loading and lifetime, and wraps around multiple available ResourceLoaders.
    /// </summary>
    public class ResourceManager : IDisposable
    {
        /// <summary>
        /// Root path for resources to manipulate resources at.
        /// </summary>
        public static string ResourceRoot { get; set; } = "Resources/";

        public static IReadOnlyDictionary<Guid, Resource> LoadedResources => _loadedResources;

        /// <summary>
        /// Emits when a resource gets loaded.
        /// </summary>
        public static Action<string>? OnLoadRequested;
        /// <summary>
        /// Emits when a resource gets unloaded.
        /// </summary>
        public static Action<Guid>? OnUnloadRequested;
        /// <summary>
        /// Emits when <see cref="ResourceManager"/> requested reload of all loaded resources.
        /// </summary>
        public static Action? OnReloaded;

        /// <summary>
        /// Adds a resource to the list of loaded resources.
        /// </summary>
        /// <param name="resource"></param>
        public static void AddResource(Guid resourceGuid, [DisallowNull] Resource resource)
        {
            _loadedResources.TryAdd(resourceGuid, resource);
        }

        /// <summary>
        /// Replaces a resource with a different one.
        /// </summary>
        /// <param name="targetResourceGuid">GUID of a resource to replace.</param>
        /// <param name="withResource">Resource to replace with.</param>
        public static void ReplaceResource(Guid targetResourceGuid, [DisallowNull] Resource withResource)
        {
            _loadedResources[targetResourceGuid] = withResource;
        }

        /// <summary>
        /// Gets a resource from a GUID.
        /// </summary>
        /// <param name="resourceGuid">GUID of the resource to get.</param>
        /// <param name="resource">Retrieved resource. Will return a default resource if resource retrieval was not successful.</param>
        /// <returns>True if resource retrieval was successful.</returns>
        public static bool GetResource<T>(Guid resourceGuid, [NotNullWhen(true)] out T? resource) where T : Resource
        {
            resource = default;

            if (!_loadedResources.ContainsKey(resourceGuid))
            {
                return false;
            }

            resource = (T)_loadedResources[resourceGuid];

            return true;
        }

        /// <summary>
        /// Removes a resource from the list of loaded resources.
        /// This method only removes a resource from the list of loaded resources. If you want to unload a Resource, use Unload.
        /// </summary>
        /// <param name="resourceGuid">GUID of resource to remove.</param>
        public static void RemoveResource(Guid resourceGuid)
        {
            _loadedResources.Remove(resourceGuid, out _);
        }

        /// <summary>
        /// Loads a resource from a given file system path.
        /// </summary>
        /// <typeparam name="T">Resource type to load</typeparam>
        /// <param name="path">File system path to the resource to load.</param>
        /// <param name="result">A ResourceRef for the loaded resource.</param>
        /// <returns>True if resource was loaded successfully, otherwise will log an error and return false.</returns>
        public static bool TryLoad<T>(string path, [NotNullWhen(true)] out ResourceRef<T>? result) where T : Resource
        {
            T? resource = default;
            result = null;

            if (!VirtualFileSystem.FileExists(path))
            {
                _logger.Error($"File at \"{path}\" doesn't exist!");
                return false;
            }

            _logger.Info($"Loading {path} as {typeof(T)}...");

            if (!TryGetLoader(out ResourceLoader<T>? loader))
            {
                return false;
            }

            var extension = Path.GetExtension(path);
            var hasExtension = loader.SupportedExtensions.Any(ext => ext == extension);

            if (!hasExtension)
            {
                _logger.Error($"Extension {extension} is not supported!");
            }

            var resourceGuid = loader.Load(path);

            if (!GetResource(resourceGuid, out T? loadedResource))
            {
                _logger.Error($"Failed to load resource at path \"{path}\"!");
                return false;
            }

            resource = loadedResource;

            _resourcePathMap.Add(path, resourceGuid);

            _logger.Info($"\"{path}\" ({resourceGuid}) was loaded successfully.");

            result = new ResourceRef<T>(resourceGuid);
            return true;
        }

        /// <summary>
        /// Reload all resources that are currently loaded.
        /// </summary>
        public void Reload()
        {
            _logger.Info("Reloading resources.");
            OnReloaded?.Invoke();
        }

        /// <summary>
        /// Unloads a resource at a path.
        /// </summary>
        /// <param name="resourcePath">Path to the resource.</param>
        /// <exception cref="KeyNotFoundException"></exception>
        public void Unload(string resourcePath)
        {
            _logger.Info($"Unloading resource at path \"{resourcePath}\"...");

            if (!_resourcePathMap.ContainsKey(resourcePath))
            {
                throw new KeyNotFoundException($"Resource at path \"{resourcePath}\" was not found!");
            }

            var guid = _resourcePathMap[resourcePath];
            RemoveResource(guid);
        }

        /// <summary>
        /// Unloads a resource by GUID.
        /// </summary>
        /// <param name="resourceGuid">GUID of a resource to unload.</param>
        /// <exception cref="KeyNotFoundException"></exception>
        public void Unload(Guid resourceGuid)
        {
            _logger.Info($"Unloading resource with guid \"{resourceGuid}\"...");

            if (!_loadedResources.ContainsKey(resourceGuid))
            {
                throw new KeyNotFoundException($"Resource with GUID {resourceGuid} was not found!");
            }

            RemoveResource(resourceGuid);
        }

        /// <summary>
        /// Unloads a resource by a ResourceRef.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="resourceRef"></param>
        public void Unload<T>(ResourceRef<T> resourceRef) where T : Resource
        {
            Unload(resourceRef.Guid);
        }

        public bool TrySave<T>(string path, in T resource) where T : Resource
        {
            if (!TryGetSaver(out IResourceSaver<T>? saver))
            {
                return false;
            }

            if (!saver.TrySave(path, in resource))
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Gets a <see cref="Resource"/> by resource's <see cref="Guid"/>.
        /// </summary>
        /// <typeparam name="T">Type of <see cref="Resource"/> to load.</typeparam>
        /// <param name="resourceGuid">Resource's GUID.</param>
        /// <param name="resource">Retrieved resource. Otherwise null if nothing got retrieved.</param>
        /// <returns>True if resource got successfully retrieved, otherwise false.</returns>
        public static T? GetResource<T>(Guid resourceGuid) where T : Resource
        {
            if (!TryGetLoader(out ResourceLoader<T>? loader))
            {
                throw new Exception($"No loader available for type {typeof(T)}!");
            }

            if (!GetResource(resourceGuid, out T? loadedResource))
            {
                if (resourceGuid == Guid.Empty)
                {
                    _logger.Warn("Trying to load a resource with an empty GUID, ignoring.");
                    return null;
                }
                throw new Exception($"No resource with GUID \"{resourceGuid}\" found!");
            }

            return loadedResource;
        }
        /// <summary>
        /// Determines if a resource is currently loaded.
        /// </summary>
        /// <param name="path">Path to the resource.</param>
        /// <returns>True if a resource at the specified path is loaded, otherwise false.</returns>
        public static bool IsResourceLoaded(string path) => _resourcePathMap.ContainsKey(path);

        /// <summary>
        /// Adds a resource loader associated with a resource type.
        /// </summary>
        /// <typeparam name="T">A type of <see cref="Resource"/>.</typeparam>
        /// <param name="loader">Loader to use for loading this resource type.</param>
        public static void AddResourceLoaderAssociation<T>(ResourceLoader<T> loader) where T : Resource
        {
            _logger.Info($"Added resource loader association for {typeof(T)}.");

            OnReloaded += loader.Reload;

            _resourceLoaderAssociations.Add(typeof(T), loader);
        }

        /// <summary>
        /// Adds a resource saver associated with a resource type.
        /// </summary>
        /// <typeparam name="T">A type of <see cref="Resource"/>.</typeparam>
        /// <param name="saver">saver to use for saving this resource type.</param>
        public void AddResourceSaverAssociation<T>(IResourceSaver<T> saver) where T : Resource
        {
            _logger.Info($"Added resource saver association for {typeof(T)}.");
            _resourceSaverAssociations.Add(typeof(T), saver);
        }

        /// <summary>
        /// Enables file watching. <see cref="ResourceManager"/> will automaticallly reload resources when they get changed.
        /// </summary>
        public void EnableFileWatching()
        {
            _fileWatcher = new FileSystemWatcher(ResourceRoot);

            _fileWatcher.NotifyFilter = NotifyFilters.Attributes
                         | NotifyFilters.CreationTime
                         | NotifyFilters.DirectoryName
                         | NotifyFilters.FileName
                         | NotifyFilters.LastWrite
                         | NotifyFilters.Security
                         | NotifyFilters.Size;

            _fileWatcher.IncludeSubdirectories = true;
            _fileWatcher.EnableRaisingEvents = true;

            _fileWatcher.Changed += FileSystemChanged;

            _logger.Info("File watching enabled.");
        }

        private static bool TryGetLoader<T>([NotNullWhen(true)] out ResourceLoader<T>? loader) where T : Resource
        {
            loader = null;

            if (!_resourceLoaderAssociations.ContainsKey(typeof(T)))
            {
                _logger.Error($"No loader association found for {typeof(T)}.");
                return false;
            }

            loader = _resourceLoaderAssociations[typeof(T)] as ResourceLoader<T>;

            if (loader is null)
            {
                _logger.Error($"No loader association found for {typeof(T)}.");
                return false;
            }

            return true;
        }

        private bool TryGetSaver<T>([NotNullWhen(true)] out IResourceSaver<T>? saver) where T : Resource
        {
            saver = null;

            if (!_resourceSaverAssociations.ContainsKey(typeof(T)))
            {
                _logger.Error($"No saver association found for {typeof(T)}.");
                return false;
            }

            saver = _resourceSaverAssociations[typeof(T)] as IResourceSaver<T>;

            if (saver is not null)
            {
                _logger.Info($"Using {saver.GetType()} for saving...");
            }
            else
            {
                _logger.Error($"No saver association found for {typeof(T)}.");
                return false;
            }

            return true;
        }

        private void FileSystemChanged(object sender, FileSystemEventArgs e)
        {
            if (e.ChangeType != WatcherChangeTypes.Changed)
            {
                return;
            }

            Reload();
        }

        public void Dispose()
        {

        }

        private static Logger _logger = new(nameof(ResourceManager));

        // TODO: don't include types from optional systems. Create a way for third-party systems to register their own loader associations.
        private static readonly Dictionary<Type, object> _resourceLoaderAssociations = new()
        {
            { typeof(Sound), new SoundLoader()},
            {typeof(Texture2d), new Texture2dLoader()},
            {typeof(Font), new FontLoader()},
        };

        private static readonly Dictionary<Type, object> _resourceSaverAssociations = new()
        {

        };

        private FileSystemWatcher? _fileWatcher;
        private static Dictionary<string, Guid> _resourcePathMap = new();

        private static ConcurrentDictionary<Guid, Resource> _loadedResources = new();
    }
}