// SPDX-License-Identifier: MIT
// Wayland based user interface.
// Copyright (C) 2020 Artem Senichev <artemsen@gmail.com>

#include "../application.h"
#include "../array.h"
#include "../fdpoll.h"
#include "../keybind.h"
#include "uiface.h"
#include "wndbuf.h"

// autogenerated wayland headers
#include "content-type-v1-client-protocol.h"
#include "cursor-shape-v1-client-protocol.h"
#include "ext-idle-notify-v1-client-protocol.h"
#include "fractional-scale-v1-client-protocol.h"
#include "viewporter-client-protocol.h"
#include "xdg-decoration-unstable-v1-client-protocol.h"
#include "xdg-shell-client-protocol.h"

#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

// Mouse buttons, from <linux/input-event-codes.h>
#ifndef BTN_LEFT
#define BTN_LEFT   0x110
#define BTN_RIGHT  0x111
#define BTN_MIDDLE 0x112
#define BTN_SIDE   0x113
#define BTN_EXTRA  0x114
#endif

// Fractional scale denominator
#define FRACTION_SCALE_DEN 120

// Timeout to hide cursor
#define CURSOR_HIDE_TIMEOUT 3 // sec

/** Wayland context. */
struct wayland {
    // wayland objects
    struct wl {
        struct wl_display* display;
        struct wl_registry* registry;
        struct wl_shm* shm;
        struct wl_compositor* compositor;
        struct wl_seat* seat;
        struct wl_keyboard* keyboard;
        struct wl_pointer* pointer;
        struct wl_surface* surface;
        struct wl_output* output;
    } wl;

    // wayland protocols objects
    struct wp {
        struct wp_viewporter* viewporter;
        struct wp_viewport* viewport;
        struct wp_cursor_shape_manager_v1* cursor;
        struct wp_content_type_manager_v1* ctype_manager;
        struct wp_content_type_v1* ctype;
        struct wp_fractional_scale_manager_v1* scale_manager;
        struct wp_fractional_scale_v1* scale;
        struct zxdg_decoration_manager_v1* decor_manager;
        struct zxdg_toplevel_decoration_v1* decor;
        struct ext_idle_notifier_v1* idle_manager;
        struct ext_idle_notification_v1* idle;
    } wp;

    // window buffers
    struct wnd {
        struct wl_buffer* buffer0;
        struct wl_buffer* buffer1;
        struct wl_buffer* current;
        size_t width;
        size_t height;
        size_t scale;
    } wnd;

    // cross-desktop
    struct xdg {
        bool initialized;
        struct xdg_wm_base* base;
        struct xdg_surface* surface;
        struct xdg_toplevel* toplevel;
    } xdg;

    // keyboard
    struct xkb {
        struct xkb_context* context;
        struct xkb_keymap* keymap;
        struct xkb_state* state;
    } xkb;

    // key repeat data
    struct repeat {
        int fd;
        xkb_keysym_t key;
        uint32_t rate;
        uint32_t delay;
    } repeat;

    // mouse state
    struct mouse {
        enum ui_cursor shape;
        uint32_t button;
        int x;
        int y;
    } mouse;

    // fullscreen mode
    bool fullscreen;

    // flag to cancel event queue
    bool event_handled;
};

/**
 * Recreate window buffers.
 * @param ctx UI context
 * @return true if operation completed successfully
 */
static bool recreate_buffers(struct wayland* ctx)
{
    const size_t width = ui_get_width();
    const size_t height = ui_get_height();

    // recreate buffers
    ctx->wnd.current = NULL;
    wndbuf_free(ctx->wnd.buffer0);
    wndbuf_free(ctx->wnd.buffer1);
    ctx->wnd.buffer0 = wndbuf_create(ctx->wl.shm, width, height);
    ctx->wnd.buffer1 = wndbuf_create(ctx->wl.shm, width, height);
    if (!ctx->wnd.buffer0 || !ctx->wnd.buffer1) {
        return false;
    }
    ctx->wnd.current = ctx->wnd.buffer0;

    return true;
}

// suppress unused parameter warnings
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"

/*******************************************************************************
 * Keyboard handlers
 ******************************************************************************/
static void on_keyboard_enter(void* data, struct wl_keyboard* wl_keyboard,
                              uint32_t serial, struct wl_surface* surface,
                              struct wl_array* keys)
{
}

static void on_keyboard_leave(void* data, struct wl_keyboard* wl_keyboard,
                              uint32_t serial, struct wl_surface* surface)
{
    struct wayland* ctx = data;
    fdtimer_reset(ctx->repeat.fd, 0, 0); // reset keyboard repeat timer
}

static void on_keyboard_modifiers(void* data, struct wl_keyboard* wl_keyboard,
                                  uint32_t serial, uint32_t mods_depressed,
                                  uint32_t mods_latched, uint32_t mods_locked,
                                  uint32_t group)
{
    struct wayland* ctx = data;
    if (ctx->xkb.state) {
        xkb_state_update_mask(ctx->xkb.state, mods_depressed, mods_latched,
                              mods_locked, 0, 0, group);
    }
}

static void on_keyboard_repeat_info(void* data, struct wl_keyboard* wl_keyboard,
                                    int32_t rate, int32_t delay)
{
    struct wayland* ctx = data;
    ctx->repeat.rate = rate;
    ctx->repeat.delay = delay;
}

static void on_keyboard_keymap(void* data, struct wl_keyboard* wl_keyboard,
                               uint32_t format, int32_t fd, uint32_t size)
{
    struct wayland* ctx = data;
    char* keymap;

    if (ctx->xkb.state) {
        xkb_state_unref(ctx->xkb.state);
        ctx->xkb.state = NULL;
    }
    if (ctx->xkb.keymap) {
        xkb_keymap_unref(ctx->xkb.keymap);
        ctx->xkb.keymap = NULL;
    }

    keymap = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
    if (keymap != MAP_FAILED) {
        ctx->xkb.keymap = xkb_keymap_new_from_string(
            ctx->xkb.context, keymap, XKB_KEYMAP_FORMAT_TEXT_V1,
            XKB_KEYMAP_COMPILE_NO_FLAGS);
        ctx->xkb.state = xkb_state_new(ctx->xkb.keymap);
        munmap(keymap, size);
    }

    close(fd);
}

static void on_keyboard_key(void* data, struct wl_keyboard* wl_keyboard,
                            uint32_t serial, uint32_t time, uint32_t key,
                            uint32_t state)
{
    struct wayland* ctx = data;

    if (state == WL_KEYBOARD_KEY_STATE_RELEASED) {
        // stop key repeat timer
        fdtimer_reset(ctx->repeat.fd, 0, 0);
    } else if (state == WL_KEYBOARD_KEY_STATE_PRESSED && ctx->xkb.state) {
        xkb_keysym_t keysym;
        key += 8;
        keysym = xkb_state_key_get_one_sym(ctx->xkb.state, key);
        if (keysym != XKB_KEY_NoSymbol) {
            app_on_keyboard(keysym, keybind_mods(ctx->xkb.state));
            // handle key repeat
            if (ctx->repeat.rate &&
                xkb_keymap_key_repeats(ctx->xkb.keymap, key)) {
                // start key repeat timer
                ctx->repeat.key = keysym;
                fdtimer_reset(ctx->repeat.fd, ctx->repeat.delay,
                              1000 / ctx->repeat.rate);
            }
        }
    }
}

static void on_pointer_enter(void* data, struct wl_pointer* wl_pointer,
                             uint32_t serial, struct wl_surface* surface,
                             wl_fixed_t surface_x, wl_fixed_t surface_y)
{
    ui_set_cursor(ui_cursor_default);
}

static void on_pointer_leave(void* data, struct wl_pointer* wl_pointer,
                             uint32_t serial, struct wl_surface* surface)
{
}

static void on_pointer_motion(void* data, struct wl_pointer* wl_pointer,
                              uint32_t time, wl_fixed_t surface_x,
                              wl_fixed_t surface_y)
{
    struct wayland* ctx = data;
    const double scale = (double)ctx->wnd.scale / FRACTION_SCALE_DEN;
    const int x = wl_fixed_to_int(surface_x * scale);
    const int y = wl_fixed_to_int(surface_y * scale);
    const int dx = x - ctx->mouse.x;
    const int dy = y - ctx->mouse.y;

    ctx->mouse.x = x;
    ctx->mouse.y = y;

    if (ctx->mouse.shape == ui_cursor_hide) {
        ui_set_cursor(ui_cursor_default);
    }

    app_on_mmove(keybind_mods(ctx->xkb.state), ctx->mouse.button, x, y, dx, dy);
}

static void on_pointer_button(void* data, struct wl_pointer* wl_pointer,
                              uint32_t serial, uint32_t time, uint32_t button,
                              uint32_t state)
{
    struct wayland* ctx = data;
    uint32_t btn;

    switch (button) {
        case BTN_LEFT:
            btn = MOUSE_BTN_LEFT;
            break;
        case BTN_RIGHT:
            btn = MOUSE_BTN_RIGHT;
            break;
        case BTN_MIDDLE:
            btn = MOUSE_BTN_MIDDLE;
            break;
        case BTN_SIDE:
            btn = MOUSE_BTN_SIDE;
            break;
        case BTN_EXTRA:
            btn = MOUSE_BTN_EXTRA;
            break;
        default:
            btn = 0;
            break;
    }

    if (btn) {
        if (state == WL_POINTER_BUTTON_STATE_RELEASED) {
            ctx->mouse.button &= ~btn;
            ui_set_cursor(ui_cursor_default);
        } else {
            const uint8_t mods = keybind_mods(ctx->xkb.state);
            ctx->mouse.button |= btn;
            app_on_mclick(mods, ctx->mouse.button, ctx->mouse.x, ctx->mouse.y);
        }
    }
}

static void on_pointer_axis(void* data, struct wl_pointer* wl_pointer,
                            uint32_t time, uint32_t axis, wl_fixed_t value)
{
    struct wayland* ctx = data;
    uint32_t key;

    if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
        key = value > 0 ? MOUSE_SCR_RIGHT : MOUSE_SCR_LEFT;
    } else {
        key = value > 0 ? MOUSE_SCR_DOWN : MOUSE_SCR_UP;
    }

    app_on_mclick(keybind_mods(ctx->xkb.state), key, ctx->mouse.x,
                  ctx->mouse.y);
}

static const struct wl_keyboard_listener keyboard_listener = {
    .keymap = on_keyboard_keymap,
    .enter = on_keyboard_enter,
    .leave = on_keyboard_leave,
    .key = on_keyboard_key,
    .modifiers = on_keyboard_modifiers,
    .repeat_info = on_keyboard_repeat_info,
};

static const struct wl_pointer_listener pointer_listener = {
    .enter = on_pointer_enter,
    .leave = on_pointer_leave,
    .motion = on_pointer_motion,
    .button = on_pointer_button,
    .axis = on_pointer_axis,
};

/*******************************************************************************
 * Idle handlers
 ******************************************************************************/
static void on_idle_begin(void* data, struct ext_idle_notification_v1* notify)
{
    ui_set_cursor(ui_cursor_hide);
}

static void on_idle_end(void* data, struct ext_idle_notification_v1* notify) { }

static const struct ext_idle_notification_v1_listener idle_listener = {
    .idled = on_idle_begin,
    .resumed = on_idle_end,
};

/*******************************************************************************
 * Seat handlers
 ******************************************************************************/
static void on_seat_name(void* data, struct wl_seat* seat, const char* name) { }

static void on_seat_capabilities(void* data, struct wl_seat* seat, uint32_t cap)
{
    struct wayland* ctx = data;

    if (cap & WL_SEAT_CAPABILITY_KEYBOARD) {
        ctx->wl.keyboard = wl_seat_get_keyboard(seat);
        wl_keyboard_add_listener(ctx->wl.keyboard, &keyboard_listener, ctx);
    } else if (ctx->wl.keyboard) {
        wl_keyboard_destroy(ctx->wl.keyboard);
        ctx->wl.keyboard = NULL;
    }

    if (cap & WL_SEAT_CAPABILITY_POINTER) {
        ctx->wl.pointer = wl_seat_get_pointer(seat);
        wl_pointer_add_listener(ctx->wl.pointer, &pointer_listener, ctx);
    } else if (ctx->wl.pointer) {
        wl_pointer_destroy(ctx->wl.pointer);
        ctx->wl.pointer = NULL;
    }

    // register idle listener
    if (ctx->wp.idle_manager) {
        ctx->wp.idle = ext_idle_notifier_v1_get_idle_notification(
            ctx->wp.idle_manager, CURSOR_HIDE_TIMEOUT * 1000, ctx->wl.seat);
        ext_idle_notification_v1_add_listener(ctx->wp.idle, &idle_listener,
                                              ctx);
    }
}

static const struct wl_seat_listener seat_listener = {
    .capabilities = on_seat_capabilities,
    .name = on_seat_name,
};

/*******************************************************************************
 * XDG handlers
 ******************************************************************************/
static void on_xdg_surface_configure(void* data, struct xdg_surface* surface,
                                     uint32_t serial)
{
    struct wayland* ctx = data;
    xdg_surface_ack_configure(surface, serial);

    if (ctx->xdg.initialized) {
        app_redraw();
    } else {
        wl_surface_attach(ctx->wl.surface, ctx->wnd.current, 0, 0);
        wl_surface_commit(ctx->wl.surface);
    }
}

static const struct xdg_surface_listener xdg_surface_listener = {
    .configure = on_xdg_surface_configure
};

static void on_xdg_ping(void* data, struct xdg_wm_base* base, uint32_t serial)
{
    xdg_wm_base_pong(base, serial);
}

static const struct xdg_wm_base_listener xdg_base_listener = {
    .ping = on_xdg_ping
};

static void handle_xdg_toplevel_configure(void* data, struct xdg_toplevel* lvl,
                                          int32_t width, int32_t height,
                                          struct wl_array* states)
{
    struct wayland* ctx = data;
    bool reset_buffers = (ctx->wnd.current == NULL);

    if (width > 0 && height > 0) {
        if (ctx->wnd.width != (size_t)width ||
            ctx->wnd.height != (size_t)height) {
            ctx->wnd.width = (size_t)width;
            ctx->wnd.height = (size_t)height;
            reset_buffers = true;
        }
        if (ctx->wp.viewport) {
            wp_viewport_set_destination(ctx->wp.viewport, width, height);
        }
        ctx->xdg.initialized = true;
    }

    if (reset_buffers) {
        if (recreate_buffers(ctx)) {
            app_on_resize();
        } else {
            app_exit(1);
        }
    }
}

static void handle_xdg_toplevel_close(void* data, struct xdg_toplevel* top)
{
    app_exit(0);
}

static const struct xdg_toplevel_listener xdg_toplevel_listener = {
    .configure = handle_xdg_toplevel_configure,
    .close = handle_xdg_toplevel_close,
};

/*******************************************************************************
 * Fractional scale handlers
 ******************************************************************************/
static void handle_scale(void* data, struct wp_fractional_scale_v1* scaler,
                         uint32_t factor)
{
    struct wayland* ctx = data;
    if (ctx->wnd.scale != factor) {
        ctx->wnd.scale = factor;
        if (recreate_buffers(ctx)) {
            app_on_scale((double)ctx->wnd.scale / FRACTION_SCALE_DEN);
            app_on_resize();
            app_redraw();
        } else {
            app_exit(1);
        }
    }
}

static const struct wp_fractional_scale_v1_listener scale_listener = {
    .preferred_scale = handle_scale,
};

/*******************************************************************************
 * Registry handlers
 ******************************************************************************/
static void on_registry_global(void* data, struct wl_registry* registry,
                               uint32_t name, const char* interface,
                               uint32_t version)
{
    struct wayland* ctx = data;
    if (strcmp(interface, wl_compositor_interface.name) == 0) {
        // wayland compositor
        ctx->wl.compositor =
            wl_registry_bind(registry, name, &wl_compositor_interface,
                             WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION);

    } else if (strcmp(interface, wl_shm_interface.name) == 0) {
        // wayland shared memory
        ctx->wl.shm = wl_registry_bind(registry, name, &wl_shm_interface,
                                       WL_SHM_POOL_CREATE_BUFFER_SINCE_VERSION);

    } else if (strcmp(interface, wp_viewporter_interface.name) == 0) {
        // viewport (fractional scale output)
        ctx->wp.viewporter =
            wl_registry_bind(registry, name, &wp_viewporter_interface,
                             WP_VIEWPORTER_GET_VIEWPORT_SINCE_VERSION);

    } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
        // xdg (app window)
        ctx->xdg.base = wl_registry_bind(registry, name, &xdg_wm_base_interface,
                                         XDG_WM_BASE_PING_SINCE_VERSION);
        xdg_wm_base_add_listener(ctx->xdg.base, &xdg_base_listener, data);

    } else if (strcmp(interface, wl_seat_interface.name) == 0) {
        // seat (keyboard and mouse)
        ctx->wl.seat = wl_registry_bind(registry, name, &wl_seat_interface,
                                        WL_KEYBOARD_REPEAT_INFO_SINCE_VERSION);
        wl_seat_add_listener(ctx->wl.seat, &seat_listener, data);

    } else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) {
        // idle notifier
        ctx->wp.idle_manager =
            wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface,
                             EXT_IDLE_NOTIFIER_V1_GET_IDLE_NOTIFICATION);

    } else if (strcmp(interface, wp_content_type_manager_v1_interface.name) ==
               0) {
        // content type
        ctx->wp.ctype_manager = wl_registry_bind(
            registry, name, &wp_content_type_manager_v1_interface,
            WP_CONTENT_TYPE_V1_SET_CONTENT_TYPE_SINCE_VERSION);

    } else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) ==
               0) {
        // cursor shape
        ctx->wp.cursor = wl_registry_bind(
            registry, name, &wp_cursor_shape_manager_v1_interface,
            WP_CURSOR_SHAPE_MANAGER_V1_GET_POINTER_SINCE_VERSION);

    } else if (strcmp(interface,
                      wp_fractional_scale_manager_v1_interface.name) == 0) {
        // fractional scale manager
        ctx->wp.scale_manager = wl_registry_bind(
            registry, name, &wp_fractional_scale_manager_v1_interface,
            WP_FRACTIONAL_SCALE_V1_PREFERRED_SCALE_SINCE_VERSION);

    } else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) ==
               0) {
        // server side window decoration
        ctx->wp.decor_manager = wl_registry_bind(
            registry, name, &zxdg_decoration_manager_v1_interface,
            ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION_SINCE_VERSION);
    }
}

static void on_registry_remove(void* data, struct wl_registry* registry,
                               uint32_t name)
{
}

static const struct wl_registry_listener registry_listener = {
    .global = on_registry_global,
    .global_remove = on_registry_remove,
};

#pragma GCC diagnostic pop // "-Wunused-parameter"

// Key repeat handler
static void on_key_repeat(void* data)
{
    struct wayland* ctx = data;
    uint64_t repeats;
    const ssize_t sz = sizeof(repeats);
    if (read(ctx->repeat.fd, &repeats, sz) == sz) {
        app_on_keyboard(ctx->repeat.key, keybind_mods(ctx->xkb.state));
    }
}

// Wayland event handler
static void on_wayland_event(void* data)
{
    struct wayland* ctx = data;
    wl_display_read_events(ctx->wl.display);
    wl_display_dispatch_pending(ctx->wl.display);
    ctx->event_handled = true;
}

static void wayland_event_prep(void* data)
{
    struct wayland* ctx = data;

    ctx->event_handled = false;
    while (wl_display_prepare_read(ctx->wl.display) != 0) {
        wl_display_dispatch_pending(ctx->wl.display);
    }
    wl_display_flush(ctx->wl.display);
}

static void wayland_event_done(void* data)
{
    struct wayland* ctx = data;
    if (!ctx->event_handled) {
        wl_display_cancel_read(ctx->wl.display);
    }
}

static struct pixmap* wayland_draw_begin(void* data)
{
    struct wayland* ctx = data;

    if (!ctx->wnd.current) {
        return NULL; // not yet initialized
    }

    // switch buffers
    if (ctx->wnd.current == ctx->wnd.buffer0) {
        ctx->wnd.current = ctx->wnd.buffer1;
    } else {
        ctx->wnd.current = ctx->wnd.buffer0;
    }

    return wndbuf_pixmap(ctx->wnd.current);
}

static void wayland_draw_commit(void* data)
{
    struct wayland* ctx = data;
    const struct pixmap* pm = wndbuf_pixmap(ctx->wnd.current);

    wl_surface_attach(ctx->wl.surface, ctx->wnd.current, 0, 0);
    wl_surface_damage_buffer(ctx->wl.surface, 0, 0, pm->width, pm->height);
    wl_surface_commit(ctx->wl.surface);
}

static void wayland_set_title(void* data, const char* title)
{
    char* txt = NULL;
    str_append(APP_NAME ": ", 0, &txt);
    str_append(title, 0, &txt);

    if (txt) {
        struct wayland* ctx = data;
        xdg_toplevel_set_title(ctx->xdg.toplevel, txt);
        free(txt);
    }
}

static void wayland_set_cursor(void* data, enum ui_cursor shape)
{
    struct wayland* ctx = data;
    enum wp_cursor_shape_device_v1_shape wlshape;
    struct wp_cursor_shape_device_v1* dev;

    if (!ctx->wl.pointer || !ctx->wp.cursor) {
        return;
    }

    ctx->mouse.shape = shape;

    switch (shape) {
        case ui_cursor_hide:
            wl_pointer_set_cursor(ctx->wl.pointer, 0, NULL, 0, 0);
            return;
        case ui_cursor_drag:
            wlshape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING;
            break;
        case ui_cursor_default:
        default:
            wlshape = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
            break;
    }

    dev =
        wp_cursor_shape_manager_v1_get_pointer(ctx->wp.cursor, ctx->wl.pointer);
    wp_cursor_shape_device_v1_set_shape(dev, 0, wlshape);
    wp_cursor_shape_device_v1_destroy(dev);
}

static void wayland_set_ctype(void* data, enum ui_ctype ctype)
{
    struct wayland* ctx = data;
    if (ctx->wp.ctype) {
        switch (ctype) {
            case ui_ctype_image:
                wp_content_type_v1_set_content_type(
                    ctx->wp.ctype, WP_CONTENT_TYPE_V1_TYPE_PHOTO);
                break;
            case ui_ctype_animation:
                wp_content_type_v1_set_content_type(
                    ctx->wp.ctype, WP_CONTENT_TYPE_V1_TYPE_VIDEO);
                break;
        }
    }
}

static size_t wayland_get_width(void* data)
{
    struct wayland* ctx = data;
    const double scale = (double)ctx->wnd.scale / FRACTION_SCALE_DEN;
    return scale * ctx->wnd.width;
}

static size_t wayland_get_height(void* data)
{
    struct wayland* ctx = data;
    const double scale = (double)ctx->wnd.scale / FRACTION_SCALE_DEN;
    return scale * ctx->wnd.height;
}

static void wayland_toggle_fullscreen(void* data)
{
    struct wayland* ctx = data;
    ctx->fullscreen = !ctx->fullscreen;

    if (ctx->xdg.toplevel) {
        if (ctx->fullscreen) {
            xdg_toplevel_set_fullscreen(ctx->xdg.toplevel, NULL);
        } else {
            xdg_toplevel_unset_fullscreen(ctx->xdg.toplevel);
        }
    }
}

static void wayland_free(void* data)
{
    struct wayland* ctx = data;

    // free protocols
    if (ctx->wp.scale_manager) {
        if (ctx->wp.scale) {
            wp_fractional_scale_v1_destroy(ctx->wp.scale);
        }
        wp_fractional_scale_manager_v1_destroy(ctx->wp.scale_manager);
    }
    if (ctx->wp.viewporter) {
        if (ctx->wp.viewport) {
            wp_viewport_destroy(ctx->wp.viewport);
        }
        wp_viewporter_destroy(ctx->wp.viewporter);
    }
    if (ctx->wp.idle_manager) {
        if (ctx->wp.idle) {
            ext_idle_notification_v1_destroy(ctx->wp.idle);
        }
        ext_idle_notifier_v1_destroy(ctx->wp.idle_manager);
    }
    if (ctx->wp.ctype_manager) {
        if (ctx->wp.ctype) {
            wp_content_type_v1_destroy(ctx->wp.ctype);
        }
        wp_content_type_manager_v1_destroy(ctx->wp.ctype_manager);
    }
    if (ctx->wp.cursor) {
        wp_cursor_shape_manager_v1_destroy(ctx->wp.cursor);
    }
    if (ctx->wp.decor_manager) {
        if (ctx->wp.decor) {
            zxdg_toplevel_decoration_v1_destroy(ctx->wp.decor);
        }
        zxdg_decoration_manager_v1_destroy(ctx->wp.decor_manager);
    }

    // keyboard related
    if (ctx->xkb.state) {
        xkb_state_unref(ctx->xkb.state);
    }
    if (ctx->xkb.keymap) {
        xkb_keymap_unref(ctx->xkb.keymap);
    }
    if (ctx->xkb.context) {
        xkb_context_unref(ctx->xkb.context);
    }
    if (ctx->repeat.fd != -1) {
        close(ctx->repeat.fd);
    }

    // xdg
    if (ctx->xdg.toplevel) {
        xdg_toplevel_destroy(ctx->xdg.toplevel);
    }
    if (ctx->xdg.surface) {
        xdg_surface_destroy(ctx->xdg.surface);
    }
    if (ctx->xdg.base) {
        xdg_wm_base_destroy(ctx->xdg.base);
    }

    // window buffers
    wndbuf_free(ctx->wnd.buffer0);
    wndbuf_free(ctx->wnd.buffer1);

    // base wayland
    if (ctx->wl.seat) {
        wl_seat_destroy(ctx->wl.seat);
    }
    if (ctx->wl.keyboard) {
        wl_keyboard_destroy(ctx->wl.keyboard);
    }
    if (ctx->wl.pointer) {
        wl_pointer_destroy(ctx->wl.pointer);
    }
    if (ctx->wl.shm) {
        wl_shm_destroy(ctx->wl.shm);
    }
    if (ctx->wl.output) {
        wl_output_destroy(ctx->wl.output);
    }
    if (ctx->wl.surface) {
        wl_surface_destroy(ctx->wl.surface);
    }
    if (ctx->wl.compositor) {
        wl_compositor_destroy(ctx->wl.compositor);
    }
    if (ctx->wl.registry) {
        wl_registry_destroy(ctx->wl.registry);
    }
    if (ctx->wl.display) {
        wl_display_disconnect(ctx->wl.display);
    }

    free(ctx);
}

void* ui_init_wl(const char* app_id, size_t width, size_t height, bool decor,
                 struct ui* handlers)
{
    struct wayland* ctx = calloc(1, sizeof(struct wayland));
    if (!ctx) {
        return NULL;
    }

    ctx->fullscreen =
        width == UI_WINDOW_FULLSCREEN && height == UI_WINDOW_FULLSCREEN;
    ctx->wnd.scale = 1 * FRACTION_SCALE_DEN;
    ctx->wnd.width = max(width, UI_WINDOW_MIN);
    ctx->wnd.height = max(height, UI_WINDOW_MIN);

    ctx->wl.display = wl_display_connect(NULL);
    if (!ctx->wl.display) {
        const int rc = errno;
        fprintf(stderr, "Failed to open wayland display: [%i] %s\n", rc,
                strerror(rc));
        goto fail;
    }
    ctx->xkb.context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
    ctx->wl.registry = wl_display_get_registry(ctx->wl.display);
    if (!ctx->wl.registry) {
        fprintf(stderr, "Failed to open wayland registry\n");
        goto fail;
    }
    wl_registry_add_listener(ctx->wl.registry, &registry_listener, ctx);
    wl_display_roundtrip(ctx->wl.display);

    ctx->wl.surface = wl_compositor_create_surface(ctx->wl.compositor);
    if (!ctx->wl.surface) {
        fprintf(stderr, "Failed to create wayland surface\n");
        goto fail;
    }

    ctx->xdg.surface =
        xdg_wm_base_get_xdg_surface(ctx->xdg.base, ctx->wl.surface);
    if (!ctx->xdg.surface) {
        fprintf(stderr, "Failed to create xdg surface\n");
        goto fail;
    }

    xdg_surface_add_listener(ctx->xdg.surface, &xdg_surface_listener, ctx);
    ctx->xdg.toplevel = xdg_surface_get_toplevel(ctx->xdg.surface);
    xdg_toplevel_add_listener(ctx->xdg.toplevel, &xdg_toplevel_listener, ctx);
    xdg_toplevel_set_app_id(ctx->xdg.toplevel, app_id);
    if (ctx->fullscreen) {
        xdg_toplevel_set_fullscreen(ctx->xdg.toplevel, NULL);
    }

    if (ctx->wp.scale_manager) {
        ctx->wp.scale = wp_fractional_scale_manager_v1_get_fractional_scale(
            ctx->wp.scale_manager, ctx->wl.surface);
        wp_fractional_scale_v1_add_listener(ctx->wp.scale, &scale_listener,
                                            ctx);
    }
    if (ctx->wp.viewporter) {
        ctx->wp.viewport =
            wp_viewporter_get_viewport(ctx->wp.viewporter, ctx->wl.surface);
    }

    if (ctx->wp.ctype_manager) {
        ctx->wp.ctype = wp_content_type_manager_v1_get_surface_content_type(
            ctx->wp.ctype_manager, ctx->wl.surface);
        wp_content_type_v1_set_content_type(ctx->wp.ctype,
                                            WP_CONTENT_TYPE_V1_TYPE_PHOTO);
    }

    if (ctx->wp.decor_manager && decor) {
        ctx->wp.decor = zxdg_decoration_manager_v1_get_toplevel_decoration(
            ctx->wp.decor_manager, ctx->xdg.toplevel);
        if (ctx->wp.decor) {
            zxdg_toplevel_decoration_v1_set_mode(
                ctx->wp.decor, ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
        }
    }

    wl_surface_commit(ctx->wl.surface);

    fdpoll_add(wl_display_get_fd(ctx->wl.display), on_wayland_event, ctx);

    ctx->repeat.fd = fdtimer_add(on_key_repeat, ctx);

    handlers->event_prep = wayland_event_prep;
    handlers->event_done = wayland_event_done;
    handlers->draw_begin = wayland_draw_begin;
    handlers->draw_commit = wayland_draw_commit;
    handlers->set_title = wayland_set_title;
    handlers->set_cursor = wayland_set_cursor;
    handlers->set_ctype = wayland_set_ctype;
    handlers->get_width = wayland_get_width;
    handlers->get_height = wayland_get_height;
    handlers->toggle_fullscreen = wayland_toggle_fullscreen;
    handlers->free = wayland_free;

    return ctx;

fail:
    wayland_free(ctx);
    return NULL;
}
