using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Window;

namespace SlatedGameToolkit.Framework.AssetSystem {
    /// <summary>
    /// A delegate that should modify a given path.
    /// </summary>
    /// <param name="path">A path to modify.</param>
    /// <returns>The modified path.</returns>
    public delegate string ModifyPath(string path);
    /// <summary>
    /// A delegate that performs the loading of the specific asset.
    /// </summary>
    /// <param name="path">The path of the asset.</param>
    /// <param name="glContext">The context to be used.</param>
    /// <typeparam name="T">The type of the resulting asset.</typeparam>
    /// <returns>A loaded, useable asset.</returns>
    public delegate T LoadAsset<T>(string path, GLContext glContext) where T : IAssetUseable;
    public class AssetManager {
        private readonly object assetThreadLock = new object();
        private Thread thread;
        private ConcurrentQueue<(string, GLContext)> loadables;
        private volatile bool load;
        public bool Complete {
            get {
                return loadables.Count == 0;
            }
        }
        private ConcurrentDictionary<string, IAssetUseable> assets;
        
        /// <summary>
        /// A concurrent dictionary containing all the path modifier's in association with their extension.
        /// This is what the manager uses to determine how to modify a filename or path dependent on it's extension.
        /// </summary>
        /// <value>A dictionary of file extension associated with respective paths.</value>
        public ConcurrentDictionary<string, ModifyPath> PathModifiers { get; private set; }

        /// <summary>
        /// A dictionary containing associations between file extensions and their loaders.
        /// All file extensions will be requested in lower cases, and therefore, the extension key in this dictionary should also be all lowercase.
        /// </summary>
        /// <value>The dictionary that associates the loaders to their file extensions.</value>
        public ConcurrentDictionary<string, LoadAsset<IAssetUseable>> Loaders { get ; private set; }
        public AssetManager() {
            this.loadables = new ConcurrentQueue<(string, GLContext)>();
            this.assets = new ConcurrentDictionary<string, IAssetUseable>();
            this.PathModifiers = new ConcurrentDictionary<string, ModifyPath>();
            this.Loaders = new ConcurrentDictionary<string, LoadAsset<IAssetUseable>>();

            thread = new Thread(Run);
            this.load = true;
            thread.Name = "Asset-Loader";
            thread.IsBackground = true;
            thread.Start();

        }

        /// <summary>
        /// Queue's the loadable for batched loading.
        /// The file name of the loadable is the one the loadable is saved under.
        /// </summary>
        /// <param name="name">path of file.</param>
        public void QueueLoad(string name, GLContext glContext = null) {
            if (glContext == null) glContext = WindowContextsManager.CurrentGL;
            loadables.Enqueue((name, glContext));
        }

        private void Run() {
            while (load) {
                lock (assetThreadLock)
                {
                ValueTuple<string, GLContext> loadable;
                    while (loadables.TryDequeue(out loadable)) {
                        Load(loadable.Item1, loadable.Item2);
                    }
                    if (load) Monitor.Wait(assetThreadLock);                
                }
            }
        }

        /// <summary>
        /// Loads the loadable and stores it under the filename given to the loadable.
        /// </summary>
        /// <param name="name">The loadable to load.</param>
        /// <param name="glContext">The OpenGL context to associate with the loaded asset if the loaded asset requires it. May be null, in which the currently active context will be associated with it.</param>
        public void Load(string name, GLContext glContext = null) {
            if (glContext == null) glContext = WindowContextsManager.CurrentGL;
            string ext = Path.GetExtension(name).ToLower();
            if (PathModifiers.ContainsKey(ext)) {
                name = PathModifiers[ext](name);
            }
            if (!Loaders.ContainsKey(ext)) throw new FrameworkUsageException(string.Format("Failed to find associated loader for file \"{0}\" with extension \"{1}\".", name, ext));
            assets[name] = Loaders[ext](name, glContext);
        }

        /// <summary>
        /// Loads the currently queued asynchronously.
        /// </summary>
        public void LoadQueued() {
            if (Monitor.TryEnter(assetThreadLock)) {
                Monitor.Pulse(assetThreadLock);
            }
        }

        /// <summary>
        /// Unloads the asset under the given name.
        /// This disposes the asset as well as removes it from the manager.
        /// </summary>
        /// <param name="name">The name of the asset to unload.</param>
        public void Unload(string name) {
            IAssetUseable asset;
            assets.TryRemove(name, out asset);
            asset.Dispose();
        }

        /// <summary>
        /// Removes all the assets.
        /// </summary>
        public void UnloadAll() {
            foreach (string name in assets.Keys)
            {
                Unload(name);
            }
        }
    }
}