
// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601

#include <thread>
#include <mutex>
#include <windowsx.h>

#include "touch.h"

#include "avs/game.h"
#include "external/imgui/imgui.h"
#include "games/jb/jb.h"
#include "hooks/graphics/graphics.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
#include "rawinput/touch.h"
#include "util/circular_buffer.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"

#include "gdi_overlay.h"
#include "handler.h"
#include "touch_gestures.h"
#include "win7.h"
#include "win8.h"

// constants
const char *SPICETOUCH_CLASS_NAME = "spiceTouchClass";
const char *SPICETOUCH_FONT_NAME = "Courier New";
const char *INSERT_CARD_TEXT = "Insert Card";

// settings
static const int TOUCH_EVENT_BUFFER_SIZE = 1024 * 4;
static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2;
static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3;

// timer id for the overlay repaint tick
static const UINT_PTR SPICETOUCH_OVERLAY_TIMER_ID = 1;

// overlay repaint interval; the WinXP-compat build stays at 30 FPS
#if !SPICE_XP
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 60;
#else
static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 30;
#endif // !SPICE_XP

// in mainline spicetools, this was false (show by default)
// in spice2x, this is true (hide by default)
bool SPICETOUCH_CARD_DISABLE = true;
HWND SPICETOUCH_TOUCH_HWND = nullptr;
int SPICETOUCH_TOUCH_X = 0;
int SPICETOUCH_TOUCH_Y = 0;
int SPICETOUCH_TOUCH_WIDTH = 0;
int SPICETOUCH_TOUCH_HEIGHT = 0;

// touch states
std::vector<TouchPoint> TOUCH_POINTS;
std::mutex TOUCH_POINTS_M;
static circular_buffer<TouchEvent> TOUCH_EVENTS(TOUCH_EVENT_BUFFER_SIZE);
std::mutex TOUCH_EVENTS_M;

// general states
static bool SPICETOUCH_INITIALIZED = false;
static bool SPICETOUCH_ATTACHED = false;
static bool SPICETOUCH_ATTACHED_DXHOOK = false;
static HWND SPICETOUCH_HWND = nullptr;
static WNDPROC SPICETOUCH_OLD_PROC = nullptr;
static bool SPICETOUCH_ENABLE_MOUSE = true;
static bool SPICETOUCH_REGISTERED_TOUCH = false;
static bool SPICETOUCH_CALL_OLD_PROC = false;
static std::thread *SPICETOUCH_TOUCH_THREAD = nullptr;
static HFONT SPICETOUCH_FONT;
static RECT SPICETOUCH_CARD_RECT;
static bool SPICETOUCH_CARD_ENABLED = false;
static const char *LOG_MODULE_NAME = "touch";

static TouchHandler *TOUCH_HANDLER = nullptr;

TouchHandler::TouchHandler(std::string name) {
    log_info("touch", "Using touch handler: {}", name);
}

class RawInputTouchHandler : public TouchHandler {
public:
    RawInputTouchHandler() : TouchHandler("rawinput") {
    }
    virtual bool window_register(HWND hWnd) override {
        return true;
    }
    virtual bool window_unregister(HWND hWnd) override {
        return true;
    }
    virtual void handle_message(msg_handler_result&, HWND, UINT, WPARAM, LPARAM) override {
    }
};

/*
 * Add touch event but take care of buffer size
 * Be careful, this doesn't lock the mutex on it's own
 */
void add_touch_event(TouchEvent *te) {

    // check if first threshold is passed
    if (TOUCH_EVENTS.size() > TOUCH_EVENT_BUFFER_THRESHOLD1) {
        switch (te->type) {
            case TOUCH_DOWN:

                // ignore touch down events after first threshold
                return;

            case TOUCH_MOVE:

                // add move event if we're not over the second threshold
                if (TOUCH_EVENTS.size() <= TOUCH_EVENT_BUFFER_THRESHOLD2) {
                    TOUCH_EVENTS.put(*te);
                }

                return;

            case TOUCH_UP:

                // check if buffer is full
                if (TOUCH_EVENTS.full()) {

                    // ignore event
                    return;
                }

                // when the buffer isn't full yet, add the touch up event
                TOUCH_EVENTS.put(*te);
                return;

            default:
                return;
        }
    }

    // add the touch up event
    TOUCH_EVENTS.put(*te);
}

static void touch_initialize() {

    // check if already initialized
    if (SPICETOUCH_INITIALIZED) {
        return;
    }
    SPICETOUCH_INITIALIZED = true;

    if (!RI_MGR) {
        log_fatal(
            LOG_MODULE_NAME,
            "touch_initialize() - RI Manager not available, called too early! "
            "This is a BUG in spice, please file a bug report with log.txt.");
    }

    // initialize handler
    if (rawinput::touch::is_enabled(RI_MGR.get())) {
        TOUCH_HANDLER = new RawInputTouchHandler();
    } else if (Win8Handler::is_available()) {
        TOUCH_HANDLER = new Win8Handler();
    } else if (Win7Handler::is_available()) {
        TOUCH_HANDLER = new Win7Handler();
    } else {
        log_warning(LOG_MODULE_NAME, "no touch handler available");
    }
}

static inline void touch_register_window(HWND hWnd) {

    // register touch handling for window
    if (TOUCH_HANDLER != nullptr) {
        TOUCH_HANDLER->window_register(hWnd);
    }
}

static inline void touch_unregister_window(HWND hWnd) {

    // unregister touch handling for window
    if (TOUCH_HANDLER != nullptr) {
        TOUCH_HANDLER->window_unregister(hWnd);
    }
}

bool is_touch_available(LPCSTR caller) {
    static bool called_once = false;

    if (!RI_MGR) {
        log_fatal(
            LOG_MODULE_NAME,
            "is_touch_available({}) - RI Manager not available, called too early! "
            "This is a BUG in spice, please file a bug report with log.txt.",
            caller);
    }

    // initialize touch handler
    touch_initialize();

    if (!called_once) {
        log_misc("touch", "is_touch_available called by: {}, returning {}",
            caller, (TOUCH_HANDLER != nullptr) ? "TRUE" : "false");
        called_once = true;
    }

    return TOUCH_HANDLER != nullptr;
}

void update_card_button() {

    // check if enabled
    if (!SPICETOUCH_CARD_ENABLED) {
        return;
    }

    // check touch points
    for (TouchPoint touchPoint : TOUCH_POINTS) {
        POINT pt {};
        pt.x = touchPoint.x;
        pt.y = touchPoint.y;

        if (PtInRect(&SPICETOUCH_CARD_RECT, pt) != 0) {
            eamuse_card_insert(0);
        }
    }
}

static void release_all_mouse_touch_points() {
    std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
    std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);

    for (size_t x = 0; x < TOUCH_POINTS.size();) {
        TouchPoint *tp = &TOUCH_POINTS[x];

        if (tp->id == 0u) {
            TouchEvent te {
                .id = tp->id,
                .x = tp->x,
                .y = tp->y,
                .type = TOUCH_UP,
                .mouse = tp->mouse,
            };
            add_touch_event(&te);

            TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
        } else {
            x++;
        }
    }
}

static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {

    // check if touch was registered
    if (!SPICETOUCH_REGISTERED_TOUCH) {
        SPICETOUCH_REGISTERED_TOUCH = true;

        // register the handler when a touch screen is present
        if (is_touch_available("SpiceTouchWndProc")) {
            TOUCH_HANDLER->window_register(hWnd);
        }

        // enable the card button whenever the option is set, even without a touch
        // screen (mouse clicks are handled as touch input and can trigger it)
        if (!SPICETOUCH_CARD_DISABLE) {
            SPICETOUCH_CARD_ENABLED = true;
        }
    }

    // update rawinput touch display resolution
    if (msg == WM_DISPLAYCHANGE) {
        log_info("touch", "detected display mode change");
        rawinput::touch::display_update();
    }

    const auto is_windowed_sub =
        (GRAPHICS_IIDX_WSUB && hWnd == TDJ_SUBSCREEN_WINDOW) ||
         (hWnd == SDVX_SUBSCREEN_WINDOW) ||
         (hWnd == POPN_SUBSCREEN_WINDOW) ||
         (hWnd == GFDM_SUBSCREEN_WINDOW);

    if (msg == WM_CLOSE) { 
        if (is_windowed_sub) {
            log_misc("touch", "ignore WM_CLOSE for subscreen window");
            return false;
        }
    }

    // check messages for dedicated window
    if (SPICETOUCH_TOUCH_THREAD != nullptr) {

        // check if overlay is enabled
        auto overlay_enabled = overlay::OVERLAY
                && overlay::OVERLAY->renderer == overlay::OverlayRenderer::SOFTWARE;

        // we don't want mouse events to interfere
        SPICETOUCH_ENABLE_MOUSE = !overlay_enabled || !overlay::OVERLAY->get_active();

        switch (msg) {
            case WM_MOUSEACTIVATE: {
                // Set the main window as the active window to reactivate the imgui cursor
                HWND parent = GetParent(hWnd);
                if (GetActiveWindow() != parent) {
                    SetActiveWindow(parent);
                }
                return MA_NOACTIVATE;
            }
            case WM_SETCURSOR: {

                // set cursor back to the overlay one
                if (overlay_enabled && LOWORD(lParam) == HTCLIENT && overlay::OVERLAY->update_cursor()) {
                    //return true; TODO: can make cursor invisible?
                }
                break;
            }
            case WM_TIMER: {

                // request a repaint; the frame is composed into an offscreen buffer, so no
                // background erase is needed (bErase = FALSE avoids a transparent flash)
                InvalidateRect(hWnd, NULL, FALSE);
                break;
            }
            case WM_PAINT: {

                // get window rect
                HWND parent = GetParent(hWnd);
                RECT windowRect {}, clientRect {};
                GetWindowRect(parent, &windowRect);
                GetClientRect(parent, &clientRect);

                // adjust to client area
                POINT p1 {.x = clientRect.left, .y = clientRect.top};
                POINT p2 {.x = clientRect.right, .y = clientRect.bottom};
                ClientToScreen(parent, &p1);
                ClientToScreen(parent, &p2);
                windowRect.left = p1.x;
                windowRect.top = p1.y;
                windowRect.right = p2.x;
                windowRect.bottom = p2.y;

                // check if rect needs to update
                RECT windowRectOld {};
                GetWindowRect(hWnd, &windowRectOld);
                if (memcmp(&windowRectOld, &windowRect, sizeof(RECT)) != 0) {
                    SetWindowPos(hWnd, HWND_TOP,
                            windowRect.left, windowRect.top,
                            windowRect.right - windowRect.left,
                            windowRect.bottom - windowRect.top,
                            SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE);
                }

                // render the software overlay before BeginPaint so only GDI composition
                // and the final blit happen while the window is being painted
                int overlay_width = 0, overlay_height = 0;
                uint32_t *overlay_pixels = nullptr;
                bool overlay_pixels_dirty = false;
                if (overlay_enabled) {
                    overlay::OVERLAY->update();
                    overlay::OVERLAY->new_frame();
                    overlay::OVERLAY->render();

                    overlay_pixels = overlay::OVERLAY.get()->sw_get_pixel_data(
                        &overlay_width, &overlay_height);
                    overlay_pixels_dirty = overlay::OVERLAY->sw_pixels_dirty;
                }
                bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active();

                // compose the whole frame into an offscreen back buffer and present it with a
                // single blit; the window never shows a half-erased (transparent) surface
                // mid-paint, which is what caused the occasional flicker at higher frame rates
                PAINTSTRUCT paint {};
                HDC hdc = BeginPaint(hWnd, &paint);

                RECT bufferRect {};
                GetClientRect(hWnd, &bufferRect);
                int buffer_width = bufferRect.right - bufferRect.left;
                int buffer_height = bufferRect.bottom - bufferRect.top;

                HBRUSH color_key_brush =
                    (HBRUSH) GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND);
                HDC draw_dc = touch_gdi_overlay_begin_frame(
                    hdc,
                    color_key_brush,
                    buffer_width,
                    buffer_height,
                    overlay_pixels,
                    overlay_pixels_dirty,
                    overlay_width,
                    overlay_height);
                if (draw_dc == nullptr) {
                    EndPaint(hWnd, &paint);
                    return 0;
                }

                // draw the insert-card button below the jubeat debug overlay; it is
                // hidden while the overlay is active
                if (SPICETOUCH_CARD_ENABLED && SPICETOUCH_FONT != nullptr && !overlay_active) {

                    // create brushes
                    HBRUSH brushBorder = CreateSolidBrush(RGB(0, 196, 0));
                    HBRUSH brushFill = CreateSolidBrush(RGB(255, 192, 203));

                    // get window rect
                    RECT windowRect {};
                    GetWindowRect(hWnd, &windowRect);

                    bool should_rotate = avs::game::is_model({ "J44", "K44" });

                    // create box rect
                    RECT boxRect {};
                    if (should_rotate) {
                        boxRect.left = windowRect.right - 75;
                        boxRect.top = 20;
                        boxRect.right = windowRect.right - 44;
                        boxRect.bottom = 151;
                    } else {
                        boxRect.left = 20;
                        boxRect.top = 44;
                        boxRect.right = 141;
                        boxRect.bottom = 75;
                    }

                    // save box rect for touch input
                    SPICETOUCH_CARD_RECT = boxRect;

                    // draw borders
                    FillRect(draw_dc, &boxRect, brushBorder);

                    // modify box rect
                    boxRect.left += 1;
                    boxRect.top += 1;
                    boxRect.right -= 1;
                    boxRect.bottom -= 1;

                    // fill box
                    FillRect(draw_dc, &boxRect, brushFill);

                    // modify box rect
                    if (should_rotate) {
                        boxRect.left += 5 + 20;
                        boxRect.top += 5;
                    } else {
                        boxRect.left += 5;
                        boxRect.top += 5;
                    }

                    // draw text
                    SelectObject(draw_dc, SPICETOUCH_FONT);
                    SetTextColor(draw_dc, RGB(0, 196, 0));
                    DrawText(draw_dc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP);

                    // delete objects
                    DeleteObject(brushFill);
                    DeleteObject(brushBorder);
                }

#if !SPICE_XP
                // draw the jubeat debug overlay on top (hidden while the overlay is active)
                if (overlay_enabled && !overlay_active && games::jb::touch_debug_overlay_enabled()) {
                    games::jb::touch_draw_debug_overlay(draw_dc);
                }
#endif // !SPICE_XP

                // present the composed frame in a single blit
                touch_gdi_overlay_present(hdc);

                EndPaint(hWnd, &paint);
                return 0;
            }
            case WM_CREATE: {

                // set to layered window
                SetWindowLong(hWnd, GWL_EXSTYLE, GetWindowLong(hWnd, GWL_EXSTYLE) | WS_EX_LAYERED);

                // set alpha value
                SetLayeredWindowAttributes(hWnd, 0, 0xFFu, LWA_ALPHA);
                SetLayeredWindowAttributes(hWnd, RGB(255, 192, 203), 0, LWA_COLORKEY);

                // get window rect
                RECT windowRect {};
                GetWindowRect(hWnd, &windowRect);

                bool should_rotate = avs::game::is_model({ "J44", "K44" });
                auto rotation = should_rotate ? 2700 : 0;

                // load font
                SPICETOUCH_FONT = CreateFont(20, 0, rotation, rotation, FW_NORMAL, FALSE, FALSE, FALSE,
                                             ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                                             NONANTIALIASED_QUALITY, DEFAULT_PITCH,
                                             SPICETOUCH_FONT_NAME);
                if (SPICETOUCH_FONT == nullptr) {
                    log_warning(LOG_MODULE_NAME, "font '{}' could not be loaded", SPICETOUCH_FONT_NAME);
                }

                return 0;
            }
            case WM_DESTROY: {
                touch_gdi_overlay_release();
                if (SPICETOUCH_FONT != nullptr) {
                    DeleteObject(SPICETOUCH_FONT);
                    SPICETOUCH_FONT = nullptr;
                }
                PostQuitMessage(0);
                return 0;
            }
            default:
                break;
        }
    }

    msg_handler_result result {
        .action = ACTION_PASS,
        .return_value = 0,
    };

    // check if imgui is handling this mouse event
    if (is_windowed_sub) {
        // do nothing, don't let imgui hijack clicks on the sub window
        result.action = ACTION_PASS;

    } else if (overlay::OVERLAY != nullptr && overlay::OVERLAY->get_active() && ImGui::GetIO().WantCaptureMouse) {
        result.action = ACTION_RETURN_DEFAULT;

    } else if (TOUCH_HANDLER != nullptr) {
        // call touch handler
        TOUCH_HANDLER->handle_message(result, hWnd, msg, wParam, lParam);
    }

    if (result.action == ACTION_RETURN_STORED) {

        // return the value from the touch handler
        return result.return_value;
    }

    if (result.action == ACTION_PASS) {

        // parse mouse messages
        switch (msg) {
            case WM_LBUTTONDOWN: {
                // check if mouse is enabled
                if (SPICETOUCH_ENABLE_MOUSE) {
                    if (is_mouse_message_from_touchscreen()) {
                        return 0;
                    }

                    // subscribe to mouse messages even when the cursor leaves the window
                    SetCapture(hWnd);

                    // release all old events before inserting a new one
                    release_all_mouse_touch_points();

                    // lock touch points
                    std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
                    std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);

                    // create touch point
                    TouchPoint tp {
                        .id = 0,
                        .x = GET_X_LPARAM(lParam),
                        .y = GET_Y_LPARAM(lParam),
                        .mouse = true,
                        .down_ms = get_performance_milliseconds(),
                    };
                    TOUCH_POINTS.push_back(tp);

                    // add touch down event
                    TouchEvent te {
                        .id = tp.id,
                        .x = tp.x,
                        .y = tp.y,
                        .type = TOUCH_DOWN,
                        .mouse = tp.mouse,
                    };
                    add_touch_event(&te);

                    // card button
                    update_card_button();
                }

                break;
            }
            case WM_MOUSEMOVE: {
                // check if mouse is enabled
                if (SPICETOUCH_ENABLE_MOUSE) {
                    if (is_mouse_message_from_touchscreen()) {
                        return 0;
                    }

                    // lock touch points
                    std::lock_guard<std::mutex> lock_points(TOUCH_POINTS_M);
                    std::lock_guard<std::mutex> lock_events(TOUCH_EVENTS_M);

                    // update point
                    for (auto &x : TOUCH_POINTS) {
                        TouchPoint *tp = &x;

                        // find ID 0
                        if (tp->id == 0u) {

                            // update touch point position
                            tp->x = GET_X_LPARAM(lParam);
                            tp->y = GET_Y_LPARAM(lParam);

                            // add touch move event
                            TouchEvent te {
                                .id = tp->id,
                                .x = tp->x,
                                .y = tp->y,
                                .type = TOUCH_MOVE,
                                .mouse = tp->mouse,
                            };
                            add_touch_event(&te);

                            break;
                        }
                    }
                }

                break;
            }
            case WM_LBUTTONUP: {
                // check if mouse is enabled
                if (SPICETOUCH_ENABLE_MOUSE) {
                    if (is_mouse_message_from_touchscreen()) {
                        return 0;
                    }

                    release_all_mouse_touch_points();
                    if (GetCapture() == hWnd) {
                        ReleaseCapture();
                    }
                }

                break;
            }
            case WM_CAPTURECHANGED:
            case WM_CANCELMODE: {
                // to deal with window losing the capture after SetCapture
                release_all_mouse_touch_points();
                if (msg == WM_CANCELMODE && GetCapture() == hWnd) {
                    ReleaseCapture();
                }
                break;
            }

            default:
                // call original function
                if (SPICETOUCH_CALL_OLD_PROC && SPICETOUCH_OLD_PROC != nullptr) {
                    return SPICETOUCH_OLD_PROC(hWnd, msg, wParam, lParam);
                }
        }
    }

    // clean up
    if (SPICETOUCH_ATTACHED_DXHOOK) {
        return 0;
    } else {
        return DefWindowProc(hWnd, msg, wParam, lParam);
    }
}

void touch_attach_wnd(HWND hWnd) {

    // check if already attached
    if (SPICETOUCH_ATTACHED) {
        return;
    }
    SPICETOUCH_ATTACHED = true;

    // initialize touch handler
    touch_initialize();
    touch_register_window(hWnd);

    // hook window process
    SPICETOUCH_HWND = hWnd;
    SPICETOUCH_OLD_PROC = (WNDPROC) SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR) SpiceTouchWndProc);

    // update rawinput
    rawinput::touch::display_update();
}

void touch_attach_dx_hook() {

    // check if already attached
    if (SPICETOUCH_ATTACHED) {
        return;
    }
    SPICETOUCH_ATTACHED = true;
    SPICETOUCH_ATTACHED_DXHOOK = true;

    // initialize touch handler
    touch_initialize();

    log_info("touch", "touch_attach_dx_hook: attaching SpiceTouchWndProc...");

    // add dx hook
    graphics_add_wnd_proc(SpiceTouchWndProc);

    // update rawinput
    rawinput::touch::display_update();
}

void touch_create_wnd(HWND hWnd, bool overlay) {

    // check if already attached
    if (SPICETOUCH_ATTACHED) {
        return;
    }
    SPICETOUCH_ATTACHED = true;

    // initialize touch handler
    touch_initialize();

    log_info("touch", "touch_create_wnd: creating SPICETOUCH_TOUCH_THREAD...");

    // create thread
    SPICETOUCH_TOUCH_THREAD = new std::thread([hWnd, overlay]() {

        // create class
        WNDCLASSEX wndClass {};
        wndClass.cbSize = sizeof(WNDCLASSEX);
        wndClass.style = 3;
        wndClass.lpfnWndProc = SpiceTouchWndProc;
        wndClass.cbClsExtra = 0;
        wndClass.cbWndExtra = 0;
        wndClass.hInstance = GetModuleHandle(nullptr);
        wndClass.hIcon = nullptr;
        wndClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
        wndClass.hbrBackground = CreateSolidBrush(RGB(255, 192, 203));
        wndClass.lpszMenuName = nullptr;
        wndClass.lpszClassName = SPICETOUCH_CLASS_NAME;
        wndClass.hIconSm = nullptr;

        // register class
        if (!RegisterClassExA(&wndClass)) {
            log_warning(LOG_MODULE_NAME, "failed to register SpiceTouch class: {}", GetLastError());
            return;
        }

        // calculate touch window dimensions 
        update_spicetouch_window_dimensions(hWnd);

        // create window
        HWND touch_window = CreateWindowExA(
                0,
                SPICETOUCH_CLASS_NAME,
                "SpiceTools Touch",
                (DWORD) CW_USEDEFAULT,
                SPICETOUCH_TOUCH_X,
                SPICETOUCH_TOUCH_Y,
                SPICETOUCH_TOUCH_WIDTH,
                SPICETOUCH_TOUCH_HEIGHT,
                hWnd,
                nullptr,
                GetModuleHandle(nullptr),
                nullptr
        );

        log_misc(
            LOG_MODULE_NAME, "create SpiceTouch window ({}x{} @ {}, {})", 
            SPICETOUCH_TOUCH_WIDTH, SPICETOUCH_TOUCH_HEIGHT, SPICETOUCH_TOUCH_X, SPICETOUCH_TOUCH_Y);

        // check window
        if (touch_window == nullptr) {
            log_warning(LOG_MODULE_NAME, "failed to create SpiceTouch window: {}", GetLastError());
            return;
        }

        // save reference
        SPICETOUCH_TOUCH_HWND = touch_window;

        // window settings
        ShowWindow(touch_window, SW_SHOWNOACTIVATE);
        UpdateWindow(touch_window);

        // disable the OS touch contact visualization for our own touch window
        disable_touch_gestures(touch_window);

        // register
        touch_register_window(touch_window);

        // overlay
        if (overlay && overlay::ENABLED) {
            if (overlay::OVERLAY) {
                log_warning(LOG_MODULE_NAME, "requested overlay, but already existing");
            } else {

                // create instance
                overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window));

                // draw overlay repaint timer (30 FPS on WinXP, 60 FPS otherwise)
                SetTimer(touch_window, SPICETOUCH_OVERLAY_TIMER_ID, SPICETOUCH_OVERLAY_TIMER_MS, NULL);
            }
        }

        // window loop
        MSG msg {};
        while (GetMessage(&msg, nullptr, 0, 0) && SPICETOUCH_TOUCH_THREAD != nullptr) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        // kill overlay
        if (overlay) {
            overlay::OVERLAY.reset();
        }

        // unregister
        touch_unregister_window(touch_window);
        log_misc(LOG_MODULE_NAME, "window closed");
    });

    // update rawinput
    rawinput::touch::display_update();
}

void touch_detach() {

    // remove window process hook
    if (SPICETOUCH_HWND != nullptr) {
        touch_unregister_window(SPICETOUCH_HWND);

        SetWindowLongPtr(SPICETOUCH_HWND, GWLP_WNDPROC, (LONG_PTR) SPICETOUCH_OLD_PROC);
        SPICETOUCH_HWND = nullptr;
        SPICETOUCH_OLD_PROC = nullptr;
    }

    // remove dx hook
    graphics_remove_wnd_proc(SpiceTouchWndProc);
    SPICETOUCH_TOUCH_THREAD = nullptr;
    SPICETOUCH_ATTACHED_DXHOOK = false;
    SPICETOUCH_ATTACHED = false;
}

void touch_write_points(std::vector<TouchPoint> *touch_points) {

    // check size first
    if (touch_points->empty()) {
        return;
    }

    // lock
    std::lock_guard<std::mutex> points(TOUCH_POINTS_M);
    std::lock_guard<std::mutex> events(TOUCH_EVENTS_M);

    // iterate through all the provided touch points
    for (auto &tp : *touch_points) {

        // find touch point to update
        bool found = false;
        for (auto &arr_tp : TOUCH_POINTS) {
            if (arr_tp.id == tp.id) {

                // mark as found
                found = true;

                // update position
                arr_tp.x = tp.x;
                arr_tp.y = tp.y;

                // add touch move event
                TouchEvent te {
                    .id = tp.id,
                    .x = tp.x,
                    .y = tp.y,
                    .type = TOUCH_MOVE,
                    .mouse = tp.mouse,
                };
                add_touch_event(&te);
            }
        }

        // create new touch point when not found
        if (!found) {

            // stamp the landing time so debounce can measure the contact's age
            tp.down_ms = get_performance_milliseconds();

            // add touch point
            TOUCH_POINTS.push_back(tp);

            // add touch down event
            TouchEvent te {
                .id = tp.id,
                .x = tp.x,
                .y = tp.y,
                .type = TOUCH_DOWN,
                .mouse = tp.mouse,
            };
            add_touch_event(&te);
        }
    }
}

void touch_remove_points(std::vector<DWORD> *touch_point_ids) {

    // check size first
    if (touch_point_ids->empty()) {
        return;
    }

    // lock
    std::lock_guard<std::mutex> points(TOUCH_POINTS_M);
    std::lock_guard<std::mutex> events(TOUCH_EVENTS_M);

    // find the touch points to remove
    for (auto id : *touch_point_ids) {
        for (size_t x = 0; x < TOUCH_POINTS.size(); x++) {
            TouchPoint *tp = &TOUCH_POINTS[x];

            // check if the IDs match
            if (tp->id == id) {

                // add touch up event
                TouchEvent te {
                    .id = id,
                    .x = tp->x,
                    .y = tp->y,
                    .type = TOUCH_UP,
                    .mouse = tp->mouse,
                };
                add_touch_event(&te);

                // delete touch point
                TOUCH_POINTS.erase(TOUCH_POINTS.begin() + x);
                break;
            }
        }
    }
}

void touch_get_points(std::vector<TouchPoint> &touch_points, bool overlay) {

    // update timeouts
    if (RI_MGR) {
        rawinput::touch::update_timeouts(RI_MGR.get());
    }

    // overlay override
    if (!overlay &&
        overlay::OVERLAY &&
        overlay::OVERLAY->get_active() &&
        !overlay::OVERLAY->has_subscreen_touch_transform() &&
        ImGui::GetIO().WantCaptureMouse) {

        return;
    }

    // lock
    std::lock_guard<std::mutex> lock(TOUCH_POINTS_M);

    // append touch points
    touch_points.insert(touch_points.end(), TOUCH_POINTS.begin(), TOUCH_POINTS.end());
}

void touch_get_events(std::vector<TouchEvent> &touch_events, bool overlay) {

    // update timeouts
    if (RI_MGR) {
        rawinput::touch::update_timeouts(RI_MGR.get());
    }

    // lock
    std::lock_guard<std::mutex> lock(TOUCH_EVENTS_M);

    // overlay override
    if (!overlay &&
        overlay::OVERLAY &&
        overlay::OVERLAY->get_active() &&
        !overlay::OVERLAY->has_subscreen_touch_transform() &&
        ImGui::GetIO().WantCaptureMouse) {

        TOUCH_EVENTS.reset();
        return;
    }

    // append touch points
    while (!TOUCH_EVENTS.empty()) {
        touch_events.push_back(TOUCH_EVENTS.get());
    }
}

void update_spicetouch_window_dimensions(HWND hWnd) {
    RECT clientRect {};
    GetClientRect(hWnd, &clientRect);

    // adjust to client area
    POINT topleft {.x = 0, .y = 0};
    ClientToScreen(hWnd, &topleft);
    POINT bottomright {.x = clientRect.right, .y = clientRect.bottom};
    ClientToScreen(hWnd, &bottomright);

    SPICETOUCH_TOUCH_X = topleft.x;
    SPICETOUCH_TOUCH_Y = topleft.y;
    SPICETOUCH_TOUCH_WIDTH = bottomright.x - topleft.x;
    SPICETOUCH_TOUCH_HEIGHT = bottomright.y - topleft.y;
}

// https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
bool is_mouse_message_from_touchscreen() {
    constexpr ULONG_PTR MI_WP_SIGNATURE = 0xFF515700;
    constexpr ULONG_PTR SIGNATURE_MASK = 0xFFFFFF00;
    return (GetMessageExtraInfo() & SIGNATURE_MASK) == MI_WP_SIGNATURE;
}
