using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;

namespace ExtensiblePortfolioSite.SDK.Plugins
{
    internal static class PluginManager
    {
        private readonly static String NativeLibraryPath;
        private readonly static Byte[][] KnownPublicKeyTokens;
        static PluginManager()
        {
            // Native/[Linux|Windows]/[X86|X64]/
            StringBuilder NativePath = new(64);
            NativePath.Append("Native");
            NativePath.Append(Path.DirectorySeparatorChar);
            NativePath.Append(RuntimeInformation.RuntimeIdentifier);
            NativePath.Append(Path.DirectorySeparatorChar);
            NativePath.Append(RuntimeInformation.ProcessArchitecture.ToString());
            NativePath.Append(Path.DirectorySeparatorChar);
            NativeLibraryPath = String.Intern(NativePath.ToString());

            //b03f5f7f11d50a3a

            List<Byte[]> PublicKeyTokens = new();
            PublicKeyTokens.Add(typeof(String).Assembly.GetName().GetPublicKeyToken()!); // Framework Libraries
            PublicKeyTokens.Add(new byte[8] { 0xb0, 0x3f, 0x5f, 0x7f, 0x11, 0xd5, 0x0a, 0x3a }); // System.Runtime
            PublicKeyTokens.Add(new byte[8] { 0xcc, 0x7b, 0x13, 0xff, 0xcd, 0x2d, 0xdd, 0x51 }); // System.Text.Json
            KnownPublicKeyTokens = PublicKeyTokens.ToArray();
        }


        internal static event PluginLogMessageDelegate? OnLogMessage;
        internal static void LogMessage(PluginErrorSeverity Severity, Plugin Plugin, String Message, Type? Target, Exception? ex) => OnLogMessage?.Invoke(Severity, Plugin, Message, Target, ex);


        public static readonly HashSet<String> LibraryPaths = new();
        public static IReadOnlyList<Plugin> Plugins => _plugins.AsReadOnly();
        private static readonly List<Plugin> _plugins = new();
        private static readonly List<Library> _libraries = new();
        private static readonly List<(String, UnmanagedLibrary)> _unmanagedlibraries = new();

        private static Boolean Initialized = false;

        public static void LoadPlugins(string RootPath)
        {
            RootPath = Path.GetFullPath(RootPath);
            if (!Directory.Exists(RootPath))
                Directory.CreateDirectory(RootPath);
            LibraryPaths.Add(RootPath);
            foreach (String Dll in Directory.EnumerateFiles(RootPath, "*.dll", SearchOption.TopDirectoryOnly))
                TryLoadPlugin(Path.GetFullPath(Dll, RootPath), out Plugin? _);
        }
        public static Boolean TryLoadPlugin(string AsmPath, out Plugin? plugin)
        {
            if (!File.Exists(AsmPath)) goto error;
            AsmPath = Path.GetFullPath(AsmPath);

            // check if assembly has an ESPPluginAttribute
            Boolean IsPlugin = false;
            using (FileStream FSTM = File.OpenRead(AsmPath))
            {
                Type EPSPluginAttribute_type = typeof(EPSPluginAttribute);

                using PEReader PE = new(FSTM, PEStreamOptions.PrefetchMetadata);
                MetadataReader MD = PE.GetMetadataReader();

                IsPlugin = HasPluginAttribute(MD);
            }

            if (!IsPlugin) goto error;
            lock (_plugins)
            {
                // ensure plugin is unique
                foreach (Plugin p in _plugins)
                    if (p.Location == AsmPath)
                        goto error;

                // load assembly
                LibraryPaths.Add(Path.GetDirectoryName(AsmPath)!);
                plugin = new Plugin(AsmPath);
                _plugins.Add(plugin);

                if (Initialized)
                    plugin.Init();

                return true;
            }
        error:
            plugin = null;
            return false;
        }



        private static Boolean HasPluginAttribute(MetadataReader MD)
        {
            Type EPSPluginAttribute_type = typeof(EPSPluginAttribute);
            foreach (CustomAttributeHandle handle in MD.GetAssemblyDefinition().GetCustomAttributes())
            {
                CustomAttribute Attr = MD.GetCustomAttribute(handle);
                if (Attr.Constructor.Kind == HandleKind.MemberReference)
                {
                    MemberReference Ctor = MD.GetMemberReference((MemberReferenceHandle)Attr.Constructor);
                    if (Ctor.Parent.Kind == HandleKind.TypeReference)
                    {
                        TypeReference AttrType = MD.GetTypeReference((TypeReferenceHandle)Ctor.Parent);
                        String AttrTypeName = MD.GetString(AttrType.Name);
                        String AttrTypeNamespace = MD.GetString(AttrType.Namespace);
                        if (
                            AttrType.ResolutionScope.Kind == HandleKind.AssemblyReference &&
                            AttrTypeName == EPSPluginAttribute_type.Name &&
                            AttrTypeNamespace == EPSPluginAttribute_type.Namespace
                            )
                        {

                            AssemblyReference AsmRef = MD.GetAssemblyReference((AssemblyReferenceHandle)AttrType.ResolutionScope);

                            String AsmName = MD.GetString(AsmRef.Name);
                            if (AsmName == EPSPluginAttribute_type.Assembly.GetName().Name)
                                return true;
                        }
                    }
                }
            }
            return false;
        }

        public static void InitializePlugins()
        {
            lock (_plugins)
            {
                Initialized = true;
                foreach (Plugin plugin in _plugins)
                    plugin.Init();
            }
        }
        public static Plugin? GetPlugin(String PluginName)
        {
            lock (_plugins)
                foreach (Plugin p in _plugins)
                    if (p.Info.Name == PluginName)
                        return p;
            return null;
        }

        internal static IPluginManagedLibrary? LoadLibrary(AssemblyName AsmName)
        {
            lock (LibraryPaths)
            {
                // Attempt to fetch pre-existing
                lock (_libraries)
                    foreach (Library library in _libraries)
                        if (library.Assembly.GetName().Name == AsmName.Name)
                            return library;
                lock (_plugins)
                    foreach (Plugin plugin in _plugins)
                        if (plugin.Assembly.GetName().Name == AsmName.Name)
                            return plugin;

                Type EPSPluginAttribute_type = typeof(EPSPluginAttribute);
                Boolean CheckDll(String Path, out Boolean IsPlugin)
                {
                    using FileStream FSTM = File.OpenRead(Path);
                    using PEReader PE = new(FSTM, PEStreamOptions.PrefetchMetadata);
                    MetadataReader MD = PE.GetMetadataReader();

                    if (MD.GetAssemblyDefinition().GetAssemblyName().Name == AsmName.Name)
                    {
                        IsPlugin = HasPluginAttribute(MD);
                        return true;
                    }
                    IsPlugin = false;
                    return false;
                }

                foreach (String LibPath in LibraryPaths)
                    foreach (String file in Directory.EnumerateFiles(LibPath, "*.dll"))
                        if (CheckDll(file, out Boolean IsPlugin))
                        {
                            if (IsPlugin)
                                throw new InvalidOperationException($"Unable to Load Plugin File, '{file}', as it has not been explicitly loaded by the PluginManager");
                            else
                            {
                                Library Lib = new(Path.GetFullPath(file, LibPath));
                                _libraries.Add(Lib);
                                return Lib;
                            }
                        }
            }
            return null;
        }
        internal static IPluginUnmanagedLibrary? LoadUnmanagedLibrary(String Name)
        {
            Int32 Version = -1;
            if (Name.StartsWith("lib"))
                Name = Name[3..];

            again:
            int lastIndex = Name.LastIndexOf('.');
            if (lastIndex != -1)
            {
                String s = Name[(lastIndex + 1)..];
                switch (s)
                {
                    case "dll":
                    case "so":
                    case "dynlib":
                        Name = Name[..lastIndex];
                        break;

                    default:
                        if (Int32.TryParse(s, out Version))
                        {
                            Name = Name[..lastIndex];
                            goto again;
                        }
                        break;
                }
            }

            lastIndex = Name.LastIndexOf('-');
            if (lastIndex != -1)
            {
                String s = Name[(lastIndex + 1)..];
                if (Int32.TryParse(s, out Version))
                    Name = Name[..lastIndex];
            }

            /* [Load Order]
             * OderId   Pattern
             * 1        *.dll
             * 2        *.dynlib
             * 3        *-[Version].so
             * 4        *.so.[Version]
             * 5        lib*-[Version].so
             * 6        lib*.so.[Version]
             * 7        *.so
             * 8        lib*.so
             */

            lock (LibraryPaths)
                lock (_unmanagedlibraries)
                {
                    // attempt to return already loaded library
                    foreach ((String, UnmanagedLibrary) lib in _unmanagedlibraries)
                    {
                        if (lib.Item1 == Name)
                            return lib.Item2;
                    }

                    // get possible entries
                    List<(int, String)> Entries = new();
                    foreach (String LibPath in LibraryPaths)
                    {
                        String Root = Path.Combine(LibPath, NativeLibraryPath);
                        foreach (String file in Directory.EnumerateFiles(Root))
                        {
                            if (file.StartsWith(Name) || (file.StartsWith("lib") && file[3..].StartsWith(Name)))
                            {
                                Int32 Order = -1;
                                String f = file;
                                Int32 v;
                            again2:
                                lastIndex = file.LastIndexOf('.');
                                String s = file[(lastIndex + 1)..];

                                switch (s)
                                {
                                    case "dll":
                                        Order = 1;
                                        goto concat_f;
                                    case "dynlib":
                                        Order = 2;
                                    concat_f:
                                        f = f[..lastIndex];
                                        break;

                                    case "so":
                                        f = f[..lastIndex];
                                        if (f.StartsWith("lib"))
                                        {
                                            lastIndex = f.IndexOf('-');
                                            if (Int32.TryParse(f[lastIndex..], out v))
                                                if (v == Version || Version == -1)
                                                    Order = 5;
                                                else
                                                    continue;
                                            else
                                                Order = Order == -2 ? 6 : 8;
                                        }
                                        else
                                        {
                                            lastIndex = f.IndexOf('-');
                                            if (Int32.TryParse(f[lastIndex..], out v))
                                                if (v == Version || Version == -1)
                                                    Order = 3;
                                                else
                                                    continue;
                                            else
                                                Order = Order == -2 ? 4 : 7;
                                        }
                                        break;

                                    default:
                                        if (Int32.TryParse(s, out v))
                                        {
                                            if (v != Version)
                                                continue;
                                            Order = -2;
                                            f = f[..lastIndex];
                                            goto again2;
                                        }
                                        break;
                                }

                                if (Order > 0)
                                    Entries.Add((Order, Path.GetFullPath(file, Root)));
                            }
                        }
                    }

                    if (Entries.Count > 0)
                    {
                        Int32 Order;
                        String BestPath;

                        (Order, BestPath) = Entries[0];
                        foreach ((Int32, String) Entry in Entries)
                            if (Entry.Item1 < Order)
                                (Order, BestPath) = Entry;

                        UnmanagedLibrary Lib = new UnmanagedLibrary(BestPath);
                        _unmanagedlibraries.Add((Name, Lib));
                        return Lib;
                    }

                }
            throw new FileNotFoundException($"Unable to load Native Dll '{Name}'");
        }

        internal static Assembly GetFixedAssembly(AssemblyName Name)
        {
            switch (Name.Name)
            {
                case "EPS.SDK":
                    return typeof(PluginManager).Assembly;
                case "ExtensiblePortfolioSite":
                    return AssemblyLoadContext.Default.LoadFromAssemblyName(Name);
            }

            Byte[]? KeyToken = Name.GetPublicKeyToken();
            if (KeyToken != null)
            {
                for (int x = 0; x < KnownPublicKeyTokens.Length; x++)
                {
                    Boolean Allow = true;
                    Byte[] PubKeyToken = KnownPublicKeyTokens[x];
                    for (int y = 0; y < PubKeyToken.Length; y++)
                        if (PubKeyToken[y] != KeyToken[y])
                        {
                            Allow = false;
                            break;
                        }
                    if (Allow)
                    {
                        return AssemblyLoadContext.Default.LoadFromAssemblyName(Name);
                    }
                }
            }

            // we want to throw when its trying to load something we don't explicitly allow!
            throw new FileNotFoundException($"Unable to Load Assembly '{Name}'");
        }

        internal static Plugin? ResolveTypeToPlugin(Type type)
        {
            String fname = type.Assembly.GetName().FullName;
            lock (_plugins)
                foreach (Plugin plugin in _plugins)
                    if (plugin.Assembly.GetName().FullName == fname)
                        return plugin;
            return null;
        }
    }
}