/**
 * Copyright 2021 Johannes Marbach
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include "indev.h"

#include "cursor/cursor.h"
#include "force_feedback.h"
#include "log.h"

#include "lvgl/lvgl.h"

#ifndef BBX_APP_BUFFYBOARD
#include <linux/input-event-codes.h>
#endif
#include <sys/epoll.h>
#include <sys/stat.h>
#ifndef BBX_APP_BUFFYBOARD
#include <xkbcommon/xkbcommon.h>
#endif
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <libinput.h>
#include <libudev.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#ifndef BBX_APP_BUFFYBOARD
/* All keyboard devices are connected to this input group */
lv_group_t *keyboard_input_group;
#endif

enum input_device_type {
    BBX_INDEV_NONE,
    BBX_INDEV_KEY,
    BBX_INDEV_KEYBOARD,
    BBX_INDEV_MOUSE,
    BBX_INDEV_TOUCHPAD,
    BBX_INDEV_TOUCHSCREEN,
    BBX_INDEV_OTHER
};

struct input_device_ext {
    /* The second pointer is used to represent the second finger on touchscreens.
       This makes typing with thumbs convenient */
    lv_indev_t* pointer1;
    lv_indev_t* pointer2;
#ifndef BBX_APP_BUFFYBOARD
    lv_indev_t* keypad;
    struct xkb_state* xkb_state;
#endif
    enum input_device_type type;
};

/* The analog of lv_indev_data_t, but without unused fields */
struct indev_ext {
    union {
        lv_point_t point;
        uint32_t key;
    };
    lv_indev_state_t state;
};

/**
 * Static variables
 */

static struct libinput *context_libinput;
static struct udev *context_udev;
static struct udev_monitor *monitor;
#ifndef BBX_APP_BUFFYBOARD
static struct xkb_keymap *keymap;
#endif
static lv_obj_t *mouse_cursor;
static bool libinput_is_suspended = false;

#ifndef BBX_APP_BUFFYBOARD
static uint8_t num_keyboards = 0;
#endif
static uint8_t num_mouses = 0;
static uint8_t num_touchpads = 0;

#ifndef BBX_APP_BUFFYBOARD
static void (*on_key_power_cb)() = NULL;
#endif

static struct {
#ifndef BBX_APP_BUFFYBOARD
    uint8_t keyboard : 1;
#endif
    uint8_t pointer : 1;
    uint8_t touchscreen : 1;
    uint8_t force_feedback : 1;
} options;

/**
 * Static functions
 */

static int open_restricted(const char *path, int flags, void *user_data) {
    LV_UNUSED(user_data);
    int fd = open(path, flags);
    return fd < 0 ? -errno : fd;
}

static void close_restricted(int fd, void *user_data) {
    LV_UNUSED(user_data);
    close(fd);
}

#ifndef BBX_APP_BUFFYBOARD
static void read_keypad(lv_indev_t *indev, lv_indev_data_t *data) {
    struct indev_ext* ext = lv_indev_get_user_data(indev);

    data->key = ext->key;
    data->state = ext->state;
}
#endif

static void read_pointer(lv_indev_t * indev, lv_indev_data_t * data) {
    struct indev_ext* ext = lv_indev_get_user_data(indev);

    data->point = ext->point;
    data->state = ext->state;
}

static enum input_device_type identify_input_device(struct udev_device* device) {
    bool is_input = false;
    enum input_device_type ret = BBX_INDEV_OTHER;

    struct udev_list_entry *entry;
    struct udev_list_entry *properties = udev_device_get_properties_list_entry(device);
    udev_list_entry_foreach(entry, properties) {
        const char* name = udev_list_entry_get_name(entry);
        const char* value = udev_list_entry_get_value(entry);

        if (strcmp(name, "ID_INPUT") == 0 && strcmp(value, "1") == 0)
            is_input = true;
        else if (strcmp(name, "ID_INPUT_KEY") == 0 && strcmp(value, "1") == 0)
            ret = BBX_INDEV_KEY;
        else if (strcmp(name, "ID_INPUT_KEYBOARD") == 0 && strcmp(value, "1") == 0)
            ret = BBX_INDEV_KEYBOARD;
        else if (strcmp(name, "ID_INPUT_MOUSE") == 0 && strcmp(value, "1") == 0)
            ret = BBX_INDEV_MOUSE;
        else if (strcmp(name, "ID_INPUT_TOUCHPAD") == 0 && strcmp(value, "1") == 0)
            ret = BBX_INDEV_TOUCHPAD;
        else if (strcmp(name, "ID_INPUT_TOUCHSCREEN") == 0 && strcmp(value, "1") == 0)
            ret = BBX_INDEV_TOUCHSCREEN;
    }

    if (!is_input) {
        bbx_log(BBX_LOG_LEVEL_WARNING, "%s is not an input device", udev_device_get_devnode(device));
        ret = BBX_INDEV_NONE;
    }

    return ret;
}

static void add_device_extension(struct libinput_device* device) {
    enum input_device_type type = (enum input_device_type) libinput_device_get_user_data(device);
    libinput_device_set_user_data(device, NULL);
    if (type == BBX_INDEV_NONE) {
        struct udev_device *dev = libinput_device_get_udev_device(device);
        if (!dev) {
            bbx_log(BBX_LOG_LEVEL_ERROR, "libinput_device_get_udev_device(%s) is failed", libinput_device_get_sysname(device));
            goto failure1;
        }
        type = identify_input_device(dev);
        udev_device_unref(dev);
        if (type == BBX_INDEV_NONE)
            goto failure1;
    }

    struct input_device_ext* ext = malloc(sizeof(struct input_device_ext));
    if (!ext) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "Out of memory");
        goto failure1;
    }

    ext->pointer1 = NULL;
    ext->pointer2 = NULL;
#ifndef BBX_APP_BUFFYBOARD
    ext->keypad = NULL;
    ext->xkb_state = NULL;
#endif
    ext->type = type;

#ifndef BBX_APP_BUFFYBOARD
    if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_KEYBOARD)) {
        ext->keypad = lv_indev_create();
        if (!ext->keypad) {
            bbx_log(BBX_LOG_LEVEL_ERROR, "Out of memory");
            goto failure2;
        }

        struct indev_ext* data = malloc(sizeof(struct indev_ext));
        if (!data) {
            bbx_log(BBX_LOG_LEVEL_ERROR, "Out of memory");
            lv_indev_delete(ext->keypad);
            goto failure2;
        }

        data->key = 0;
        data->state = LV_INDEV_STATE_RELEASED;

        lv_indev_set_type(ext->keypad, LV_INDEV_TYPE_KEYPAD);
        lv_indev_set_mode(ext->keypad, LV_INDEV_MODE_EVENT);
        lv_indev_set_user_data(ext->keypad, data);
        lv_indev_set_read_cb(ext->keypad, read_keypad);
        lv_indev_set_group(ext->keypad, keyboard_input_group);

        if (ext->type == BBX_INDEV_KEYBOARD) {
            ext->xkb_state = xkb_state_new(keymap);
            if (!ext->xkb_state)
                bbx_log(BBX_LOG_LEVEL_WARNING, "Can't create xkb_state for %s", libinput_device_get_sysname(device));
        }
    }
#endif

    if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER) ||
        libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TOUCH)) {
        ext->pointer1 = lv_indev_create();
        if (!ext->pointer1) {
            bbx_log(BBX_LOG_LEVEL_ERROR, "Out of memory");
            goto failure3;
        }

        struct indev_ext* data1 = malloc(sizeof(struct indev_ext));
        if (!data1) {
            bbx_log(BBX_LOG_LEVEL_ERROR, "Out of memory");
            lv_indev_delete(ext->pointer1);
            goto failure3;
        }

        lv_display_t* display = lv_indev_get_display(ext->pointer1);
        data1->point.x = display->hor_res / 2;
        data1->point.y = display->ver_res / 2;
        data1->state = LV_INDEV_STATE_RELEASED;

        lv_indev_set_type(ext->pointer1, LV_INDEV_TYPE_POINTER);
        lv_indev_set_mode(ext->pointer1, LV_INDEV_MODE_EVENT);
        lv_indev_set_user_data(ext->pointer1, data1);
        lv_indev_set_read_cb(ext->pointer1, read_pointer);

        if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TOUCH) &&
            libinput_device_touch_get_touch_count(device) != 1) {
            ext->pointer2 = lv_indev_create();
            if (!ext->pointer2) {
                bbx_log(BBX_LOG_LEVEL_ERROR, "Out of memory");
                goto failure3;
            }

            struct indev_ext* data2 = malloc(sizeof(struct indev_ext));
            if (!data2) {
                bbx_log(BBX_LOG_LEVEL_ERROR, "Out of memory");
                lv_indev_delete(ext->pointer2);
                goto failure3;
            }

            data2->point.x = display->hor_res / 2;
            data2->point.y = display->ver_res / 2;
            data2->state = LV_INDEV_STATE_RELEASED;

            lv_indev_set_type(ext->pointer2, LV_INDEV_TYPE_POINTER);
            lv_indev_set_mode(ext->pointer2, LV_INDEV_MODE_EVENT);
            lv_indev_set_user_data(ext->pointer2, data2);
            lv_indev_set_read_cb(ext->pointer2, read_pointer);
        }

        if (ext->type == BBX_INDEV_MOUSE || ext->type == BBX_INDEV_TOUCHPAD) {
            lv_indev_read(ext->pointer1);
            lv_indev_set_cursor(ext->pointer1, mouse_cursor);
        }
    }

#ifndef BBX_APP_BUFFYBOARD
    if (!ext->pointer1 && !ext->keypad) {
#else
    if (!ext->pointer1) {
#endif
        bbx_log(BBX_LOG_LEVEL_VERBOSE, "%s does not have required capabilities, ignoring.",
            libinput_device_get_sysname(device));
        goto failure2;
    }

    libinput_device_set_user_data(device, ext);

#ifndef BBX_APP_BUFFYBOARD
    if (ext->type == BBX_INDEV_KEYBOARD)
        num_keyboards++;
    else
#endif
    if (ext->type == BBX_INDEV_MOUSE)
        num_mouses++;
    else if (ext->type == BBX_INDEV_TOUCHPAD)
        num_touchpads++;
    if (num_mouses != 0 || num_touchpads != 0)
        lv_obj_remove_flag(mouse_cursor, LV_OBJ_FLAG_HIDDEN);

    bbx_log(BBX_LOG_LEVEL_VERBOSE, "New input device: %s", libinput_device_get_name(device));
    return;


failure3:
    if (ext->pointer1) {
        free(lv_indev_get_user_data(ext->pointer1));
        lv_indev_delete(ext->pointer1);
    }
    if (ext->pointer2) {
        free(lv_indev_get_user_data(ext->pointer2));
        lv_indev_delete(ext->pointer2);
    }
#ifndef BBX_APP_BUFFYBOARD
    if (ext->keypad) {
        free(lv_indev_get_user_data(ext->keypad));
        lv_indev_delete(ext->keypad);
    }
    if (ext->xkb_state) {
        xkb_state_unref(ext->xkb_state);
    }
#endif
failure2:
    free(ext);
failure1:
    libinput_path_remove_device(device);
    return;
}

static void on_input_event() {
    libinput_dispatch(context_libinput);

    struct libinput_event * event;
    while ((event = libinput_get_event(context_libinput))) {
        struct libinput_device* device = libinput_event_get_device(event);
        struct input_device_ext* ext = libinput_device_get_user_data(device);

        switch (libinput_event_get_type(event)) {
        case LIBINPUT_EVENT_DEVICE_ADDED: {
            add_device_extension(device);
            break;
        }

        case LIBINPUT_EVENT_DEVICE_REMOVED: {
            if (!ext)
                break;

            if (ext->pointer1) {
                free(lv_indev_get_user_data(ext->pointer1));
                lv_indev_delete(ext->pointer1);
            }
            if (ext->pointer2) {
                free(lv_indev_get_user_data(ext->pointer2));
                lv_indev_delete(ext->pointer2);
            }
#ifndef BBX_APP_BUFFYBOARD
            if (ext->keypad) {
                free(lv_indev_get_user_data(ext->keypad));
                lv_indev_delete(ext->keypad);
            }
            if (ext->xkb_state) {
                xkb_state_unref(ext->xkb_state);
            }

            if (ext->type == BBX_INDEV_KEYBOARD) {
                assert(num_keyboards > 0);
                num_keyboards--;
            } else
#endif
            if (ext->type == BBX_INDEV_MOUSE) {
                assert(num_mouses > 0);
                num_mouses--;
            } else if (ext->type == BBX_INDEV_TOUCHPAD) {
                assert(num_touchpads > 0);
                num_touchpads--;
            }
            if (num_mouses == 0 && num_touchpads == 0)
                lv_obj_add_flag(mouse_cursor, LV_OBJ_FLAG_HIDDEN);

            free(ext);
            break;
        }

#ifndef BBX_APP_BUFFYBOARD
        case LIBINPUT_EVENT_KEYBOARD_KEY: {
            struct libinput_event_keyboard *kb_event = libinput_event_get_keyboard_event(event);

            struct indev_ext *data = lv_indev_get_user_data(ext->keypad);
            uint32_t key = libinput_event_keyboard_get_key(kb_event);
            bool pressed = libinput_event_keyboard_get_key_state(kb_event) == LIBINPUT_KEY_STATE_PRESSED;
            xkb_keycode_t keycode = key + 8;
            bool ignore = false;

            switch (key) {
            case KEY_UP:
                data->key = LV_KEY_UP;
                break;
            case KEY_DOWN:
                data->key = LV_KEY_DOWN;
                break;
            case KEY_RIGHT:
                data->key = LV_KEY_RIGHT;
                break;
            case KEY_LEFT:
                data->key = LV_KEY_LEFT;
                break;
            case KEY_ESC:
                data->key = LV_KEY_ESC;
                break;
            case KEY_DELETE:
                data->key = LV_KEY_DEL;
                break;
            case KEY_BACKSPACE:
                data->key = LV_KEY_BACKSPACE;
                break;
            case KEY_ENTER:
                data->key = LV_KEY_ENTER;
                break;
            case KEY_NEXT:
            case KEY_TAB:
                data->key = LV_KEY_NEXT;
                break;
            case KEY_PREVIOUS:
                data->key = LV_KEY_PREV;
                break;
            case KEY_HOME:
                data->key = LV_KEY_HOME;
                break;
            case KEY_END:
                data->key = LV_KEY_END;
                break;
            case KEY_POWER:
                if (pressed && on_key_power_cb)
                    on_key_power_cb();
                ignore = true;
                break;
            default: {
                if (!ext->xkb_state) {
                    ignore = true;
                    break;
                }
                char buffer[] = { 0, 0, 0, 0, 0 };
                int size = xkb_state_key_get_utf8(ext->xkb_state, keycode, buffer, sizeof(buffer));
                if (size == 0) {
                    ignore = true;
                    break;
                }
                data->key = *((uint32_t*) buffer);
                break;
            }}

            if (ext->xkb_state)
                xkb_state_update_key(ext->xkb_state, keycode, pressed ? XKB_KEY_DOWN : XKB_KEY_UP);

            if (ignore)
                break;

            /* If a new key is pressed while the previous is still held (i.e. fast typers),
            synthesize a release so LVGL sees the required state transition and
            the new keypress is registered */
            if (pressed && data->state == LV_INDEV_STATE_PRESSED) {
                data->state = LV_INDEV_STATE_RELEASED;
                lv_indev_read(ext->keypad);
            }

            data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;

            lv_indev_read(ext->keypad);
            break;
        }
#endif

        case LIBINPUT_EVENT_POINTER_MOTION: {
            struct libinput_event_pointer *pointer_event = libinput_event_get_pointer_event(event);

            struct indev_ext *data = lv_indev_get_user_data(ext->pointer1);
            lv_display_t* display = lv_indev_get_display(ext->pointer1);

            int32_t x = data->point.x + (int32_t) libinput_event_pointer_get_dx(pointer_event);
            data->point.x = LV_CLAMP(0, x, display->hor_res - 1);
            int32_t y = data->point.y + (int32_t) libinput_event_pointer_get_dy(pointer_event);
            data->point.y = LV_CLAMP(0, y, display->ver_res - 1);

            lv_indev_read(ext->pointer1);
            break;
        }

        case LIBINPUT_EVENT_POINTER_BUTTON: {
            struct libinput_event_pointer *pointer_event = libinput_event_get_pointer_event(event);

            struct indev_ext *data = lv_indev_get_user_data(ext->pointer1);
            data->state = libinput_event_pointer_get_button_state(pointer_event) == LIBINPUT_BUTTON_STATE_PRESSED ?
                LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;

            lv_indev_read(ext->pointer1);
            break;
        }

        case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: {
            struct libinput_event_pointer *pointer_event = libinput_event_get_pointer_event(event);

            struct indev_ext *data = lv_indev_get_user_data(ext->pointer1);
            lv_display_t* display = lv_indev_get_display(ext->pointer1);

            int32_t x = libinput_event_pointer_get_absolute_x_transformed(pointer_event, display->physical_hor_res);
            int32_t y = libinput_event_pointer_get_absolute_y_transformed(pointer_event, display->physical_ver_res);
            x -= display->offset_x;
            y -= display->offset_y;
            if (x < 0 || x >= display->hor_res || y < 0 || y >= display->ver_res)
                break;
            data->point.x = x;
            data->point.y = y;

            lv_indev_read(ext->pointer1);
            break;
        }

        case LIBINPUT_EVENT_TOUCH_MOTION: {
            struct libinput_event_touch *touch_event = libinput_event_get_touch_event(event);
            int32_t slot = libinput_event_touch_get_slot(touch_event);

            lv_indev_t *pointer;
            if (slot <= 0)
                pointer = ext->pointer1;
            else if (slot == 1)
                pointer = ext->pointer2;
            else
                break;

            struct indev_ext *data = lv_indev_get_user_data(pointer);
            lv_display_t* display = lv_indev_get_display(pointer);

            int32_t x = libinput_event_touch_get_x_transformed(touch_event, display->physical_hor_res);
            int32_t y = libinput_event_touch_get_y_transformed(touch_event, display->physical_ver_res);
            x -= display->offset_x;
            y -= display->offset_y;
            if (x < 0 || x >= display->hor_res || y < 0 || y >= display->ver_res)
                break;
            data->point.x = x;
            data->point.y = y;

            lv_indev_read(pointer);
            break;
        }

        case LIBINPUT_EVENT_TOUCH_DOWN: {
            struct libinput_event_touch *touch_event = libinput_event_get_touch_event(event);
            int32_t slot = libinput_event_touch_get_slot(touch_event);

            lv_indev_t *pointer;
            if (slot <= 0)
                pointer = ext->pointer1;
            else if (slot == 1)
                pointer = ext->pointer2;
            else
                break;

            struct indev_ext *data = lv_indev_get_user_data(pointer);
            lv_display_t* display = lv_indev_get_display(pointer);

            int32_t x = libinput_event_touch_get_x_transformed(touch_event, display->physical_hor_res);
            int32_t y = libinput_event_touch_get_y_transformed(touch_event, display->physical_ver_res);
            x -= display->offset_x;
            y -= display->offset_y;
            if (x < 0 || x >= display->hor_res || y < 0 || y >= display->ver_res)
                break;
            data->point.x = x;
            data->point.y = y;
            data->state = LV_INDEV_STATE_PRESSED;

            lv_indev_read(pointer);
            break;
        }

        case LIBINPUT_EVENT_TOUCH_UP:
        case LIBINPUT_EVENT_TOUCH_CANCEL: {
            struct libinput_event_touch *touch_event = libinput_event_get_touch_event(event);
            int32_t slot = libinput_event_touch_get_slot(touch_event);

            lv_indev_t *pointer;
            if (slot <= 0)
                pointer = ext->pointer1;
            else if (slot == 1)
                pointer = ext->pointer2;
            else
                break;

            struct indev_ext *data = lv_indev_get_user_data(pointer);
            data->state = LV_INDEV_STATE_RELEASED;

            lv_indev_read(pointer);
            break;
        }

        default:
            break;
        }

        libinput_event_destroy(event);
    }
}


static void attach_input_device(struct udev_device* device) {
    const char* node = udev_device_get_devnode(device);

    enum input_device_type type = identify_input_device(device);
    switch (type) {
    case BBX_INDEV_NONE:
        return;
    case BBX_INDEV_KEY:
#ifndef BBX_APP_BUFFYBOARD
        break;
#else
        bbx_log(BBX_LOG_LEVEL_VERBOSE, "Key %s is ignored", node);
        return;
#endif
    case BBX_INDEV_KEYBOARD:
#ifndef BBX_APP_BUFFYBOARD
        if (!options.keyboard) {
            bbx_log(BBX_LOG_LEVEL_VERBOSE, "Keyboard %s is ignored", node);
            return;
        }
        break;
#else
        bbx_log(BBX_LOG_LEVEL_VERBOSE, "Keyboard %s is ignored", node);
        return;
#endif
    case BBX_INDEV_MOUSE:
        if (!options.pointer) {
            bbx_log(BBX_LOG_LEVEL_VERBOSE, "Mouse %s is ignored", node);
            return;
        }
        break;
    case BBX_INDEV_TOUCHPAD:
        if (!options.pointer) {
            bbx_log(BBX_LOG_LEVEL_VERBOSE, "Touchpad %s is ignored", node);
            return;
        }
        break;
    case BBX_INDEV_TOUCHSCREEN:
        if (!options.touchscreen) {
            bbx_log(BBX_LOG_LEVEL_VERBOSE, "Touchscreen %s is ignored", node);
            return;
        }
        break;
    default:
        break;
    }

    /* A device can be a force feedback device and an input device independently,
       so we check for both capabilities. */

    if (options.force_feedback)
        bbx_force_feedback_connect(device);

    struct libinput_device* dev = libinput_path_add_device(context_libinput, node);
    if (!dev) {
        bbx_log(BBX_LOG_LEVEL_WARNING, "libinput can't use %s", node);
        return;
    }

    static_assert(sizeof(type) <= sizeof(void*));
    libinput_device_set_user_data(dev, (void*) type);

    on_input_event(); /* Process LIBINPUT_EVENT_DEVICE_ADDED immediately */
}

static void attach_input_devices() {
    DIR* dir = opendir("/dev/input");
    if (!dir) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "Can't open /dev/input: %s", strerror(errno));
        return;
    }

    struct dirent* entry;
    while ((entry = readdir(dir))) {
        if (strncmp(entry->d_name, "event", 5) != 0)
            continue;

        char node[sizeof("/dev/input") + strlen(entry->d_name) + 1];
        sprintf(node, "/dev/input/%s", entry->d_name);

        struct stat buffer;
        int r = stat(node, &buffer);
        if (r != 0) {
            bbx_log(BBX_LOG_LEVEL_ERROR, "stat(%s) is failed: %s", node, strerror(errno));
            continue;
        }

        assert(S_ISCHR(buffer.st_mode));

        struct udev_device *device = udev_device_new_from_devnum(context_udev, 'c', buffer.st_rdev);
        if (!device) {
            bbx_log(BBX_LOG_LEVEL_ERROR, "Can't create udev device for %s", node);
            continue;
        }

        attach_input_device(device);

        udev_device_unref(device);
    }

    closedir(dir);
    return;
}

static void on_input_device_event() {
    bool attached = false;

    struct udev_device *device;
    while ((device = udev_monitor_receive_device(monitor))) {
        const char *action = udev_device_get_action(device);
        const char *devnode = udev_device_get_devnode(device);

        if (strcmp(action, "add") != 0)
            goto skip;

        if (!devnode || strncmp(devnode, "/dev/input/event", sizeof("/dev/input/event") - 1) != 0)
            goto skip;

        attach_input_device(device);
        attached = true;
skip:
        udev_device_unref(device);
    }

    /* Libinput does not store the state, and activates added devices immediately.
       So we have to suspend it again. */
    if (attached && libinput_is_suspended)
        libinput_suspend(context_libinput);
}


/**
 * Public functions
 */

uint8_t bbx_indev_init(int fd_epoll, const struct bbx_indev_opts* opts) {
    static const struct libinput_interface interface = {
        .open_restricted = open_restricted,
        .close_restricted = close_restricted,
    };

    context_libinput = libinput_path_create_context(&interface, NULL);
    if (!context_libinput) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "Could not create libinput context");
        return 0;
    }

    context_udev = udev_new();
    if (!context_udev) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "Could not create udev context");
        goto failure1;
    }

    monitor = udev_monitor_new_from_netlink(context_udev, "udev");
    if (!monitor) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "Could not create udev monitor");
        goto failure2;
    }

    if (udev_monitor_filter_add_match_subsystem_devtype(monitor, "input", NULL) != 0)
        bbx_log(BBX_LOG_LEVEL_WARNING, "Could not add a filter for udev monitor");

    if (udev_monitor_filter_update(monitor) != 0)
        bbx_log(BBX_LOG_LEVEL_WARNING, "Could not update a filter for udev monitor");

#ifndef BBX_APP_BUFFYBOARD
    struct xkb_context *context_xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
    if (!context_xkb) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "Could not create xkb_context");
        goto failure3;
    }

    keymap = xkb_keymap_new_from_names(context_xkb, opts->keymap, XKB_KEYMAP_COMPILE_NO_FLAGS);
    if (!keymap) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "Can't compile xkb_rule_names: %s:%s:%s:%s:%s",
            opts->keymap->rules,
            opts->keymap->model,
            opts->keymap->layout,
            opts->keymap->variant,
            opts->keymap->options);
        goto failure4;
    }
#endif

    lv_display_t *display = lv_display_get_default();
    assert(display->physical_hor_res > 0);
    assert(display->physical_ver_res > 0);

    mouse_cursor = lv_image_create(lv_display_get_layer_sys(display));
    if (!mouse_cursor) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "lv_image_create() is failed");
        goto failure5;
    }
    lv_obj_add_flag(mouse_cursor, LV_OBJ_FLAG_HIDDEN);
    lv_image_set_src(mouse_cursor, &cursor);

#ifndef BBX_APP_BUFFYBOARD
    keyboard_input_group = lv_group_create();
    if (!keyboard_input_group) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "lv_group_create() is failed");
        goto failure6;
    }

    options.keyboard = opts->keyboard;
#endif
    options.pointer = opts->pointer;
    options.touchscreen = opts->touchscreen;
    options.force_feedback = opts->force_feedback;

    attach_input_devices();

    uint8_t ret = 0;

    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.ptr = __extension__ (void*) on_input_event;

    int r = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, libinput_get_fd(context_libinput), &event);
    if (r == -1) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "EPOLL_CTL_ADD for libinput context is failed");
        return ret;
    }
    ret++;

    event.events = EPOLLIN;
    event.data.ptr = __extension__ (void*) on_input_device_event;

    r = epoll_ctl(fd_epoll, EPOLL_CTL_ADD, udev_monitor_get_fd(monitor), &event);
    if (r == -1) {
        bbx_log(BBX_LOG_LEVEL_ERROR, "EPOLL_CTL_ADD for udev_monitor is failed");
        return ret;
    }
    ret++;

    return ret;


#ifndef BBX_APP_BUFFYBOARD
failure6:
    lv_obj_delete(mouse_cursor);
#endif
failure5:
#ifndef BBX_APP_BUFFYBOARD
    xkb_keymap_unref(keymap);
failure4:
    xkb_context_unref(context_xkb);
failure3:
#endif
    udev_monitor_unref(monitor);
failure2:
    udev_unref(context_udev);
failure1:
    libinput_unref(context_libinput);
    return 0;
}

void bbx_indev_suspend() {
    if (!libinput_is_suspended) {
        libinput_suspend(context_libinput);
        libinput_is_suspended = true;
        bbx_log(BBX_LOG_LEVEL_VERBOSE, "libinput is suspended");
    }
}

void bbx_indev_resume() {
    if (libinput_is_suspended) {
        libinput_resume(context_libinput);
        libinput_is_suspended = false;
        bbx_log(BBX_LOG_LEVEL_VERBOSE, "libinput is resumed");
    }
}

#ifndef BBX_APP_BUFFYBOARD
bool bbx_indev_is_keyboard_connected() {
    return num_keyboards != 0;
}

void bbx_indev_set_key_power_cb(void (*callback)()) {
    on_key_power_cb = callback;
}
#endif
