/*
x11window.c - X11 RVVM Window, Xlib backend
Copyright (C) 2021  LekKit <github.com/LekKit>

This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

// Must be defined before including <sys/ipc.h>
#undef _GNU_SOURCE
#define _GNU_SOURCE
#undef _BSD_SOURCE
#define _BSD_SOURCE
#undef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#undef _DARWIN_C_SOURCE
#define _DARWIN_C_SOURCE

#include "compiler.h"
#include "gui_window.h"

SOURCE_OPTIMIZATION_SIZE

#define USE_XSHM

// Resolve symbols at runtime
#define X11_DLIB_SYM(sym) static __typeof__(sym)* MACRO_CONCAT(sym, _dlib) = NULL;

#if defined(USE_X11) && !(CHECK_INCLUDE(X11/Xlib.h, 1) && CHECK_INCLUDE(X11/Xutil.h, 1) && CHECK_INCLUDE(X11/keysym.h, 1))
// No X11 headers found
#undef USE_X11
#warning Disabling X11 support as <X11/Xlib.h> is unavailable
#endif

#if defined(USE_XSHM) && defined(__ANDROID__)
// There's no SysV SHM on Android
#warning Disabling XShm support on Android
#undef USE_XSHM
#endif

#if defined(USE_XSHM) && !CHECK_INCLUDE(X11/extensions/XShm.h, 1)
// No XShm headers found
#warning Disabling XShm support as <X11/extensions/XShm.h> is unavailable
#undef USE_XSHM
#endif

#if defined(USE_XSHM) && !(CHECK_INCLUDE(sys/ipc.h, 1) && CHECK_INCLUDE(sys/shm.h, 1))
// No SysV SHM headers found
#warning Disabling XShm support as <X11/extensions/XShm.h> is unavailable
#undef USE_XSHM
#endif

#ifdef USE_X11

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <sys/time.h>

#ifdef USE_XSHM

#include <X11/extensions/XShm.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#if !defined(USE_FULL_LINKING)

// Needed XShm functions
X11_DLIB_SYM(XShmQueryExtension)
X11_DLIB_SYM(XShmCreateImage)
X11_DLIB_SYM(XShmAttach)
X11_DLIB_SYM(XShmPutImage)
X11_DLIB_SYM(XShmDetach)

#define XShmQueryExtension XShmQueryExtension_dlib
#define XShmCreateImage    XShmCreateImage_dlib
#define XShmAttach         XShmAttach_dlib
#define XShmPutImage       XShmPutImage_dlib
#define XShmDetach         XShmDetach_dlib

#endif

#endif

#if !defined(USE_FULL_LINKING)

// Needed initialization functions
X11_DLIB_SYM(XInitThreads)
X11_DLIB_SYM(XOpenDisplay)
X11_DLIB_SYM(XInternAtom)
X11_DLIB_SYM(XListPixmapFormats)
X11_DLIB_SYM(XSetErrorHandler)

// Needed window functions
X11_DLIB_SYM(XCreateWindow)
X11_DLIB_SYM(XCreateGC)
X11_DLIB_SYM(XStoreName)
X11_DLIB_SYM(XSetWMNormalHints)
X11_DLIB_SYM(XSetWMProtocols)
X11_DLIB_SYM(XCreateBitmapFromData)
X11_DLIB_SYM(XCreatePixmapCursor)
X11_DLIB_SYM(XDefineCursor)
X11_DLIB_SYM(XCreateImage)
X11_DLIB_SYM(XMapWindow)
X11_DLIB_SYM(XPutImage)
X11_DLIB_SYM(XGrabKeyboard)
X11_DLIB_SYM(XGrabPointer)
X11_DLIB_SYM(XQueryPointer)
X11_DLIB_SYM(XUngrabKeyboard)
X11_DLIB_SYM(XUngrabPointer)

// Needed event handling functions
X11_DLIB_SYM(XPending)
X11_DLIB_SYM(XNextEvent)
X11_DLIB_SYM(XPeekEvent)
X11_DLIB_SYM(XWarpPointer)
X11_DLIB_SYM(XDisplayKeycodes)
X11_DLIB_SYM(XGetKeyboardMapping)

// Needed cleanup & helper functions
X11_DLIB_SYM(XSync)
X11_DLIB_SYM(XFlush)
X11_DLIB_SYM(XFree)
X11_DLIB_SYM(XFreeCursor)
X11_DLIB_SYM(XFreePixmap)
X11_DLIB_SYM(XFreeGC)
X11_DLIB_SYM(XDestroyWindow)
X11_DLIB_SYM(XCloseDisplay)

#define XInitThreads          XInitThreads_dlib
#define XOpenDisplay          XOpenDisplay_dlib
#define XInternAtom           XInternAtom_dlib
#define XListPixmapFormats    XListPixmapFormats_dlib
#define XSetErrorHandler      XSetErrorHandler_dlib

#define XCreateWindow         XCreateWindow_dlib
#define XCreateGC             XCreateGC_dlib
#define XStoreName            XStoreName_dlib
#define XSetWMNormalHints     XSetWMNormalHints_dlib
#define XSetWMProtocols       XSetWMProtocols_dlib
#define XCreateBitmapFromData XCreateBitmapFromData_dlib
#define XCreatePixmapCursor   XCreatePixmapCursor_dlib
#define XDefineCursor         XDefineCursor_dlib
#define XCreateImage          XCreateImage_dlib
#define XMapWindow            XMapWindow_dlib
#define XPutImage             XPutImage_dlib
#define XGrabKeyboard         XGrabKeyboard_dlib
#define XGrabPointer          XGrabPointer_dlib
#define XQueryPointer         XQueryPointer_dlib
#define XUngrabKeyboard       XUngrabKeyboard_dlib
#define XUngrabPointer        XUngrabPointer_dlib

#define XPending              XPending_dlib
#define XNextEvent            XNextEvent_dlib
#define XPeekEvent            XPeekEvent_dlib
#define XWarpPointer          XWarpPointer_dlib
#define XDisplayKeycodes      XDisplayKeycodes_dlib
#define XGetKeyboardMapping   XGetKeyboardMapping_dlib

#define XSync                 XSync_dlib
#define XFlush                XFlush_dlib
#define XFree                 XFree_dlib
#define XFreeCursor           XFreeCursor_dlib
#define XFreePixmap           XFreePixmap_dlib
#define XFreeGC               XFreeGC_dlib
#define XDestroyWindow        XDestroyWindow_dlib
#define XCloseDisplay         XCloseDisplay_dlib

#endif

// Internal headers come after system headers because of safe_free()
#include "dlib.h"
#include "spinlock.h"
#include "threading.h"
#include "utils.h"
#include "vector.h"
#include "vma_ops.h"

// X11 window state
typedef struct {
    Window  window;
    GC      gc;
    XImage* ximage;
    void*   image_buffer;

#ifdef USE_XSHM
    XShmSegmentInfo seginfo;
#endif

    // Input grabbed
    bool grab;

    // These are used to restore the original pointer position
    Window grab_root;
    int    grab_pointer_x;
    int    grab_pointer_y;
} x11_win_t;

// Global X11 state
static Display* x11_dsp;
static Atom     x11_wm_delete;
static KeySym*  x11_keycodemap;
static int      x11_min_keycode;
static int      x11_max_keycode;
static int      x11_keysyms_per_keycode;

static vector_t(gui_window_t*) x11_windows;
static thread_ctx_t*           x11_thread;
static spinlock_t              x11_lock;

static hid_key_t x11_keysym_to_hid(KeySym keysym)
{
    // XK_ definitions are huge numbers, don't use a table
    // clang-format off
    switch (keysym) {
        case XK_a:            return HID_KEY_A;
        case XK_b:            return HID_KEY_B;
        case XK_c:            return HID_KEY_C;
        case XK_d:            return HID_KEY_D;
        case XK_e:            return HID_KEY_E;
        case XK_f:            return HID_KEY_F;
        case XK_g:            return HID_KEY_G;
        case XK_h:            return HID_KEY_H;
        case XK_i:            return HID_KEY_I;
        case XK_j:            return HID_KEY_J;
        case XK_k:            return HID_KEY_K;
        case XK_l:            return HID_KEY_L;
        case XK_m:            return HID_KEY_M;
        case XK_n:            return HID_KEY_N;
        case XK_o:            return HID_KEY_O;
        case XK_p:            return HID_KEY_P;
        case XK_q:            return HID_KEY_Q;
        case XK_r:            return HID_KEY_R;
        case XK_s:            return HID_KEY_S;
        case XK_t:            return HID_KEY_T;
        case XK_u:            return HID_KEY_U;
        case XK_v:            return HID_KEY_V;
        case XK_w:            return HID_KEY_W;
        case XK_x:            return HID_KEY_X;
        case XK_y:            return HID_KEY_Y;
        case XK_z:            return HID_KEY_Z;
        case XK_0:            return HID_KEY_0;
        case XK_1:            return HID_KEY_1;
        case XK_2:            return HID_KEY_2;
        case XK_3:            return HID_KEY_3;
        case XK_4:            return HID_KEY_4;
        case XK_5:            return HID_KEY_5;
        case XK_6:            return HID_KEY_6;
        case XK_7:            return HID_KEY_7;
        case XK_8:            return HID_KEY_8;
        case XK_9:            return HID_KEY_9;
        case XK_Return:       return HID_KEY_ENTER;
        case XK_Escape:       return HID_KEY_ESC;
        case XK_BackSpace:    return HID_KEY_BACKSPACE;
        case XK_Tab:          return HID_KEY_TAB;
        case XK_space:        return HID_KEY_SPACE;
        case XK_minus:        return HID_KEY_MINUS;
        case XK_equal:        return HID_KEY_EQUAL;
        case XK_bracketleft:  return HID_KEY_LEFTBRACE;
        case XK_bracketright: return HID_KEY_RIGHTBRACE;
        case XK_backslash:    return HID_KEY_BACKSLASH;
        case XK_semicolon:    return HID_KEY_SEMICOLON;
        case XK_apostrophe:   return HID_KEY_APOSTROPHE;
        case XK_grave:        return HID_KEY_GRAVE;
        case XK_comma:        return HID_KEY_COMMA;
        case XK_period:       return HID_KEY_DOT;
        case XK_slash:        return HID_KEY_SLASH;
        case XK_Caps_Lock:    return HID_KEY_CAPSLOCK;
        case XK_F1:           return HID_KEY_F1;
        case XK_F2:           return HID_KEY_F2;
        case XK_F3:           return HID_KEY_F3;
        case XK_F4:           return HID_KEY_F4;
        case XK_F5:           return HID_KEY_F5;
        case XK_F6:           return HID_KEY_F6;
        case XK_F7:           return HID_KEY_F7;
        case XK_F8:           return HID_KEY_F8;
        case XK_F9:           return HID_KEY_F9;
        case XK_F10:          return HID_KEY_F10;
        case XK_F11:          return HID_KEY_F11;
        case XK_F12:          return HID_KEY_F12;
        case XK_Print:        return HID_KEY_SYSRQ;
        case XK_Scroll_Lock:  return HID_KEY_SCROLLLOCK;
        case XK_Pause:        return HID_KEY_PAUSE;
        case XK_Insert:       return HID_KEY_INSERT;
        case XK_Home:         return HID_KEY_HOME;
        case XK_Prior:        return HID_KEY_PAGEUP; // ?
        case XK_Delete:       return HID_KEY_DELETE;
        case XK_End:          return HID_KEY_END;
        case XK_Next:         return HID_KEY_PAGEDOWN; // ?
        case XK_Right:        return HID_KEY_RIGHT;
        case XK_Left:         return HID_KEY_LEFT;
        case XK_Down:         return HID_KEY_DOWN;
        case XK_Up:           return HID_KEY_UP;
        case XK_Num_Lock:     return HID_KEY_NUMLOCK;
        case XK_KP_Divide:    return HID_KEY_KPSLASH;
        case XK_KP_Multiply:  return HID_KEY_KPASTERISK;
        case XK_KP_Subtract:  return HID_KEY_KPMINUS;
        case XK_KP_Add:       return HID_KEY_KPPLUS;
        case XK_KP_Enter:     return HID_KEY_KPENTER;
        case XK_KP_End:       return HID_KEY_KP1;
        case XK_KP_Down:      return HID_KEY_KP2;
        case XK_KP_Page_Down: return HID_KEY_KP3;
        case XK_KP_Left:      return HID_KEY_KP4;
        case XK_KP_Begin:     return HID_KEY_KP5;
        case XK_KP_Right:     return HID_KEY_KP6;
        case XK_KP_Home:      return HID_KEY_KP7;
        case XK_KP_Up:        return HID_KEY_KP8;
        case XK_KP_Page_Up:   return HID_KEY_KP9;
        case XK_KP_Insert:    return HID_KEY_KP0;
        case XK_KP_Delete:    return HID_KEY_KPDOT;
        case XK_less:         return HID_KEY_102ND;
        case XK_Multi_key:    return HID_KEY_COMPOSE;
        case XK_KP_Equal:     return HID_KEY_KPEQUAL;
        case XK_KP_Separator: return HID_KEY_KPCOMMA;
        case 0x04db:          return HID_KEY_RO;
        case 0xff27:          return HID_KEY_KATAKANAHIRAGANA;
        case XK_yen:          return HID_KEY_YEN;
        case 0xff23:          return HID_KEY_HENKAN;
        case 0xff22:          return HID_KEY_MUHENKAN;
        case 0x04a4:          return HID_KEY_KPJPCOMMA;
        case 0xff31:          return HID_KEY_HANGEUL;
        case 0xff34:          return HID_KEY_HANJA;
        case 0xff26:          return HID_KEY_KATAKANA;
        case 0xff25:          return HID_KEY_HIRAGANA;
        case 0xff2a:          return HID_KEY_ZENKAKUHANKAKU;
        case XK_Menu:         return HID_KEY_MENU;
        case XK_Control_L:    return HID_KEY_LEFTCTRL;
        case XK_Shift_L:      return HID_KEY_LEFTSHIFT;
        case XK_Alt_L:        return HID_KEY_LEFTALT;
        case XK_Super_L:      return HID_KEY_LEFTMETA;
        case XK_Control_R:    return HID_KEY_RIGHTCTRL;
        case XK_Shift_R:      return HID_KEY_RIGHTSHIFT;
        case XK_Alt_R:        return HID_KEY_RIGHTALT;
        case XK_Super_R:      return HID_KEY_RIGHTMETA;
    }
    // clang-format on
    return HID_KEY_NONE;
}

static hid_key_t x11_event_key_to_hid(int keycode)
{
    if (!x11_keycodemap) {
        rvvm_warn("XKeycodemap not initialized!");
        return HID_KEY_NONE;
    } else if (keycode < x11_min_keycode || keycode > x11_max_keycode) {
        rvvm_warn("XEvent keycode out of keycodemap range!");
        return HID_KEY_NONE;
    } else {
        uint32_t entry = (keycode - x11_min_keycode) * x11_keysyms_per_keycode;
        return x11_keysym_to_hid(x11_keycodemap[entry]);
    }
}

static void x11_update_keymap(void)
{
    XDisplayKeycodes(x11_dsp, &x11_min_keycode, &x11_max_keycode);
    KeySym* keycodemap = XGetKeyboardMapping(x11_dsp, x11_min_keycode, x11_max_keycode - x11_min_keycode + 1,
                                             &x11_keysyms_per_keycode);
    if (keycodemap) {
        if (x11_keycodemap) {
            XFree(x11_keycodemap);
        }
        x11_keycodemap = keycodemap;
    } else {
        rvvm_warn("XGetKeyboardMapping() failed!");
    }
}

static rgb_fmt_t x11_get_rgb_format(void)
{
    rgb_fmt_t            format = RGB_FMT_A8R8G8B8;
    int                  nfmts  = 0;
    XPixmapFormatValues* fmts   = XListPixmapFormats(x11_dsp, &nfmts);
    if (fmts) {
        for (int i = 0; i < nfmts; ++i) {
            if (fmts[i].depth == DefaultDepth(x11_dsp, DefaultScreen(x11_dsp))) {
                format = rgb_format_from_bpp(fmts[i].bits_per_pixel);
                break;
            }
        }
        XFree(fmts);
    }
    return format;
}

static gui_window_t* x11_find_window(Window x11_win)
{
    vector_foreach (x11_windows, i) {
        gui_window_t* win = vector_at(x11_windows, i);
        x11_win_t*    x11 = win->win_data;
        if (x11->window == x11_win) {
            return win;
        }
    }
    rvvm_warn("Invalid X11 window in event");
    return NULL;
}

static void x11_mouse_press(XButtonEvent* xbutton)
{
    gui_window_t* win = x11_find_window(xbutton->window);
    if (win && win->on_mouse_press) {
        switch (xbutton->button) {
            case Button1:
                win->on_mouse_press(win, HID_BTN_LEFT);
                return;
            case Button2:
                win->on_mouse_press(win, HID_BTN_MIDDLE);
                return;
            case Button3:
                win->on_mouse_press(win, HID_BTN_RIGHT);
                return;
            case Button4:
                if (win->on_mouse_scroll) {
                    win->on_mouse_scroll(win, HID_SCROLL_UP);
                }
                return;
            case Button5:
                if (win->on_mouse_scroll) {
                    win->on_mouse_scroll(win, HID_SCROLL_DOWN);
                }
                return;
        }
    }
}

static void x11_mouse_release(XButtonEvent* xbutton)
{
    gui_window_t* win = x11_find_window(xbutton->window);
    if (win && win->on_mouse_release) {
        switch (xbutton->button) {
            case Button1:
                win->on_mouse_release(win, HID_BTN_LEFT);
                return;
            case Button2:
                win->on_mouse_release(win, HID_BTN_MIDDLE);
                return;
            case Button3:
                win->on_mouse_release(win, HID_BTN_RIGHT);
                return;
        }
    }
}

static void x11_handle_mouse_motion(XMotionEvent* xmotion)
{
    gui_window_t* win = x11_find_window(xmotion->window);
    if (win) {
        x11_win_t* x11 = win->win_data;
        if (x11->grab && win->on_mouse_move) {
            int center_x = win->fb.width / 2;
            int center_y = win->fb.height / 2;
            int dx       = xmotion->x - center_x;
            int dy       = xmotion->y - center_y;
            if (dx || dy) {
                XWarpPointer(x11_dsp, None, x11->window, 0, 0, 0, 0, center_x, center_y);
                XFlush(x11_dsp);
                win->on_mouse_move(win, dx, dy);
            }
        } else if (win->on_mouse_place) {
            win->on_mouse_place(win, xmotion->x, xmotion->y);
        }
    }
}

static void x11_keyboard_press(XKeyEvent* xkey)
{
    gui_window_t* win = x11_find_window(xkey->window);
    if (win && win->on_key_press) {
        win->on_key_press(win, x11_event_key_to_hid(xkey->keycode));
    }
}

static void x11_keyboard_release(XKeyEvent* xkey)
{
    gui_window_t* win = x11_find_window(xkey->window);
    if (win && win->on_key_release) {
        win->on_key_release(win, x11_event_key_to_hid(xkey->keycode));
    }
}

static void x11_dispatch_events(void)
{
    size_t pending = 0;
    while ((pending = XPending(x11_dsp))) {
        XEvent ev = {0};
        XNextEvent(x11_dsp, &ev);

        switch (ev.type) {
            case ButtonPress:
                x11_mouse_press(&ev.xbutton);
                break;
            case ButtonRelease:
                x11_mouse_release(&ev.xbutton);
                break;
            case MotionNotify:
                x11_handle_mouse_motion(&ev.xmotion);
                break;
            case KeyPress:
                x11_keyboard_press(&ev.xkey);
                break;
            case KeyRelease:
                if (pending > 1) {
                    XEvent tmp = {0};
                    XPeekEvent(x11_dsp, &tmp);
                    if (tmp.type == KeyPress && tmp.xkey.time == ev.xkey.time && tmp.xkey.keycode == ev.xkey.keycode) {
                        // Skip the repeated key release event, repeated presses are filtered by hid_keyboard
                        break;
                    }
                }
                x11_keyboard_release(&ev.xkey);
                break;
            case MappingNotify:
                if (ev.xmapping.request == MappingKeyboard) {
                    // Keymap update event
                    x11_update_keymap();
                }
                break;
            case ClientMessage:
                if (((Atom)ev.xclient.data.l[0]) == x11_wm_delete) {
                    // Attempted to close window
                    gui_window_t* win = x11_find_window(ev.xclient.window);
                    if (win && win->on_close) {
                        win->on_close(win);
                    }
                }
                break;
            case FocusOut:
                if (ev.xfocus.mode == NotifyNormal) {
                    gui_window_t* win = x11_find_window(ev.xclient.window);
                    if (win && win->on_focus_lost) {
                        win->on_focus_lost(win);
                    }
                }
                break;
        }
    }
}

static void* x11_event_thread(void* arg)
{
    fd_set fds     = {0};
    int    fd      = ConnectionNumber(x11_dsp);
    bool   running = true;

    while (running) {
        FD_ZERO(&fds);
        FD_SET(fd, &fds);
        if (select(fd + 1, &fds, NULL, NULL, NULL) > 0 && FD_ISSET(fd, &fds)) {
            scoped_spin_lock (&x11_lock) {
                if (vector_size(x11_windows)) {
                    x11_dispatch_events();
                } else {
                    running = false;
                }
            }
        }
    }

    return arg;
}

static bool x11_init_display(void)
{
    if (!x11_dsp) {
        DO_ONCE(XInitThreads());
        x11_dsp = XOpenDisplay(NULL);
        if (!x11_dsp) {
            return false;
        }
        x11_wm_delete = XInternAtom(x11_dsp, "WM_DELETE_WINDOW", False);
        x11_update_keymap();
    }
    return true;
}

static void x11_init_event_thread(void)
{
    if (x11_dsp && !x11_thread) {
        x11_thread = thread_create(x11_event_thread, NULL);
    }
}

static void x11_shutdown_display(void)
{
    if (x11_dsp && !vector_size(x11_windows)) {
        XFlush(x11_dsp);
        if (x11_thread) {
            thread_join(x11_thread);
            x11_thread = NULL;
        }
        XCloseDisplay(x11_dsp);
        x11_dsp = NULL;
        if (x11_keycodemap) {
            XFree(x11_keycodemap);
            x11_keycodemap = NULL;
        }
        vector_free(x11_windows);
    }
}

#if defined(USE_XSHM)

static bool xshm_error = false;

static int x11_dummy_error_handler(Display* display, XErrorEvent* error)
{
    UNUSED(display);
    UNUSED(error);
    xshm_error = true;
    return 0;
}

static void x11_free_xshm(x11_win_t* x11)
{
    if (x11->seginfo.shmaddr) {
        XShmDetach(x11_dsp, &x11->seginfo);
        shmdt(x11->seginfo.shmaddr);
    }
    x11->seginfo.shmaddr = NULL;
}

static void* x11_xshm_init(gui_window_t* win)
{
    x11_win_t* x11 = win->win_data;

    if (!XShmQueryExtension(x11_dsp)) {
        rvvm_info("XShm extension not supported");
        return NULL;
    }

    x11->ximage = XShmCreateImage(x11_dsp, DefaultVisual(x11_dsp, DefaultScreen(x11_dsp)),
                                  DefaultDepth(x11_dsp, DefaultScreen(x11_dsp)), ZPixmap, NULL, &x11->seginfo,
                                  win->fb.width, win->fb.height);
    if (!x11->ximage) {
        rvvm_error("XShmCreateImage() failed!");
        return NULL;
    }

    x11->seginfo.shmid = shmget(IPC_PRIVATE, framebuffer_size(&win->fb), IPC_CREAT | 0777);
    if (x11->seginfo.shmid < 0) {
        rvvm_error("XShm shmget() failed!");
        return NULL;
    }

    x11->seginfo.shmaddr = shmat(x11->seginfo.shmid, NULL, 0);
    shmctl(x11->seginfo.shmid, IPC_RMID, NULL);
    if (x11->seginfo.shmaddr == (void*)-1 || x11->seginfo.shmaddr == NULL) {
        rvvm_error("XShm shmget() failed!");
        return NULL;
    }

    x11->ximage->data = x11->seginfo.shmaddr;
    if (!XShmAttach(x11_dsp, &x11->seginfo)) {
        rvvm_error("XShmAttach() failed!");
        return NULL;
    }

    return x11->seginfo.shmaddr;
}

static void* x11_xshm_attach(gui_window_t* win)
{
    x11_win_t* x11         = win->win_data;
    void*      old_handler = XSetErrorHandler(x11_dummy_error_handler);
    void*      xshm        = x11_xshm_init(win);

    // Process errors, if any
    XSync(x11_dsp, False);

    // Cleanup on error
    if (xshm == NULL || xshm_error) {
        rvvm_info("XShm failed to initialize");
        x11_free_xshm(x11);
        if (x11->ximage) {
            XDestroyImage(x11->ximage);
            x11->ximage = NULL;
        }
        xshm = NULL;
    }

    XSync(x11_dsp, False);
    XSetErrorHandler(old_handler);
    return xshm;
}

#endif

static void x11_window_draw(gui_window_t* win)
{
    x11_win_t* x11 = win->win_data;
    scoped_spin_lock (&x11_lock) {
#if defined(USE_XSHM)
        if (x11->seginfo.shmaddr) {
            // Update XShm image
            XShmPutImage(x11_dsp, x11->window, x11->gc, x11->ximage, //
                         0, 0, 0, 0, x11->ximage->width, x11->ximage->height, False);
            XFlush(x11_dsp);
            break;
        }
#endif
        // Update XImage
        XPutImage(x11_dsp, x11->window, x11->gc, x11->ximage, //
                  0, 0, 0, 0, x11->ximage->width, x11->ximage->height);
        XFlush(x11_dsp);
    }
}

static void x11_window_grab_input(gui_window_t* win, bool grab)
{
    x11_win_t* x11 = win->win_data;
    if (x11->grab != grab) {
        x11->grab = grab;
        if (grab) {
            // Save the original cursor position
            Window       child = None;
            int          win_x = 0, win_y = 0;
            unsigned int mask = 0;
            x11->grab_root    = None;
            XQueryPointer(x11_dsp, x11->window, &x11->grab_root, &child, &x11->grab_pointer_x, &x11->grab_pointer_y,
                          &win_x, &win_y, &mask);

            // Grab the input
            XGrabKeyboard(x11_dsp, x11->window, True, GrabModeAsync, GrabModeAsync, CurrentTime);
            XGrabPointer(x11_dsp, x11->window, True, ButtonPressMask | ButtonReleaseMask | PointerMotionMask,
                         GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
            XWarpPointer(x11_dsp, None, x11->window, 0, 0, 0, 0, win->fb.width / 2, win->fb.height / 2);
        } else {
            // Ungrab
            XUngrabKeyboard(x11_dsp, CurrentTime);
            XUngrabPointer(x11_dsp, CurrentTime);

            // Restore the original cursor position
            if (x11->grab_root) {
                XWarpPointer(x11_dsp, None, x11->grab_root, 0, 0, 0, 0, x11->grab_pointer_x, x11->grab_pointer_y);
                x11->grab_root = None;
            }
        }
    }
}

static void x11_window_set_title(gui_window_t* win, const char* title)
{
    x11_win_t* x11 = win->win_data;
    XStoreName(x11_dsp, x11->window, title);
}

static void x11_window_remove(gui_window_t* win)
{
    x11_win_t* x11 = win->win_data;
    scoped_spin_lock (&x11_lock) {
        vector_foreach_back (x11_windows, i) {
            if (vector_at(x11_windows, i) == win) {
                vector_erase(x11_windows, i);
                break;
            }
        }

        // Restore input
        x11_window_grab_input(win, false);

        // Destroy window
        XDestroyWindow(x11_dsp, x11->window);

        // Cleanup
#if defined(USE_XSHM)
        x11_free_xshm(x11);
#endif
        if (x11->image_buffer) {
            // Free framebuffer VMA
            vma_free(x11->image_buffer, framebuffer_size(&win->fb));
            x11->ximage->data = NULL;
        }

        XDestroyImage(x11->ximage);
        XFreeGC(x11_dsp, x11->gc);
        free(x11);
    }

    // Shutdown display connection & event thread if needed
    x11_shutdown_display();
}

#define X11_DLIB_RESOLVE(lib, sym)                                                                                     \
    do {                                                                                                               \
        sym     = dlib_resolve(lib, #sym);                                                                             \
        success = success && !!sym;                                                                                    \
    } while (0)

static bool x11_init_libs(void)
{
    bool success = true;
#if !defined(USE_FULL_LINKING)
    dlib_ctx_t* libx11 = dlib_open("X11", DLIB_NAME_PROBE);

    X11_DLIB_RESOLVE(libx11, XInitThreads);
    X11_DLIB_RESOLVE(libx11, XOpenDisplay);
    X11_DLIB_RESOLVE(libx11, XInternAtom);
    X11_DLIB_RESOLVE(libx11, XListPixmapFormats);
    X11_DLIB_RESOLVE(libx11, XSetErrorHandler);

    X11_DLIB_RESOLVE(libx11, XCreateWindow);
    X11_DLIB_RESOLVE(libx11, XCreateGC);
    X11_DLIB_RESOLVE(libx11, XStoreName);
    X11_DLIB_RESOLVE(libx11, XSetWMNormalHints);
    X11_DLIB_RESOLVE(libx11, XSetWMProtocols);
    X11_DLIB_RESOLVE(libx11, XCreateBitmapFromData);
    X11_DLIB_RESOLVE(libx11, XCreatePixmapCursor);
    X11_DLIB_RESOLVE(libx11, XDefineCursor);
    X11_DLIB_RESOLVE(libx11, XCreateImage);
    X11_DLIB_RESOLVE(libx11, XMapWindow);
    X11_DLIB_RESOLVE(libx11, XPutImage);
    X11_DLIB_RESOLVE(libx11, XGrabKeyboard);
    X11_DLIB_RESOLVE(libx11, XGrabPointer);
    X11_DLIB_RESOLVE(libx11, XQueryPointer);
    X11_DLIB_RESOLVE(libx11, XUngrabKeyboard);
    X11_DLIB_RESOLVE(libx11, XUngrabPointer);

    X11_DLIB_RESOLVE(libx11, XPending);
    X11_DLIB_RESOLVE(libx11, XNextEvent);
    X11_DLIB_RESOLVE(libx11, XPeekEvent);
    X11_DLIB_RESOLVE(libx11, XWarpPointer);
    X11_DLIB_RESOLVE(libx11, XDisplayKeycodes);
    X11_DLIB_RESOLVE(libx11, XGetKeyboardMapping);

    X11_DLIB_RESOLVE(libx11, XSync);
    X11_DLIB_RESOLVE(libx11, XFlush);
    X11_DLIB_RESOLVE(libx11, XFree);
    X11_DLIB_RESOLVE(libx11, XFreeCursor);
    X11_DLIB_RESOLVE(libx11, XFreePixmap);
    X11_DLIB_RESOLVE(libx11, XFreeGC);
    X11_DLIB_RESOLVE(libx11, XDestroyWindow);
    X11_DLIB_RESOLVE(libx11, XCloseDisplay);

    dlib_close(libx11);

#if defined(USE_XSHM)
    dlib_ctx_t* libxext = dlib_open("Xext", DLIB_NAME_PROBE);

    X11_DLIB_RESOLVE(libxext, XShmQueryExtension);
    X11_DLIB_RESOLVE(libxext, XShmCreateImage);
    X11_DLIB_RESOLVE(libxext, XShmAttach);
    X11_DLIB_RESOLVE(libxext, XShmPutImage);
    X11_DLIB_RESOLVE(libxext, XShmDetach);

    dlib_close(libxext);
#endif

#endif
    return success;
}

bool x11_window_init(gui_window_t* win)
{
    static bool libx11_avail = false;
    DO_ONCE(libx11_avail = x11_init_libs());
    if (!libx11_avail) {
        rvvm_info("Failed to load libX11");
        return false;
    }

    if (!x11_init_display()) {
        rvvm_info("Could not open a connection to the X server");
        return false;
    }

    x11_win_t* x11 = safe_new_obj(x11_win_t);

    // Initialize callbacks
    win->win_data   = x11;
    win->draw       = x11_window_draw;
    win->remove     = x11_window_remove;
    win->grab_input = x11_window_grab_input;
    win->set_title  = x11_window_set_title;

    XSetWindowAttributes attributes = {
        .event_mask = KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask
                    | FocusChangeMask | StructureNotifyMask,
    };
    x11->window = XCreateWindow(x11_dsp, DefaultRootWindow(x11_dsp), 0, 0, win->fb.width, win->fb.height, 0,
                                DefaultDepth(x11_dsp, DefaultScreen(x11_dsp)), InputOutput, CopyFromParent, CWEventMask,
                                &attributes);
    x11->gc     = XCreateGC(x11_dsp, x11->window, 0, NULL);

    // Set window title
    XStoreName(x11_dsp, x11->window, "RVVM");

    // Window size hints
    XSizeHints hints = {
        .flags      = PMinSize | PMaxSize,
        .min_width  = win->fb.width,
        .min_height = win->fb.height,
        .max_width  = win->fb.width,
        .max_height = win->fb.height,
    };
    XSetWMNormalHints(x11_dsp, x11->window, &hints);

    // Handle window close
    XSetWMProtocols(x11_dsp, x11->window, &x11_wm_delete, 1);

    // Hide cursor
    XColor     color     = {0};
    const char pixels[8] = {0};
    Pixmap     pixmap    = XCreateBitmapFromData(x11_dsp, x11->window, pixels, 8, 8);
    Cursor     cursor    = XCreatePixmapCursor(x11_dsp, pixmap, pixmap, &color, &color, 0, 0);
    XDefineCursor(x11_dsp, x11->window, cursor);
    XFreeCursor(x11_dsp, cursor);
    XFreePixmap(x11_dsp, pixmap);

    win->fb.format = x11_get_rgb_format();

#if defined(USE_XSHM)
    win->fb.buffer = x11_xshm_attach(win);
#endif

    if (!win->fb.buffer) {
        x11->ximage       = XCreateImage(x11_dsp, DefaultVisual(x11_dsp, DefaultScreen(x11_dsp)),
                                         DefaultDepth(x11_dsp, DefaultScreen(x11_dsp)), ZPixmap, 0, NULL, win->fb.width,
                                         win->fb.height, 8, 0);
        x11->image_buffer = vma_alloc(NULL, framebuffer_size(&win->fb), VMA_RDWR);
        x11->ximage->data = x11->image_buffer;
        win->fb.buffer    = x11->image_buffer;
    }

    // Show window
    XMapWindow(x11_dsp, x11->window);

    XFlush(x11_dsp);

    // Initialize event thread
    scoped_spin_lock (&x11_lock) {
        vector_push_back(x11_windows, win);
    }

    x11_init_event_thread();

    return true;
}

#else

bool x11_window_init(gui_window_t* win)
{
    UNUSED(win);
    return false;
}

#endif
