using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using TweetDuck.Plugins.Enums;

namespace TweetDuck.Plugins{
    sealed class Plugin{
        public string Identifier { get; }
        public PluginGroup Group { get; }
        public PluginEnvironment Environments { get; private set; }

        public string Name => metadata["NAME"];
        public string Description => metadata["DESCRIPTION"];
        public string Author => metadata["AUTHOR"];
        public string Version => metadata["VERSION"];
        public string Website => metadata["WEBSITE"];
        public string ConfigFile => metadata["CONFIGFILE"];
        public string ConfigDefault => metadata["CONFIGDEFAULT"];
        public string RequiredVersion => metadata["REQUIRES"];

        public bool CanRun{
            get => canRun ?? (canRun = CheckRequiredVersion(RequiredVersion)).Value;
        }

        public bool HasConfig{
            get => ConfigFile.Length > 0 && GetFullPathIfSafe(PluginFolder.Data, ConfigFile).Length > 0;
        }

        public string ConfigPath{
            get => HasConfig ? Path.Combine(GetPluginFolder(PluginFolder.Data), ConfigFile) : string.Empty;
        }

        public bool HasDefaultConfig{
            get => ConfigDefault.Length > 0 && GetFullPathIfSafe(PluginFolder.Root, ConfigDefault).Length > 0;
        }

        public string DefaultConfigPath{
            get => HasDefaultConfig ? Path.Combine(GetPluginFolder(PluginFolder.Root), ConfigDefault) : string.Empty;
        }

        private readonly string pathRoot;
        private readonly string pathData;
        private readonly Dictionary<string, string> metadata = new Dictionary<string, string>(4){
            { "NAME", "" },
            { "DESCRIPTION", "" },
            { "AUTHOR", "(anonymous)" },
            { "VERSION", "(unknown)" },
            { "WEBSITE", "" },
            { "CONFIGFILE", "" },
            { "CONFIGDEFAULT", "" },
            { "REQUIRES", "*" }
        };

        private bool? canRun;

        private Plugin(string path, PluginGroup group){
            string name = Path.GetFileName(path);
            System.Diagnostics.Debug.Assert(name != null);

            this.pathRoot = path;
            this.pathData = Path.Combine(Program.PluginDataPath, group.GetIdentifierPrefix(), name);

            this.Identifier = group.GetIdentifierPrefix()+name;
            this.Group = group;
            this.Environments = PluginEnvironment.None;
        }

        private void OnMetadataLoaded(){
            string configPath = ConfigPath, defaultConfigPath = DefaultConfigPath;

            if (configPath.Length > 0 && defaultConfigPath.Length > 0 && !File.Exists(configPath) && File.Exists(defaultConfigPath)){
                string dataFolder = GetPluginFolder(PluginFolder.Data);

                try{
                    Directory.CreateDirectory(dataFolder);
                    File.Copy(defaultConfigPath, configPath, false);
                }catch(Exception e){
                    Program.Reporter.HandleException("Plugin Loading Error", "Could not generate a configuration file for '"+Identifier+"' plugin.", true, e);
                }
            }
        }

        public string GetScriptPath(PluginEnvironment environment){
            if (Environments.HasFlag(environment)){
                string file = environment.GetScriptFile();
                return file != null ? Path.Combine(pathRoot, file) : string.Empty;
            }
            else{
                return string.Empty;
            }
        }

        public string GetPluginFolder(PluginFolder folder){
            switch(folder){
                case PluginFolder.Root: return pathRoot;
                case PluginFolder.Data: return pathData;
                default: return string.Empty;
            }
        }

        public string GetFullPathIfSafe(PluginFolder folder, string relativePath){
            string rootFolder = GetPluginFolder(folder);
            string fullPath = Path.Combine(rootFolder, relativePath);

            try{
                string folderPathName = new DirectoryInfo(rootFolder).FullName;
                DirectoryInfo currentInfo = new DirectoryInfo(fullPath);

                while(currentInfo.Parent != null){
                    if (currentInfo.Parent.FullName == folderPathName){
                        return fullPath;
                    }
                    
                    currentInfo = currentInfo.Parent;
                }
            }
            catch{
                // ignore
            }

            return string.Empty;
        }

        public override string ToString(){
            return Identifier;
        }

        public override int GetHashCode(){
            return Identifier.GetHashCode();
        }

        public override bool Equals(object obj){
            return obj is Plugin plugin && plugin.Identifier.Equals(Identifier);
        }

        public static Plugin CreateFromFolder(string path, PluginGroup group, out string error){
            Plugin plugin = new Plugin(path, group);

            if (!LoadMetadata(path, plugin, out error)){
                return null;
            }

            if (!LoadEnvironments(path, plugin, out error)){
                return null;
            }

            error = string.Empty;
            return plugin;
        }

        private static bool LoadEnvironments(string path, Plugin plugin, out string error){
            foreach(string file in Directory.EnumerateFiles(path, "*.js", SearchOption.TopDirectoryOnly).Select(Path.GetFileName)){
                plugin.Environments |= PluginEnvironmentExtensions.Values.FirstOrDefault(env => file.Equals(env.GetScriptFile(), StringComparison.Ordinal));
            }

            if (plugin.Environments == PluginEnvironment.None){
                error = "Plugin has no script files.";
                return false;
            }

            error = string.Empty;
            return true;
        }

        private static readonly string[] endTag = { "[END]" };

        private static bool LoadMetadata(string path, Plugin plugin, out string error){
            string metaFile = Path.Combine(path, ".meta");

            if (!File.Exists(metaFile)){
                error = "Missing .meta file.";
                return false;
            }

            string[] lines = File.ReadAllLines(metaFile, Encoding.UTF8);
            string currentTag = null, currentContents = "";

            foreach(string line in lines.Concat(endTag).Select(line => line.TrimEnd()).Where(line => line.Length > 0)){
                if (line[0] == '[' && line[line.Length-1] == ']'){
                    if (currentTag != null){
                        plugin.metadata[currentTag] = currentContents;
                    }

                    currentTag = line.Substring(1, line.Length-2).ToUpper();
                    currentContents = "";

                    if (line.Equals(endTag[0])){
                        break;
                    }

                    if (!plugin.metadata.ContainsKey(currentTag)){
                        error = "Invalid metadata tag: "+currentTag;
                        return false;
                    }
                }
                else if (currentTag != null){
                    currentContents = currentContents.Length == 0 ? line : currentContents+"\r\n"+line;
                }
                else{
                    error = "Missing metadata tag before value: "+line;
                    return false;
                }
            }

            if (plugin.Name.Length == 0){
                error = "Plugin is missing a name in the .meta file.";
                return false;
            }

            if (plugin.RequiredVersion.Length == 0 || !(plugin.RequiredVersion.Equals("*") || System.Version.TryParse(plugin.RequiredVersion, out Version _))){
                error = "Plugin contains invalid version: "+plugin.RequiredVersion;
                return false;
            }

            plugin.OnMetadataLoaded();

            error = string.Empty;
            return true;
        }

        private static bool CheckRequiredVersion(string requires){
            return requires.Equals("*", StringComparison.Ordinal) || Program.Version >= new Version(requires);
        }
    }
}