/* libUIOHook: Cross-platform keyboard and mouse hooking from userland.
 * Copyright (C) 2006-2023 Alexander Barker.  All Rights Reserved.
 * https://github.com/kwhat/libuiohook/
 *
 * libUIOHook 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.
 *
 * libUIOHook 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 General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <inttypes.h>
#include <uiohook.h>
#include <windows.h>

#include "input_helper.h"
#include "logger.h"

#ifndef FOREGROUND_TIMER_MS
#define FOREGROUND_TIMER_MS 83 // 12 fps
#endif

// Thread and hook handles.
static DWORD hook_thread_id = 0;
static HHOOK keyboard_event_hhook = NULL, mouse_event_hhook = NULL;
static HWINEVENTHOOK win_foreground_hhook = NULL, win_minimizeend_hhook = NULL;
static UINT_PTR foreground_timer = 0;

static HWND foreground_window = NULL;
static bool is_blocked_by_uipi = true;

static UINT WM_UIOHOOK_UIPI_TEST = WM_NULL;

// The handle to the DLL module pulled in DllMain on DLL_PROCESS_ATTACH.
extern HINSTANCE hInst;

// Modifiers for tracking key masks.
static unsigned short int current_modifiers = 0x0000;

// Click count globals.
static unsigned short click_count = 0;
static DWORD click_time = 0;
static unsigned short int click_button = MOUSE_NOBUTTON;
static POINT last_click;

// Static event memory.
static uiohook_event event;

// Event dispatch callback.
static dispatcher_t dispatcher = NULL;

UIOHOOK_API void hook_set_dispatch_proc(dispatcher_t dispatch_proc) {
    logger(LOG_LEVEL_DEBUG, "%s [%u]: Setting new dispatch callback to %#p.\n",
            __FUNCTION__, __LINE__, dispatch_proc);

    dispatcher = dispatch_proc;
}

// Send out an event if a dispatcher was set.
static inline void dispatch_event(uiohook_event *const event) {
    if (dispatcher != NULL) {
        logger(LOG_LEVEL_DEBUG, "%s [%u]: Dispatching event type %u.\n",
                __FUNCTION__, __LINE__, event->type);

        dispatcher(event);
    } else {
        logger(LOG_LEVEL_WARN, "%s [%u]: No dispatch callback set!\n",
                __FUNCTION__, __LINE__);
    }
}

static void initialize_modifiers();

// Set the native modifier mask for future events.
static inline void set_modifier_mask(unsigned short int mask) {
    current_modifiers |= mask;
}

// Unset the native modifier mask for future events.
static inline void unset_modifier_mask(unsigned short int mask) {
    current_modifiers &= ~mask;
}

// Get the current native modifier mask state.
static inline unsigned short int get_modifiers() {
    if (is_blocked_by_uipi) {
        initialize_modifiers();
        is_blocked_by_uipi = false;
    }
    return current_modifiers;
}

// Initialize the modifier mask to the current modifiers.
static void initialize_modifiers() {
    current_modifiers = 0x0000;

    // NOTE We are checking the high order bit, so it will be < 0 for a singed short.
    if (GetAsyncKeyState(VK_LSHIFT)   < 0) { set_modifier_mask(MASK_SHIFT_L);     }
    if (GetAsyncKeyState(VK_RSHIFT)   < 0) { set_modifier_mask(MASK_SHIFT_R);     }
    if (GetAsyncKeyState(VK_LCONTROL) < 0) { set_modifier_mask(MASK_CTRL_L);      }
    if (GetAsyncKeyState(VK_RCONTROL) < 0) { set_modifier_mask(MASK_CTRL_R);      }
    if (GetAsyncKeyState(VK_LMENU)    < 0) { set_modifier_mask(MASK_ALT_L);       }
    if (GetAsyncKeyState(VK_RMENU)    < 0) { set_modifier_mask(MASK_ALT_R);       }
    if (GetAsyncKeyState(VK_LWIN)     < 0) { set_modifier_mask(MASK_META_L);      }
    if (GetAsyncKeyState(VK_RWIN)     < 0) { set_modifier_mask(MASK_META_R);      }

    if (GetAsyncKeyState(VK_LBUTTON)  < 0) { set_modifier_mask(MASK_BUTTON1);     }
    if (GetAsyncKeyState(VK_RBUTTON)  < 0) { set_modifier_mask(MASK_BUTTON2);     }
    if (GetAsyncKeyState(VK_MBUTTON)  < 0) { set_modifier_mask(MASK_BUTTON3);     }
    if (GetAsyncKeyState(VK_XBUTTON1) < 0) { set_modifier_mask(MASK_BUTTON4);     }
    if (GetAsyncKeyState(VK_XBUTTON2) < 0) { set_modifier_mask(MASK_BUTTON5);     }

    if (GetAsyncKeyState(VK_NUMLOCK)  < 0) { set_modifier_mask(MASK_NUM_LOCK);    }
    if (GetAsyncKeyState(VK_CAPITAL)  < 0) { set_modifier_mask(MASK_CAPS_LOCK);   }
    if (GetAsyncKeyState(VK_SCROLL)   < 0) { set_modifier_mask(MASK_SCROLL_LOCK); }
}

void check_and_update_uipi_state(HWND hwnd) {
    SetLastError(ERROR_SUCCESS);
    PostMessage(hwnd, WM_UIOHOOK_UIPI_TEST, 0, 0);
    if (GetLastError() == ERROR_ACCESS_DENIED) {
        is_blocked_by_uipi = true;
    }
}

void unregister_running_hooks() {
    // Stop the event hook and any timer still running.
    if (win_foreground_hhook != NULL) {
        UnhookWinEvent(win_foreground_hhook);
        win_foreground_hhook = NULL;
    }

    if (win_minimizeend_hhook != NULL) {
        UnhookWinEvent(win_minimizeend_hhook);
        win_minimizeend_hhook = NULL;
    }

    if (foreground_timer != 0) {
        KillTimer(NULL, foreground_timer);
        foreground_timer = 0;
    }

    // Destroy the native hooks.
    if (keyboard_event_hhook != NULL) {
        UnhookWindowsHookEx(keyboard_event_hhook);
        keyboard_event_hhook = NULL;
    }

    if (mouse_event_hhook != NULL) {
        UnhookWindowsHookEx(mouse_event_hhook);
        mouse_event_hhook = NULL;
    }
}

void hook_start_proc() {
    // Initialize native input helper functions.
    load_input_helper();

    // Get the local system time in UNIX epoch form.
    uint64_t timestamp = GetMessageTime();

    // Populate the hook start event.
    event.time = timestamp;
    event.reserved = 0x00;

    event.type = EVENT_HOOK_ENABLED;
    event.mask = 0x00;

    // Fire the hook start event.
    dispatch_event(&event);
}

void hook_stop_proc() {
    // Get the local system time in UNIX epoch form.
    uint64_t timestamp = GetMessageTime();

    // Populate the hook stop event.
    event.time = timestamp;
    event.reserved = 0x00;

    event.type = EVENT_HOOK_DISABLED;
    event.mask = 0x00;

    // Fire the hook stop event.
    dispatch_event(&event);

    // Deinitialize native input helper functions.
    unload_input_helper();
}

static void process_key_pressed(KBDLLHOOKSTRUCT *kbhook) {
    // Check and setup modifiers.
    if      (kbhook->vkCode == VK_LSHIFT)   { set_modifier_mask(MASK_SHIFT_L);     }
    else if (kbhook->vkCode == VK_RSHIFT)   { set_modifier_mask(MASK_SHIFT_R);     }
    else if (kbhook->vkCode == VK_LCONTROL) { set_modifier_mask(MASK_CTRL_L);      }
    else if (kbhook->vkCode == VK_RCONTROL) { set_modifier_mask(MASK_CTRL_R);      }
    else if (kbhook->vkCode == VK_LMENU)    { set_modifier_mask(MASK_ALT_L);       }
    else if (kbhook->vkCode == VK_RMENU)    { set_modifier_mask(MASK_ALT_R);       }
    else if (kbhook->vkCode == VK_LWIN)     { set_modifier_mask(MASK_META_L);      }
    else if (kbhook->vkCode == VK_RWIN)     { set_modifier_mask(MASK_META_R);      }
    else if (kbhook->vkCode == VK_NUMLOCK)  { set_modifier_mask(MASK_NUM_LOCK);    }
    else if (kbhook->vkCode == VK_CAPITAL)  { set_modifier_mask(MASK_CAPS_LOCK);   }
    else if (kbhook->vkCode == VK_SCROLL)   { set_modifier_mask(MASK_SCROLL_LOCK); }

    // Populate key pressed event.
    event.time = kbhook->time;
    event.reserved = 0x00;

    event.type = EVENT_KEY_PRESSED;
    event.mask = get_modifiers();

    event.data.keyboard.keycode = keycode_to_scancode(kbhook->vkCode, kbhook->flags);
    event.data.keyboard.rawcode = (uint16_t) kbhook->vkCode;
    event.data.keyboard.keychar = CHAR_UNDEFINED;

    logger(LOG_LEVEL_DEBUG, "%s [%u]: Key %#X pressed. (%#X)\n",
            __FUNCTION__, __LINE__, event.data.keyboard.keycode, event.data.keyboard.rawcode);

    // Populate key pressed event.
    dispatch_event(&event);

    if ((event.mask & (MASK_CTRL)) &&
        (event.mask & (MASK_ALT)) &&
        (event.data.keyboard.keycode == VC_DELETE)) {
        current_modifiers = 0;
    }

    // If the pressed event was not consumed...
    if (event.reserved ^ 0x01) {
    }
}

static void process_key_released(KBDLLHOOKSTRUCT *kbhook) {
    // Check and setup modifiers.
    if      (kbhook->vkCode == VK_LSHIFT)   { unset_modifier_mask(MASK_SHIFT_L);     }
    else if (kbhook->vkCode == VK_RSHIFT)   { unset_modifier_mask(MASK_SHIFT_R);     }
    else if (kbhook->vkCode == VK_LCONTROL) { unset_modifier_mask(MASK_CTRL_L);      }
    else if (kbhook->vkCode == VK_RCONTROL) { unset_modifier_mask(MASK_CTRL_R);      }
    else if (kbhook->vkCode == VK_LMENU)    { unset_modifier_mask(MASK_ALT_L);       }
    else if (kbhook->vkCode == VK_RMENU)    { unset_modifier_mask(MASK_ALT_R);       }
    else if (kbhook->vkCode == VK_LWIN)     { unset_modifier_mask(MASK_META_L);      }
    else if (kbhook->vkCode == VK_RWIN)     { unset_modifier_mask(MASK_META_R);      }
    else if (kbhook->vkCode == VK_NUMLOCK)  { unset_modifier_mask(MASK_NUM_LOCK);    }
    else if (kbhook->vkCode == VK_CAPITAL)  { unset_modifier_mask(MASK_CAPS_LOCK);   }
    else if (kbhook->vkCode == VK_SCROLL)   { unset_modifier_mask(MASK_SCROLL_LOCK); }

    // Populate key pressed event.
    event.time = kbhook->time;
    event.reserved = 0x00;

    event.type = EVENT_KEY_RELEASED;
    event.mask = get_modifiers();

    event.data.keyboard.keycode = keycode_to_scancode(kbhook->vkCode, kbhook->flags);
    event.data.keyboard.rawcode = (uint16_t) kbhook->vkCode;
    event.data.keyboard.keychar = CHAR_UNDEFINED;

    logger(LOG_LEVEL_DEBUG, "%s [%u]: Key %#X released. (%#X)\n",
            __FUNCTION__, __LINE__, event.data.keyboard.keycode, event.data.keyboard.rawcode);

    // Fire key released event.
    dispatch_event(&event);
}

LRESULT CALLBACK keyboard_hook_event_proc(int nCode, WPARAM wParam, LPARAM lParam) {
    KBDLLHOOKSTRUCT *kbhook = (KBDLLHOOKSTRUCT *) lParam;
    switch (wParam) {
        case WM_KEYDOWN:
        case WM_SYSKEYDOWN:
            process_key_pressed(kbhook);
            break;

        case WM_KEYUP:
        case WM_SYSKEYUP:
            process_key_released(kbhook);
            break;

        default:
            // In theory this *should* never execute.
            logger(LOG_LEVEL_DEBUG, "%s [%u]: Unhandled Windows keyboard event: %#X.\n",
                    __FUNCTION__, __LINE__, (unsigned int) wParam);
            break;
    }

    LRESULT hook_result = -1;
    if (nCode < 0 || event.reserved ^ 0x01) {
        hook_result = CallNextHookEx(keyboard_event_hhook, nCode, wParam, lParam);
    } else {
        logger(LOG_LEVEL_DEBUG, "%s [%u]: Consuming the current event. (%li)\n",
                __FUNCTION__, __LINE__, (long) hook_result);
    }

    return hook_result;
}


static void process_button_pressed(MSLLHOOKSTRUCT *mshook, uint16_t button) {
    DWORD timestamp = mshook->time;

    // Track the number of clicks, the button must match the previous button.
    if (button == click_button && (long int) (timestamp - click_time) <= hook_get_multi_click_time()) {
        if (click_count < USHRT_MAX) {
            click_count++;
        } else {
            logger(LOG_LEVEL_WARN, "%s [%u]: Click count overflow detected!\n",
                    __FUNCTION__, __LINE__);
        }
    } else {
        // Reset the click count.
        click_count = 1;

        // Set the previous button.
        click_button = button;
    }

    // Save this events time to calculate the click_count.
    click_time = timestamp;

    // Store the last click point.
    last_click.x = mshook->pt.x;
    last_click.y = mshook->pt.y;

    // Populate mouse pressed event.
    event.time = timestamp;
    event.reserved = 0x00;

    event.type = EVENT_MOUSE_PRESSED;
    event.mask = get_modifiers();

    event.data.mouse.button = button;
    event.data.mouse.clicks = click_count;

    event.data.mouse.x = (int16_t) mshook->pt.x;
    event.data.mouse.y = (int16_t) mshook->pt.y;

    logger(LOG_LEVEL_DEBUG, "%s [%u]: Button %u  pressed %u time(s). (%u, %u)\n",
            __FUNCTION__, __LINE__, event.data.mouse.button, event.data.mouse.clicks,
            event.data.mouse.x, event.data.mouse.y);

    // Fire mouse pressed event.
    dispatch_event(&event);
}

static void process_button_released(MSLLHOOKSTRUCT *mshook, uint16_t button) {
    // Populate mouse released event.
    event.time = mshook->time;
    event.reserved = 0x00;

    event.type = EVENT_MOUSE_RELEASED;
    event.mask = get_modifiers();

    event.data.mouse.button = button;
    event.data.mouse.clicks = click_count;

    event.data.mouse.x = (int16_t) mshook->pt.x;
    event.data.mouse.y = (int16_t) mshook->pt.y;

    logger(LOG_LEVEL_DEBUG, "%s [%u]: Button %u released %u time(s). (%u, %u)\n",
            __FUNCTION__, __LINE__, event.data.mouse.button,
            event.data.mouse.clicks,
            event.data.mouse.x, event.data.mouse.y);

    // Fire mouse released event.
    dispatch_event(&event);

    // If the pressed event was not consumed...
    if (event.reserved ^ 0x01 && last_click.x == mshook->pt.x && last_click.y == mshook->pt.y) {
        // Populate mouse clicked event.
        event.time = mshook->time;
        event.reserved = 0x00;

        event.type = EVENT_MOUSE_CLICKED;
        event.mask = get_modifiers();

        event.data.mouse.button = button;
        event.data.mouse.clicks = click_count;
        event.data.mouse.x = (int16_t) mshook->pt.x;
        event.data.mouse.y = (int16_t) mshook->pt.y;

        logger(LOG_LEVEL_DEBUG, "%s [%u]: Button %u clicked %u time(s). (%u, %u)\n",
                __FUNCTION__, __LINE__, event.data.mouse.button, event.data.mouse.clicks,
                event.data.mouse.x, event.data.mouse.y);

        // Fire mouse clicked event.
        dispatch_event(&event);
    }

    // Reset the number of clicks.
    if (button == click_button && (long int) (event.time - click_time) > hook_get_multi_click_time()) {
        // Reset the click count.
        click_count = 0;
    }
}

static void process_mouse_moved(MSLLHOOKSTRUCT *mshook) {
    uint64_t timestamp = mshook->time;

    // We received a mouse move event with the mouse actually moving.
    // This verifies that the mouse was moved after being depressed.
    if (last_click.x != mshook->pt.x || last_click.y != mshook->pt.y) {
        // Reset the click count.
        if (click_count != 0 && (long) (timestamp - click_time) > hook_get_multi_click_time()) {
            click_count = 0;
        }

        // Populate mouse move event.
        event.time = timestamp;
        event.reserved = 0x00;

        event.mask = get_modifiers();

        // Check the modifier mask range for MASK_BUTTON1 - 5.
        bool mouse_dragged = event.mask & (MASK_BUTTON1 | MASK_BUTTON2 | MASK_BUTTON3 | MASK_BUTTON4 | MASK_BUTTON5);
        if (mouse_dragged) {
            // Create Mouse Dragged event.
            event.type = EVENT_MOUSE_DRAGGED;
        } else {
            // Create a Mouse Moved event.
            event.type = EVENT_MOUSE_MOVED;
        }

        event.data.mouse.button = MOUSE_NOBUTTON;
        event.data.mouse.clicks = click_count;
        event.data.mouse.x = (int16_t) mshook->pt.x;
        event.data.mouse.y = (int16_t) mshook->pt.y;

        logger(LOG_LEVEL_DEBUG, "%s [%u]: Mouse %s to %u, %u.\n",
                __FUNCTION__, __LINE__,  mouse_dragged ? "dragged" : "moved",
                event.data.mouse.x, event.data.mouse.y);

        // Fire mouse move event.
        dispatch_event(&event);
    }
}

static void process_mouse_wheel(MSLLHOOKSTRUCT *mshook, uint8_t direction) {
    // Track the number of clicks.
    // Reset the click count and previous button.
    click_count = 1;
    click_button = MOUSE_NOBUTTON;

    // Populate mouse wheel event.
    event.time = mshook->time;
    event.reserved = 0x00;

    event.type = EVENT_MOUSE_WHEEL;
    event.mask = get_modifiers();

    event.data.wheel.clicks = click_count;
    event.data.wheel.x = (int16_t) mshook->pt.x;
    event.data.wheel.y = (int16_t) mshook->pt.y;

    event.data.wheel.rotation = get_scroll_wheel_rotation(mshook->mouseData, direction);

    UINT  uiAction = SPI_GETWHEELSCROLLCHARS;
    if (direction == WHEEL_VERTICAL_DIRECTION) {
        uiAction = SPI_GETWHEELSCROLLLINES;
    }

    UINT wheel_amount = 3;
    if (SystemParametersInfo(uiAction, 0, &wheel_amount, 0)) {
        if (wheel_amount == WHEEL_PAGESCROLL) {
            event.data.wheel.type = WHEEL_BLOCK_SCROLL;
            event.data.wheel.amount = 1;
        } else {
            event.data.wheel.type = WHEEL_UNIT_SCROLL;
            event.data.wheel.amount = (uint16_t) wheel_amount;
        }

        // Set the direction based on what event was received.
        event.data.wheel.direction = direction;

        logger(LOG_LEVEL_DEBUG, "%s [%u]: Mouse wheel type %u, rotated %i units in the %u direction at %u, %u.\n",
                __FUNCTION__, __LINE__,
                event.data.wheel.type,
                event.data.wheel.amount * event.data.wheel.rotation,
                event.data.wheel.direction,
                event.data.wheel.x, event.data.wheel.y);

        // Fire mouse wheel event.
        dispatch_event(&event);
    } else {
        event.reserved = 0x01;
        logger(LOG_LEVEL_WARN, "%s [%u]: SystemParametersInfo() failed, event will be ignored.\n",
                __FUNCTION__, __LINE__);
    }
}

LRESULT CALLBACK mouse_hook_event_proc(int nCode, WPARAM wParam, LPARAM lParam) {
    MSLLHOOKSTRUCT *mshook = (MSLLHOOKSTRUCT *) lParam;
    switch (wParam) {
        case WM_LBUTTONDOWN:
            set_modifier_mask(MASK_BUTTON1);
            process_button_pressed(mshook, MOUSE_BUTTON1);
            break;

        case WM_RBUTTONDOWN:
            set_modifier_mask(MASK_BUTTON2);
            process_button_pressed(mshook, MOUSE_BUTTON2);
            break;

        case WM_MBUTTONDOWN:
            set_modifier_mask(MASK_BUTTON3);
            process_button_pressed(mshook, MOUSE_BUTTON3);
            break;

        case WM_XBUTTONDOWN:
        case WM_NCXBUTTONDOWN:
            if (HIWORD(mshook->mouseData) == XBUTTON1) {
                set_modifier_mask(MASK_BUTTON4);
                process_button_pressed(mshook, MOUSE_BUTTON4);
            } else if (HIWORD(mshook->mouseData) == XBUTTON2) {
                set_modifier_mask(MASK_BUTTON5);
                process_button_pressed(mshook, MOUSE_BUTTON5);
            } else {
                // Extra mouse buttons.
                uint16_t button = HIWORD(mshook->mouseData);

                // Add support for mouse 4 & 5.
                if (button == 4) {
                    set_modifier_mask(MOUSE_BUTTON4);
                } else if (button == 5) {
                    set_modifier_mask(MOUSE_BUTTON5);
                }

                process_button_pressed(mshook, button);
            }
            break;


        case WM_LBUTTONUP:
            unset_modifier_mask(MASK_BUTTON1);
            process_button_released(mshook, MOUSE_BUTTON1);
            break;

        case WM_RBUTTONUP:
            unset_modifier_mask(MASK_BUTTON2);
            process_button_released(mshook, MOUSE_BUTTON2);
            break;

        case WM_MBUTTONUP:
            unset_modifier_mask(MASK_BUTTON3);
            process_button_released(mshook, MOUSE_BUTTON3);
            break;

        case WM_XBUTTONUP:
        case WM_NCXBUTTONUP:
            if (HIWORD(mshook->mouseData) == XBUTTON1) {
                unset_modifier_mask(MASK_BUTTON4);
                process_button_released(mshook, MOUSE_BUTTON4);
            } else if (HIWORD(mshook->mouseData) == XBUTTON2) {
                unset_modifier_mask(MASK_BUTTON5);
                process_button_released(mshook, MOUSE_BUTTON5);
            } else {
                // Extra mouse buttons.
                uint16_t button = HIWORD(mshook->mouseData);

                // Add support for mouse 4 & 5.
                if (button == 4) {
                    unset_modifier_mask(MOUSE_BUTTON4);
                } else if (button == 5) {
                    unset_modifier_mask(MOUSE_BUTTON5);
                }

                process_button_released(mshook, MOUSE_BUTTON5);
            }
            break;

        case WM_MOUSEMOVE:
            process_mouse_moved(mshook);
            break;

        case WM_MOUSEWHEEL:
            process_mouse_wheel(mshook, WHEEL_VERTICAL_DIRECTION);
            break;

        /* For horizontal scroll wheel support.
         * NOTE Windows >= Vista
         * case 0x020E:
         */
        case WM_MOUSEHWHEEL:
            process_mouse_wheel(mshook, WHEEL_HORIZONTAL_DIRECTION);
            break;

        default:
            // In theory this *should* never execute.
            logger(LOG_LEVEL_DEBUG, "%s [%u]: Unhandled Windows mouse event: %#X.\n",
                    __FUNCTION__, __LINE__, (unsigned int) wParam);
            break;
    }

    LRESULT hook_result = -1;
    if (nCode < 0 || event.reserved ^ 0x01) {
        hook_result = CallNextHookEx(mouse_event_hhook, nCode, wParam, lParam);
    } else {
        logger(LOG_LEVEL_DEBUG, "%s [%u]: Consuming the current event. (%li)\n",
                __FUNCTION__, __LINE__, (long) hook_result);
    }

    return hook_result;
}


// Callback function that handles events.
void CALLBACK win_hook_event_proc(HWINEVENTHOOK hook, DWORD event, HWND hWnd, LONG idObject, LONG idChild, DWORD dwEventThread, DWORD dwmsEventTime) {
    foreground_window = hWnd;
    check_and_update_uipi_state(hWnd);
}

static VOID CALLBACK foreground_timer_proc(HWND _hwnd, UINT msg, UINT_PTR timerId, DWORD dwmsEventTime)
{
    HWND system_foreground = GetForegroundWindow();

    if (foreground_window != system_foreground) {
        foreground_window = system_foreground;
        check_and_update_uipi_state(system_foreground);
    }
}


UIOHOOK_API int hook_run() {
    int status = UIOHOOK_FAILURE;

    // Set the thread id we want to signal later.
    hook_thread_id = GetCurrentThreadId();

    // Spot check the hInst in case the library was statically linked and DllMain
    // did not receive a pointer on load.
    if (hInst == NULL) {
        logger(LOG_LEVEL_WARN, "%s [%u]: hInst was not set by DllMain().\n",
                __FUNCTION__, __LINE__);

        hInst = GetModuleHandle(NULL);
        if (hInst != NULL) {
            // Initialize native input helper functions.
            load_input_helper();
        } else {
            logger(LOG_LEVEL_ERROR, "%s [%u]: Could not determine hInst for SetWindowsHookEx()! (%#lX)\n",
                    __FUNCTION__, __LINE__, (unsigned long) GetLastError());

            status = UIOHOOK_ERROR_GET_MODULE_HANDLE;
        }
    }

    // Create the native hooks.
    keyboard_event_hhook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook_event_proc, hInst, 0);
    mouse_event_hhook = SetWindowsHookEx(WH_MOUSE_LL, mouse_hook_event_proc, hInst, 0);

    win_foreground_hhook = SetWinEventHook(
            EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
            NULL, win_hook_event_proc, 0, 0, WINEVENT_OUTOFCONTEXT);
    win_minimizeend_hhook = SetWinEventHook(
            EVENT_SYSTEM_MINIMIZEEND, EVENT_SYSTEM_MINIMIZEEND,
            NULL, win_hook_event_proc, 0, 0, WINEVENT_OUTOFCONTEXT);
    foreground_timer = SetTimer(NULL, 0, FOREGROUND_TIMER_MS, foreground_timer_proc);

    WM_UIOHOOK_UIPI_TEST = RegisterWindowMessage("UIOHOOK_UIPI_TEST");

    foreground_window = GetForegroundWindow();
    is_blocked_by_uipi = true; // init modifiers

    // If we did not encounter a problem, start processing events.
    if (keyboard_event_hhook != NULL && mouse_event_hhook != NULL) {
        if (win_foreground_hhook == NULL || win_minimizeend_hhook == NULL) {
            logger(LOG_LEVEL_WARN, "%s [%u]: SetWinEventHook() failed!\n",
                    __FUNCTION__, __LINE__);
        }

        logger(LOG_LEVEL_DEBUG, "%s [%u]: SetWindowsHookEx() successful.\n",
                __FUNCTION__, __LINE__);

        // Set the exit status.
        status = UIOHOOK_SUCCESS;

        // Windows does not have a hook start event or callback so we need to
        // manually fake it.
        hook_start_proc();

        // Block until the thread receives an WM_QUIT request.
        MSG message;
        while (GetMessage(&message, (HWND) NULL, 0, 0) > 0) {
            TranslateMessage(&message);
            DispatchMessage(&message);
        }
    } else {
        logger(LOG_LEVEL_ERROR, "%s [%u]: SetWindowsHookEx() failed! (%#lX)\n",
                __FUNCTION__, __LINE__, (unsigned long) GetLastError());

        status = UIOHOOK_ERROR_SET_WINDOWS_HOOK_EX;
    }


    // Unregister any hooks that may still be installed.
    unregister_running_hooks();

    // We must explicitly call the cleanup handler because Windows does not
    // provide a thread cleanup method like POSIX pthread_cleanup_push/pop.
    hook_stop_proc();

    return status;
}

UIOHOOK_API int hook_stop() {
    int status = UIOHOOK_FAILURE;

    // Try to exit the thread naturally.
    if (PostThreadMessage(hook_thread_id, WM_QUIT, (WPARAM) NULL, (LPARAM) NULL)) {
        status = UIOHOOK_SUCCESS;
    }

    logger(LOG_LEVEL_DEBUG, "%s [%u]: Status: %#X.\n",
            __FUNCTION__, __LINE__, status);

    return status;
}
