using Ryujinx.Common.Logging.Targets;
using Ryujinx.Common.SystemInterop;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Ryujinx.Common.Logging
{
    public static class Logger
    {
        private static readonly Stopwatch m_Time;

        private static readonly bool[] m_EnabledClasses;

        private static readonly List<ILogTarget> m_LogTargets;

        private static readonly StdErrAdapter _stdErrAdapter;

        public static event EventHandler<LogEventArgs> Updated;

        public readonly struct Log
        {
            internal readonly LogLevel Level;

            internal Log(LogLevel level)
            {
                Level = level;
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void PrintMsg(LogClass logClass, string message)
            {
                if (m_EnabledClasses[(int)logClass])
                {
                    Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, "", message)));
                }
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void Print(LogClass logClass, string message, [CallerMemberName] string caller = "")
            {
                if (m_EnabledClasses[(int)logClass])
                {
                    Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message)));
                }
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void Print(LogClass logClass, string message, object data, [CallerMemberName] string caller = "")
            {
                if (m_EnabledClasses[(int)logClass])
                {
                    Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message), data));
                }
            }

            [StackTraceHidden]
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void PrintStack(LogClass logClass, string message, [CallerMemberName] string caller = "")
            {
                if (m_EnabledClasses[(int)logClass])
                {
                    Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, message), new StackTrace(true)));
                }
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void PrintStub(LogClass logClass, string message = "", [CallerMemberName] string caller = "")
            {
                if (m_EnabledClasses[(int)logClass])
                {
                    Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed. " + message)));
                }
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void PrintStub(LogClass logClass, object data, [CallerMemberName] string caller = "")
            {
                if (m_EnabledClasses[(int)logClass])
                {
                    Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed."), data));
                }
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void PrintStub(LogClass logClass, string message, object data, [CallerMemberName] string caller = "")
            {
                if (m_EnabledClasses[(int)logClass])
                {
                    Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, FormatMessage(logClass, caller, "Stubbed. " + message), data));
                }
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public void PrintRawMsg(string message)
            {
                Updated?.Invoke(null, new LogEventArgs(Level, m_Time.Elapsed, Thread.CurrentThread.Name, message));
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            private static string FormatMessage(LogClass Class, string Caller, string Message) => $"{Class} {Caller}: {Message}";
        }

        public static Log? Debug     { get; private set; }
        public static Log? Info      { get; private set; }
        public static Log? Warning   { get; private set; }
        public static Log? Error     { get; private set; }
        public static Log? Guest     { get; private set; }
        public static Log? AccessLog { get; private set; }
        public static Log? Stub      { get; private set; }
        public static Log? Trace     { get; private set; }
        public static Log  Notice    { get; } // Always enabled

        static Logger()
        {
            m_EnabledClasses = new bool[Enum.GetNames<LogClass>().Length];

            for (int index = 0; index < m_EnabledClasses.Length; index++)
            {
                m_EnabledClasses[index] = true;
            }

            m_LogTargets = new List<ILogTarget>();

            m_Time = Stopwatch.StartNew();

            // Logger should log to console by default
            AddTarget(new AsyncLogTargetWrapper(
                new ConsoleLogTarget("console"),
                1000,
                AsyncLogTargetOverflowAction.Discard));

            Notice = new Log(LogLevel.Notice);

            // Enable important log levels before configuration is loaded
            Error = new Log(LogLevel.Error);
            Warning = new Log(LogLevel.Warning);
            Info = new Log(LogLevel.Info);
            Trace = new Log(LogLevel.Trace);

            _stdErrAdapter = new StdErrAdapter();
        }

        public static void RestartTime()
        {
            m_Time.Restart();
        }

        private static ILogTarget GetTarget(string targetName)
        {
            foreach (var target in m_LogTargets)
            {
                if (target.Name.Equals(targetName))
                {
                    return target;
                }
            }

            return null;
        }

        public static void AddTarget(ILogTarget target)
        {
            m_LogTargets.Add(target);

            Updated += target.Log;
        }

        public static void RemoveTarget(string target)
        {
            ILogTarget logTarget = GetTarget(target);

            if (logTarget != null)
            {
                Updated -= logTarget.Log;

                m_LogTargets.Remove(logTarget);

                logTarget.Dispose();
            }
        }

        public static void Shutdown()
        {
            Updated = null;

            _stdErrAdapter.Dispose();

            foreach (var target in m_LogTargets)
            {
                target.Dispose();
            }

            m_LogTargets.Clear();
        }

        public static IReadOnlyCollection<LogLevel> GetEnabledLevels()
        {
            var logs = new Log?[] { Debug, Info, Warning, Error, Guest, AccessLog, Stub, Trace };
            List<LogLevel> levels = new List<LogLevel>(logs.Length);
            foreach (var log in logs)
            {
                if (log.HasValue)
                {
                    levels.Add(log.Value.Level);
                }
            }

            return levels;
        }

        public static void SetEnable(LogLevel logLevel, bool enabled)
        {
            switch (logLevel)
            {
                case LogLevel.Debug     : Debug     = enabled ? new Log(LogLevel.Debug)    : new Log?(); break;
                case LogLevel.Info      : Info      = enabled ? new Log(LogLevel.Info)     : new Log?(); break;
                case LogLevel.Warning   : Warning   = enabled ? new Log(LogLevel.Warning)  : new Log?(); break;
                case LogLevel.Error     : Error     = enabled ? new Log(LogLevel.Error)    : new Log?(); break;
                case LogLevel.Guest     : Guest     = enabled ? new Log(LogLevel.Guest)    : new Log?(); break;
                case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog): new Log?(); break;
                case LogLevel.Stub      : Stub      = enabled ? new Log(LogLevel.Stub)     : new Log?(); break;
                case LogLevel.Trace     : Trace     = enabled ? new Log(LogLevel.Trace)    : new Log?(); break;
                default: throw new ArgumentException("Unknown Log Level");
            }
        }

        public static void SetEnable(LogClass logClass, bool enabled)
        {
            m_EnabledClasses[(int)logClass] = enabled;
        }
    }
}