/*
 * Copyright (c) 2019 - 2025 Andri Yngvason
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
 * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
 * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 * PERFORMANCE OF THIS SOFTWARE.
 *
 * Acknowledgements: Reading Josef Gajdusek's wvnc code helped me understand
 * how to use the xkbcommon API to interface with the wayland virtual keyboard
 * interface.
 */

#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>
#include <wayland-client-protocol.h>
#include <xkbcommon/xkbcommon-keysyms.h>
#include <xkbcommon/xkbcommon.h>
#include <wayland-client.h>
#include <neatvnc.h>

#include "virtual-keyboard-unstable-v1.h"
#include "keyboard.h"
#include "shm.h"
#include "intset.h"

#define MAYBE_UNUSED __attribute__((unused))

struct table_entry {
	xkb_keysym_t symbol;
	xkb_keycode_t code;
	int level;
	int group;
};

struct key_iter_context {
	struct keyboard* kb;
	int group;
};

struct kb_mods {
	xkb_mod_mask_t depressed, latched, locked;
};

static void save_mods(struct keyboard* self, struct kb_mods* mods);

static void append_entry(struct keyboard* self, xkb_keysym_t symbol,
                         xkb_keycode_t code, int level, int group)
{
	if (self->lookup_table_size <= self->lookup_table_length) {
		size_t new_size = self->lookup_table_size * 2;
		struct table_entry* table =
			realloc(self->lookup_table, new_size * sizeof(*table));
		if (!table)
			return; // TODO: Report this

		self->lookup_table_size = new_size;
		self->lookup_table = table;
	}

	struct table_entry* entry =
		&self->lookup_table[self->lookup_table_length++];

	entry->symbol = symbol;
	entry->code = code;
	entry->level = level;
	entry->group = group;
}

static void key_iter(struct xkb_keymap* map, xkb_keycode_t code, void* userdata)
{
	struct key_iter_context* ctx = userdata;
	struct keyboard* self = ctx->kb;
	int group = ctx->group;

	size_t n_levels = xkb_keymap_num_levels_for_key(map, code, group);

	for (size_t level = 0; level < n_levels; level++) {
		const xkb_keysym_t* symbols;
		size_t n_syms = xkb_keymap_key_get_syms_by_level(map, code,
				group, level, &symbols);

		for (size_t sym_idx = 0; sym_idx < n_syms; sym_idx++)
			append_entry(self, symbols[sym_idx], code, level, group);
	}
}

static int compare_symbols(const void* a, const void* b)
{
	const struct table_entry* x = a;
	const struct table_entry* y = b;

	if (x->group != y->group)
		return x->group < y->group ? -1 : 1;

	if (x->symbol != y->symbol)
		return x->symbol < y->symbol ? -1 : 1;

	return x->code < y->code ? -1 : x->code > y->code;
}

static int compare_symbols2(const void* a, const void* b)
{
	const struct table_entry* x = a;
	const struct table_entry* y = b;

	if (x->group != y->group)
		return x->group < y->group ? -1 : 1;

	return x->symbol < y->symbol ? -1 : x->symbol > y->symbol;
}

static int create_lookup_table(struct keyboard* self)
{
	self->lookup_table_length = 0;
	self->lookup_table_size = 128;

	self->lookup_table =
		malloc(self->lookup_table_size * sizeof(*self->lookup_table));
	if (!self->lookup_table)
		return -1;

	int n_groups = xkb_keymap_num_layouts(self->keymap);
	for (int group = 0; group < n_groups; ++group) {
		struct key_iter_context ctx = {
			.kb = self,
			.group = group,
		};

		xkb_keymap_key_for_each(self->keymap, key_iter, &ctx);
	}

	qsort(self->lookup_table, self->lookup_table_length,
	      sizeof(*self->lookup_table), compare_symbols);

	return 0;
}

static char* get_symbol_name(xkb_keysym_t sym, char* dst, size_t size)
{
	if (xkb_keysym_get_name(sym, dst, size) >= 0)
		return dst;

	snprintf(dst, size, "UNKNOWN (%x)", sym);
	return dst;
}

static void keyboard__dump_entry(const struct keyboard* self,
                                 const struct table_entry* entry)
{
	char sym_name[256];
	get_symbol_name(entry->symbol, sym_name, sizeof(sym_name));

	const char *group_name MAYBE_UNUSED =
		xkb_keymap_layout_get_name(self->keymap, entry->group);

	const char* code_name MAYBE_UNUSED =
		xkb_keymap_key_get_name(self->keymap, entry->code);

	bool is_pressed MAYBE_UNUSED =
		intset_is_set(&self->key_state, entry->code);

	nvnc_log(NVNC_LOG_DEBUG, "group=%s symbol=%s level=%d code=%s %s",
			group_name, sym_name, entry->level, code_name,
			is_pressed ? "pressed" : "released");
}

void keyboard_dump_lookup_table(const struct keyboard* self)
{
	for (size_t i = 0; i < self->lookup_table_length; i++)
		keyboard__dump_entry(self, &self->lookup_table[i]);
}

int keyboard_init(struct keyboard* self, const struct xkb_rule_names* rule_names)
{
	self->context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
	if (!self->context)
		return -1;

	if (intset_init(&self->key_state, 0) < 0)
		goto key_state_failure;

	self->keymap = xkb_keymap_new_from_names(self->context, rule_names, 0);
	if (!self->keymap)
		goto keymap_failure;

	self->state = xkb_state_new(self->keymap);
	if (!self->state)
		goto state_failure;

	if (create_lookup_table(self) < 0)
		goto table_failure;

//	keyboard_dump_lookup_table(self);

	char* keymap_string =
		xkb_keymap_get_as_string(self->keymap,
		                         XKB_KEYMAP_FORMAT_TEXT_V1);
	if (!keymap_string)
		goto keymap_string_failure;

	size_t keymap_size = strlen(keymap_string) + 1;

	int keymap_fd = shm_alloc_fd(keymap_size);
	if (keymap_fd < 0)
		goto fd_failure;

	size_t written = 0;
	while (written < keymap_size) {
		ssize_t ret = write(keymap_fd, keymap_string + written, keymap_size - written);
		if (ret == -1 && errno == EINTR)
			continue;
		if (ret == -1)
			goto write_failure;
		written += ret;
	}

	free(keymap_string);

	zwp_virtual_keyboard_v1_keymap(self->virtual_keyboard,
	                               WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1,
	                               keymap_fd, keymap_size);

	close(keymap_fd);

	return 0;

write_failure:
	close(keymap_fd);
fd_failure:
	free(keymap_string);
keymap_string_failure:
	free(self->lookup_table);
table_failure:
	xkb_state_unref(self->state);
state_failure:
	xkb_keymap_unref(self->keymap);
keymap_failure:
	intset_destroy(&self->key_state);
key_state_failure:
	xkb_context_unref(self->context);
	return -1;
}

void keyboard_destroy(struct keyboard* self)
{
	zwp_virtual_keyboard_v1_destroy(self->virtual_keyboard);
	free(self->lookup_table);
	xkb_state_unref(self->state);
	xkb_keymap_unref(self->keymap);
	intset_destroy(&self->key_state);
	xkb_context_unref(self->context);
}

static xkb_layout_index_t get_current_layout_group(const struct keyboard* self)
{
	int n_groups = xkb_keymap_num_layouts(self->keymap);
	assert(n_groups > 0);

	for (int i = 0; i < n_groups; ++i) {
		if (xkb_state_layout_index_is_active(self->state, i,
					XKB_STATE_LAYOUT_EFFECTIVE))
			return i;
	}

	return -1;
}

static struct table_entry* keyboard_find_symbol_in_group(const struct keyboard* self,
                                         xkb_keysym_t symbol, int group)
{
	struct table_entry cmp = {
		.group = group,
		.symbol = symbol,
	};

	struct table_entry* entry =
		bsearch(&cmp, self->lookup_table, self->lookup_table_length,
		        sizeof(*self->lookup_table), compare_symbols2);

	if (!entry)
		return NULL;

	while (entry != self->lookup_table && (entry - 1)->symbol == symbol)
		--entry;

	return entry;
}

static struct table_entry* keyboard_find_symbol(struct keyboard* self,
                                         xkb_keysym_t symbol)
{
	int n_groups = xkb_keymap_num_layouts(self->keymap);
	struct table_entry* entry = NULL;
	int current_group = get_current_layout_group(self);
	int group = -1;

	for (int g = 0; g < n_groups; ++g) {
		group = (current_group + g) % n_groups;
		entry = keyboard_find_symbol_in_group(self, symbol, group);
		if (entry)
			break;
	}

	assert(group >= 0);

	if (entry && group != current_group) {
		nvnc_log(NVNC_LOG_DEBUG, "Keyboard symbol was not found in active layout group; changing to a different one where it was found...");
		struct kb_mods mods;
		save_mods(self, &mods);
		xkb_state_update_mask(self->state, mods.depressed, mods.latched,
				mods.locked, 0, 0, group);
	}

	return entry;
}

static void keyboard_send_mods(struct keyboard* self)
{
	xkb_mod_mask_t depressed, latched, locked, group;

	depressed = xkb_state_serialize_mods(self->state, XKB_STATE_MODS_DEPRESSED);
	latched = xkb_state_serialize_mods(self->state, XKB_STATE_MODS_LATCHED);
	locked = xkb_state_serialize_mods(self->state, XKB_STATE_MODS_LOCKED);
	group = get_current_layout_group(self);
	self->last_sent_group = group;

	zwp_virtual_keyboard_v1_modifiers(self->virtual_keyboard, depressed,
	                                  latched, locked, group);
}

static void keyboard_apply_mods(struct keyboard* self, xkb_keycode_t code,
                                bool is_pressed)
{
	enum xkb_state_component comp, compmask;

	comp = xkb_state_update_key(self->state, code,
	                            is_pressed ? XKB_KEY_DOWN : XKB_KEY_UP);

	compmask = XKB_STATE_MODS_DEPRESSED |
	           XKB_STATE_MODS_LATCHED |
	           XKB_STATE_MODS_LOCKED |
	           XKB_STATE_MODS_EFFECTIVE;

	int current_group = get_current_layout_group(self);

	if (!(comp & compmask) && self->last_sent_group == current_group)
		return;

	keyboard_send_mods(self);
}

static struct table_entry* match_level(struct keyboard* self,
                                       struct table_entry* entry)
{
	xkb_keysym_t symbol = entry->symbol;

	while (true) {
		int level;

		level = xkb_state_key_get_level(self->state, entry->code,
				entry->group);

		if (entry->level == level)
			return entry;

		if (++entry >= &self->lookup_table[self->lookup_table_length] ||
		    entry->symbol != symbol)
			break;
	}

	return NULL;
}

static bool keyboard_symbol_is_mod(xkb_keysym_t symbol)
{
	switch (symbol) {
	case XKB_KEY_Shift_L:
	case XKB_KEY_Shift_R:
	case XKB_KEY_Control_L:
	case XKB_KEY_Caps_Lock:
	case XKB_KEY_Shift_Lock:
	case XKB_KEY_Meta_L:
	case XKB_KEY_Meta_R:
	case XKB_KEY_Alt_L:
	case XKB_KEY_Alt_R:
	case XKB_KEY_Super_L:
	case XKB_KEY_Super_R:
	case XKB_KEY_Hyper_L:
	case XKB_KEY_Hyper_R:
	case XKB_KEY_ISO_Level5_Shift:
	case XKB_KEY_ISO_Level5_Lock:
		return true;
	}

	return false;
}

static void send_key(struct keyboard* self, xkb_keycode_t code, bool is_pressed)
{
	zwp_virtual_keyboard_v1_key(self->virtual_keyboard, 0, code - 8,
	                            is_pressed ? WL_KEYBOARD_KEY_STATE_PRESSED
	                                       : WL_KEYBOARD_KEY_STATE_RELEASED);
}

static void save_mods(struct keyboard* self, struct kb_mods* mods)
{
	mods->depressed = xkb_state_serialize_mods(self->state,
			XKB_STATE_MODS_DEPRESSED);
	mods->latched = xkb_state_serialize_mods(self->state,
			XKB_STATE_MODS_LATCHED);
	mods->locked = xkb_state_serialize_mods(self->state,
			XKB_STATE_MODS_LOCKED);
}

static void restore_mods(struct keyboard* self, struct kb_mods* mods)
{
	int current_group = get_current_layout_group(self);
	xkb_state_update_mask(self->state, mods->depressed, mods->latched,
			mods->locked, 0, 0, current_group);
}

static void send_key_with_level(struct keyboard* self, xkb_keycode_t code,
		bool is_pressed, int level)
{
	int current_group = get_current_layout_group(self);
	struct kb_mods save;
	save_mods(self, &save);

	xkb_mod_mask_t mods = 0;
	xkb_keymap_key_get_mods_for_level(self->keymap, code,
			current_group, level, &mods, 1);
	xkb_state_update_mask(self->state, mods, 0, 0, 0, 0,
			current_group);
	keyboard_send_mods(self);

	nvnc_log(NVNC_LOG_DEBUG, "send key with level: old mods: %x, new mods: %x",
			save.latched | save.locked | save.depressed, mods);

	send_key(self, code, is_pressed);

	restore_mods(self, &save);
	keyboard_send_mods(self);
}

static bool update_key_state(struct keyboard* self, xkb_keycode_t code,
		bool is_pressed)
{
	bool was_pressed = intset_is_set(&self->key_state, code);
	if (was_pressed == is_pressed)
		return false;

	if (is_pressed)
		intset_set(&self->key_state, code);
	else
		intset_clear(&self->key_state, code);

	return true;
}

void keyboard_feed(struct keyboard* self, xkb_keysym_t symbol, bool is_pressed)
{
	struct table_entry* entry = keyboard_find_symbol(self, symbol);
	if (!entry) {
		char name[256];
		nvnc_log(NVNC_LOG_ERROR, "Failed to look up keyboard symbol: %s",
		          get_symbol_name(symbol, name, sizeof(name)));
		return;
	}

	bool level_is_match = true;

	if (!keyboard_symbol_is_mod(symbol)) {
		struct table_entry* level_entry = match_level(self, entry);
		if (level_entry)
			entry = level_entry;
		else
			level_is_match = false;
	}

#ifndef NDEBUG
	keyboard__dump_entry(self, entry);
#endif

	if (!update_key_state(self, entry->code, is_pressed))
		return;

	keyboard_apply_mods(self, entry->code, is_pressed);

	if (level_is_match)
		send_key(self, entry->code, is_pressed);
	else
		send_key_with_level(self, entry->code, is_pressed,
				entry->level);
}

void keyboard_feed_code(struct keyboard* self, xkb_keycode_t code,
		bool is_pressed)
{
	if (update_key_state(self, code, is_pressed)) {
		keyboard_apply_mods(self, code, is_pressed);
		send_key(self, code, is_pressed);
	}
}

enum nvnc_keyboard_led_state keyboard_get_led_state(
		const struct keyboard* self)
{
	enum nvnc_keyboard_led_state led_state = 0;

	if (xkb_state_led_name_is_active(self->state, XKB_LED_NAME_SCROLL))
		led_state |= NVNC_KEYBOARD_LED_SCROLL_LOCK;
	if (xkb_state_led_name_is_active(self->state, XKB_LED_NAME_NUM))
		led_state |= NVNC_KEYBOARD_LED_NUM_LOCK;
	if (xkb_state_led_name_is_active(self->state, XKB_LED_NAME_CAPS))
		led_state |= NVNC_KEYBOARD_LED_CAPS_LOCK;

	return led_state;
}
