using CefSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;

namespace TweetDuck.Plugins{
    sealed class PluginManager{
        private const int InvalidToken = 0;

        private static readonly Dictionary<PluginEnvironment, string> PluginSetupScripts = new Dictionary<PluginEnvironment, string>(4){
            { PluginEnvironment.None, ScriptLoader.LoadResource("plugins.js") },
            { PluginEnvironment.Browser, ScriptLoader.LoadResource("plugins.browser.js") },
            { PluginEnvironment.Notification, ScriptLoader.LoadResource("plugins.notification.js") }
        };

        public string PathOfficialPlugins => Path.Combine(rootPath, "official");
        public string PathCustomPlugins => Path.Combine(rootPath, "user");

        public IEnumerable<Plugin> Plugins => plugins;
        public PluginConfig Config { get; }
        public PluginBridge Bridge { get; }
        
        public event EventHandler<PluginErrorEventArgs> Reloaded;
        public event EventHandler<PluginErrorEventArgs> Executed;
        public event EventHandler<PluginChangedStateEventArgs> PluginChangedState;

        private readonly string rootPath;
        private readonly string configPath;

        private readonly HashSet<Plugin> plugins = new HashSet<Plugin>();
        private readonly Dictionary<int, Plugin> tokens = new Dictionary<int, Plugin>();
        private readonly Random rand = new Random();

        private List<string> loadErrors;

        public PluginManager(string rootPath, string configPath){
            this.rootPath = rootPath;
            this.configPath = configPath;

            this.Config = new PluginConfig();
            this.Bridge = new PluginBridge(this);

            Config.Load(configPath);
            Config.InternalPluginChangedState += Config_InternalPluginChangedState;
        }

        private void Config_InternalPluginChangedState(object sender, PluginChangedStateEventArgs e){
            PluginChangedState?.Invoke(this, e);
            Config.Save(configPath);
        }

        public bool IsPluginInstalled(string identifier){
            return plugins.Any(plugin => plugin.Identifier.Equals(identifier));
        }

        public bool HasAnyPlugin(PluginEnvironment environment){
            return plugins.Any(plugin => plugin.Environments.HasFlag(environment));
        }

        public int GetTokenFromPlugin(Plugin plugin){
            foreach(KeyValuePair<int, Plugin> kvp in tokens){
                if (kvp.Value.Equals(plugin)){
                    return kvp.Key;
                }
            }

            return InvalidToken;
        }

        public Plugin GetPluginFromToken(int token){
            return tokens.TryGetValue(token, out Plugin plugin) ? plugin : null;
        }

        public void Reload(){
            Config.Load(configPath);

            plugins.Clear();
            tokens.Clear();

            loadErrors = new List<string>(2);
            
            foreach(Plugin plugin in LoadPluginsFrom(PathOfficialPlugins, PluginGroup.Official)){
                plugins.Add(plugin);
            }

            foreach(Plugin plugin in LoadPluginsFrom(PathCustomPlugins, PluginGroup.Custom)){
                plugins.Add(plugin);
            }

            Reloaded?.Invoke(this, new PluginErrorEventArgs(loadErrors));
        }

        public void ExecutePlugins(IFrame frame, PluginEnvironment environment){
            if (HasAnyPlugin(environment)){
                ScriptLoader.ExecuteScript(frame, PluginSetupScripts[environment], environment.GetScriptIdentifier());
                ScriptLoader.ExecuteScript(frame, PluginSetupScripts[PluginEnvironment.None], PluginEnvironment.None.GetScriptIdentifier());
                ExecutePluginScripts(frame, environment);
            }
        }

        private void ExecutePluginScripts(IFrame frame, PluginEnvironment environment){
            bool includeDisabled = environment.IncludesDisabledPlugins();

            if (includeDisabled){
                ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GenerateConfig(Config), "gen:pluginconfig");
            }

            List<string> failedPlugins = new List<string>(1);

            foreach(Plugin plugin in Plugins){
                string path = plugin.GetScriptPath(environment);
                if (string.IsNullOrEmpty(path) || (!includeDisabled && !Config.IsEnabled(plugin)) || !plugin.CanRun)continue;

                string script;

                try{
                    script = File.ReadAllText(path);
                }catch(Exception e){
                    failedPlugins.Add(plugin.Identifier+" ("+Path.GetFileName(path)+"): "+e.Message);
                    continue;
                }

                int token;

                if (tokens.ContainsValue(plugin)){
                    token = GetTokenFromPlugin(plugin);
                }
                else{
                    token = GenerateToken();
                    tokens[token] = plugin;
                }

                ScriptLoader.ExecuteScript(frame, PluginScriptGenerator.GeneratePlugin(plugin.Identifier, script, token, environment), "plugin:"+plugin);
            }

            Executed?.Invoke(this, new PluginErrorEventArgs(failedPlugins));
        }

        private IEnumerable<Plugin> LoadPluginsFrom(string path, PluginGroup group){
            if (!Directory.Exists(path)){
                yield break;
            }

            foreach(string fullDir in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)){
                Plugin plugin = Plugin.CreateFromFolder(fullDir, group, out string error);

                if (plugin == null){
                    loadErrors.Add(group.GetIdentifierPrefix()+Path.GetFileName(fullDir)+": "+error);
                }
                else{
                    yield return plugin;
                }
            }
        }

        private int GenerateToken(){
            for(int attempt = 0; attempt < 1000; attempt++){
                int token = rand.Next();

                if (!tokens.ContainsKey(token) && token != InvalidToken){
                    return token;
                }
            }

            return -tokens.Count;
        }
    }
}