/*
 * backend-windows.c - Win32 backend.
 *
 * Overlay   : one per-monitor layered window (WS_EX_LAYERED) that is
 *             click-through (WS_EX_TRANSPARENT) and topmost. Its pixels come
 *             straight from the core's ARGB8888 buffer via UpdateLayeredWindow
 *             with per-pixel alpha - no conversion, since the buffer is already
 *             premultiplied BGRA, which is exactly what a 32bpp DIB holds.
 * Keyboard  : a low-level keyboard hook (WH_KEYBOARD_LL) grabs every key
 *             globally, so no window focus is needed; consumed keys are
 *             swallowed.
 * Pointer   : SendInput with absolute, virtual-desktop coordinates.
 */
#define WINVER 0x0A00
#define _WIN32_WINNT 0x0A00
#include "platform.h"

#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#define KQ_SIZE 64

struct win_screen {
	HWND hwnd;
	HDC memdc;
	HBITMAP dib;
	HGDIOBJ old;		/* default bitmap, restored on teardown */
	int x, y, w, h;		/* monitor rect, physical pixels */
	struct mb_buffer pub;	/* px points straight at the DIB bits */
};

struct win_state {
	struct win_screen scr[MB_MAX_SCREENS];
	int n;
	HHOOK hook;
	DWORD tid;		/* thread that owns the hook + message loop */
	uint32_t kq[KQ_SIZE];	/* keys decoded by the hook, drained by next_key */
	int kq_head, kq_tail;
};

static struct platform g_platform;
static struct win_state g_ws;

void platform_sleep_ms(int ms)
{
	Sleep((DWORD)ms);
}

void platform_early_init(void)
{
	/* GUI-subsystem binary: launching it (e.g. from a hotkey) creates no
	 * console window. When started from a terminal, attach to its console
	 * so --help, --dry-run and --selftest still print. */
	if (AttachConsole(ATTACH_PARENT_PROCESS)) {
		(void)freopen("CONOUT$", "w", stdout);
		(void)freopen("CONOUT$", "w", stderr);
		(void)freopen("CONIN$", "r", stdin);
	}
}

/* ---- key queue ------------------------------------------------------- */

static void kq_push(uint32_t k)
{
	int n = (g_ws.kq_tail + 1) % KQ_SIZE;
	if (n == g_ws.kq_head)
		return; /* full; drop */
	g_ws.kq[g_ws.kq_tail] = k;
	g_ws.kq_tail = n;
}

/* ---- keyboard hook --------------------------------------------------- */

/* Translate a virtual-key + scancode to an MB_KEY. The few editing keys are
 * mapped directly; everything else goes through the active keyboard layout so
 * labels follow the user's layout, same as the Wayland xkb path. */
static uint32_t map_key(DWORD vk, DWORD scan)
{
	switch (vk) {
	case VK_ESCAPE:
		return MB_KEY_ESC;
	case VK_BACK:
		return MB_KEY_BACKSPACE;
	case VK_RETURN:
		return MB_KEY_ENTER;
	}
	/* GetKeyboardState is stale on a thread that pumps no keyboard input -
	 * keys arrive only through the LL hook - so sample the modifiers that
	 * affect translation directly. */
	BYTE ks[256];
	memset(ks, 0, sizeof ks);
	if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
		ks[VK_SHIFT] = 0x80;
	if (GetKeyState(VK_CAPITAL) & 1)
		ks[VK_CAPITAL] = 1;
	WCHAR buf[8];
	HWND foreground = GetForegroundWindow();
	DWORD tid = foreground ? GetWindowThreadProcessId(foreground, NULL) : 0;
	int n = ToUnicodeEx(vk, scan, ks, buf, 8, 0, GetKeyboardLayout(tid));
	if (n == 1 && buf[0] >= 0x20 && buf[0] < 0x7f)
		return (uint32_t)buf[0];
	return MB_KEY_NONE;
}

/* Bare modifiers do nothing on their own, and swallowing their key-ups would
 * leave the OS thinking e.g. the launch hotkey's Win key is still held after
 * exit - so they pass through the hook untouched. */
static int is_modifier(DWORD vk)
{
	switch (vk) {
	case VK_SHIFT:
	case VK_LSHIFT:
	case VK_RSHIFT:
	case VK_CONTROL:
	case VK_LCONTROL:
	case VK_RCONTROL:
	case VK_MENU:
	case VK_LMENU:
	case VK_RMENU:
	case VK_LWIN:
	case VK_RWIN:
	case VK_CAPITAL:
		return 1;
	}
	return 0;
}

static LRESULT CALLBACK ll_hook(int code, WPARAM w, LPARAM l)
{
	if (code == HC_ACTION) {
		const KBDLLHOOKSTRUCT *k = (const KBDLLHOOKSTRUCT *)l;
		if (is_modifier(k->vkCode))
			return CallNextHookEx(NULL, code, w, l);
		if (w == WM_KEYDOWN || w == WM_SYSKEYDOWN) {
			uint32_t mb = map_key(k->vkCode, k->scanCode);
			if (mb != MB_KEY_NONE) {
				kq_push(mb);
				/* Wake the GetMessage() in next_key; with no
				 * focused window the raw key posts no message
				 * of its own. */
				PostThreadMessage(g_ws.tid, WM_NULL, 0, 0);
			}
		}
		/* Swallow every non-modifier key while the overlay is up:
		 * nothing leaks to the focused application. */
		return 1;
	}
	return CallNextHookEx(NULL, code, w, l);
}

static uint32_t win_next_key(struct platform *p)
{
	(void)p;
	for (;;) {
		if (g_ws.kq_head != g_ws.kq_tail) {
			uint32_t k = g_ws.kq[g_ws.kq_head];
			g_ws.kq_head = (g_ws.kq_head + 1) % KQ_SIZE;
			return k;
		}
		MSG m;
		int r = GetMessage(&m, NULL, 0, 0);
		if (r <= 0)
			return MB_KEY_NONE; /* WM_QUIT or error */
		TranslateMessage(&m);
		DispatchMessage(&m);
	}
}

/* ---- frame interface ------------------------------------------------- */

static struct mb_buffer *win_frame_begin(struct platform *p, int s)
{
	(void)p;
	struct win_screen *w = &g_ws.scr[s];
	memset(w->pub.px, 0, (size_t)w->w * w->h * 4);
	return &w->pub;
}

static int win_frame_commit(struct platform *p)
{
	(void)p;
	for (int s = 0; s < g_ws.n; s++) {
		struct win_screen *w = &g_ws.scr[s];
		POINT src = { 0, 0 };
		POINT dst = { w->x, w->y };
		SIZE size = { w->w, w->h };
		BLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
		if (!UpdateLayeredWindow(w->hwnd, NULL, &dst, &size, w->memdc,
					 &src, 0, &bf, ULW_ALPHA)) {
			fprintf(stderr, "mouseboard: UpdateLayeredWindow failed: %lu\n",
				(unsigned long)GetLastError());
			return -1;
		}
	}
	return 0;
}

/* ---- pointer --------------------------------------------------------- */

static int win_pointer_move(struct platform *p, int x, int y)
{
	(void)p;
	int vx = GetSystemMetrics(SM_XVIRTUALSCREEN);
	int vy = GetSystemMetrics(SM_YVIRTUALSCREEN);
	int vw = GetSystemMetrics(SM_CXVIRTUALSCREEN);
	int vh = GetSystemMetrics(SM_CYVIRTUALSCREEN);
	if (vw < 2)
		vw = 2;
	if (vh < 2)
		vh = 2;
	INPUT in;
	memset(&in, 0, sizeof in);
	in.type = INPUT_MOUSE;
	in.mi.dx = (LONG)(((int64_t)(x - vx) * 65535) / (vw - 1));
	in.mi.dy = (LONG)(((int64_t)(y - vy) * 65535) / (vh - 1));
	in.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE |
			MOUSEEVENTF_VIRTUALDESK;
	SetLastError(0);
	if (SendInput(1, &in, sizeof in) != 1) {
		fprintf(stderr, "mouseboard: SendInput move failed: %lu\n",
			(unsigned long)GetLastError());
		return -1;
	}
	return 0;
}

static int win_pointer_button(struct platform *p, enum mb_button b, int press)
{
	(void)p;
	DWORD f;
	if (b == MB_LEFT)
		f = press ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP;
	else if (b == MB_MIDDLE)
		f = press ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP;
	else
		f = press ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP;
	INPUT in;
	memset(&in, 0, sizeof in);
	in.type = INPUT_MOUSE;
	in.mi.dwFlags = f;
	SetLastError(0);
	if (SendInput(1, &in, sizeof in) != 1) {
		fprintf(stderr, "mouseboard: SendInput button failed: %lu\n",
			(unsigned long)GetLastError());
		return -1;
	}
	return 0;
}

/* ---- window + monitor setup ------------------------------------------ */

static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l)
{
	if (m == WM_DESTROY) {
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(h, m, w, l);
}

static BOOL CALLBACK mon_proc(HMONITOR hm, HDC dc, LPRECT rc, LPARAM lp)
{
	(void)dc;
	(void)rc;
	(void)lp;
	if (g_ws.n >= MB_MAX_SCREENS)
		return TRUE;
	MONITORINFO mi;
	mi.cbSize = sizeof mi;
	if (!GetMonitorInfo(hm, &mi))
		return TRUE;
	struct win_screen *w = &g_ws.scr[g_ws.n++];
	w->x = mi.rcMonitor.left;
	w->y = mi.rcMonitor.top;
	w->w = mi.rcMonitor.right - mi.rcMonitor.left;
	w->h = mi.rcMonitor.bottom - mi.rcMonitor.top;
	return TRUE;
}

static int screen_setup(struct win_screen *w, HINSTANCE hinst)
{
	w->hwnd = CreateWindowExW(WS_EX_LAYERED | WS_EX_TRANSPARENT |
					 WS_EX_TOPMOST | WS_EX_TOOLWINDOW |
					 WS_EX_NOACTIVATE,
				 L"mouseboard", L"", WS_POPUP, w->x, w->y, w->w,
				 w->h, NULL, NULL, hinst, NULL);
	if (!w->hwnd)
		return -1;

	HDC screen = GetDC(NULL);
	w->memdc = CreateCompatibleDC(screen);
	ReleaseDC(NULL, screen);
	if (!w->memdc)
		return -1;

	BITMAPINFO bi;
	memset(&bi, 0, sizeof bi);
	bi.bmiHeader.biSize = sizeof bi.bmiHeader;
	bi.bmiHeader.biWidth = w->w;
	bi.bmiHeader.biHeight = -w->h; /* top-down: row 0 is the top */
	bi.bmiHeader.biPlanes = 1;
	bi.bmiHeader.biBitCount = 32;
	bi.bmiHeader.biCompression = BI_RGB;
	void *bits = NULL;
	w->dib = CreateDIBSection(w->memdc, &bi, DIB_RGB_COLORS, &bits, NULL, 0);
	if (!w->dib)
		return -1;
	w->old = SelectObject(w->memdc, w->dib);

	w->pub.px = bits;
	w->pub.w = w->w;
	w->pub.h = w->h;
	w->pub.stride = w->w; /* device pixels per row */
	w->pub.scale = 1;     /* physical pixels: per-monitor DPI aware */

	ShowWindow(w->hwnd, SW_SHOWNOACTIVATE);
	return 0;
}

/* ---- teardown -------------------------------------------------------- */

static void win_teardown(struct platform *p)
{
	(void)p;
	if (g_ws.hook) {
		UnhookWindowsHookEx(g_ws.hook);
		g_ws.hook = NULL;
	}
	for (int s = 0; s < g_ws.n; s++) {
		struct win_screen *w = &g_ws.scr[s];
		if (w->memdc) {
			if (w->old)
				SelectObject(w->memdc, w->old);
			DeleteDC(w->memdc);
		}
		if (w->dib)
			DeleteObject(w->dib);
		if (w->hwnd)
			DestroyWindow(w->hwnd);
	}
	g_ws.n = 0;
}

/* ---- init ------------------------------------------------------------ */

struct platform *platform_init(void)
{
	/* Physical pixels everywhere: monitor rects, the overlay buffers and
	 * the SendInput coordinates then share one coordinate space. */
	SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

	HINSTANCE hinst = GetModuleHandle(NULL);
	WNDCLASSEXW wc;
	memset(&wc, 0, sizeof wc);
	wc.cbSize = sizeof wc;
	wc.lpfnWndProc = wndproc;
	wc.hInstance = hinst;
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
	wc.lpszClassName = L"mouseboard";
	if (!RegisterClassExW(&wc)) {
		fprintf(stderr, "mouseboard: RegisterClassEx failed\n");
		return NULL;
	}

	EnumDisplayMonitors(NULL, NULL, mon_proc, 0);
	if (g_ws.n == 0) {
		fprintf(stderr, "mouseboard: no monitors found\n");
		return NULL;
	}

	for (int s = 0; s < g_ws.n; s++) {
		if (screen_setup(&g_ws.scr[s], hinst) < 0) {
			fprintf(stderr,
				"mouseboard: overlay window setup failed\n");
			win_teardown(&g_platform);
			return NULL;
		}
	}

	g_ws.tid = GetCurrentThreadId();
	g_ws.hook = SetWindowsHookEx(WH_KEYBOARD_LL, ll_hook, hinst, 0);
	if (!g_ws.hook) {
		fprintf(stderr, "mouseboard: cannot install keyboard hook\n");
		win_teardown(&g_platform);
		return NULL;
	}

	g_platform.priv = &g_ws;
	g_platform.n_screens = g_ws.n;
	for (int s = 0; s < g_ws.n; s++)
		g_platform.screens[s] = (struct mb_screen){
			g_ws.scr[s].x, g_ws.scr[s].y, g_ws.scr[s].w, g_ws.scr[s].h
		};
	g_platform.frame_begin = win_frame_begin;
	g_platform.frame_commit = win_frame_commit;
	g_platform.next_key = win_next_key;
	g_platform.pointer_move = win_pointer_move;
	g_platform.pointer_button = win_pointer_button;
	g_platform.teardown = win_teardown;
	return &g_platform;
}
