#include "dxvsyncsource.h"

// Useful references:
// https://bugs.chromium.org/p/chromium/issues/detail?id=467617
// https://chromium.googlesource.com/chromium/src.git/+/c564f2fe339b2b2abb0c8773c90c83215670ea71/gpu/ipc/service/gpu_vsync_provider_win.cc

DxVsyncSource::DxVsyncSource(Pacer* pacer) :
    m_Pacer(pacer),
    m_Thread(nullptr),
    m_Gdi32Handle(nullptr)
{
    SDL_AtomicSet(&m_Stopping, 0);
}

DxVsyncSource::~DxVsyncSource()
{
    if (m_Thread != nullptr) {
        SDL_AtomicSet(&m_Stopping, 1);
        SDL_WaitThread(m_Thread, nullptr);
    }

    if (m_Gdi32Handle != nullptr) {
        FreeLibrary(m_Gdi32Handle);
    }
}

bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
{
    m_DisplayFps = displayFps;

    m_Gdi32Handle = LoadLibraryA("gdi32.dll");
    if (m_Gdi32Handle == nullptr) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "Failed to load gdi32.dll: %d",
                     GetLastError());
        return false;
    }

    m_D3DKMTOpenAdapterFromHdc = (PFND3DKMTOPENADAPTERFROMHDC)GetProcAddress(m_Gdi32Handle, "D3DKMTOpenAdapterFromHdc");
    m_D3DKMTCloseAdapter = (PFND3DKMTCLOSEADAPTER)GetProcAddress(m_Gdi32Handle, "D3DKMTCloseAdapter");
    m_D3DKMTWaitForVerticalBlankEvent = (PFND3DKMTWAITFORVERTICALBLANKEVENT)GetProcAddress(m_Gdi32Handle, "D3DKMTWaitForVerticalBlankEvent");

    if (m_D3DKMTOpenAdapterFromHdc == nullptr ||
            m_D3DKMTCloseAdapter == nullptr ||
            m_D3DKMTWaitForVerticalBlankEvent == nullptr) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "Missing required function in gdi32.dll");
        return false;
    }

    SDL_SysWMinfo info;

    SDL_VERSION(&info.version);

    if (!SDL_GetWindowWMInfo(window, &info)) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "SDL_GetWindowWMInfo() failed: %s",
                     SDL_GetError());
        return false;
    }

    m_Window = info.info.win.window;

    m_Thread = SDL_CreateThread(vsyncThread, "DXVsync", this);
    if (m_Thread == nullptr) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                     "Unable to create DX V-sync thread: %s",
                     SDL_GetError());
        return false;
    }

    return true;
}

int DxVsyncSource::vsyncThread(void* context)
{
    DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);

#if SDL_VERSION_ATLEAST(2, 0, 9)
    SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
#else
    SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
#endif

    D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
    HMONITOR lastMonitor = nullptr;
    DEVMODEA monitorMode;
    monitorMode.dmSize = sizeof(monitorMode);

    while (SDL_AtomicGet(&me->m_Stopping) == 0) {
        D3DKMT_WAITFORVERTICALBLANKEVENT waitForVblankEventParams;
        NTSTATUS status;

        // If the monitor has changed from last time, open the new adapter
        HMONITOR currentMonitor = MonitorFromWindow(me->m_Window, MONITOR_DEFAULTTONEAREST);
        if (currentMonitor != lastMonitor) {
            MONITORINFOEXA monitorInfo = {};
            monitorInfo.cbSize = sizeof(monitorInfo);
            if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                             "GetMonitorInfo() failed: %d",
                             GetLastError());
                SDL_Delay(10);
                continue;
            }

            if (!EnumDisplaySettingsA(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, &monitorMode)) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                             "EnumDisplaySettings() failed: %d",
                             GetLastError());
                SDL_Delay(10);
                continue;
            }

            SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
                        "Monitor changed: %s %d Hz",
                        monitorInfo.szDevice,
                        monitorMode.dmDisplayFrequency);

            if (openAdapterParams.hAdapter != 0) {
                D3DKMT_CLOSEADAPTER closeAdapterParams = {};
                closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
                me->m_D3DKMTCloseAdapter(&closeAdapterParams);
            }

            openAdapterParams.hDc = CreateDCA(nullptr, monitorInfo.szDevice, nullptr, nullptr);
            if (!openAdapterParams.hDc) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                             "CreateDC() failed: %d",
                             GetLastError());
                SDL_Delay(10);
                continue;
            }

            status = me->m_D3DKMTOpenAdapterFromHdc(&openAdapterParams);
            DeleteDC(openAdapterParams.hDc);

            if (status != STATUS_SUCCESS) {
                SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                             "D3DKMTOpenAdapterFromHdc() failed: %x",
                             status);
                SDL_Delay(10);
                continue;
            }

            lastMonitor = currentMonitor;
        }

        waitForVblankEventParams.hAdapter = openAdapterParams.hAdapter;
        waitForVblankEventParams.hDevice = 0;
        waitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId;

        status = me->m_D3DKMTWaitForVerticalBlankEvent(&waitForVblankEventParams);
        if (status != STATUS_SUCCESS) {
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                         "D3DKMTWaitForVerticalBlankEvent() failed: %x",
                         status);
            SDL_Delay(10);
            continue;
        }

        me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
    }

    if (openAdapterParams.hAdapter != 0) {
        D3DKMT_CLOSEADAPTER closeAdapterParams = {};
        closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
        me->m_D3DKMTCloseAdapter(&closeAdapterParams);
    }

    return 0;
}
