using Nerfed.Runtime.Audio;
using Nerfed.Runtime.Graphics;
using SDL2;
using System.Diagnostics;

namespace Nerfed.Runtime;

public static class Engine
{
    public static event Action OnInitialize;
    public static event Action OnUpdate;
    public static event Action OnRender;
    public static event Action OnQuit;

    public static TimeSpan MaxDeltaTime { get; set; } = TimeSpan.FromMilliseconds(100);
    public static bool VSync { get; set; }

    public static GraphicsDevice GraphicsDevice { get; private set; }
    public static AudioDevice AudioDevice { get; private set; }
    public static Window MainWindow { get; private set; }
    public static TimeSpan Timestep { get; private set; }

    private static bool quit;
    private static Stopwatch gameTimer;
    private static long previousTicks = 0;
    private static TimeSpan accumulatedUpdateTime = TimeSpan.Zero;

    private static TimeSpan accumulatedDrawTime = TimeSpan.Zero;

    // must be a power of 2 so we can do a bitmask optimization when checking worst case
    private const int previousSleepTimeCount = 128;
    private const int sleepTimeMask = previousSleepTimeCount - 1;
    private static readonly TimeSpan[] previousSleepTimes = new TimeSpan[previousSleepTimeCount];
    private static int sleepTimeIndex;
    private static TimeSpan worstCaseSleepPrecision = TimeSpan.FromMilliseconds(1);
    private static bool framerateCapped;
    private static TimeSpan framerateCapTimeSpan = TimeSpan.Zero;

    //TODO: These are temp
    private const int MaxFps = 300;
    private const int TargetTimestep = 60;
    private const int WindowWidth = 1280;
    private const int WindowHeight = 720;
    private const string WindowTitle = "Nerfed";
    //..

    public static void Run(string[] args)
    {
        Timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / TargetTimestep);
        gameTimer = Stopwatch.StartNew();
        SetFrameLimiter(new FrameLimiterSettings(FrameLimiterMode.Capped, MaxFps));

        for (int i = 0; i < previousSleepTimes.Length; i += 1)
        {
            previousSleepTimes[i] = TimeSpan.FromMilliseconds(1);
        }

        if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0)
        {
            throw new Exception("Failed to init SDL");
        }
        
        GraphicsDevice = new GraphicsDevice(BackendFlags.All);
        GraphicsDevice.LoadDefaultPipelines();

        MainWindow = new Window(GraphicsDevice, new WindowCreateInfo(WindowTitle, WindowWidth, WindowHeight, ScreenMode.Windowed));
        if (!GraphicsDevice.ClaimWindow(MainWindow, SwapchainComposition.SDR, VSync ? PresentMode.VSync : PresentMode.Mailbox))
        {
            throw new Exception("Failed to claim window");
        }

        AudioDevice = new AudioDevice();

        OnInitialize?.Invoke();

        while (!quit)
        {
            Tick();
        }

        OnQuit?.Invoke();

        GraphicsDevice.UnclaimWindow(MainWindow);
        MainWindow.Dispose();
        GraphicsDevice.Dispose();
        AudioDevice.Dispose();
        SDL.SDL_Quit();
    }

    /// <summary>
    /// Updates the frame limiter settings.
    /// </summary>
    public static void SetFrameLimiter(FrameLimiterSettings settings)
    {
        framerateCapped = settings.Mode == FrameLimiterMode.Capped;

        if (framerateCapped)
        {
            framerateCapTimeSpan = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / settings.Cap);
        }
        else
        {
            framerateCapTimeSpan = TimeSpan.Zero;
        }
    }

    public static void Quit()
    {
        quit = true;
    }

    private static void Tick()
    {
        AdvanceElapsedTime();

        if (framerateCapped)
        {
            /* We want to wait until the framerate cap,
             * but we don't want to oversleep. Requesting repeated 1ms sleeps and
             * seeing how long we actually slept for lets us estimate the worst case
             * sleep precision so we don't oversleep the next frame.
             */
            while (accumulatedDrawTime + worstCaseSleepPrecision < framerateCapTimeSpan)
            {
                Thread.Sleep(1);
                TimeSpan timeAdvancedSinceSleeping = AdvanceElapsedTime();
                UpdateEstimatedSleepPrecision(timeAdvancedSinceSleeping);
            }

            /* Now that we have slept into the sleep precision threshold, we need to wait
             * for just a little bit longer until the target elapsed time has been reached.
             * SpinWait(1) works by pausing the thread for very short intervals, so it is
             * an efficient and time-accurate way to wait out the rest of the time.
             */
            while (accumulatedDrawTime < framerateCapTimeSpan)
            {
                Thread.SpinWait(1);
                AdvanceElapsedTime();
            }
        }

        // Do not let any step take longer than our maximum.
        if (accumulatedUpdateTime > MaxDeltaTime)
        {
            accumulatedUpdateTime = MaxDeltaTime;
        }

        if (!quit)
        {
            while (accumulatedUpdateTime >= Timestep)
            {
                Keyboard.Update();
                Mouse.Update();
                GamePad.Update();

                ProcessSDLEvents();

                // Tick game here...
                OnUpdate?.Invoke();

                AudioDevice.WakeThread();
                accumulatedUpdateTime -= Timestep;
            }

            double alpha = accumulatedUpdateTime / Timestep;

            // Render here..
            OnRender?.Invoke();

            accumulatedDrawTime -= framerateCapTimeSpan;
        }
    }

    private static TimeSpan AdvanceElapsedTime()
    {
        long currentTicks = gameTimer.Elapsed.Ticks;
        TimeSpan timeAdvanced = TimeSpan.FromTicks(currentTicks - previousTicks);
        accumulatedUpdateTime += timeAdvanced;
        accumulatedDrawTime += timeAdvanced;
        previousTicks = currentTicks;
        return timeAdvanced;
    }

    private static void ProcessSDLEvents()
    {
        while (SDL.SDL_PollEvent(out SDL.SDL_Event ev) == 1)
        {
            switch (ev.type)
            {
                case SDL.SDL_EventType.SDL_QUIT:
                    Quit();
                    break;
                case SDL.SDL_EventType.SDL_TEXTINPUT:
                case SDL.SDL_EventType.SDL_KEYDOWN:
                case SDL.SDL_EventType.SDL_KEYUP:
                    Keyboard.ProcessEvent(ref ev);
                    break;
                case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN:
                case SDL.SDL_EventType.SDL_MOUSEBUTTONUP:
                case SDL.SDL_EventType.SDL_MOUSEWHEEL:
                case SDL.SDL_EventType.SDL_MOUSEMOTION:
                    Mouse.ProcessEvent(ref ev);
                    break;
                case SDL.SDL_EventType.SDL_CONTROLLERDEVICEADDED:
                case SDL.SDL_EventType.SDL_CONTROLLERDEVICEREMOVED:
                case SDL.SDL_EventType.SDL_CONTROLLERBUTTONDOWN:
                case SDL.SDL_EventType.SDL_CONTROLLERBUTTONUP:
                case SDL.SDL_EventType.SDL_CONTROLLERAXISMOTION:
                case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADDOWN:
                case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADUP:
                case SDL.SDL_EventType.SDL_CONTROLLERTOUCHPADMOTION:
                case SDL.SDL_EventType.SDL_CONTROLLERSENSORUPDATE:
                    GamePad.ProcessEvent(ref ev);
                    break;
                case SDL.SDL_EventType.SDL_WINDOWEVENT:
                    Window.ProcessEvent(ref ev);
                    break;
            }
        }
    }

    /* To calculate the sleep precision of the OS, we take the worst case
     * time spent sleeping over the results of previous requests to sleep 1ms.
     */
    private static void UpdateEstimatedSleepPrecision(TimeSpan timeSpentSleeping)
    {
        /* It is unlikely that the scheduler will actually be more imprecise than
         * 4ms and we don't want to get wrecked by a single long sleep so we cap this
         * value at 4ms for sanity.
         */
        TimeSpan upperTimeBound = TimeSpan.FromMilliseconds(4);

        if (timeSpentSleeping > upperTimeBound)
        {
            timeSpentSleeping = upperTimeBound;
        }

        /* We know the previous worst case - it's saved in worstCaseSleepPrecision.
         * We also know the current index. So the only way the worst case changes
         * is if we either 1) just got a new worst case, or 2) the worst case was
         * the oldest entry on the list.
         */
        if (timeSpentSleeping >= worstCaseSleepPrecision)
        {
            worstCaseSleepPrecision = timeSpentSleeping;
        }
        else if (previousSleepTimes[sleepTimeIndex] == worstCaseSleepPrecision)
        {
            TimeSpan maxSleepTime = TimeSpan.MinValue;
            for (int i = 0; i < previousSleepTimes.Length; i++)
            {
                if (previousSleepTimes[i] > maxSleepTime)
                {
                    maxSleepTime = previousSleepTimes[i];
                }
            }

            worstCaseSleepPrecision = maxSleepTime;
        }

        previousSleepTimes[sleepTimeIndex] = timeSpentSleeping;
        sleepTimeIndex = (sleepTimeIndex + 1) & sleepTimeMask;
    }
}
