using ExtensiblePortfolioSite.SDK.Plugins;

using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;

namespace ExtensiblePortfolioSite
{
    public sealed class LoggerProvider : ILoggerProvider
    {
        private sealed class Logger : ILogger, IDisposable
        {
            // can't see what the scopes are event used for.
            //  0 restraints on the genericType so what am i supposed to do with them.
            //  log their type? serialize the scope object with every message?
            //  it seems a bit rediculus.

            private sealed class Scope : IDisposable
            {
                private readonly Logger Parent;
                public readonly String Identifier;

                public Scope(Logger Parent, String Identifier)
                {
                    this.Parent = Parent;
                    this.Identifier = Identifier;
                    lock (this.Parent.ScopeStack)
                        this.Parent.ScopeStack.AddLast(this);
                }

                public void Dispose()
                {
                    lock (this.Parent.ScopeStack)
                        this.Parent.ScopeStack.Remove(this);
                }
            }

            private readonly LinkedList<Scope> ScopeStack = new();
            private readonly LoggerProvider Provider;
            public readonly String Category;

            public Logger(LoggerProvider Provider, String Category)
            {
                this.Provider = Provider;
                this.Category = Category;
            }


            public IDisposable BeginScope<TState>(TState state) => new Scope(this, ScopeToString(state));
            public bool IsEnabled(LogLevel logLevel) => true;
            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) =>
                Provider.WriteLog(this, logLevel, eventId, formatter(state, exception));

            public void WriteScope(StringBuilder MessageBuilder)
            {
                LinkedListNode<Scope>? Curr = ScopeStack.First;

                while (Curr != null)
                {
                    MessageBuilder.Append('>');
                    MessageBuilder.Append(Curr.Value.Identifier);
                    Curr = Curr.Next;
                }
            }

            public void Dispose()
            {
                lock (ScopeStack)
                {
                    LinkedListNode<Scope>? Curr = ScopeStack.First;
                    while (Curr != null)
                    {
                        LinkedListNode<Scope>? next = Curr.Next;
                        Curr.Value.Dispose();
                        Curr = next;
                    }
                }
            }
        }

        #region Static
        private static readonly Encoding ENCODING = Encoding.Unicode;
        private static readonly Queue<StringBuilder> SBCache = new();
        private static StringBuilder GetStringBuilder()
        {
            lock (SBCache)
                if (SBCache.Count > 1)
                    return SBCache.Dequeue();

            return new StringBuilder(1024);
        }
        private static void ReclaimStringBuilder(StringBuilder sb)
        {
            if (sb.Capacity > 1024)
                return;
            sb.Clear();
            lock (SBCache)
                SBCache.Enqueue(sb);
        }

        private static String ScopeToString<TState>(TState Obj)
        {
#pragma warning disable IDE0046 // Convert to conditional expression

            if (Obj == null)
                return "null";

            if (Obj is Thread thread)
                return $"{thread.ManagedThreadId}:{thread.Name}";

            if (Obj is Socket socket)
                return socket.RemoteEndPoint?.ToString() ?? "null";

            return Obj.ToString() ?? "null";
#pragma warning restore IDE0046 // Convert to conditional expression
        }
        #endregion Static


        private readonly Lazy<Logger> PluginLogger;

        private Boolean Running;
        private readonly Thread LogWriter;
        private readonly TextWriter? COut;
        private readonly StreamWriter? FOut;
        public LoggerProvider(Boolean ConsoleLogging, String FileOutput)
        {
            this.COut = ConsoleLogging ? Console.Out : null;

            String Dir = Path.GetDirectoryName(FileOutput)!;
            if (!Directory.Exists(Dir)) Directory.CreateDirectory(Dir);
            this.FOut = new StreamWriter(new FileStream(FileOutput, FileMode.Append, FileAccess.Write, FileShare.Read, 1024, false), ENCODING, 1024, false);

            this.LogWriter = new Thread(LogWriter_thread)
            {
                Name = "Log Writer",
                Priority = ThreadPriority.BelowNormal,
                IsBackground = true,
            };

            this.PluginLogger = new Lazy<Logger>(() => (Logger)CreateLogger("PluginManager"), true);

            this.Running = true;
            this.LogWriter.Start();
        }
        public LoggerProvider(Boolean ConsoleLogging)
        {
            this.FOut = null;
            this.COut = ConsoleLogging ? Console.Out : null;

            this.LogWriter = new Thread(LogWriter_thread)
            {
                Name = "Log Writer",
                Priority = ThreadPriority.BelowNormal,
                IsBackground = true,
            };

            this.PluginLogger = new Lazy<Logger>(() => (Logger)CreateLogger("PluginManager"), true);

            this.Running = true;
            this.LogWriter.Start();
        }


        internal void LogPluginManagerError(PluginErrorSeverity Severity, Plugin Plugin, String Message, Type? Target, Exception? ex)
        {
            StringBuilder MessageBuilder = GetStringBuilder();
            {
                WritePrefix(MessageBuilder, this.PluginLogger.Value, Severity switch
                {
                    PluginErrorSeverity.Fatal => LogLevel.Critical,
                    PluginErrorSeverity.Info => LogLevel.Information,
                    PluginErrorSeverity.Warning => LogLevel.Warning,
                    PluginErrorSeverity.Error => LogLevel.Error,
                    _ => throw new ArgumentException("Unknown PluginErrorSeverity Value", nameof(Severity))
                });
                MessageBuilder.Append('{');
                MessageBuilder.Append(Plugin.Info.Name);
                MessageBuilder.Append(':');
                MessageBuilder.Append(Plugin.Info.Version.Major);
                MessageBuilder.Append('.');
                MessageBuilder.Append(Plugin.Info.Version.Minor);
                MessageBuilder.Append('.');
                MessageBuilder.Append(Plugin.Info.Version.Build);
                MessageBuilder.Append('.');
                MessageBuilder.Append(Plugin.Info.Version.Revision);
                MessageBuilder.Append('}');
                if (Target != null)
                {
                    MessageBuilder.Append('@');
                    MessageBuilder.Append(Target.FullName);
                }
                MessageBuilder.Append("::");
                MessageBuilder.Append(Message);
                if (ex != null)
                {
                    MessageBuilder.Append("\n            ");
                    MessageBuilder.Append(ex.Message);
                    if (ex.StackTrace != null)
                    {
                        MessageBuilder.Append("\n            ");
                        MessageBuilder.Append(ex.StackTrace.Replace("\n", "\n            "));
                    }
                }
                lock (LogQueue)
                    LogQueue.Enqueue(MessageBuilder.ToString());
            }
            ReclaimStringBuilder(MessageBuilder);
        }


        private readonly SortedDictionary<String, Logger> LoggerLookup = new();
        public ILogger CreateLogger(string categoryName)
        {
            // optimize string matching by enforcing it into the strings table
            categoryName = string.IsInterned(categoryName) ?? string.Intern(categoryName);

            if (LoggerLookup.TryGetValue(categoryName, out Logger? logger))
                return logger;

            lock (LoggerLookup)
            {
                if (LoggerLookup.TryGetValue(categoryName, out logger))
                    return logger;

                logger = new Logger(this, categoryName);
                LoggerLookup.Add(categoryName, logger);
                return logger;
            }
        }
        public void Dispose()
        {
            this.Running = false;
            lock (LoggerLookup)
                foreach (Logger logger in LoggerLookup.Values)
                    logger.Dispose();

            this.LogWriter.Join();
        }

        private void WriteLog(Logger logger, LogLevel logLevel, EventId eventId, String Message)
        {
            StringBuilder MessageBuilder = GetStringBuilder();
            {
                WritePrefix(MessageBuilder, logger, logLevel);
                MessageBuilder.Append('{');
                MessageBuilder.Append(eventId.Id);
                MessageBuilder.Append(':');
                MessageBuilder.Append(eventId.Name);
                logger.WriteScope(MessageBuilder);
                MessageBuilder.Append("}::");
                MessageBuilder.Append(Message);

                lock (LogQueue)
                    LogQueue.Enqueue(MessageBuilder.ToString());
            }
            ReclaimStringBuilder(MessageBuilder);
        }

        private static void WritePrefix(StringBuilder MessageBuilder, Logger logger, LogLevel logLevel)
        {
            MessageBuilder.Append('[');
            MessageBuilder.Append(DateTime.Now.ToShortTimeString());
            MessageBuilder.Append('\\');
            MessageBuilder.Append(logLevel.ToString());
            MessageBuilder.Append('\\');
            MessageBuilder.Append(logger.Category);
            MessageBuilder.Append(']');
        }


        private readonly ConcurrentQueue<String> LogQueue = new();
        private void LogWriter_thread()
        {
            [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
            void WriteLog(String Log)
            {
                Task[] tasks = new Task[2];
                int x = 0;

                if (COut != null)
                    tasks[x++] = COut.WriteLineAsync(Log);

                if (FOut != null)
                    tasks[x++] = FOut.WriteLineAsync(Log);

                Task.WaitAll(tasks[0..x]);
            }
            while (this.Running)
            {
                if (LogQueue.Count > 0)
                    lock (LogQueue)
                        while (LogQueue.TryDequeue(out String? Log))
                            WriteLog(Log);

                Thread.Sleep(1000);
            }
            lock (LogQueue)
                while (LogQueue.TryDequeue(out String? Log))
                    WriteLog(Log);
        }
    }
}
