#include <initguid.h>

#include "graphics.h"

#include <set>
#include <vector>
#include <mutex>
#include <condition_variable>

#include "avs/game.h"
#include "cfg/icon.h"
#include "cfg/screen_resize.h"
#include "games/ddr/ddr.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "launcher/shutdown.h"
#include "overlay/overlay.h"
#include "touch/touch.h"
#include "touch/touch_indicators.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/fileutils.h"
#include "util/utils.h"
#include "misc/wintouchemu.h"
#include "util/time.h"
#include "rawinput/rawinput.h"

struct CaptureData {
    std::shared_ptr<uint8_t[]> data;
    unsigned short width, height;
    uint64_t timestamp;
};

HWND TDJ_SUBSCREEN_WINDOW = nullptr;
HWND SDVX_SUBSCREEN_WINDOW = nullptr;

// icon
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));

// state
static WNDPROC WNDPROC_ORIG = nullptr;
static WNDPROC WSUB_WNDPROC_ORIG = nullptr;
static std::vector<WNDPROC> WNDPROC_CUSTOM {};
static bool GRAPHICS_SCREENSHOT_TRIGGER = false;
static std::set<int> GRAPHICS_SCREENS { 0 };
static std::mutex GRAPHICS_SCREENS_M {};
static std::vector<int> GRAPHICS_CAPTURE_SCREENS;
static const size_t GRAPHICS_CAPTURE_SCREEN_NO = 4;
static std::mutex GRAPHICS_CAPTURE_SCREENS_M {};
static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {};

// flag settings
bool GRAPHICS_CAPTURE_CURSOR = false;
bool GRAPHICS_LOG_HRESULT = false;
bool GRAPHICS_SDVX_FORCE_720 = false;
bool GRAPHICS_SHOW_CURSOR = false;
graphics_orientation GRAPHICS_ADJUST_ORIENTATION = ORIENTATION_NORMAL;
bool GRAPHICS_WINDOWED = false;
std::vector<HWND> GRAPHICS_WINDOWS;
UINT GRAPHICS_FORCE_REFRESH = 0;
bool GRAPHICS_FORCE_SINGLE_ADAPTER = false;
bool GRAPHICS_PREVENT_SECONDARY_WINDOW = false;
graphics_dx9on12_state GRAPHICS_9_ON_12_STATE = DX9ON12_AUTO;
bool GRAPHICS_9_ON_12_REQUESTED_BY_GAME = false;
bool SUBSCREEN_FORCE_REDRAW = false;
bool D3D9_DEVICE_HOOK_DISABLE = false;

// settings
std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146";
std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots";

static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr;
static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr;
static decltype(ClipCursor) *ClipCursor_orig = nullptr;
static decltype(CreateWindowExA) *CreateWindowExA_orig = nullptr;
static decltype(CreateWindowExW) *CreateWindowExW_orig = nullptr;
static decltype(EnableWindow) *EnableWindow_orig = nullptr;
static decltype(EnumDisplayDevicesA) *EnumDisplayDevicesA_orig = nullptr;
static decltype(MoveWindow) *MoveWindow_orig = nullptr;
static decltype(PeekMessageA) *PeekMessageA_orig = nullptr;
static decltype(RegisterClassA) *RegisterClassA_orig = nullptr;
static decltype(RegisterClassExA) *RegisterClassExA_orig = nullptr;
static decltype(RegisterClassW) *RegisterClassW_orig = nullptr;
static decltype(RegisterClassExW) *RegisterClassExW_orig = nullptr;
static decltype(ShowCursor) *ShowCursor_orig = nullptr;
static decltype(SetCursor) *SetCursor_orig = nullptr;
static decltype(SetWindowLongA) *SetWindowLongA_orig = nullptr;
static decltype(SetWindowLongW) *SetWindowLongW_orig = nullptr;
static decltype(SetWindowPos) *SetWindowPos_orig = nullptr;

static void reset_window_hook(HWND hWnd) {
    overlay::destroy(hWnd);

    if (WNDPROC_ORIG) {
        SetWindowLongPtrA(hWnd, GWLP_WNDPROC, (LONG_PTR) WNDPROC_ORIG);
        WNDPROC_ORIG = nullptr;
    }
}

// window procedure
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

    // terminate
    if (uMsg == WM_CLOSE) {
        log_info("graphics", "detected WM_CLOSE, terminating...");
        launcher::shutdown(0);
        return false;
    }

    // overlay specific
    if (overlay::OVERLAY) {
        switch (uMsg) {
            case WM_CHAR: {

                // input characters if overlay is active
                if (overlay::OVERLAY->has_focus()) {
                    overlay::OVERLAY->input_char((unsigned int) wParam);
                    return true;
                }
                break;
            }
            case WM_DESTROY: {
                auto wndproc = WNDPROC_ORIG;
                reset_window_hook(hWnd);
                return CallWindowProcA(wndproc, hWnd, uMsg, wParam, lParam);
            }
            case WM_SETCURSOR: {

                // set cursor back to the overlay one
                if (LOWORD(lParam) == HTCLIENT && overlay::OVERLAY->update_cursor()) {
                    return true;
                }
                break;
            }
            default:
                break;
        }
    }

    if (wintouchemu::INJECT_MOUSE_AS_WM_TOUCH) {
        // drop mouse inputs since only wintouches should be used
        switch (uMsg) {
            case WM_LBUTTONDOWN:
            case WM_LBUTTONUP:
            case WM_MBUTTONDOWN:
            case WM_MBUTTONUP:
            case WM_RBUTTONDOWN:
            case WM_RBUTTONUP:
            case WM_XBUTTONDOWN:
            case WM_XBUTTONUP:
                return true;
        }
    }

    // window resize
    graphics_windowed_wndproc(hWnd, uMsg, wParam, lParam);

    // call custom procedures
    for (WNDPROC wndProc : WNDPROC_CUSTOM) {
        wndProc(hWnd, uMsg, wParam, lParam);
    }

    // capture mouse
    if (GRAPHICS_CAPTURE_CURSOR) {
        bool free_cursor = false;
        bool capture_cursor = false;
        bool early_return = false;
        switch (uMsg) {
            case WM_SETFOCUS:
                capture_cursor = true;
                early_return = true;
                break;
            case WM_KILLFOCUS:
                free_cursor = true;
                early_return = true;
                break;
            case WM_WINDOWPOSCHANGED:
                // known issue: dragging with the title bar results in WM_WINDOWPOSCHANGED
                // getting called, which calls ClipCursor successfully, but doesn't actually
                // confine the cursor for some reason; seems like odd Windows behavior
                // (can be fixed if focus is shifted to another window and then back to the game
                // window)
                if (hWnd == GetActiveWindow()) {
                    capture_cursor = true;
                } else {
                    free_cursor = true;
                }
                // do not return early; may result in WM_SIZE / WM_MOVE no longer being called
            default:
                break;
        }

        if (free_cursor) {
            ClipCursor(nullptr);
        } else if (capture_cursor) {
            RECT WINDOW_RECT;
            GetWindowRect(hWnd, &WINDOW_RECT);
            ClipCursor(&WINDOW_RECT);
        }
        if (early_return) {
            return true;
        }
    }

    // drop keydown messages
    switch (uMsg) {
        case WM_SYSKEYDOWN:
        case WM_SYSKEYUP:
        case WM_KEYDOWN:
        case WM_KEYUP:
            return true;
        default:
            break;
    }

    switch (uMsg) {
        case WM_MOVE:
        case WM_SIZE: {
            // Update SPICETOUCH space when the main window changes size or moves.
            // The update happens regardless of whether the "fake" spicetouch window is present or not.
            // This allows touches received on subscreen window to be translated correctly.
            update_spicetouch_window_dimensions(hWnd);
            // log_misc(
            //     "graphics", "detected window change ({}x{} @ {}, {}), updating touch coord-space to match", 
            //     SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y);
                
            // Update SPICETOUCH window if present
            if (SPICETOUCH_TOUCH_HWND) {
                SetWindowPos(
                    SPICETOUCH_TOUCH_HWND, HWND_TOP,
                    SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y,
                    SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT,
                    SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
            }
        }
        default:
            break;
    }

    // call default
    return CallWindowProcA(WNDPROC_ORIG, hWnd, uMsg, wParam, lParam);
}

// window procedure for subscreen
// this might be replaced by spicetouch hook later
static LRESULT CALLBACK WsubWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    if (uMsg == WM_CLOSE) {
        log_misc("graphics", "ignore WM_CLOSE for subscreen window");
        return false;
    }
    return CallWindowProcA(WSUB_WNDPROC_ORIG, hWnd, uMsg, wParam, lParam);
}

static LONG WINAPI ChangeDisplaySettingsA_hook(DEVMODEA *lpDevMode, DWORD dwflags) {
    log_misc("graphics", "ChangeDisplaySettingsA hook hit");

    // ignore display settings changes when running windowed
    if (GRAPHICS_WINDOWED) {
        return DISP_CHANGE_SUCCESSFUL;
    }

    // call original
    return ChangeDisplaySettingsA_orig(lpDevMode, dwflags);
}

static LONG WINAPI ChangeDisplaySettingsExA_hook(LPCSTR lpszDeviceName, DEVMODEA *lpDevMode, HWND hwnd,
        DWORD dwflags, LPVOID lParam)
{
    log_misc("graphics", "ChangeDisplaySettingsExA hook hit");

    // ignore display settings changes when running windowed
    if (GRAPHICS_WINDOWED) {
        return DISP_CHANGE_SUCCESSFUL;
    }

    // call original
    return ChangeDisplaySettingsExA_orig(lpszDeviceName, lpDevMode, hwnd, dwflags, lParam);
}

static BOOL WINAPI ClipCursor_hook(const RECT *lpRect) {
    log_misc("graphics", "ClipCursor hook hit");

    // ignore cursor confine when having no explicit cursor confine
    if (!GRAPHICS_CAPTURE_CURSOR) {
        return TRUE;
    }

    // call original
    return ClipCursor_orig(lpRect);
}

static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPCSTR lpWindowName,
                                        DWORD dwStyle, int x, int y, int nWidth, int nHeight,
                                        HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
                                        LPVOID lpParam)
{
    const std::string window_name(lpWindowName != nullptr ? lpWindowName : "(null)");

    log_misc("graphics", "CreateWindowExA hook hit (0x{:08x}, {}, {}, 0x{:08x}, {}, {}, {}, {}, {}, {}, {}, {})",
            dwExStyle,
            fmt::ptr(lpClassName),
            window_name,
            dwStyle,
            x,
            y,
            nWidth,
            nHeight,
            fmt::ptr(hWndParent),
            fmt::ptr(hMenu),
            fmt::ptr(hInstance),
            fmt::ptr(lpParam));

    // set display orientation and/or refresh rate
    // only set orientation when the target window is portrait
    // (avoid doing this for SDVX subscreen which is in landscape, for example)
    const auto adjust_orientation =
        (GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ||
         GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CCW);

    if ((nHeight > nWidth && adjust_orientation) || GRAPHICS_FORCE_REFRESH > 0) {
        DEVMODE mode {};

        // get display settings
        if (EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &mode)) {
            if (adjust_orientation) {
                DWORD orientation = GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ? DMDO_90 : DMDO_270;
                log_misc(
                    "graphics",
                    "auto-rotate: call ChangeDisplaySettings and rotate display to DMDO_xx mode {}",
                    orientation);
                mode.dmPelsWidth = nWidth;
                mode.dmPelsHeight = nHeight;
                mode.dmDisplayOrientation = orientation;
            }

            if (GRAPHICS_FORCE_REFRESH > 0) {
                log_info(
                    "graphics",
                    "call ChangeDisplaySettings to force refresh rate: {} => {} Hz (-graphics-force-refresh)",
                    mode.dmDisplayFrequency,
                    GRAPHICS_FORCE_REFRESH);

                mode.dmDisplayFrequency = GRAPHICS_FORCE_REFRESH;
            }

            const auto disp_res = ChangeDisplaySettings(&mode, CDS_FULLSCREEN);
            if (disp_res != DISP_CHANGE_SUCCESSFUL) {
                log_warning("graphics", "failed to change display settings: {}", disp_res);
            }
        } else {
            log_warning("graphics", "failed to get display settings");
        }
    }

    // gfdm
    if (avs::game::is_model({"J32", "J33", "K32", "K33", "L32", "L33", "M32"})) {
        // set window name
        if (!lpWindowName) {
            lpWindowName = "GITADORA";
        }
    }

    bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
    bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen");

    // TDJ windowed mode with subscreen: hide maximize button
    if ((is_tdj_sub_window && GRAPHICS_IIDX_WSUB) || is_sdvx_sub_window) {
        dwStyle &= ~(WS_MAXIMIZEBOX);
    }

    // call original
    HWND result = CreateWindowExA_orig(dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight,
            hWndParent, hMenu, hInstance, lpParam);
    GRAPHICS_WINDOWS.push_back(result);

    if (is_tdj_sub_window) {
        // TDJ windowed mode: remember the subscreen window handle for later
        TDJ_SUBSCREEN_WINDOW = result;
    
        // hook for preventing the closing of subscreen window
        if (GRAPHICS_IIDX_WSUB) {
            graphics_hook_subscreen_window(TDJ_SUBSCREEN_WINDOW);
        }
    }

    // hook for preventing the closing of subscreen window
    if (is_sdvx_sub_window) {
        SDVX_SUBSCREEN_WINDOW = result;
        graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW);
    }

    disable_touch_indicators(result);
    return result;
}

static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LPCWSTR lpWindowName,
                                        DWORD dwStyle, int x, int y, int nWidth, int nHeight,
                                        HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
                                        LPVOID lpParam)
{
    log_misc("graphics", "CreateWindowExW hook hit ({:x}, {}, {}, {:x}, {}, {}, {}, {}, {}, {}, {}, {})",
            dwExStyle,
            fmt::ptr(lpClassName),
            lpWindowName != nullptr ? ws2s(lpWindowName) : "(null)",
            dwStyle,
            x,
            y,
            nWidth,
            nHeight,
            fmt::ptr(hWndParent),
            fmt::ptr(hMenu),
            fmt::ptr(hInstance),
            fmt::ptr(lpParam));

    // DDR specific stuff
    if (avs::game::is_model("MDX")) {

        // set window name
        if (!lpWindowName) {
            lpWindowName = L"Dance Dance Revolution";
        }

        // windowed mode adjustments
        if (GRAPHICS_WINDOWED) {

            // change window style
            dwExStyle = 0;
            dwStyle |= WS_OVERLAPPEDWINDOW;

            // adjust window size to include window decoration
            RECT rect {};
            if (games::ddr::SDMODE) {
                SetRect(&rect, 0, 0, 800, 600);
            } else {
                SetRect(&rect, 0, 0, 1280, 720);
            }
            AdjustWindowRect(&rect, dwStyle, (hMenu != nullptr));

            nWidth = rect.right - rect.left;
            nHeight = rect.bottom - rect.top;
        }
    }

    // DanEvo specific stuff
    if (avs::game::is_model("KDM")) {

        // set window name
        if (!lpWindowName) {
            lpWindowName = L"Dance Evolution";
        }
    }

    // call original
    HWND result = CreateWindowExW_orig(
            dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight,
            hWndParent, hMenu, hInstance, lpParam);
    GRAPHICS_WINDOWS.push_back(result);

    disable_touch_indicators(result);
    return result;
}

static BOOL WINAPI EnableWindow_hook(HWND hWnd, BOOL bEnable) {
    return TRUE;
}

static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum,
        PDISPLAY_DEVICE lpDisplayDevice, DWORD dwFlags) {

    // call original
    BOOL value = EnumDisplayDevicesA_orig(lpDevice, iDevNum, lpDisplayDevice, dwFlags);

#ifndef SPICE64
    // older IIDX games check for hardcoded PCI vendor/device ID pair of GPU
    if ((avs::game::is_model("JDZ") || avs::game::is_model("KDZ")) && value) {
        log_info(
            "graphics",
            "EnumDisplayDevicesA_hook: swap DeviceID {} with {} (for IIDX 18/19)",
            lpDisplayDevice->DeviceID,
            GRAPHICS_DEVICEID.c_str());
        memcpy(&lpDisplayDevice->DeviceID, GRAPHICS_DEVICEID.c_str(), GRAPHICS_DEVICEID.size() + 1);
    }
#endif

    // return original result
    return value;
}

static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) {
    log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
        fmt::ptr(hWnd),
        X,
        Y,
        nWidth,
        nHeight,
        bRepaint);

    // sound voltex windowed mode adjustments
    if (GRAPHICS_WINDOWED && GRAPHICS_SDVX_FORCE_720 && avs::game::is_model("KFC")) {
        RECT rect {};
        DWORD dwStyle;

        dwStyle = GetWindowLongA(hWnd, GWL_STYLE);

        // luckily, SDVX does not draw a menu. So we can leave the last
        // argument to `AdjustWindowRect` as `0`.
        SetRect(&rect, 0, 0, 720, 1280);
        AdjustWindowRect(&rect, dwStyle, 0);

        nWidth = rect.right - rect.left;
        nHeight = rect.bottom - rect.top;
    }

    // iidx windowed TDJ mode
    if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
       if (GRAPHICS_IIDX_WSUB) {
           // (Experimental) Show subscreen in windowed mode
            graphics_load_windowed_subscreen_parameters();
 
            RECT rect {};
            DWORD dwStyle;

            dwStyle = GetWindowLongA(hWnd, GWL_STYLE);

            SetRect(&rect, 0, 0, GRAPHICS_IIDX_WSUB_WIDTH, GRAPHICS_IIDX_WSUB_HEIGHT);
            AdjustWindowRect(&rect, dwStyle, 0);

            X = GRAPHICS_IIDX_WSUB_X;
            Y = GRAPHICS_IIDX_WSUB_Y;

            nWidth = rect.right - rect.left;
            nHeight = rect.bottom - rect.top;

            touch_attach_wnd(TDJ_SUBSCREEN_WINDOW);
        } else {
            // Existing behaviour: suppress subscreen window and prompt user to use overlay instead
             log_info(
                "graphics",
                "MoveWindow hook - hiding TDJ subscreen window {}; please use subscreen overlay instead (-iidxtdjw)",
                fmt::ptr(hWnd));
            SendMessage(hWnd, WM_CLOSE, 0, 0);
            TDJ_SUBSCREEN_WINDOW = nullptr;
            return TRUE;
        }
    }

    // call original
    return MoveWindow_orig(hWnd, X, Y, nWidth, nHeight, bRepaint);
}

static BOOL WINAPI PeekMessageA_hook(
        LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg) {

    // SDVX polls for messages too slowly
    if (avs::game::is_model("KFC")) {

        // process remaining messages
        BOOL ret;
        while ((ret = PeekMessageA_orig(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, PM_REMOVE)) != 0) {
            if (ret == -1) {
                return ret;
            } else {
                TranslateMessage(lpMsg);
                DispatchMessageA(lpMsg);
            }
        }

        // return no message
        return FALSE;
    }

    return PeekMessageA_orig(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg);
}

static int WINAPI ShowCursor_hook(BOOL bShow) {

    // prevent game from hiding cursor when option is enabled
    if (GRAPHICS_SHOW_CURSOR && !bShow) {
        return 1;
    }

    // call original
    return ShowCursor_orig(bShow);
}

static HCURSOR WINAPI SetCursor_hook(HCURSOR hCursor) {
    if (GRAPHICS_SHOW_CURSOR && hCursor == NULL) {
        return GetCursor();
    }
    return SetCursor_orig(hCursor);
}

static LONG WINAPI SetWindowLongA_hook(HWND hWnd, int nIndex, LONG dwNewLong) {

    // DDR window style fix
    if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
        dwNewLong |= WS_OVERLAPPEDWINDOW;
    }

    // call original
    return SetWindowLongA_orig(hWnd, nIndex, dwNewLong);
}

static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {

    // DDR overlapped window fix
    if (nIndex == GWL_STYLE && avs::game::is_model("MDX")) {
        dwNewLong |= WS_OVERLAPPEDWINDOW;
    }

    // call original
    return SetWindowLongW_orig(hWnd, nIndex, dwNewLong);
}

static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
        int X, int Y, int cx, int cy, UINT uFlags) {

    // windowed mode adjustments
    if (GRAPHICS_WINDOWED && (avs::game::is_model("LMA") || avs::game::is_model("MDX"))) {
        return TRUE;
    }

    // call original
    return SetWindowPos_orig(hWnd, hWndInsertAfter, X, Y, cx, cy, uFlags);
}

static ATOM WINAPI RegisterClassA_hook(const WNDCLASSA *lpWndClass) {

    // check for null
    if (!lpWndClass) {
        return RegisterClassA_orig(lpWndClass);
    }

    // copy struct and use own icon
    WNDCLASSA wnd = *lpWndClass;
    wnd.hIcon = WINDOW_ICON;

    // call original
    return RegisterClassA_orig(&wnd);
}

static ATOM WINAPI RegisterClassExA_hook(const WNDCLASSEXA *Arg1) {

    // check for null
    if (!Arg1) {
        return RegisterClassExA_orig(Arg1);
    }

    // copy struct and use own icon
    WNDCLASSEXA wnd = *Arg1;
    wnd.hIcon = WINDOW_ICON;
    wnd.hIconSm = WINDOW_ICON;

    // call original
    return RegisterClassExA_orig(&wnd);
}

static ATOM WINAPI RegisterClassW_hook(const WNDCLASSW *lpWndClass) {

    // check for null
    if (!lpWndClass) {
        return RegisterClassW_orig(lpWndClass);
    }

    // copy struct and use own icon
    WNDCLASSW wnd = *lpWndClass;
    wnd.hIcon = WINDOW_ICON;

    // call original
    return RegisterClassW_orig(&wnd);
}

static ATOM WINAPI RegisterClassExW_hook(const WNDCLASSEXW *Arg1) {

    // check for null
    if (!Arg1) {
        return RegisterClassExW_orig(Arg1);
    }

    // copy struct and use own icon
    WNDCLASSEXW wnd = *Arg1;
    wnd.hIcon = WINDOW_ICON;
    wnd.hIconSm = WINDOW_ICON;

    // call original
    return RegisterClassExW_orig(&wnd);
}

static HHOOK WINAPI SetWindowsHookExA_hook(int, HOOKPROC, HINSTANCE, DWORD) {
    log_misc("graphics", "SetWindowsHookExA hook hit");

    // we don't do hooks
    return nullptr;
}

static BOOL WINAPI SetCursorPos_hook(int, int) {

    // prevent games from messing with the cursor position themselves
    return TRUE;
}

static int WINAPI MessageBoxA_hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {

    auto text = lpText != nullptr ? lpText : "(null)";
    auto title = lpCaption != nullptr ? lpCaption : "(null)";

    log_info("graphics", "MessageBoxA: {} - {}", title, text);
    return IDOK;
}

static int WINAPI MessageBoxExA_hook(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType, WORD wLanguageId) {

    auto text = lpText != nullptr ? lpText : "(null)";
    auto title = lpCaption != nullptr ? lpCaption : "(null)";

    log_info("graphics", "MessageBoxExA: {} - {}", title, text);
    return IDOK;
}

static int WINAPI MessageBoxW_hook(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) {

    auto text = lpText != nullptr ? lpText : L"(null)";
    auto title = lpCaption != nullptr ? lpCaption : L"(null)";

    log_info("graphics", "MessageBoxW: {} - {}", ws2s(title), ws2s(text));
    return IDOK;
}

static int WINAPI MessageBoxExW_hook(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType, WORD wLanguageId) {

    auto text = lpText != nullptr ? lpText : L"(null)";
    auto title = lpCaption != nullptr ? lpCaption : L"(null)";

    log_info("graphics", "MessageBoxExW: {} - {}", ws2s(title), ws2s(text));
    return IDOK;
}

void graphics_init() {
    log_info("graphics", "initializing");

    // init screen resize
    log_info("ScreenResize", "initializing");
    if(cfg::SCREENRESIZE == nullptr){
        cfg::SCREENRESIZE = std::make_unique<cfg::ScreenResize>();
    }

    // init backends
    graphics_d3d9_init();

    // general hooks
    ChangeDisplaySettingsA_orig = detour::iat_try("ChangeDisplaySettingsA", ChangeDisplaySettingsA_hook);
    ChangeDisplaySettingsExA_orig = detour::iat_try("ChangeDisplaySettingsExA", ChangeDisplaySettingsExA_hook);
    ClipCursor_orig = detour::iat_try("ClipCursor", ClipCursor_hook);
    CreateWindowExA_orig = detour::iat_try("CreateWindowExA", CreateWindowExA_hook);
    CreateWindowExW_orig = detour::iat_try("CreateWindowExW", CreateWindowExW_hook);
    EnableWindow_orig = detour::iat_try("EnableWindow", EnableWindow_hook);
    EnumDisplayDevicesA_orig = detour::iat_try("EnumDisplayDevicesA", EnumDisplayDevicesA_hook);
    MoveWindow_orig = detour::iat_try("MoveWindow", MoveWindow_hook);
    PeekMessageA_orig = detour::iat_try("PeekMessageA", PeekMessageA_hook);
    RegisterClassA_orig = detour::iat_try("RegisterClassA", RegisterClassA_hook);
    RegisterClassExA_orig = detour::iat_try("RegisterClassExA", RegisterClassExA_hook);
    RegisterClassW_orig = detour::iat_try("RegisterClassW", RegisterClassW_hook);
    RegisterClassExW_orig = detour::iat_try("RegisterClassExW", RegisterClassExW_hook);
    ShowCursor_orig = detour::iat_try("ShowCursor", ShowCursor_hook);
    SetCursor_orig = detour::iat_try("SetCursor", SetCursor_hook);
    SetWindowLongA_orig = detour::iat_try("SetWindowLongA", SetWindowLongA_hook);
    SetWindowLongW_orig = detour::iat_try("SetWindowLongW", SetWindowLongW_hook);
    SetWindowPos_orig = detour::iat_try("SetWindowPos", SetWindowPos_hook);

    detour::iat_try("MessageBoxA", MessageBoxA_hook);
    detour::iat_try("MessageBoxExA", MessageBoxExA_hook);
    detour::iat_try("MessageBoxW", MessageBoxW_hook);
    detour::iat_try("MessageBoxExW", MessageBoxExW_hook);

    detour::iat_try("SetWindowsHookExA", SetWindowsHookExA_hook);
    detour::iat_try("SetCursorPos", SetCursorPos_hook);
}

void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters) {

    // update window size for a few games
    // TODO: make this work on everything
    if (pPresentationParameters != nullptr && GRAPHICS_WINDOWED
    && (avs::game::is_model({ "K39", "L39", "M39", "JMP", "LDJ" }))) {

        // check dimensions
        auto new_width = pPresentationParameters->BackBufferWidth;
        auto new_height = pPresentationParameters->BackBufferHeight;
        if (new_width != 0 && new_height != 0) {
            RECT rect {};
            GetWindowRect(hWnd, &rect);

            auto width = rect.right - rect.left;
            auto height = rect.bottom - rect.top;

            log_info("graphics", "resized window: {}x{} -> {}x{}", width, height, new_width, new_height);

            DWORD dwStyle = GetWindowLongA(hWnd, GWL_STYLE);
            DWORD dwExStyle = GetWindowLongA(hWnd, GWL_EXSTYLE);
            HMENU menu = GetMenu(hWnd);
            SetRect(&rect, 0, 0, new_width, new_height);
            AdjustWindowRectEx(&rect, dwStyle, (menu != nullptr), dwExStyle);

            // make sure the window does not go off the screen
            if (rect.top < 0) {
                rect.bottom += -rect.top;
                rect.top = 0;
            }

            width = rect.right - rect.left;
            height = rect.bottom - rect.top;

            SetWindowPos(hWnd, HWND_TOP, rect.left, rect.top, width, height, 0);
        }
    }

    // show cursor
    if (GRAPHICS_SHOW_CURSOR) {
        ShowCursor(TRUE);
    }

    // capture mouse
    if (GRAPHICS_CAPTURE_CURSOR) {
        RECT rect {};
        GetWindowRect(hWnd, &rect);
        ClipCursor(&rect);
    }

    // hook window procedure
    if (WNDPROC_ORIG == nullptr) {
        WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
        SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WindowProc));

        // NOLEGACY causes WM_CHAR to be not received
        // reflec beat game engine does not pass WM_CHAR through for some reason (unrelated to SpiceTouch)
        if (!rawinput::NOLEGACY && !(avs::game::is_model({"KBR", "LBR", "MBR"}))) {
            overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = true;
        }
        graphics_capture_initial_window(hWnd);
    }
}

void graphics_add_wnd_proc(WNDPROC wnd_proc) {
    WNDPROC_CUSTOM.push_back(wnd_proc);
}

void graphics_remove_wnd_proc(WNDPROC wndProc) {
    for (size_t x = 0; x < WNDPROC_CUSTOM.size(); x++) {
        if (WNDPROC_CUSTOM[x] == wndProc) {
            WNDPROC_CUSTOM.erase(WNDPROC_CUSTOM.begin() + x);
        }
    }
}

void graphics_hook_subscreen_window(HWND hWnd) {
    // hook window procedure
    if (WSUB_WNDPROC_ORIG == nullptr) {
        WSUB_WNDPROC_ORIG = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(hWnd, GWLP_WNDPROC));
        SetWindowLongPtrA(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(WsubWindowProc));
    }
}

void graphics_screens_register(int screen) {
    std::lock_guard<std::mutex> lock(GRAPHICS_SCREENS_M);

    GRAPHICS_SCREENS.insert(screen);
}

void graphics_screens_unregister(int screen) {
    std::lock_guard<std::mutex> lock(GRAPHICS_SCREENS_M);

    GRAPHICS_SCREENS.erase(screen);
}

void graphics_screens_get(std::vector<int> &screens) {
    std::lock_guard<std::mutex> lock(GRAPHICS_SCREENS_M);

    screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
}

void graphics_screenshot_trigger() {
    GRAPHICS_SCREENSHOT_TRIGGER = true;
}

bool graphics_screenshot_consume() {
    auto flag = GRAPHICS_SCREENSHOT_TRIGGER;
    GRAPHICS_SCREENSHOT_TRIGGER = false;
    return flag;
}

void graphics_capture_trigger(int screen) {
    std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);

    GRAPHICS_CAPTURE_SCREENS.push_back(screen);
}

bool graphics_capture_consume(int *screen) {
    auto flag = !GRAPHICS_CAPTURE_SCREENS.empty();
    if (flag) {
        std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);

        *screen = GRAPHICS_CAPTURE_SCREENS.back();
        GRAPHICS_CAPTURE_SCREENS.pop_back();
    }
    return flag;
}

void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height) {
    GRAPHICS_CAPTURE_BUFFER_M[screen].lock();
    auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
    capture.data.reset(data);
    capture.width = width;
    capture.height = height;
    capture.timestamp = get_performance_milliseconds();
    GRAPHICS_CAPTURE_BUFFER_M[screen].unlock();
    GRAPHICS_CAPTURE_CV[screen].notify_one();
}

void graphics_capture_skip(int screen) {
    GRAPHICS_CAPTURE_CV[screen].notify_one();
}

bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
        bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp,
        int *width, int *height) {

    // wait for capture event
    std::unique_lock<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
    GRAPHICS_CAPTURE_CV[screen].wait(lock, [screen] {
        return GRAPHICS_CAPTURE_BUFFER[screen].data != nullptr;
    });
    auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
    auto capture_data = capture.data;
    auto capture_width = capture.width;
    auto capture_height = capture.height;
    auto capture_timestamp = capture.timestamp;
    lock.unlock();

    // validate data
    if (!capture_data || !capture_width || !capture_height) {
        return false;
    }

    // divide image size
    if (divide > 1) {

        // get new resolution (round up)
        int width_new = (capture_width + divide - 1) / divide;
        int height_new = (capture_height + divide - 1) / divide;

        // allocate new data
        auto data_old = capture_data.get();
        auto data_new = new uint8_t[width_new * height_new * 3];

        // copy pixel data
        int data_y = 0;
        for (int y = 0; y < capture_height; y += divide) {
            int data_y_offset_old = y * capture_width;
            int data_y_offset = data_y * width_new;
            int data_x = 0;
            for (int x = 0; x < capture_width; x += divide) {
                auto pixel_new = &data_new[(data_x + data_y_offset) * 3];
                auto pixel_old = &data_old[(data_y_offset_old + x) * 3];
                memcpy(pixel_new, pixel_old, 3);
                data_x++;
            }
            data_y++;
        }

        // update capture data
        capture_data.reset(data_new);
        capture_width = width_new;
        capture_height = height_new;
    }

    // compress
    auto success = TooJpeg::writeJpeg(
            receiver, capture_data.get(),
            capture_width, capture_height,
            rgb, quality, downsample);

    // status
    if (timestamp) {
        *timestamp = capture_timestamp;
    }
    if (width) {
        *width = capture_width;
    }
    if (height) {
        *height = capture_height;
    }

    // clean up
    return success;
}

std::string graphics_screenshot_genpath() {

    // verify dir path
    if (GRAPHICS_SCREENSHOT_DIR.empty()) {
        return "";
    } else {
        auto last_char = GRAPHICS_SCREENSHOT_DIR.back();
        if (last_char == '\\' || last_char == '/') {
            GRAPHICS_SCREENSHOT_DIR.pop_back();
        }
    }

    // ensure the output directory exists
    if (!fileutils::dir_exists(GRAPHICS_SCREENSHOT_DIR)) {
        if (!fileutils::dir_create_recursive(GRAPHICS_SCREENSHOT_DIR)) {
            log_warning("graphics", "could not create screenshot dir: {}", GRAPHICS_SCREENSHOT_DIR);
            return "";
        }
    }

    // generate date prefix
    auto t_now = std::time(nullptr);
    auto tm_now = *std::gmtime(&t_now);
    auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d"));

    // find next filename
    size_t id = 0;
    while (true) {
        auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id);
        if (!fileutils::file_exists(filepath)) {
            return filepath;
        }

        id++;
    }
}
