/*
 * Copyright (C) Nemirtingas
 * This file is part of the ingame overlay project
 *
 * The ingame overlay project is free software; you can redistribute it
 * and/or modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * The ingame overlay project is distributed in the hope that it will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with the ingame overlay project; if not, see
 * <http://www.gnu.org/licenses/>.
 */

#include <cassert>

#include "Renderer_Detector.h"

#include "System/Encoding.hpp"
#include "System/String.hpp"
#include "System/System.h"
#include "System/Library.h"

#if defined(WIN64) || defined(_WIN64) || defined(__MINGW64__) \
 || defined(WIN32) || defined(_WIN32) || defined(__MINGW32__)
    #define RENDERERDETECTOR_OS_WINDOWS
#elif defined(__linux__) || defined(linux)
    #define RENDERERDETECTOR_OS_LINUX
#elif defined(__APPLE__)
    #define RENDERERDETECTOR_OS_APPLE
#endif

#ifdef RENDERERDETECTOR_OS_WINDOWS
#define GLAD_GL_IMPLEMENTATION
#include <glad/gl.h>

#include "windows/DX12_Hook.h"
#include "windows/DX11_Hook.h"
#include "windows/DX10_Hook.h"
#include "windows/DX9_Hook.h"
#include "windows/OpenGL_Hook.h"
#include "windows/Vulkan_Hook.h"

#include "windows/DirectX_VTables.h"

#include <random>

#ifdef GetModuleHandle
#undef GetModuleHandle
#endif

class Renderer_Detector
{
    static Renderer_Detector* instance;
public:
    static Renderer_Detector* Inst()
    {
        if (instance == nullptr)
        {
            instance = new Renderer_Detector;
        }
        return instance;
    }

    ~Renderer_Detector()
    {
        delete dx9_hook;
        delete dx10_hook;
        delete dx11_hook;
        delete dx12_hook;
        delete opengl_hook;
        delete vulkan_hook;
    }

private:
    Renderer_Detector():
        dxgi_hooked(false),
        dxgi1_2_hooked(false),
        dx12_hooked(false),
        dx11_hooked(false),
        dx10_hooked(false),
        dx9_hooked(false),
        opengl_hooked(false),
        vulkan_hooked(false),
        renderer_hook(nullptr),
        dx9_hook(nullptr),
        dx10_hook(nullptr),
        dx11_hook(nullptr),
        dx12_hook(nullptr),
        opengl_hook(nullptr),
        vulkan_hook(nullptr),
        detection_done(false)
    {
        std::wstring tmp(4096, L'\0');
        tmp.resize(GetSystemDirectoryW(&tmp[0], tmp.size()));
        _SystemDir = System::Encoding::WCharToUtf8(tmp);

        System::String::ToLower(_SystemDir);

        wchar_t random_str[] = L"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        std::random_device rd;
        std::mt19937_64 gen(rd());

        std::uniform_int_distribution<uint64_t> dis(0, 61);
        _WindowClassName.resize(64);
        for (int i = 0; i < 64; ++i)
            _WindowClassName[i] = random_str[dis(gen)];
    }

    std::timed_mutex detector_mutex;
    std::mutex renderer_mutex;

    decltype(&IDXGISwapChain::Present)       IDXGISwapChainPresent;
    decltype(&IDXGISwapChain1::Present1)     IDXGISwapChainPresent1;
    decltype(&IDirect3DDevice9::Present)     IDirect3DDevice9Present;
    decltype(&IDirect3DDevice9Ex::PresentEx) IDirect3DDevice9ExPresentEx;
    decltype(&IDirect3DSwapChain9::Present)  IDirect3DSwapChain9Present;
    decltype(::SwapBuffers)*                 wglSwapBuffers;
    decltype(::vkQueuePresentKHR)*           vkQueuePresentKHR;

    bool dxgi_hooked;
    bool dxgi1_2_hooked;
    bool dx12_hooked;
    bool dx11_hooked;
    bool dx10_hooked;
    bool dx9_hooked;
    bool opengl_hooked;
    bool vulkan_hooked;

    Base_Hook detection_hooks;
    Renderer_Hook* renderer_hook;
    DX12_Hook*   dx12_hook;
    DX11_Hook*   dx11_hook;
    DX10_Hook*   dx10_hook;
    DX9_Hook*    dx9_hook;
    OpenGL_Hook* opengl_hook;
    Vulkan_Hook* vulkan_hook;
    
    bool detection_done;

    HWND dummyWindow = nullptr;
    std::wstring _WindowClassName;
    std::string _SystemDir;
    ATOM atom = 0;

    std::string FindPreferedModulePath(std::string const& name)
    {
        std::string res;
        std::string tmp;
        auto modules = System::GetModules();
        for (auto& item : modules)
        {
            tmp = System::String::CopyLower(item);
            if (tmp.length() >= name.length() && strcmp(tmp.c_str() + tmp.length() - name.length(), name.c_str()) == 0)
            {
                if (strncmp(tmp.c_str(), _SystemDir.c_str(), _SystemDir.length()) == 0)
                    return item;

                // I don't care which one is picked if we can't find a library in the system32 folder...
                res = std::move(item);
            }
        }

        return res;
    }

    HWND CreateHWND()
    {
        if (dummyWindow == nullptr)
        {
            HINSTANCE hInst = GetModuleHandleW(nullptr);
            if (atom == 0)
            {
                // Register a window class for creating our render window with.
                WNDCLASSEXW windowClass = {};

                windowClass.cbSize = sizeof(WNDCLASSEX);
                windowClass.style = CS_HREDRAW | CS_VREDRAW;
                windowClass.lpfnWndProc = DefWindowProc;
                windowClass.cbClsExtra = 0;
                windowClass.cbWndExtra = 0;
                windowClass.hInstance = hInst;
                windowClass.hIcon = NULL;
                windowClass.hCursor = ::LoadCursor(NULL, IDC_ARROW);
                windowClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
                windowClass.lpszMenuName = NULL;
                windowClass.lpszClassName = _WindowClassName.c_str();
                windowClass.hIconSm = NULL;

                atom = ::RegisterClassExW(&windowClass);
            }

            if (atom > 0)
            {
                dummyWindow = ::CreateWindowExW(
                    NULL,
                    _WindowClassName.c_str(),
                    L"",
                    WS_OVERLAPPEDWINDOW,
                    0,
                    0,
                    1,
                    1,
                    NULL,
                    NULL,
                    hInst,
                    nullptr
                );

                assert(dummyWindow && "Failed to create window");
            }
        }

        return dummyWindow;
    }

    void DestroyHWND()
    {
        if (dummyWindow != nullptr)
        {
            DestroyWindow(dummyWindow);
            UnregisterClassW(_WindowClassName.c_str(), GetModuleHandleW(nullptr));

            dummyWindow = nullptr;
            atom = 0;
        }
    }

    template<typename T>
    void HookDetected(T*& detected_renderer)
    {
        detection_hooks.UnhookAll();
        renderer_hook = static_cast<Renderer_Hook*>(detected_renderer);
        detected_renderer = nullptr;
        detection_done = true;
        DestroyHWND();
    }

    void DeduceDXVersionFromSwapChain(IDXGISwapChain* pSwapChain)
    {
        IUnknown* pDevice = nullptr;
        if (Inst()->dx12_hooked)
        {
            pSwapChain->GetDevice(IID_PPV_ARGS(reinterpret_cast<ID3D12Device**>(&pDevice)));
        }
        if (pDevice != nullptr)
        {
            HookDetected(dx12_hook);
        }
        else
        {
            if (dx11_hooked)
            {
                pSwapChain->GetDevice(IID_PPV_ARGS(reinterpret_cast<ID3D11Device**>(&pDevice)));
            }
            if (pDevice != nullptr)
            {
                HookDetected(dx11_hook);
            }
            else
            {
                if (dx10_hooked)
                {
                    pSwapChain->GetDevice(IID_PPV_ARGS(reinterpret_cast<ID3D10Device**>(&pDevice)));
                }
                if (pDevice != nullptr)
                {
                    HookDetected(dx10_hook);
                }
            }
        }
        if (pDevice != nullptr)
        {
            pDevice->Release();
        }
    }

    static HRESULT STDMETHODCALLTYPE MyIDXGISwapChain_Present(IDXGISwapChain* _this, UINT SyncInterval, UINT Flags)
    {
        auto inst = Inst();
        HRESULT res;
        bool locked;
        std::unique_lock<std::mutex> lk(inst->renderer_mutex, std::defer_lock);

        // It appears that (NVidia at least) calls IDXGISwapChain when calling OpenGL or Vulkan SwapBuffers.
        // So only lock when OpenGL or Vulkan hasn't already locked the mutex.
        locked = lk.try_lock();
        res = (_this->*inst->IDXGISwapChainPresent)(SyncInterval, Flags);
        if (!locked || inst->detection_done)
            return res;

        inst->DeduceDXVersionFromSwapChain(_this);

        return res;
    }

    static HRESULT STDMETHODCALLTYPE MyIDXGISwapChain_Present1(IDXGISwapChain1* _this, UINT SyncInterval, UINT Flags, const DXGI_PRESENT_PARAMETERS* pPresentParameters)
    {
        auto inst = Inst();
        HRESULT res;
        bool locked;
        std::unique_lock<std::mutex> lk(inst->renderer_mutex, std::defer_lock);

        // It appears that (NVidia at least) calls IDXGISwapChain when calling OpenGL or Vulkan SwapBuffers.
        // So only lock when OpenGL or Vulkan hasn't already locked the mutex.
        locked = lk.try_lock();
        res = (_this->*inst->IDXGISwapChainPresent1)(SyncInterval, Flags, pPresentParameters);
        if (!locked || inst->detection_done)
            return res;

        inst->DeduceDXVersionFromSwapChain(_this);

        return res;
    }

    static HRESULT STDMETHODCALLTYPE MyDX9Present(IDirect3DDevice9* _this, CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion)
    {
        auto inst = Inst();
        std::lock_guard<std::mutex> lk(inst->renderer_mutex);

        auto res = (_this->*inst->IDirect3DDevice9Present)(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion);
        if (inst->detection_done)
            return res;

        inst->HookDetected(inst->dx9_hook);

        return res;
    }

    static HRESULT STDMETHODCALLTYPE MyDX9PresentEx(IDirect3DDevice9Ex* _this, CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion, DWORD dwFlags)
    {
        auto inst = Inst();
        std::lock_guard<std::mutex> lk(inst->renderer_mutex);

        auto res = (_this->*inst->IDirect3DDevice9ExPresentEx)(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags);
        if (inst->detection_done)
            return res;

        inst->HookDetected(inst->dx9_hook);

        return res;
    }

    static HRESULT STDMETHODCALLTYPE MyDX9SwapChainPresent(IDirect3DSwapChain9* _this, CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion, DWORD dwFlags)
    {
        auto inst = Inst();
        std::lock_guard<std::mutex> lk(inst->renderer_mutex);

        auto res = (_this->*inst->IDirect3DSwapChain9Present)(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags);
        if (inst->detection_done)
            return res;

        inst->HookDetected(inst->dx9_hook);

        return res;
    }

    static BOOL WINAPI MywglSwapBuffers(HDC hDC)
    {
        auto inst = Inst();
        std::lock_guard<std::mutex> lk(inst->renderer_mutex);

        auto res = inst->wglSwapBuffers(hDC);
        if (inst->detection_done)
            return res;

        if (gladLoaderLoadGL() >= GLAD_MAKE_VERSION(3, 1))
        {
            inst->HookDetected(inst->opengl_hook);
        }

        return res;
    }

    static VkResult VKAPI_CALL MyvkQueuePresentKHR(VkQueue Queue, const VkPresentInfoKHR* pPresentInfo)
    {
        auto inst = Inst();
        std::lock_guard<std::mutex> lk(inst->renderer_mutex);

        auto res = inst->vkQueuePresentKHR(Queue, pPresentInfo);
        if (inst->detection_done)
            return res;

        inst->HookDetected(inst->vulkan_hook);
        
        return res;
    }

    void HookDXGIPresent(IDXGISwapChain* pSwapChain, decltype(&IDXGISwapChain::Present)& pfnPresent, decltype(&IDXGISwapChain::ResizeBuffers)& pfnResizeBuffers, decltype(&IDXGISwapChain::ResizeTarget)& pfnResizeTarget)
    {
        void** vTable = *reinterpret_cast<void***>(pSwapChain);
        (void*&)pfnPresent = vTable[(int)IDXGISwapChainVTable::Present];
        (void*&)pfnResizeBuffers = vTable[(int)IDXGISwapChainVTable::ResizeBuffers];
        (void*&)pfnResizeTarget = vTable[(int)IDXGISwapChainVTable::ResizeTarget];

        if (!dxgi_hooked)
        {
            dxgi_hooked = true;

            (void*&)IDXGISwapChainPresent = vTable[(int)IDXGISwapChainVTable::Present];
            detection_hooks.BeginHook();
            detection_hooks.HookFunc(std::pair<void**, void*>{ (void**)&IDXGISwapChainPresent, (void*)&MyIDXGISwapChain_Present});
            detection_hooks.EndHook();
        }
    }

    void HookDXGIPresent1(IDXGISwapChain1* pSwapChain1, decltype(&IDXGISwapChain1::Present1)& pfnPresent1)
    {
        void** vTable = *reinterpret_cast<void***>(pSwapChain1);
        (void*&)pfnPresent1 = vTable[(int)IDXGISwapChainVTable::Present1];

        if (!dxgi1_2_hooked)
        {
            dxgi1_2_hooked = true;

            (void*&)IDXGISwapChainPresent1 = vTable[(int)IDXGISwapChainVTable::Present1];
            detection_hooks.BeginHook();
            detection_hooks.HookFunc(std::pair<void**, void*>{ (void**)&IDXGISwapChainPresent1, (void*)&MyIDXGISwapChain_Present1});
            detection_hooks.EndHook();
        }
    }

    void HookDX9Present(IDirect3DDevice9* pDevice, bool ex, IDirect3DSwapChain9* pSwapChain,
        decltype(&IDirect3DDevice9::Present)& pfnPresent,
        decltype(&IDirect3DDevice9::Reset)& pfnReset,
        decltype(&IDirect3DDevice9Ex::PresentEx)& pfnPresentEx,
        decltype(&IDirect3DSwapChain9::Present)& pfnSwapChainPresent)
    {
        void** vTable = *reinterpret_cast<void***>(pDevice);
        (void*&)pfnPresent   = vTable[(int)IDirect3DDevice9VTable::Present];
        (void*&)pfnReset     = vTable[(int)IDirect3DDevice9VTable::Reset];
        
        (void*&)IDirect3DDevice9Present = vTable[(int)IDirect3DDevice9VTable::Present];
        
        detection_hooks.BeginHook();
        detection_hooks.HookFunc(std::pair<void**, void*>{ (void**)&IDirect3DDevice9Present, (void*)&MyDX9Present });
        detection_hooks.EndHook();
        
        if (ex)
        {
            (void*&)pfnPresentEx = vTable[(int)IDirect3DDevice9VTable::PresentEx];
            (void*&)IDirect3DDevice9ExPresentEx = vTable[(int)IDirect3DDevice9VTable::PresentEx];
        
            detection_hooks.BeginHook();
            detection_hooks.HookFunc(std::pair<void**, void*>{ (void**)&IDirect3DDevice9ExPresentEx, (void*)&MyDX9PresentEx });
            detection_hooks.EndHook();
        }
        else
        {
            pfnPresentEx = nullptr;
            IDirect3DDevice9ExPresentEx = nullptr;
        }
        
        if (pSwapChain != nullptr)
        {
            IDirect3DSwapChain9VTable* vTable = *reinterpret_cast<IDirect3DSwapChain9VTable**>(pSwapChain);
            pfnSwapChainPresent = vTable->pPresent;
            IDirect3DSwapChain9Present = vTable->pPresent;
        
            detection_hooks.BeginHook();
            detection_hooks.HookFunc(std::pair<void**, void*>{ (void**)&IDirect3DSwapChain9Present, (void*)&MyDX9SwapChainPresent });
            detection_hooks.EndHook();
        }
        else
        {
            pfnSwapChainPresent = nullptr;
        }
    }

    void HookwglSwapBuffers(decltype(::SwapBuffers)* _wglSwapBuffers)
    {
        wglSwapBuffers = _wglSwapBuffers;

        detection_hooks.BeginHook();
        detection_hooks.HookFunc(std::pair<void**, void*>{ (void**)&wglSwapBuffers, (void*)&MywglSwapBuffers });
        detection_hooks.EndHook();
    }

    void HookvkQueuePresentKHR(decltype(::vkQueuePresentKHR)* _vkQueuePresentKHR)
    {
        vkQueuePresentKHR = _vkQueuePresentKHR;

        detection_hooks.BeginHook();
        detection_hooks.HookFuncs(
            std::pair<void**, void*>{ (void**)&vkQueuePresentKHR, (void*)&MyvkQueuePresentKHR }
        );
        detection_hooks.EndHook();
    }

    void hook_dx9(std::string const& library_path)
    {
        if (!dx9_hooked)
        {
            System::Library::Library libD3d9;
            if (!libD3d9.OpenLibrary(library_path, false))
            {
                SPDLOG_WARN("Failed to load {} to detect DX9", library_path);
                return;
            }

            IDirect3D9Ex* pD3D = nullptr;
            IDirect3DDevice9* pDevice = nullptr;
            IDirect3DSwapChain9* pSwapChain = nullptr;

            auto Direct3DCreate9Ex = libD3d9.GetSymbol<decltype(::Direct3DCreate9Ex)>("Direct3DCreate9Ex");

            D3DPRESENT_PARAMETERS params = {};
            params.BackBufferWidth = 1;
            params.BackBufferHeight = 1;
            params.hDeviceWindow = dummyWindow;
            params.BackBufferCount = 1;
            params.Windowed = TRUE;
            params.SwapEffect = D3DSWAPEFFECT_DISCARD;

            if (Direct3DCreate9Ex != nullptr)
            {
                Direct3DCreate9Ex(D3D_SDK_VERSION, &pD3D);
                pD3D->CreateDeviceEx(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, NULL, D3DCREATE_HARDWARE_VERTEXPROCESSING, &params, NULL, reinterpret_cast<IDirect3DDevice9Ex**>(&pDevice));
            }
            
            if (pDevice == nullptr)
            {
                Direct3DCreate9Ex = nullptr;
                auto Direct3DCreate9 = libD3d9.GetSymbol<decltype(::Direct3DCreate9)>("Direct3DCreate9");
                if (Direct3DCreate9 != nullptr)
                {
                    // D3DDEVTYPE_HAL
                    pD3D = reinterpret_cast<IDirect3D9Ex*>(Direct3DCreate9(D3D_SDK_VERSION));
                    pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, NULL, D3DCREATE_HARDWARE_VERTEXPROCESSING, &params, &pDevice);
                }
            }

            if (pDevice != nullptr)
            {
                SPDLOG_INFO("Hooked D3D9::Present to detect DX Version");
                
                dx9_hooked = true;

                pDevice->GetSwapChain(0, &pSwapChain);

                decltype(&IDirect3DDevice9::Present) pfnPresent;
                decltype(&IDirect3DDevice9::Reset) pfnReset;
                decltype(&IDirect3DDevice9Ex::PresentEx) pfnPresentEx;
                decltype(&IDirect3DSwapChain9::Present) pfnSwapChainPresent;

                HookDX9Present(pDevice, Direct3DCreate9Ex != nullptr, pSwapChain, pfnPresent, pfnReset, pfnPresentEx, pfnSwapChainPresent);

                dx9_hook = DX9_Hook::Inst();
                dx9_hook->LibraryName = library_path;
                dx9_hook->LoadFunctions(pfnPresent, pfnReset, pfnPresentEx, pfnSwapChainPresent);
            }
            else
            {
                SPDLOG_WARN("Failed to hook D3D9::Present to detect DX Version");
            }

            if (pSwapChain) pSwapChain->Release();
            if (pDevice) pDevice->Release();
            if (pD3D) pD3D->Release();
        }
    }

    void hook_dx10(std::string const& library_path)
    {
        if (!dx10_hooked)
        {
            System::Library::Library libD3d10;
            if (!libD3d10.OpenLibrary(library_path, false))
            {
                SPDLOG_WARN("Failed to load {} to detect DX10", library_path);
                return;
            }
            std::string dxgi_path = FindPreferedModulePath("dxgi.dll");

            IDXGISwapChain* pSwapChain = nullptr;
            ID3D10Device* pDevice = nullptr;
            int version = 0;

            if (!dxgi_path.empty())
            {
                HMODULE dxgi = GetModuleHandleA(dxgi_path.c_str());
                if (dxgi != nullptr)
                {
                    IDXGIFactory2* pDXGIFactory = nullptr;

                    auto D3D10CreateDevice = libD3d10.GetSymbol<decltype(::D3D10CreateDevice)>("D3D10CreateDevice");
                    decltype(CreateDXGIFactory1)* CreateDXGIFactory1 = (decltype(CreateDXGIFactory1))GetProcAddress(dxgi, "CreateDXGIFactory1");

                    if (D3D10CreateDevice != nullptr && CreateDXGIFactory1 != nullptr)
                    {
                        D3D10CreateDevice(NULL, D3D10_DRIVER_TYPE_NULL, NULL, 0, D3D10_SDK_VERSION, &pDevice);
                        if (pDevice != nullptr)
                        {
                            CreateDXGIFactory1(IID_PPV_ARGS(&pDXGIFactory));
                            if (pDXGIFactory != nullptr)
                            {
                                DXGI_SWAP_CHAIN_DESC1 SwapChainDesc = {};
                                SwapChainDesc.BufferCount = 2;
                                SwapChainDesc.Width = 1;
                                SwapChainDesc.Height = 1;
                                SwapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
                                SwapChainDesc.Stereo = FALSE;
                                SwapChainDesc.SampleDesc = { 1, 0 };
                                SwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
                                SwapChainDesc.Scaling = DXGI_SCALING_NONE;
                                SwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
                                SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;

                                pDXGIFactory->CreateSwapChainForHwnd(pDevice, dummyWindow, &SwapChainDesc, NULL, NULL, reinterpret_cast<IDXGISwapChain1**>(&pSwapChain));
                            }
                        }
                    }

                    if (pDXGIFactory) pDXGIFactory->Release();
                }
            }

            if (pDevice != nullptr && pSwapChain != nullptr)
            {
                version = 1;
            }
            else
            {
                SPDLOG_WARN("Failed to instanciate IDXGISwapChain1, fallback to pure DX10 detection");

                auto D3D10CreateDeviceAndSwapChain = libD3d10.GetSymbol<decltype(::D3D10CreateDeviceAndSwapChain)>("D3D10CreateDeviceAndSwapChain");

                if (D3D10CreateDeviceAndSwapChain != nullptr)
                {
                    DXGI_SWAP_CHAIN_DESC SwapChainDesc = {};

                    SwapChainDesc.BufferCount = 1;
                    SwapChainDesc.BufferDesc.Width = 1;
                    SwapChainDesc.BufferDesc.Height = 1;
                    SwapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
                    SwapChainDesc.BufferDesc.RefreshRate.Numerator = 0;
                    SwapChainDesc.BufferDesc.RefreshRate.Denominator = 0;
                    SwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
                    SwapChainDesc.OutputWindow = dummyWindow;
                    SwapChainDesc.SampleDesc.Count = 1;
                    SwapChainDesc.SampleDesc.Quality = 0;
                    SwapChainDesc.Windowed = TRUE;

                    D3D10CreateDeviceAndSwapChain(NULL, D3D10_DRIVER_TYPE_NULL, NULL, 0, D3D10_SDK_VERSION, &SwapChainDesc, &pSwapChain, &pDevice);
                }
            }

            if (pSwapChain != nullptr)
            {
                SPDLOG_INFO("Hooked IDXGISwapChain::Present to detect DX Version");

                dx10_hooked = true;

                decltype(&IDXGISwapChain::Present) pfnPresent;
                decltype(&IDXGISwapChain::ResizeBuffers) pfnResizeBuffers;
                decltype(&IDXGISwapChain::ResizeTarget) pfnResizeTarget;
                decltype(&IDXGISwapChain1::Present1) pfnPresent1 = nullptr;

                HookDXGIPresent(pSwapChain, pfnPresent, pfnResizeBuffers, pfnResizeTarget);
                if (version > 0)
                {
                    HookDXGIPresent1(reinterpret_cast<IDXGISwapChain1*>(pSwapChain), pfnPresent1);
                }

                dx10_hook = DX10_Hook::Inst();
                dx10_hook->LibraryName = library_path;
                dx10_hook->LoadFunctions(pfnPresent, pfnResizeBuffers, pfnResizeTarget, pfnPresent1);
            }
            else
            {
                SPDLOG_WARN("Failed to Hook IDXGISwapChain::Present to detect DX Version");
            }
            if (pDevice)pDevice->Release();
            if (pSwapChain)pSwapChain->Release();
        }
    }

    void hook_dx11(std::string const& library_path)
    {
        if (!dx11_hooked)
        {
            System::Library::Library libD3d11;
            if (!libD3d11.OpenLibrary(library_path, false))
            {
                SPDLOG_WARN("Failed to load {} to detect DX11", library_path);
                return;
            }
            std::string dxgi_path = FindPreferedModulePath("dxgi.dll");

            IDXGISwapChain* pSwapChain = nullptr;
            ID3D11Device* pDevice = nullptr;
            int version = 0;

            if (!dxgi_path.empty())
            {
                HMODULE dxgi = GetModuleHandleA(dxgi_path.c_str());
                if (dxgi != nullptr)
                {
                    IDXGIFactory2* pDXGIFactory = nullptr;

                    auto D3D11CreateDevice = libD3d11.GetSymbol<decltype(::D3D11CreateDevice)>("D3D11CreateDevice");
                    decltype(CreateDXGIFactory1)* CreateDXGIFactory1 = (decltype(CreateDXGIFactory1))GetProcAddress(dxgi, "CreateDXGIFactory1");

                    if (D3D11CreateDevice != nullptr && CreateDXGIFactory1 != nullptr)
                    {
                        D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_NULL, NULL, 0, NULL, 0, D3D11_SDK_VERSION, &pDevice, NULL, NULL);
                        if (pDevice != nullptr)
                        {
                            CreateDXGIFactory1(IID_PPV_ARGS(&pDXGIFactory));
                            if (pDXGIFactory != nullptr)
                            {
                                DXGI_SWAP_CHAIN_DESC1 SwapChainDesc = {};
                                SwapChainDesc.BufferCount = 2;
                                SwapChainDesc.Width = 1;
                                SwapChainDesc.Height = 1;
                                SwapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
                                SwapChainDesc.Stereo = FALSE;
                                SwapChainDesc.SampleDesc = { 1, 0 };
                                SwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
                                SwapChainDesc.Scaling = DXGI_SCALING_NONE;
                                SwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
                                SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;

                                pDXGIFactory->CreateSwapChainForHwnd(pDevice, dummyWindow, &SwapChainDesc, NULL, NULL, reinterpret_cast<IDXGISwapChain1**>(&pSwapChain));
                            }
                        }
                    }

                    if (pDXGIFactory) pDXGIFactory->Release();
                }
            }
            
            if (pDevice != nullptr && pSwapChain != nullptr)
            {
                version = 1;
            }
            else
            {
                SPDLOG_WARN("Failed to instanciate IDXGISwapChain1, fallback to pure DX11 detection");

                auto D3D11CreateDeviceAndSwapChain = libD3d11.GetSymbol<decltype(::D3D11CreateDeviceAndSwapChain)>("D3D11CreateDeviceAndSwapChain");

                if (D3D11CreateDeviceAndSwapChain != nullptr)
                {
                    DXGI_SWAP_CHAIN_DESC SwapChainDesc = {};

                    SwapChainDesc.BufferCount = 1;
                    SwapChainDesc.BufferDesc.Width = 1;
                    SwapChainDesc.BufferDesc.Height = 1;
                    SwapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
                    SwapChainDesc.BufferDesc.RefreshRate.Numerator = 0;
                    SwapChainDesc.BufferDesc.RefreshRate.Denominator = 0;
                    SwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
                    SwapChainDesc.OutputWindow = dummyWindow;
                    SwapChainDesc.SampleDesc.Count = 1;
                    SwapChainDesc.SampleDesc.Quality = 0;
                    SwapChainDesc.Windowed = TRUE;

                    D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_NULL, NULL, 0, NULL, NULL, D3D11_SDK_VERSION, &SwapChainDesc, &pSwapChain, &pDevice, NULL, NULL);
                }
            }

            if (pSwapChain != nullptr)
            {
                SPDLOG_INFO("Hooked IDXGISwapChain::Present to detect DX Version");

                dx11_hooked = true;

                decltype(&IDXGISwapChain::Present) pfnPresent = nullptr;
                decltype(&IDXGISwapChain::ResizeBuffers) pfnResizeBuffers = nullptr;
                decltype(&IDXGISwapChain::ResizeTarget) pfnResizeTarget = nullptr;
                decltype(&IDXGISwapChain1::Present1) pfnPresent1 = nullptr;

                HookDXGIPresent(pSwapChain, pfnPresent, pfnResizeBuffers, pfnResizeTarget);
                if (version > 0)
                {
                    HookDXGIPresent1(reinterpret_cast<IDXGISwapChain1*>(pSwapChain), pfnPresent1);
                }

                dx11_hook = DX11_Hook::Inst();
                dx11_hook->LibraryName = library_path;
                dx11_hook->LoadFunctions(pfnPresent, pfnResizeBuffers, pfnResizeTarget, pfnPresent1);
            }
            else
            {
                SPDLOG_WARN("Failed to Hook IDXGISwapChain::Present to detect DX Version");
            }

            if (pDevice) pDevice->Release();
            if (pSwapChain) pSwapChain->Release();
        }
    }

    void hook_dx12(std::string const& library_path)
    {
        if (!dx12_hooked)
        {
            System::Library::Library libD3d12;
            if (!libD3d12.OpenLibrary(library_path, false))
            {
                SPDLOG_WARN("Failed to load {} to detect DX12", library_path);
                return;
            }
            std::string dxgi_path = FindPreferedModulePath("dxgi.dll");
            if (dxgi_path.empty())
            {
                SPDLOG_WARN("Failed to load dxgi.dll to detect DX12");
                return;
            }

            IDXGIFactory4* pDXGIFactory = nullptr;
            IDXGISwapChain1* pSwapChain = nullptr;
            ID3D12CommandQueue* pCommandQueue = nullptr;
            ID3D12Device* pDevice = nullptr;

            auto D3D12CreateDevice = libD3d12.GetSymbol<decltype(::D3D12CreateDevice)>("D3D12CreateDevice");

            if (D3D12CreateDevice != nullptr)
            {
                D3D12CreateDevice(NULL, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&pDevice));

                if (pDevice != nullptr)
                {
                    DXGI_SWAP_CHAIN_DESC1 SwapChainDesc = {};
                    SwapChainDesc.BufferCount = 2;
                    SwapChainDesc.Width = 1;
                    SwapChainDesc.Height = 1;
                    SwapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
                    SwapChainDesc.Stereo = FALSE;
                    SwapChainDesc.SampleDesc = { 1, 0 };
                    SwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
                    SwapChainDesc.Scaling = DXGI_SCALING_NONE;
                    SwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
                    SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;

                    D3D12_COMMAND_QUEUE_DESC queueDesc = {};
                    queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
                    queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
                    pDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&pCommandQueue));

                    if (pCommandQueue != nullptr)
                    {
                        HMODULE dxgi = GetModuleHandleA(dxgi_path.c_str());
                        if (dxgi != nullptr)
                        {
                            decltype(CreateDXGIFactory1)* CreateDXGIFactory1 = (decltype(CreateDXGIFactory1))GetProcAddress(dxgi, "CreateDXGIFactory1");
                            if (CreateDXGIFactory1 != nullptr)
                            {
                                CreateDXGIFactory1(IID_PPV_ARGS(&pDXGIFactory));
                                if (pDXGIFactory != nullptr)
                                {
                                    pDXGIFactory->CreateSwapChainForHwnd(pCommandQueue, dummyWindow, &SwapChainDesc, NULL, NULL, &pSwapChain);
                                }
                            }
                        }
                    }
                }//if (pDevice != nullptr)
            }//if (D3D12CreateDevice != nullptr)

            if (pCommandQueue != nullptr && pSwapChain != nullptr)
            {
                SPDLOG_INFO("Hooked IDXGISwapChain::Present to detect DX Version");

                dx12_hooked = true;

                decltype(&IDXGISwapChain::Present) pfnPresent = nullptr;
                decltype(&IDXGISwapChain::ResizeBuffers) pfnResizeBuffers = nullptr;
                decltype(&IDXGISwapChain::ResizeTarget) pfnResizeTarget = nullptr;
                decltype(&IDXGISwapChain1::Present1) pfnPresent1 = nullptr;

                HookDXGIPresent(pSwapChain, pfnPresent, pfnResizeBuffers, pfnResizeTarget);
                HookDXGIPresent1(reinterpret_cast<IDXGISwapChain1*>(pSwapChain), pfnPresent1);

                void** vTable = *reinterpret_cast<void***>(pCommandQueue);
                decltype(&ID3D12CommandQueue::ExecuteCommandLists) pfnExecuteCommandLists;
                (void*&)pfnExecuteCommandLists = vTable[(int)ID3D12CommandQueueVTable::ExecuteCommandLists];

                dx12_hook = DX12_Hook::Inst();
                dx12_hook->LibraryName = library_path;
                dx12_hook->LoadFunctions(pfnPresent, pfnResizeBuffers, pfnResizeTarget, pfnExecuteCommandLists, pfnPresent1);
            }
            else
            {
                SPDLOG_WARN("Failed to Hook IDXGISwapChain::Present to detect DX Version");
            }

            if (pSwapChain) pSwapChain->Release();
            if (pDXGIFactory) pDXGIFactory->Release();
            if (pCommandQueue) pCommandQueue->Release();
            if (pDevice) pDevice->Release();
        }
    }

    void hook_opengl(std::string const& library_path)
    {
        if (!opengl_hooked)
        {
            System::Library::Library libOpenGL;
            if (!libOpenGL.OpenLibrary(library_path, false))
            {
                SPDLOG_WARN("Failed to load {} to detect OpenGL", library_path);
                return;
            }

            auto wglSwapBuffers = libOpenGL.GetSymbol<decltype(::SwapBuffers)>("wglSwapBuffers");
            if (wglSwapBuffers != nullptr)
            {
                SPDLOG_INFO("Hooked wglSwapBuffers to detect OpenGL");

                opengl_hooked = true;

                opengl_hook = OpenGL_Hook::Inst();
                opengl_hook->LibraryName = library_path;
                opengl_hook->LoadFunctions(wglSwapBuffers);

                HookwglSwapBuffers(wglSwapBuffers);
            }
            else
            {
                SPDLOG_WARN("Failed to Hook wglSwapBuffers to detect OpenGL");
            }
        }
    }

    void hook_vulkan(std::string const& library_path)
    {
        // Vulkan hook disabled until proper implementation.
        return;
        if (!vulkan_hooked)
        {
            System::Library::Library libVulkan;
            if (!libVulkan.OpenLibrary(library_path, false))
            {
                SPDLOG_WARN("Failed to load {} to detect Vulkan", library_path);
                return;
            }

            auto vkCreateInstance = libVulkan.GetSymbol<decltype(::vkCreateInstance)>("vkCreateInstance");
            auto vkDestroyInstance = libVulkan.GetSymbol<decltype(::vkDestroyInstance)>("vkDestroyInstance");
            auto vkGetInstanceProcAddr = libVulkan.GetSymbol<decltype(::vkGetInstanceProcAddr)>("vkGetInstanceProcAddr");

            decltype(::vkQueuePresentKHR)* vkQueuePresentKHR = nullptr;
            decltype(::vkAcquireNextImageKHR)* vkAcquireNextImageKHR = nullptr;
            decltype(::vkAcquireNextImage2KHR)* vkAcquireNextImage2KHR = nullptr;

            VkInstanceCreateInfo instance_infos{};
            VkInstance instance{};
            std::vector<VkPhysicalDevice> phyDevices;
            VkDeviceCreateInfo create_info{};
            VkDevice pDevice{};
            uint32_t count = 0;

            instance_infos.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
            vkCreateInstance(&instance_infos, nullptr, &instance);

            auto vkCreateDevice = (decltype(::vkCreateDevice)*)vkGetInstanceProcAddr(instance, "vkCreateDevice");
            auto vkDestroyDevice = (decltype(::vkDestroyDevice)*)vkGetInstanceProcAddr(instance, "vkDestroyDevice");
            auto vkGetDeviceProcAddr = (decltype(::vkGetDeviceProcAddr)*)vkGetInstanceProcAddr(instance, "vkGetDeviceProcAddr");
            auto vkEnumeratePhysicalDevices = (decltype(::vkEnumeratePhysicalDevices)*)vkGetInstanceProcAddr(instance, "vkEnumeratePhysicalDevices");
            auto vkEnumerateDeviceExtensionProperties = (decltype(::vkEnumerateDeviceExtensionProperties)*)vkGetInstanceProcAddr(instance, "vkEnumerateDeviceExtensionProperties");
            auto vkGetPhysicalDeviceProperties = (decltype(::vkGetPhysicalDeviceProperties)*)vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceProperties");

            vkEnumeratePhysicalDevices(instance, &count, nullptr);
            phyDevices.resize(count);
            vkEnumeratePhysicalDevices(instance, &count, phyDevices.data());

            [&]() {
                VkPhysicalDeviceProperties props{};
                std::vector<VkExtensionProperties> ext_props;

                for (auto& device : phyDevices)
                {
                    vkGetPhysicalDeviceProperties(device, &props);
                    if (props.deviceType == VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU || props.deviceType == VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU)
                    {
                        count = 0;
                        vkEnumerateDeviceExtensionProperties(device, nullptr, &count, nullptr);
                        ext_props.resize(count);
                        vkEnumerateDeviceExtensionProperties(device, nullptr, &count, ext_props.data());

                        for (auto& ext : ext_props)
                        {
                            if (strcmp(ext.extensionName, "VK_KHR_swapchain") == 0)
                            {
                                create_info.sType = VkStructureType::VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
                                create_info.enabledExtensionCount = 1;
                                const char* str = "VK_KHR_swapchain";
                                create_info.ppEnabledExtensionNames = &str;
                                vkCreateDevice(device, &create_info, nullptr, &pDevice);
                                if(pDevice != nullptr)
                                    return;
                            }
                        }
                    }
                }
            }();

            if (pDevice != nullptr)
            {
                vkQueuePresentKHR = (decltype(::vkQueuePresentKHR)*)vkGetDeviceProcAddr(pDevice, "vkQueuePresentKHR");
                vkAcquireNextImageKHR = (decltype(::vkAcquireNextImageKHR)*)vkGetDeviceProcAddr(pDevice, "vkAcquireNextImageKHR");
                vkAcquireNextImage2KHR = (decltype(::vkAcquireNextImage2KHR)*)vkGetDeviceProcAddr(pDevice, "vkAcquireNextImage2KHR");
                vkDestroyDevice(pDevice, nullptr);
            }
            vkDestroyInstance(instance, nullptr);

            if (vkQueuePresentKHR != nullptr /* && (vkAcquireNextImageKHR != nullptr || vkAcquireNextImage2KHR != nullptr)*/)
            {
                SPDLOG_INFO("Hooked vkQueuePresentKHR to detect Vulkan");

                vulkan_hooked = true;
                
                vulkan_hook = Vulkan_Hook::Inst();
                vulkan_hook->LibraryName = library_path;
                vulkan_hook->LoadFunctions(vkQueuePresentKHR);
                
                HookvkQueuePresentKHR(vkQueuePresentKHR);
            }
            else
            {
                SPDLOG_WARN("Failed to Hook vkQueuePresentKHR to detect Vulkan");
            }
        }
    }

public:
    Renderer_Hook* detect_renderer(std::chrono::milliseconds timeout)
    {
        std::unique_lock<std::timed_mutex> detection_lock(detector_mutex, std::defer_lock);
        
        if (!detection_lock.try_lock_for(timeout))
            return nullptr;

        {
            std::lock_guard<std::mutex> lk(renderer_mutex);
            if (detection_done)
            {
                return renderer_hook;
            }

            if (CreateHWND() == nullptr)
            {
                return nullptr;
            }
        }

        SPDLOG_TRACE("Started renderer detection.");

        std::pair<std::string, void(Renderer_Detector::*)(std::string const&)> libraries[]{
            {OpenGL_Hook::DLL_NAME, &Renderer_Detector::hook_opengl},
            {Vulkan_Hook::DLL_NAME, &Renderer_Detector::hook_vulkan},
            {  DX12_Hook::DLL_NAME, &Renderer_Detector::hook_dx12  },
            {  DX11_Hook::DLL_NAME, &Renderer_Detector::hook_dx11  },
            {  DX10_Hook::DLL_NAME, &Renderer_Detector::hook_dx10  },
            {   DX9_Hook::DLL_NAME, &Renderer_Detector::hook_dx9   },
        };
        std::string name;

        auto start_time = std::chrono::steady_clock::now();
        do
        {
            for (auto const& library : libraries)
            {
                void* lib_handle = System::Library::GetLibraryHandle(library.first.c_str());
                if (lib_handle != nullptr)
                {
                    std::lock_guard<std::mutex> lk(renderer_mutex);
                    name = FindPreferedModulePath(library.first);
                    (this->*library.second)(name);
                }
            }
            std::this_thread::sleep_for(std::chrono::milliseconds{ 100 });
        } while (!detection_done && (timeout.count() == -1 || (std::chrono::steady_clock::now() - start_time) <= timeout));

        {
            std::lock_guard<std::mutex> lk(renderer_mutex);
            DestroyHWND();

            detection_done = true;
            delete dx9_hook   ; dx9_hook    = nullptr;
            delete dx10_hook  ; dx10_hook   = nullptr;
            delete dx11_hook  ; dx11_hook   = nullptr;
            delete dx12_hook  ; dx12_hook   = nullptr;
            delete opengl_hook; opengl_hook = nullptr;
            delete vulkan_hook; vulkan_hook = nullptr;
            detection_hooks.UnhookAll();
        }

        SPDLOG_TRACE("Renderer detection done {}.", (void*)renderer_hook);

        return renderer_hook;
    }
};

Renderer_Detector* Renderer_Detector::instance = nullptr;

#elif defined(RENDERERDETECTOR_OS_LINUX)
#define GLAD_GL_IMPLEMENTATION
#include <glad/gl.h>

#include "linux/OpenGLX_Hook.h"

class Renderer_Detector
{
    static Renderer_Detector* instance;
public:
    static Renderer_Detector* Inst()
    {
        if (instance == nullptr)
        {
            instance = new Renderer_Detector;
        }
        return instance;
    }

    ~Renderer_Detector()
    {
        delete openglx_hook;
        //delete vulkan_hook;
    }

private:
    Renderer_Detector() :
        openglx_hooked(false),
        renderer_hook(nullptr),
        openglx_hook(nullptr),
        //vulkan_hook(nullptr),
        detection_done(false)
    {}

    std::timed_mutex detector_mutex;
    std::mutex renderer_mutex;

    Base_Hook detection_hooks;

    decltype(::glXSwapBuffers)* glXSwapBuffers;

    bool openglx_hooked;
    //bool vulkan_hooked;

    Renderer_Hook* renderer_hook;
    OpenGLX_Hook* openglx_hook;

    bool detection_done;

    static void MyglXSwapBuffers(Display* dpy, GLXDrawable drawable)
    {
        auto inst = Inst();
        std::lock_guard<std::mutex> lk(inst->renderer_mutex);
        inst->glXSwapBuffers(dpy, drawable);
        if (inst->detection_done)
            return;

        if (gladLoaderLoadGL() >= GLAD_MAKE_VERSION(3, 1))
        {
            inst->detection_hooks.UnhookAll();
            inst->renderer_hook = static_cast<Renderer_Hook*>(Inst()->openglx_hook);
            inst->openglx_hook = nullptr;
            inst->detection_done = true;
        }
    }

    void HookglXSwapBuffers(decltype(::glXSwapBuffers)* _glXSwapBuffers)
    {
        glXSwapBuffers = _glXSwapBuffers;

        detection_hooks.BeginHook();
        detection_hooks.HookFunc(std::pair<void**, void*>{ (void**)&glXSwapBuffers, (void*)&MyglXSwapBuffers });
        detection_hooks.EndHook();
    }

    void hook_openglx(std::string const& library_path)
    {
        if (!openglx_hooked)
        {
            System::Library::Library libGLX;
            if (!libGLX.OpenLibrary(library_path, false))
            {
                SPDLOG_WARN("Failed to load {} to detect OpenGLX", library_path);
                return;
            }

            auto glXSwapBuffers = libGLX.GetSymbol<decltype(::glXSwapBuffers)>("glXSwapBuffers");
            if (glXSwapBuffers != nullptr)
            {
                SPDLOG_INFO("Hooked glXSwapBuffers to detect OpenGLX");

                openglx_hooked = true;

                openglx_hook = OpenGLX_Hook::Inst();
                openglx_hook->LibraryName = library_path;
                openglx_hook->LoadFunctions(glXSwapBuffers);

                HookglXSwapBuffers(glXSwapBuffers);
            }
            else
            {
                SPDLOG_WARN("Failed to Hook glXSwapBuffers to detect OpenGLX");
            }
        }
    }

public:
    Renderer_Hook* detect_renderer(std::chrono::milliseconds timeout)
    {
        std::pair<const char*, void(Renderer_Detector::*)(std::string const&)> libraries[]{
            std::pair<const char*, void(Renderer_Detector::*)(std::string const&)>{OpenGLX_Hook::DLL_NAME, &Renderer_Detector::hook_openglx},
        };

        std::unique_lock<std::timed_mutex> detection_lock(detector_mutex, std::defer_lock);

        if (!detection_lock.try_lock_for(timeout))
            return nullptr;

        {
            std::lock_guard<std::mutex> lk(renderer_mutex);
            if (detection_done)
                return renderer_hook;
        }

        SPDLOG_TRACE("Started renderer detection.");

        auto start_time = std::chrono::steady_clock::now();
        do
        {
            for (auto const& library : libraries)
            {
                void* lib_handle = System::Library::GetLibraryHandle(library.first);
                if (lib_handle != nullptr)
                {
                    std::lock_guard<std::mutex> lk(renderer_mutex);
                    std::string lib_path = System::Library::GetLibraryPath(lib_handle);
                    (this->*library.second)(lib_path);
                }
            }
            std::this_thread::sleep_for(std::chrono::milliseconds{ 100 });
        } while (!detection_done && (timeout.count() == -1 || (std::chrono::steady_clock::now() - start_time) <= timeout));

        {
            std::lock_guard<std::mutex> lk(renderer_mutex);
            detection_done = true;
            delete openglx_hook; openglx_hook = nullptr;
            //delete vulkan_hook; vulkan_hook = nullptr;
        }

        SPDLOG_TRACE("Renderer detection done {}.", (void*)renderer_hook);

        return renderer_hook;
    }
};

Renderer_Detector* Renderer_Detector::instance = nullptr;

#elif defined(RENDERERDETECTOR_OS_APPLE)
#include "macosx/OpenGL_Hook.h"
#define GLAD_GL_IMPLEMENTATION
#include <glad/gl.h>

class Renderer_Detector
{
   static Renderer_Detector* instance;
public:
   static Renderer_Detector* Inst()
   {
       if (instance == nullptr)
       {
           instance = new Renderer_Detector;
       }
       return instance;
   }

   ~Renderer_Detector()
   {
       delete opengl_hook;
   }

private:
   Renderer_Detector():
       opengl_hooked(false),
       renderer_hook(nullptr),
       opengl_hook(nullptr),
       detection_done(false)
   {}

   std::timed_mutex detector_mutex;
   std::mutex renderer_mutex;

   Base_Hook detection_hooks;

   decltype(::CGLFlushDrawable)* CGLFlushDrawable;

   bool opengl_hooked;

   Renderer_Hook* renderer_hook;
   OpenGL_Hook* opengl_hook;

   bool detection_done;

   static int64_t MyCGLFlushDrawable(CGLDrawable_t* glDrawable)
   {
       auto inst = Inst();
       std::lock_guard<std::mutex> lk(inst->renderer_mutex);
       int64_t res = inst->CGLFlushDrawable(glDrawable);

       if (gladLoaderLoadGL() >= GLAD_MAKE_VERSION(2, 0))
       {
           inst->detection_hooks.UnhookAll();
           inst->renderer_hook = static_cast<Renderer_Hook*>(Inst()->opengl_hook);
           inst->opengl_hook = nullptr;
           inst->detection_done = true;
       }

       return res;
   }

   void HookglFlushDrawable(decltype(::CGLFlushDrawable)* _CGLFlushDrawable)
   {
       CGLFlushDrawable = _CGLFlushDrawable;

       detection_hooks.BeginHook();
       detection_hooks.HookFunc(std::pair<void**, void*>{ (void**)&CGLFlushDrawable, (void*)&MyCGLFlushDrawable });
       detection_hooks.EndHook();
   }

   void hook_opengl(std::string const& library_path)
   {
       if (!opengl_hooked)
       {
           System::Library::Library libOpenGL;
           if (!libOpenGL.OpenLibrary(library_path, false))
           {
               SPDLOG_WARN("Failed to load {} to detect OpenGL", library_path);
               return;
           }

           auto CGLFlushDrawable = libOpenGL.GetSymbol<decltype(::CGLFlushDrawable)>("CGLFlushDrawable");
           if (CGLFlushDrawable != nullptr)
           {
               SPDLOG_INFO("Hooked CGLFlushDrawable to detect OpenGL");

               opengl_hooked = true;

               opengl_hook = OpenGL_Hook::Inst();
               opengl_hook->LibraryName = library_path;
               opengl_hook->LoadFunctions(CGLFlushDrawable);

               HookglFlushDrawable(CGLFlushDrawable);
           }
           else
           {
               SPDLOG_WARN("Failed to Hook CGLFlushDrawable to detect OpenGL");
           }
       }
   }

public:
   Renderer_Hook* detect_renderer(std::chrono::milliseconds timeout)
   {
       std::pair<const char*, void(Renderer_Detector::*)(std::string const&)> libraries[]{
           std::pair<const char*, void(Renderer_Detector::*)(std::string const&)>{OpenGL_Hook::DLL_NAME, &Renderer_Detector::hook_opengl}
       };

       std::unique_lock<std::timed_mutex> detection_lock(detector_mutex, std::defer_lock);

       if (!detection_lock.try_lock_for(timeout))
           return nullptr;

       {
           std::lock_guard<std::mutex> lk(renderer_mutex);
           if (detection_done)
               return renderer_hook;
       }

       SPDLOG_TRACE("Started renderer detection.");

       auto start_time = std::chrono::steady_clock::now();
       do
       {
           for (auto const& library : libraries)
           {
               void* lib_handle = System::Library::GetLibraryHandle(library.first);
               if (lib_handle != nullptr)
               {
                   std::lock_guard<std::mutex> lk(renderer_mutex);
                   std::string lib_path = System::Library::GetLibraryPath(lib_handle);
                   (this->*library.second)(lib_path);
               }
           }
           std::this_thread::sleep_for(std::chrono::milliseconds{ 100 });
       } while (!detection_done && (timeout.count() == -1 || (std::chrono::steady_clock::now() - start_time) <= timeout));

       {
           std::lock_guard<std::mutex> lk(renderer_mutex);
           detection_done = true;
           delete opengl_hook; opengl_hook = nullptr;
           //delete vulkan_hook; vulkan_hook = nullptr;
       }

       SPDLOG_TRACE("Renderer detection done {}.", (void*)renderer_hook);

       return renderer_hook;
   }
};

Renderer_Detector* Renderer_Detector::instance = nullptr;

#endif

std::future<Renderer_Hook*> detect_renderer(std::chrono::milliseconds timeout)
{
    return std::async(std::launch::async, &Renderer_Detector::detect_renderer, Renderer_Detector::Inst(), timeout);
}