// SPDX-License-Identifier: MIT
// User interface: Window management, keyboard input, etc.
// Copyright (C) 2020 Artem Senichev <artemsen@gmail.com>

#include "ui.h"

#include "application.h"
#include "buildcfg.h"
#include "wndbuf.h"

// autogenerated wayland headers
#include "content-type-v1-client-protocol.h"
#include "cursor-shape-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 <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/timerfd.h>
#include <unistd.h>

// Window size
#define WINDOW_MIN            10
#define WINDOW_MAX            100000
#define WINDOW_DEFAULT_WIDTH  800
#define WINDOW_DEFAULT_HEIGHT 600

// 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

// Uncomment the following line to enable printing draw time
// #define TRACE_DRAW_TIME

/** UI context */
struct ui {
    // 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;
    } 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;
#ifdef TRACE_DRAW_TIME
        struct timespec draw_time;
#endif
    } 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 drag
    struct mouse {
        bool active;
        int x;
        int y;
    } mouse;

    // fullscreen mode
    bool fullscreen;

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

/** Global UI context instance. */
static struct ui ctx = {
    .wnd.scale = FRACTION_SCALE_DEN,
    .repeat.fd = -1,
};

/**
 * Fill timespec structure.
 * @param ts destination structure
 * @param ms time in milliseconds
 */
static inline void set_timespec(struct timespec* ts, uint32_t ms)
{
    ts->tv_sec = ms / 1000;
    ts->tv_nsec = (ms % 1000) * 1000000;
}

/**
 * Recreate window buffers.
 * @return true if operation completed successfully
 */
static bool recreate_buffers(void)
{
    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)
{
    // reset keyboard repeat timer
    struct itimerspec ts = { 0 };
    timerfd_settime(ctx.repeat.fd, 0, &ts, NULL);
}

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)
{
    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)
{
    // save keyboard repeat preferences
    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)
{
    char* keymap;

    xkb_state_unref(ctx.xkb.state);
    xkb_keymap_unref(ctx.xkb.keymap);

    keymap = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
    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 itimerspec ts = { 0 };

    if (state == WL_KEYBOARD_KEY_STATE_RELEASED) {
        // stop key repeat timer
        timerfd_settime(ctx.repeat.fd, 0, &ts, NULL);
    } else if (state == WL_KEYBOARD_KEY_STATE_PRESSED) {
        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;
                set_timespec(&ts.it_value, ctx.repeat.delay);
                set_timespec(&ts.it_interval, 1000 / ctx.repeat.rate);
                timerfd_settime(ctx.repeat.fd, 0, &ts, NULL);
            }
        }
    }
}

/**
 * Set mouse pointer shape.
 * @param wl_pointer wayland pointer instance
 * @param grabbing true to set grabbing shape, false to use default
 */
static void set_pointer_shape(struct wl_pointer* wl_pointer, bool grabbing)
{
    if (ctx.wp.cursor) {
        const uint32_t shape = grabbing
            ? WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING
            : WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT;
        struct wp_cursor_shape_device_v1* cursor_device =
            wp_cursor_shape_manager_v1_get_pointer(ctx.wp.cursor, wl_pointer);
        wp_cursor_shape_device_v1_set_shape(cursor_device, 0, shape);
        wp_cursor_shape_device_v1_destroy(cursor_device);
    }
}

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)
{
    set_pointer_shape(wl_pointer, false);
}

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)
{
    const int x = wl_fixed_to_int(surface_x);
    const int y = wl_fixed_to_int(surface_y);

    if (ctx.mouse.active) {
        const int dx = x - ctx.mouse.x;
        const int dy = y - ctx.mouse.y;
        if (dx || dy) {
            app_on_drag(dx, dy);
        }
    }

    ctx.mouse.x = x;
    ctx.mouse.y = y;
}

static void on_pointer_button(void* data, struct wl_pointer* wl_pointer,
                              uint32_t serial, uint32_t time, uint32_t button,
                              uint32_t state)
{
    const bool pressed = (state == WL_POINTER_BUTTON_STATE_PRESSED);

    if (button == BTN_LEFT) {
        ctx.mouse.active = pressed;
        set_pointer_shape(wl_pointer, pressed);
    }

    if (pressed) {
        xkb_keysym_t key;
        switch (button) {
            // TODO: Configurable drag
            // case BTN_LEFT:
            //     key = VKEY_MOUSE_LEFT;
            //     break;
            case BTN_RIGHT:
                key = VKEY_MOUSE_RIGHT;
                break;
            case BTN_MIDDLE:
                key = VKEY_MOUSE_MIDDLE;
                break;
            case BTN_SIDE:
                key = VKEY_MOUSE_SIDE;
                break;
            case BTN_EXTRA:
                key = VKEY_MOUSE_EXTRA;
                break;
            default:
                key = XKB_KEY_NoSymbol;
                break;
        }
        if (key != XKB_KEY_NoSymbol) {
            app_on_keyboard(key, keybind_mods(ctx.xkb.state));
        }
    }
}

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

    if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) {
        key = value > 0 ? VKEY_SCROLL_RIGHT : VKEY_SCROLL_LEFT;
    } else {
        key = value > 0 ? VKEY_SCROLL_DOWN : VKEY_SCROLL_UP;
    }

    app_on_keyboard(key, keybind_mods(ctx.xkb.state));
}

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,
};

/*******************************************************************************
 * 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)
{
    if (cap & WL_SEAT_CAPABILITY_KEYBOARD) {
        ctx.wl.keyboard = wl_seat_get_keyboard(seat);
        wl_keyboard_add_listener(ctx.wl.keyboard, &keyboard_listener, NULL);
    } 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, NULL);
    } else if (ctx.wl.pointer) {
        wl_pointer_destroy(ctx.wl.pointer);
        ctx.wl.pointer = NULL;
    }
}

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)
{
    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)
{
    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()) {
            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)
{
    if (ctx.wnd.scale != factor) {
        ctx.wnd.scale = factor;
        if (recreate_buffers()) {
            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)
{
    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 (keayboard 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, 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(__attribute__((unused)) void* 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(__attribute__((unused)) void* data)
{
    wl_display_read_events(ctx.wl.display);
    wl_display_dispatch_pending(ctx.wl.display);
    ctx.event_handled = true;
}

bool ui_init(const char* app_id, size_t width, size_t height, bool decor)
{
    ctx.wnd.width = width;
    ctx.wnd.height = height;
    if (ctx.wnd.width < WINDOW_MIN || ctx.wnd.height < WINDOW_MIN ||
        ctx.wnd.width > WINDOW_MAX || ctx.wnd.height > WINDOW_MAX) {
        ctx.wnd.width = WINDOW_DEFAULT_WIDTH;
        ctx.wnd.height = WINDOW_DEFAULT_HEIGHT;
    }

    ctx.wl.display = wl_display_connect(NULL);
    if (!ctx.wl.display) {
        fprintf(stderr, "Failed to open display\n");
        return false;
    }
    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 registry\n");
        ui_destroy();
        return false;
    }
    wl_registry_add_listener(ctx.wl.registry, &registry_listener, NULL);
    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 surface\n");
        ui_destroy();
        return false;
    }

    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");
        ui_destroy();
        return false;
    }

    xdg_surface_add_listener(ctx.xdg.surface, &xdg_surface_listener, NULL);
    ctx.xdg.toplevel = xdg_surface_get_toplevel(ctx.xdg.surface);
    xdg_toplevel_add_listener(ctx.xdg.toplevel, &xdg_toplevel_listener, NULL);
    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,
                                            NULL);
    }
    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);

    app_watch(wl_display_get_fd(ctx.wl.display), on_wayland_event, NULL);

    ctx.repeat.fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
    app_watch(ctx.repeat.fd, on_key_repeat, NULL);

    return true;
}

void ui_destroy(void)
{
    // 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.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);
    }
}

void ui_event_prepare(void)
{
    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);
}

void ui_event_done(void)
{
    if (!ctx.event_handled) {
        wl_display_cancel_read(ctx.wl.display);
    }
}

struct pixmap* ui_draw_begin(void)
{
    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;
    }

#ifdef TRACE_DRAW_TIME
    clock_gettime(CLOCK_MONOTONIC, &ctx.wnd.draw_time);
#endif

    return wndbuf_pixmap(ctx.wnd.current);
}

void ui_draw_commit(void)
{
    const struct pixmap* pm = wndbuf_pixmap(ctx.wnd.current);

#ifdef TRACE_DRAW_TIME
    struct timespec curr;
    clock_gettime(CLOCK_MONOTONIC, &curr);
    const double ns = (curr.tv_sec * 1000000000 + curr.tv_nsec) -
        (ctx.wnd.draw_time.tv_sec * 1000000000 + ctx.wnd.draw_time.tv_nsec);
    printf("Rendered in %.6f sec\n", ns / 1000000000);
#endif

    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);
}

void ui_set_title(const char* name)
{
    char* title = NULL;

    str_append(APP_NAME ": ", 0, &title);
    str_append(name, 0, &title);

    if (title) {
        xdg_toplevel_set_title(ctx.xdg.toplevel, title);
        free(title);
    }
}

void ui_set_content_type_animated(bool animated)
{
    if (ctx.wp.ctype) {
        if (animated) {
            wp_content_type_v1_set_content_type(ctx.wp.ctype,
                                                WP_CONTENT_TYPE_V1_TYPE_VIDEO);
        } else {
            wp_content_type_v1_set_content_type(ctx.wp.ctype,
                                                WP_CONTENT_TYPE_V1_TYPE_PHOTO);
        }
    }
}

size_t ui_get_width(void)
{
    const double scale = (double)ctx.wnd.scale / FRACTION_SCALE_DEN;
    return scale * ctx.wnd.width;
}

size_t ui_get_height(void)
{
    const double scale = (double)ctx.wnd.scale / FRACTION_SCALE_DEN;
    return scale * ctx.wnd.height;
}

void ui_toggle_fullscreen(void)
{
    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);
        }
    }
}
