using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using IPA.Config.Providers;
using IPA.Utilities;
#if NET3
using Net3_Proxy;
using Path = Net3_Proxy.Path;
using Array = Net3_Proxy.Array;
#endif

namespace IPA.Config
{
    /// <summary>
    /// An abstraction of a config file on disk, which handles synchronizing between a memory representation and the
    /// disk representation.
    /// </summary>
    public class Config
    {
        static Config()
        {
            JsonConfigProvider.RegisterConfig();
        }

        /// <summary>
        /// Specifies that a particular parameter is preferred to use a particular <see cref="IConfigProvider" />. 
        /// If it is not available, also specifies backups. If none are available, the default is used.
        /// </summary>
        [AttributeUsage(AttributeTargets.Parameter)]
        public sealed class PreferAttribute : Attribute
        {
            /// <summary>
            /// The order of preference for the config type. 
            /// </summary>
            /// <value>the list of config extensions in order of preference</value>
            // ReSharper disable once UnusedAutoPropertyAccessor.Global
            public string[] PreferenceOrder { get; private set; }

            /// <inheritdoc />
            /// <summary>
            /// Constructs the attribute with a specific preference list. Each entry is the extension without a '.'
            /// </summary>
            /// <param name="preference">The preferences in order of preference.</param>
            public PreferAttribute(params string[] preference)
            {
                PreferenceOrder = preference;
            }
        }

        /// <summary>
        /// Specifies a preferred config name, instead of using the plugin's name.
        /// </summary>
        [AttributeUsage(AttributeTargets.Parameter)]
        public sealed class NameAttribute : Attribute
        {
            /// <summary>
            /// The name to use for the config.
            /// </summary>
            /// <value>the name to use for the config</value>
            // ReSharper disable once UnusedAutoPropertyAccessor.Global
            public string Name { get; private set; }

            /// <inheritdoc />
            /// <summary>
            /// Constructs the attribute with a specific name.
            /// </summary>
            /// <param name="name">the name to use for the config.</param>
            public NameAttribute(string name)
            {
                Name = name;
            }
        }

        private static readonly Dictionary<string, IConfigProvider> registeredProviders = new Dictionary<string, IConfigProvider>();

        /// <summary>
        /// Registers a <see cref="IConfigProvider"/> to use for configs.
        /// </summary>
        /// <typeparam name="T">the type to register</typeparam>
        public static void Register<T>() where T : IConfigProvider, new() => Register(typeof(T));

        /// <summary>
        /// Registers a <see cref="IConfigProvider"/> to use for configs.
        /// </summary>
        /// <param name="type">the type to register</param>
        public static void Register(Type type)
        {
            var inst = Activator.CreateInstance(type) as IConfigProvider;
            if (inst == null)
                throw new ArgumentException($"Type not an {nameof(IConfigProvider)}");

            if (registeredProviders.ContainsKey(inst.Extension))
                throw new InvalidOperationException($"Extension provider for {inst.Extension} already exists");

            registeredProviders.Add(inst.Extension, inst);
        }

        /// <summary>
        /// Gets a <see cref="Config"/> object using the specified list of preferred config types.
        /// </summary>
        /// <param name="configName">the name of the mod for this config</param>
        /// <param name="extensions">the preferred config types to try to get</param>
        /// <returns>a <see cref="Config"/> using the requested format, or of type JSON.</returns>
        public static Config GetConfigFor(string configName, params string[] extensions)
        {
            var chosenExt = extensions.FirstOrDefault(s => registeredProviders.ContainsKey(s)) ?? "json";
            var provider = registeredProviders[chosenExt];

            var filename = Path.Combine(UnityGame.UserDataPath, configName + "." + provider.Extension);
            var config = new Config(configName, provider, new FileInfo(filename));

            ConfigRuntime.RegisterConfig(config);

            return config;
        }
        
        internal static Config GetConfigFor(string modName, ParameterInfo info)
        {
            var prefs = Array.Empty<string>();
            if (info.GetCustomAttribute<PreferAttribute>() is PreferAttribute prefer)
                prefs = prefer.PreferenceOrder;
            if (info.GetCustomAttribute<NameAttribute>() is NameAttribute name)
                modName = name.Name;

            return GetConfigFor(modName, prefs);
        }

        /// <summary>
        /// Gets the name associated with this <see cref="Config"/> object.
        /// </summary>
        public string Name { get; }
        /// <summary>
        /// Gets the <see cref="IConfigProvider"/> associated with this <see cref="Config"/> object.
        /// </summary>
        public IConfigProvider Provider { get; }

        internal IConfigStore Store = null;
        internal readonly FileInfo File;
        internal readonly ConfigProvider configProvider;
        internal int Writes = 0;

        /// <summary>
        /// Sets this object's <see cref="IConfigStore"/>. Can only be called once.
        /// </summary>
        /// <param name="store">the <see cref="IConfigStore"/> to add to this instance</param>
        /// <exception cref="InvalidOperationException">If this was called before.</exception>
        public void SetStore(IConfigStore store)
        {
            if (Store != null)
                throw new InvalidOperationException($"{nameof(SetStore)} can only be called once");
            Store = store;
            ConfigRuntime.ConfigChanged();
        }

        /// <summary>
        /// Forces a synchronous load from disk.
        /// </summary>
        public void LoadSync() => LoadAsync().Wait();

        /// <summary>
        /// Forces an asynchronous load from disk.
        /// </summary>
        public Task LoadAsync() => ConfigRuntime.TriggerFileLoad(this);

        private Config(string name, IConfigProvider provider, FileInfo file)
        {
            Name = name; Provider = provider; File = file; 
            configProvider = new ConfigProvider(file, provider);
        }
    }
}
