/** Support for keyboard interface
 * @file
 *
 * @todo TODO: move stuff from here to itrm.{c,h} and mouse.{c,h}
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_STDINT_H
#include <stdint.h>
#endif
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_TERMIOS_H
#include <termios.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef __hpux__
#include <limits.h>
#define HPUX_PIPE	(len > PIPE_BUF || errno != EAGAIN)
#else
#define HPUX_PIPE	1
#endif

#include "elinks.h"

#include "config/options.h"
#include "intl/libintl.h"
#include "main/main.h"
#include "main/select.h"
#include "main/timer.h"
#include "osdep/ascii.h"
#include "osdep/osdep.h"
#include "terminal/hardio.h"
#include "terminal/itrm.h"
#include "terminal/kbd.h"
#ifdef CONFIG_KITTY
#include "terminal/map.h"
#endif
#include "terminal/mouse.h"
#include "terminal/terminal.h"
#include "util/error.h"
#include "util/memory.h"
#include "util/string.h"
#include "util/time.h"

struct itrm *ditrm = NULL;

static void free_itrm(struct itrm *);
static void in_sock(struct itrm *);
static int process_queue(struct itrm *);
static void handle_itrm_stdin(struct itrm *);
static void unhandle_itrm_stdin(struct itrm *);

#ifdef CONFIG_DEBUG
/** This hack makes GCC put enum term_event_special_key in the debug
 * information even though it is not otherwise used.  The const
 * prevents an unused-variable warning.  */
static const enum term_event_special_key dummy_term_event_special_key;
#endif


int
is_blocked(void)
{
	ELOG
	return ditrm && ditrm->blocked;
}


void
free_all_itrms(void)
{
	ELOG
	if (ditrm) free_itrm(ditrm);
}


/** A select_handler_T write_func for itrm_out.sock.  This is called
 * when there is data in @c itrm->out.queue and it is possible to write
 * it to @c itrm->out.sock.  When @c itrm->out.queue becomes empty, this
 * handler is temporarily removed.  */
static void
itrm_queue_write(struct itrm *itrm)
{
	ELOG
	int written;
	int qlen = int_min(itrm->out.queue.len, 128);

	assertm(qlen, "event queue empty");
	if_assert_failed return;

	written = safe_write(itrm->out.sock, itrm->out.queue.data, qlen);
	if (written <= 0) {
		if (written < 0) free_itrm(itrm); /* write error */
		return;
	}

	itrm->out.queue.len -= written;

	if (itrm->out.queue.len == 0) {
		set_handlers(itrm->out.sock,
			     get_handler(itrm->out.sock, SELECT_HANDLER_READ),
			     NULL,
			     get_handler(itrm->out.sock, SELECT_HANDLER_ERROR),
			     get_handler_data(itrm->out.sock));
	} else {
		assert(itrm->out.queue.len > 0);
		memmove(itrm->out.queue.data, itrm->out.queue.data + written, itrm->out.queue.len);
	}
}


void
itrm_queue_event(struct itrm *itrm, char *data, int len)
{
	ELOG
	int w = 0;

	if (!len) return;

	if (!itrm->out.queue.len && can_write(itrm->out.sock)) {
		w = safe_write(itrm->out.sock, data, len);
		if (w <= 0 && HPUX_PIPE) {
			register_bottom_half(free_itrm, itrm);
			return;
		}
	}

	if (w < len) {
		int left = len - w;
		unsigned char *c = (unsigned char *)mem_realloc(itrm->out.queue.data,
					       itrm->out.queue.len + left);

		if (!c) {
			free_itrm(itrm);
			return;
		}

		itrm->out.queue.data = c;
		memcpy(itrm->out.queue.data + itrm->out.queue.len, data + w, left);
		itrm->out.queue.len += left;
		set_handlers(itrm->out.sock,
			     get_handler(itrm->out.sock, SELECT_HANDLER_READ),
			     (select_handler_T) itrm_queue_write,
			     (select_handler_T) free_itrm, itrm);
	}
}


void
kbd_ctrl_c(void)
{
	ELOG
	struct interlink_event ev;

	if (!ditrm) return;
	/* This is called because of a signal, and there may be input
	 * pending from the terminal, so do not reset
	 * ditrm->bracketed_pasting.  */
	set_kbd_interlink_event(&ev, KBD_CTRL_C, KBD_MOD_NONE);
	itrm_queue_event(ditrm, (char *) &ev, sizeof(ev));
}

#define write_sequence(fd, seq) \
	hard_write(fd, seq, sizeof(seq) - 1)


#define INIT_TERMINAL_SEQ	"\033)0\0337"	/**< Special Character and Line Drawing Set, Save Cursor */
#define INIT_ALT_SCREEN_SEQ	"\033[?47h"	/**< Use Alternate Screen Buffer */
#define INIT_BRACKETED_PASTE_SEQ "\033[?2004h"	/**< Enable XTerm bracketed paste mode */

static void
send_init_sequence(int h, int altscreen)
{
	ELOG
#ifdef CONFIG_OS_DOS
	save_terminal();
#endif
	want_draw();
	write_sequence(h, INIT_TERMINAL_SEQ);

	/* If alternate screen is supported switch to it. */
	if (altscreen) {
		write_sequence(h, INIT_ALT_SCREEN_SEQ);
	}
#ifdef CONFIG_MOUSE
	if (mouse_enabled) {
		send_mouse_init_sequence(h);
	}
#endif
#ifndef CONFIG_OS_DOS
	write_sequence(h, INIT_BRACKETED_PASTE_SEQ);
#endif
	done_draw();
}

#define DONE_CLS_SEQ		"\033[2J"	/**< Erase in Display, Clear All */
#define DONE_TERMINAL_SEQ	"\0338\r \b"	/**< Restore Cursor (DECRC) + ??? */
#define DONE_ALT_SCREEN_SEQ	"\033[?47l"	/**< Use Normal Screen Buffer */
#define DONE_BRACKETED_PASTE_SEQ "\033[?2004l"	/**< Disable XTerm bracketed paste mode */

static void
send_done_sequence(int h, int altscreen)
{
	ELOG
	want_draw();
#ifndef CONFIG_OS_DOS
	write_sequence(h, DONE_BRACKETED_PASTE_SEQ);
#endif
	write_sequence(h, DONE_CLS_SEQ);

#ifdef CONFIG_MOUSE
	send_mouse_done_sequence(h);
#endif

	/* Switch from alternate screen. */
	if (altscreen) {
		write_sequence(h, DONE_ALT_SCREEN_SEQ);
	}

	write_sequence(h, DONE_TERMINAL_SEQ);
	done_draw();
#ifdef CONFIG_OS_DOS
	restore_terminal();
#endif
}


#undef write_sequence

void
resize_terminal(void)
{
	ELOG
	struct interlink_event ev;
	int width, height;
	int cell_width, cell_height;

	get_terminal_size(ditrm->out.std, &width, &height, &cell_width, &cell_height);
	set_resize_interlink_event(&ev, width, height, cell_width, cell_height);
	itrm_queue_event(ditrm, (char *) &ev, sizeof(ev));
}

void
get_terminal_name(char name[MAX_TERM_LEN])
{
	ELOG
	char *term = getenv("TERM");
	int i;

	memset(name, 0, MAX_TERM_LEN);

	if (!term) return;

	for (i = 0; term[i] != 0 && i < MAX_TERM_LEN - 1; i++)
		name[i] = isident(term[i]) ? term[i] : '-';
}


static int
setraw(struct itrm *itrm, int save_orig)
{
	ELOG
	struct termios t;

	memset(&t, 0, sizeof(t));
	if (tcgetattr(itrm->in.ctl, &t)) return -1;

	if (save_orig) copy_struct(&itrm->t, &t);

#ifdef VERASE
	long vdisable = -1;
#ifdef _POSIX_VDISABLE
	vdisable = _POSIX_VDISABLE;
#elif defined(HAVE_FPATHCONF)
	vdisable = fpathconf(itrm->in.ctl, _PC_VDISABLE);
#endif
/* Is VERASE defined on Windows? */
	if (vdisable != -1 && t.c_cc[VERASE] == vdisable)
		itrm->verase = -1;
	else
		itrm->verase = (unsigned char) t.c_cc[VERASE];
#else
	itrm->verase = -1;
#endif
	elinks_cfmakeraw(&t);
	t.c_lflag |= ISIG;
#ifdef TOSTOP
	t.c_lflag |= get_opt_bool("ui.tostop", NULL) ? TOSTOP : 0;
#endif
	t.c_oflag |= OPOST;
	if (tcsetattr(itrm->in.ctl, TCSANOW, &t)) return -1;

	return 0;
}

/** Construct the struct itrm of this process, make ::ditrm point to it,
 * set up select() handlers, and send the initial interlink packet.
 *
 * The first five parameters are file descriptors that this function
 * saves in submembers of struct itrm, and for which this function may
 * set select() handlers.  Please see the definitions of struct
 * itrm_in and struct itrm_out for further explanations.
 *
 * @param std_in	itrm_in.std: read tty device (or pipe)
 * @param std_out	itrm_out.std: write tty device (or pipe)
 * @param sock_in	itrm_in.sock
 *			- If master: == @a std_out (masterhood flag)
 *			- If slave: read socket from master
 * @param sock_out	itrm_out.sock
 *			- If master: write pipe to same process
 *			- If slave: write socket to master
 * @param ctl_in	itrm_in.ctl: control tty device
 *
 * The remaining three parameters control the initial interlink packet.
 *
 * @param init_string	A string to be passed to the master process.  Need
 *			not be null-terminated.  If @a remote == 0, this is
 *			a URI.  Otherwise, this is a remote command.
 * @param init_len	The length of init_string, in bytes.
 * @param remote	= 0 if asking the master to start a new session
 *			and display it via this process.  Otherwise,
 *			enum ::remote_session_flags.  */
void
handle_trm(int std_in, int std_out, int sock_in, int sock_out, int ctl_in,
	   void *init_string, int init_len, int remote)
{
	ELOG
	struct itrm *itrm;
	struct terminal_info info;
	struct interlink_event_size *size = &info.event.info.size;
	char *ts;

	memset(&info, 0, sizeof(info));

#ifdef CONFIG_OS_DOS
	dos_setraw(ctl_in, 1);
#endif

	get_terminal_size(ctl_in, &size->width, &size->height, &size->cell_width, &size->cell_height);
	info.event.ev = EVENT_INIT;
	info.system_env = get_system_env();
	info.length = init_len;

	if (remote) {
		info.session_info = remote;
		info.magic = INTERLINK_REMOTE_MAGIC;
	} else {
		info.session_info = get_cmd_opt_int("base-session");
		info.magic = INTERLINK_NORMAL_MAGIC;
	}

	itrm = (struct itrm *)mem_calloc(1, sizeof(*itrm));
	if (!itrm) return;

	itrm->in.queue.data = (unsigned char *)mem_calloc(1, ITRM_IN_QUEUE_SIZE);
	if (!itrm->in.queue.data) {
		mem_free(itrm);
		return;
	}
	itrm->in.queue.size = ITRM_IN_QUEUE_SIZE;

	ditrm = itrm;
	itrm->in.std = std_in;
	itrm->out.std = std_out;
	itrm->in.sock = sock_in;
	itrm->out.sock = sock_out;
	itrm->in.ctl = ctl_in;
	itrm->timer = TIMER_ID_UNDEF;
	itrm->remote = !!remote;

	/* If the master does not tell which charset it's using in
	 * this terminal, assume it's some ISO 8859.  Because that's
	 * what older versions of ELinks did.  */
	itrm->title_codepage = get_cp_index("ISO-8859-1");

	/* FIXME: Combination altscreen + xwin does not work as it should,
	 * mouse clicks are reportedly partially ignored. */
	if (info.system_env & (ENV_SCREEN | ENV_XWIN))
		itrm->altscreen = 1;

	if (!remote) {
		if (ctl_in >= 0) setraw(itrm, 1);
		send_init_sequence(std_out, itrm->altscreen);
		handle_terminal_resize(ctl_in, resize_terminal);
#ifdef CONFIG_MOUSE
		enable_mouse();
#endif
		handle_itrm_stdin(itrm);
	} else {
		/* elinks -remote may not have a valid stdin if not run from a tty (bug 938) */
		if (std_in >= 0) handle_itrm_stdin(itrm);
	}

	if (sock_in != std_out) {
		set_handlers(sock_in, (select_handler_T) in_sock,
			     NULL, (select_handler_T) free_itrm, itrm);
	}

	get_terminal_name(info.name);

	ts = get_cwd();
	if (ts) {
		memcpy(info.cwd, ts, int_min(strlen(ts), MAX_CWD_LEN));
		mem_free(ts);
	}

	itrm_queue_event(itrm, (char *) &info, TERMINAL_INFO_SIZE);
	itrm_queue_event(itrm, (char *) init_string, init_len);
}


/** A select_handler_T read_func and error_func for the pipe (intptr_t) @a h.
 * This is called when the subprocess started on the terminal of this
 * ELinks process exits.  ELinks then resumes using the terminal.  */
static void
unblock_itrm_x(void *h)
{
	ELOG
	close_handle(h);
	if (!ditrm) return;
	unblock_itrm();

	/* Note: External editor support depends on this resize event. */
	resize_terminal();
}


int
unblock_itrm(void)
{
	ELOG
	if (!ditrm) return -1;

	if (ditrm->in.ctl >= 0 && setraw(ditrm, 0)) return -1;
	ditrm->blocked = 0;
	send_init_sequence(ditrm->out.std, ditrm->altscreen);

	handle_itrm_stdin(ditrm);
	resume_mouse(ditrm->mouse_h);

	handle_terminal_resize(ditrm->in.ctl, resize_terminal);
	unblock_stdin();

	return 0;
}


void
block_itrm(void)
{
	ELOG
	if (!ditrm) return;

	ditrm->blocked = 1;
	block_stdin();
	kill_timer(&ditrm->timer);
	ditrm->in.queue.len = 0;
	unhandle_terminal_resize(ditrm->in.ctl);
	send_done_sequence(ditrm->out.std, ditrm->altscreen);
	tcsetattr(ditrm->in.ctl, TCSANOW, &ditrm->t);
	unhandle_itrm_stdin(ditrm);
	suspend_mouse(ditrm->mouse_h);
}

static void
free_itrm(struct itrm *itrm)
{
	ELOG
	if (!itrm) return;

	if (!itrm->remote) {
		if (itrm->orig_title && *itrm->orig_title) {
			set_window_title(itrm->orig_title, itrm->title_codepage);

		} else if (itrm->touched_title) {
			/* Set the window title to the value of $TERM if X11
			 * wasn't compiled in. Should hopefully make at least
			 * half the users happy. (debian bug #312955) */
			char title[MAX_TERM_LEN];

			get_terminal_name(title);
			if (*title)
				set_window_title(title,
						 get_cp_index("US-ASCII"));
		}


		unhandle_terminal_resize(itrm->in.ctl);
#ifdef CONFIG_MOUSE
		disable_mouse();
#endif
		send_done_sequence(itrm->out.std, itrm->altscreen);
		tcsetattr(itrm->in.ctl, TCSANOW, &itrm->t);
	}

	mem_free_set(&itrm->orig_title, NULL);

	/* elinks -remote may not have a valid stdin if not run from a tty (bug 938) */
	if (!itrm->remote || itrm->in.std >= 0) clear_handlers(itrm->in.std);
	clear_handlers(itrm->in.sock);
	clear_handlers(itrm->out.std);
	clear_handlers(itrm->out.sock);

	kill_timer(&itrm->timer);

	if (itrm == ditrm) ditrm = NULL;
	mem_free_if(itrm->out.queue.data);
	mem_free_if(itrm->in.queue.data);
	mem_free(itrm);

	program.terminate = 1;
}

/** Resize terminal to dimensions specified by @a text string.
 * @a text should look like "width,height,old-width,old-height"
 * where width and height are integers. */
static inline void
resize_terminal_from_str(const char *text_)
{
	ELOG
	enum { NEW_WIDTH = 0, NEW_HEIGHT, OLD_WIDTH, OLD_HEIGHT, NUMBERS };
	int numbers[NUMBERS];
	int i;
	char *text2, *text;

	assert(text_ && *text_);
	if_assert_failed return;

	text2 = stracpy(text_);
	if (!text2) {
		return;
	}
	text = text2;

	for (i = 0; i < NUMBERS; i++) {
		char *p = strchr(text, ',');

		if (p) {
			*p++ = '\0';

		} else if (i < OLD_HEIGHT) {
			mem_free(text2);
			return;
		}

		numbers[i] = atoi(text);

		if (p) text = p;
	}

	resize_window(numbers[NEW_WIDTH], numbers[NEW_HEIGHT],
		      numbers[OLD_WIDTH], numbers[OLD_HEIGHT]);
	resize_terminal();
	mem_free(text2);
}

void
dispatch_special(const char *text)
{
	ELOG
	switch (text[0]) {
		case TERM_FN_TITLE:
			if (ditrm) {
				if (ditrm->remote)
					break;

				/* If ditrm->touched_title is 0, then
				 * ditrm->orig_title should be NULL,
				 * but check it to prevent any leak.  */
				if (!ditrm->orig_title && !ditrm->touched_title)
					ditrm->orig_title = get_window_title(
						ditrm->title_codepage);
				ditrm->touched_title = 1;
			}
			/* TODO: Is it really possible to get here with
			 * ditrm == NULL, and which charset would then
			 * be most appropriate?  */
			set_window_title(text + 1,
					 ditrm ? ditrm->title_codepage
					 : get_cp_index("US-ASCII"));
			break;
		case TERM_FN_RESIZE:
			if (ditrm && ditrm->remote)
				break;

			resize_terminal_from_str(text + 1);
			break;
		case TERM_FN_TITLE_CODEPAGE:
			if (ditrm) {
				int cp = get_cp_index(text + 1);

				/* If the master sends the name of an
				 * unrecognized charset, assume only
				 * that it's ASCII compatible.  */
				if (cp == -1)
					cp = get_cp_index("US-ASCII");
				ditrm->title_codepage = cp;
			}
			break;
	}
}

static void inline
safe_hard_write(int fd, const char *buf, int len)
{
	ELOG
	if (is_blocked()) return;

	want_draw();
	hard_write(fd, buf, len);
	done_draw();
}

/** A select_handler_T read_func for itrm_in.sock.  A slave process
 * calls this when the master sends it data to be displayed.  The
 * master process never calls this.  */
static void
in_sock(struct itrm *itrm)
{
	ELOG
	struct string path;
	struct string delete_;
	char ch;
	int fg; /* enum term_exec */
	ssize_t bytes_read, i, p;
	char buf[ITRM_OUT_QUEUE_SIZE];

	bytes_read = safe_read(itrm->in.sock, buf, ITRM_OUT_QUEUE_SIZE);
	if (bytes_read <= 0) goto free_and_return;

qwerty:
	for (i = 0; i < bytes_read; i++)
		if (!buf[i])
			goto has_nul_byte;

	safe_hard_write(itrm->out.std, buf, bytes_read);
	return;

has_nul_byte:
	if (i) safe_hard_write(itrm->out.std, buf, i);

	i++;
	assert(ITRM_OUT_QUEUE_SIZE - i > 0);
	memmove(buf, buf + i, ITRM_OUT_QUEUE_SIZE - i);
	bytes_read -= i;
	p = 0;

#define RD(xx) {							\
		char cc;					\
									\
		if (p < bytes_read)					\
			cc = buf[p++];					\
		else if ((hard_read(itrm->in.sock, &cc, 1)) <= 0)	\
			goto free_and_return;				\
		xx = cc;						\
	}

	RD(fg);

	if (!init_string(&path)) goto free_and_return;

	while (1) {
		RD(ch);
		if (!ch) break;
		add_char_to_string(&path, ch);
	}

	if (!init_string(&delete_)) {
		done_string(&path);
		goto free_and_return;
	}

	while (1) {
		RD(ch);
		if (!ch) break;
		add_char_to_string(&delete_, ch);
	}

#undef RD

	if (!*path.source) {
		dispatch_special(delete_.source);

	} else {
		int blockh = 0;
		char *param;
		int path_len, del_len, param_len;

		/* TODO: Should this be changed to allow TERM_EXEC_NEWWIN
		 * in a blocked terminal?  There is similar code in
		 * exec_on_terminal().  --KON, 2007 */
		if (is_blocked() && fg != TERM_EXEC_BG) {
			if (*delete_.source) unlink(delete_.source);
			goto nasty_thing;
		}

		path_len = path.length;
		del_len = delete_.length;
		param_len = path_len + del_len + 3;

		param = (char *)mem_alloc(param_len);
		if (!param) goto nasty_thing;

		param[0] = fg;
		memcpy(param + 1, path.source, path_len + 1);
		memcpy(param + 1 + path_len + 1, delete_.source, del_len + 1);

		if (fg == TERM_EXEC_FG) block_itrm();

#ifndef WIN32
		blockh = start_thread((void (*)(void *, int)) exec_thread,
				      param, param_len);
#else
		exec_thread(param, param_len);

#endif
		mem_free(param);

		if (blockh == -1) {
			if (fg == TERM_EXEC_FG)
				unblock_itrm();

			goto nasty_thing;
		}

		if (fg == TERM_EXEC_FG) {
			set_handlers(blockh, (select_handler_T) unblock_itrm_x,
				     NULL, (select_handler_T) unblock_itrm_x,
				     (void *) (intptr_t) blockh);

		} else {
			set_handlers(blockh, close_handle, NULL, close_handle,
				     (void *) (intptr_t) blockh);
		}
	}

nasty_thing:
	done_string(&path);
	done_string(&delete_);
	assert(ITRM_OUT_QUEUE_SIZE - p > 0);
	memmove(buf, buf + p, ITRM_OUT_QUEUE_SIZE - p);
	bytes_read -= p;

	goto qwerty;

free_and_return:
	free_itrm(itrm);
}

static int
get_esc_code(unsigned char *str, int len, unsigned char *code, int *num, int *el)
{
	ELOG
	int pos;
	*num = 0;
	for (pos = 2; pos < len; pos++) {
		if (str[pos] < '0' || str[pos] > '9' || pos > 7) {
			*el = pos + 1;
			*code = str[pos];
			return 0;
		}
		*num = *num * 10 + str[pos] - '0';
	}
	return -1;
}

/* Define it to dump queue content in a readable form,
 * it may help to determine terminal sequences, and see what goes on. --Zas */
/* #define DEBUG_ITRM_QUEUE */

#ifdef DEBUG_ITRM_QUEUE
#include <stdio.h>
#include <ctype.h>	/* isprint() isspace((unsigned char)) */
#endif

int ui_double_esc;

static inline int
get_ui_double_esc(void)
{
	ELOG
	return ui_double_esc;
}

#ifdef CONFIG_KITTY
static int
decode_kitty_escape_sequence(struct itrm *itrm)
{
	ELOG
	int i = 3;
	int was_esc = 0;

	if (itrm->in.queue.len < 3) {
		return -1;
	}

	if (itrm->in.queue.data[2] != 'G') {
		return -1;
	}

	for (i = 3; i < itrm->in.queue.len; i++) {
		if (itrm->in.queue.data[i] == ASCII_ESC) {
			was_esc = i + 1;
			break;
		}
	}
	if (!was_esc) {
		return -1;
	}

	if (was_esc >= itrm->in.queue.len || itrm->in.queue.data[was_esc] != '\\') {
		return -1;
	}
	itrm->in.queue.data[was_esc] = '\0';
	const char *data = (const char *)(itrm->in.queue.data + 3);
	char *ok = strstr(data, "OK");
	int id = -1;
	int ID = -1;

	if (ok) {
		char *ieq = strstr(data, "i=");

		if (ieq) {
			id = atoi(ieq + 2);
		}
		char *Ieq = strstr(data, "I=");
		if (Ieq) {
			ID = atoi(Ieq + 2);
		}
		if (id >= 0 && ID >= 0) {
			set_id_ID(id, ID);
		}
	} else {
		char *enoent = strstr(data, "ENOENT");

		if (enoent) {
			char *ieq = strstr(data, "i=");

			if (ieq) {
				id = atoi(ieq + 2);
			}
			if (id >= 0) {
				remove_id(id);
			}
		}
	}
	itrm->in.queue.data[was_esc] = '\\';

	return was_esc + 1;
}
#endif

/** Decode a control sequence that begins with CSI (CONTROL SEQUENCE
 * INTRODUCER) encoded as ESC [, and set @a *ev accordingly.
 * (ECMA-48 also allows 0x9B as a single-byte CSI, but we don't
 * support that here.)
 *
 * @returns one of:
 * - -1 if the control sequence is not yet complete; the caller sets a timer.
 * - 0 if the control sequence should be parsed by some other function.
 * - The length of the control sequence otherwise.
 * Returning >0 does not imply this function has altered @a *ev.  */
static int
decode_terminal_escape_sequence(struct itrm *itrm, struct interlink_event *ev)
{
	ELOG
	struct term_event_keyboard kbd = { KBD_UNDEF, KBD_MOD_NONE };
	unsigned char c;
	int v;
	int el;
	int res;

	if (itrm->in.queue.len == 2 && itrm->in.queue.data[1] == ASCII_ESC && get_ui_double_esc()) {
		kbd.key = KBD_ESC;
		set_kbd_interlink_event(ev, kbd.key, kbd.modifier);
		return 2;
	}
	if (itrm->in.queue.len < 3) return -1;

	if (itrm->in.queue.data[2] == '[') {
		/* The terminfo entry for linux has "kf1=\E[[A", etc.
		 * These are not control sequences compliant with
		 * clause 5.4 of ECMA-48.  (According to ECMA-48,
		 * "\E[[" is SRS - START REVERSED STRING.)  */
		if (itrm->in.queue.len >= 4
		    && itrm->in.queue.data[3] >= 'A'
		    && itrm->in.queue.data[3] <= 'L') {
			kbd.key = number_to_kbd_fkey(itrm->in.queue.data[3] - 'A' + 1);
			set_kbd_interlink_event(ev, kbd.key, kbd.modifier);
			return 4;
		}

		return -1;
	}

	res = get_esc_code(itrm->in.queue.data, itrm->in.queue.len, &c, &v, &el);

	if (res == -1) {
		/* If the control sequence is incomplete but itrm->in.queue
		 * is already full, then we must not wait for more input:
		 * kbd_timeout might call in_kbd and thus process_input
		 * and come right back here.  Better just reject the whole
		 * thing and let the initial CSI be handled as Alt-[.  */
		if (itrm->in.queue.len == itrm->in.queue.size)
			return 0;
		else
			return -1;
	}
#ifdef DEBUG_ITRM_QUEUE
	fprintf(stderr, "esc code: %c v=%d c=%c el=%d\n", itrm->in.queue.data[1], v, c, el);
	fflush(stderr);
#endif

	/* The following information should be listed for each escape
	 * sequence recognized here:
	 *
	 * 1. Which control function ECMA-48 assigns to the sequence.
	 *    Put parentheses around this if the control function
	 *    seems unrelated to how ELinks actually treats the
	 *    sequence.  Write "private" if it is a control sequence
	 *    reserved for private or experimental use in ECMA-48.
	 *    (Those have a Final Byte in the range 0x70 to 0x7F,
	 *    optionally preceded by a single Intermediate Byte 0x20.)
	 *
	 * 2. The capname used by Terminfo, if any.  These should help
	 *    when ELinks is eventually changed to read escape
	 *    sequences from Terminfo (bug 96).
	 *
	 * 3. The $TERM identifier of some terminal that generates
	 *    this escape sequence with the meaning expected by
	 *    ELinks.  Escape sequences with no known terminal may end
	 *    up being removed from ELinks when bug 96 is fixed.
	 */					/* ECMA-48 Terminfo $TERM */
	switch (c) {				/* ------- -------- ----- */
	case 'A': kbd.key = KBD_UP; break;	/*    CUU  kcuu1    vt200 */
	case 'B': kbd.key = KBD_DOWN; break;	/*    CUD  kcud1    vt200 */
	case 'C': kbd.key = KBD_RIGHT; break;	/*    CUF  kcuf1    vt200 */
	case 'D': kbd.key = KBD_LEFT; break;	/*    CUB  kcub1    vt200 */
	case 'F':				/*   (CPL) kend     cons25 */
	case 'e': kbd.key = KBD_END; break;	/*   (VPR) kend */
	case 'H': kbd.key = KBD_HOME; break;	/*    CUP  khome    cons25 */
	case 'I': kbd.key = KBD_PAGE_UP; break; /*   (CHT) kpp      cons25 */
	case 'G': kbd.key = KBD_PAGE_DOWN; break; /* (CHA) knp      cons25 */
	case 'L': kbd.key = KBD_INS; break;	/*   (IL)  kich1    cons25 */
/* Free BSD (TERM=cons25 etc.) */
/*	case 'M': kbd.key = KBD_F1; break;*/	/*   (DL)  kf1      cons25 */
	case 'N': kbd.key = KBD_F2; break;	/*   (EF)  kf2      cons25 */
	case 'O': kbd.key = KBD_F3; break;	/*   (EA)  kf3      cons25 */
	case 'P': kbd.key = KBD_F4; break;	/*   (DCH) kf4      cons25 */
	case 'Q': kbd.key = KBD_F5; break;	/*   (SEE) kf5      cons25 */
/*	case 'R': kbd.key = KBD_F6; break;*/	/*   (CPR) kf6      cons25 */
	case 'S': kbd.key = KBD_F7; break;	/*   (SU)  kf7      cons25 */
	case 'T': kbd.key = KBD_F8; break;	/*   (SD)  kf8      cons25 */
	case 'U': kbd.key = KBD_F9; break;	/*   (NP)  kf9      cons25 */
	case 'V': kbd.key = KBD_F10; break;	/*   (PP)  kf10     cons25 */
	case 'W': kbd.key = KBD_F11; break;	/*   (CTC) kf11     cons25 */
	case 'X': kbd.key = KBD_F12; break;	/*   (ECH) kf12     cons25 */

	case 'Z':				/*    CBT  kcbt     cons25 */
		kbd.key = KBD_TAB; kbd.modifier = KBD_MOD_SHIFT; break;

	case 'z': switch (v) {			/* private */
		case 247: kbd.key = KBD_INS; break;     /* kich1 */
		case 214: kbd.key = KBD_HOME; break;	/* khome    sun */
		case 220: kbd.key = KBD_END; break;	/* kend     sun */
		case 216: kbd.key = KBD_PAGE_UP; break; /* kpp      sun */
		case 222: kbd.key = KBD_PAGE_DOWN; break; /* knp    sun */
		case 249: kbd.key = KBD_DEL; break;	/* kdch1 */
		} break;

	case '~': switch (v) {			/* private */
		case 1: kbd.key = KBD_HOME; break;      /* khome    linux */
		case 2: kbd.key = KBD_INS; break;	/* kich1    linux */
		case 3: kbd.key = KBD_DEL; break;	/* kdch1    linux */
		case 4: kbd.key = KBD_END; break;	/* kend     linux */
		case 5: kbd.key = KBD_PAGE_UP; break;	/* kpp      linux */
		case 6: kbd.key = KBD_PAGE_DOWN; break; /* knp      linux */
		case 7: kbd.key = KBD_HOME; break;	/* khome    rxvt */
		case 8: kbd.key = KBD_END; break;	/* kend     rxvt */

		case 11: kbd.key = KBD_F1; break;	/* kf1      rxvt */
		case 12: kbd.key = KBD_F2; break;	/* kf2      rxvt */
		case 13: kbd.key = KBD_F3; break;	/* kf3      rxvt */
		case 14: kbd.key = KBD_F4; break;	/* kf4      rxvt */
		case 15: kbd.key = KBD_F5; break;	/* kf5      rxvt */

		case 17: kbd.key = KBD_F6; break;	/* kf6      vt200 */
		case 18: kbd.key = KBD_F7; break;	/* kf7      vt200 */
		case 19: kbd.key = KBD_F8; break;	/* kf8      vt200 */
		case 20: kbd.key = KBD_F9; break;	/* kf9      vt200 */
		case 21: kbd.key = KBD_F10; break;	/* kf10     vt200 */

		case 23: kbd.key = KBD_F11; break;	/* kf11     vt200 */
		case 24: kbd.key = KBD_F12; break;	/* kf12     vt200 */

		/* Give preference to F11 and F12 over shifted F1 and F2. */
		/*
		case 23: kbd.key = KBD_F1; kbd.modifier = KBD_MOD_SHIFT; break;
		case 24: kbd.key = KBD_F2; kbd.modifier = KBD_MOD_SHIFT; break;
		*/

		case 25: kbd.key = KBD_F3; kbd.modifier = KBD_MOD_SHIFT; break;
		case 26: kbd.key = KBD_F4; kbd.modifier = KBD_MOD_SHIFT; break;

		case 28: kbd.key = KBD_F5; kbd.modifier = KBD_MOD_SHIFT; break;
		case 29: kbd.key = KBD_F6; kbd.modifier = KBD_MOD_SHIFT; break;

		case 31: kbd.key = KBD_F7; kbd.modifier = KBD_MOD_SHIFT; break;
		case 32: kbd.key = KBD_F8; kbd.modifier = KBD_MOD_SHIFT; break;
		case 33: kbd.key = KBD_F9; kbd.modifier = KBD_MOD_SHIFT; break;
		case 34: kbd.key = KBD_F10; kbd.modifier = KBD_MOD_SHIFT; break;

		case 200: itrm->bracketed_pasting = 1; break;    /* xterm */
		case 201: itrm->bracketed_pasting = 0; break;    /* xterm */

		} break;

	case 'R': resize_terminal(); break;	/*    CPR  u6 */
	case 'M':                               /*   (DL)  kmous    xterm */
#ifdef CONFIG_MOUSE
		el = decode_terminal_mouse_escape_sequence(itrm, ev, el, v);
#endif /* CONFIG_MOUSE */
		break;

	case '<':                               /*   (DL)  kmous    xterm */
#ifdef CONFIG_MOUSE
		el = decode_terminal_mouse_escape_sequence_256(itrm, ev, el, v);
#endif /* CONFIG_MOUSE */
		break;
	}

	/* KBD_UNDEF here means it was unrecognized or a mouse event.  */
	if (kbd.key != KBD_UNDEF)
		set_kbd_interlink_event(ev, kbd.key, kbd.modifier);

	return el;
}

/** Decode an escape sequence that begins with SS3 (SINGLE SHIFT 3).
 * These are used for application cursor keys and the application keypad.
 * @returns one of:
 * - -1 if the escape sequence is not yet complete; the caller sets a timer.
 * - 0 if the escape sequence should be parsed by some other function.
 * - The length of the escape sequence otherwise.
 * Returning >0 does not imply this function has altered @a *ev.  */
static int
decode_terminal_application_key(struct itrm *itrm, struct interlink_event *ev)
{
	ELOG
	unsigned char c;
	struct interlink_event_keyboard kbd = { KBD_UNDEF, KBD_MOD_NONE };

	assert(itrm->in.queue.len >= 2);
	assert(itrm->in.queue.data[0] == ASCII_ESC);
	assert(itrm->in.queue.data[1] == 0x4F); /* == 'O', incidentally */
	if_assert_failed return 0;

	if (itrm->in.queue.len < 3) return -1;
	/* According to ECMA-35 section 8.4, a single (possibly multibyte)
	 * character follows the SS3.  We now assume the code identifies
	 * GL as the single-shift area and the designated set has 94
	 * characters.  */
	c = itrm->in.queue.data[2];
	if (c < 0x21 || c > 0x7E) return 0;

	switch (c) {					/* Terminfo $TERM */
	case ' ': kbd.key = ' '; break;			/*          xterm */
	case 'A': kbd.key = KBD_UP; break;		/* kcuu1    vt100 */
	case 'B': kbd.key = KBD_DOWN; break;		/* kcud1    vt100 */
	case 'C': kbd.key = KBD_RIGHT; break;		/* kcuf1    vt100 */
	case 'D': kbd.key = KBD_LEFT; break;		/* kcub1    vt100 */
	case 'F': kbd.key = KBD_END; break;		/* kend     xterm */
	case 'H': kbd.key = KBD_HOME; break;		/* khome    xterm */
	case 'I': kbd.key = KBD_TAB; break;		/*          xterm */
	case 'M': kbd.key = KBD_ENTER; break;		/* kent     vt100 */
		/* FIXME: xterm generates ESC O 2 P for Shift-PF1 */
	case 'P': kbd.key = KBD_F1; break;		/* kf1      vt100 */
	case 'Q': kbd.key = KBD_F2; break;		/* kf2      vt100 */
	case 'R': kbd.key = KBD_F3; break;		/* kf3      vt100 */
	case 'S': kbd.key = KBD_F4; break;		/* kf4      vt100 */
	case 'X': kbd.key = '='; break;			/*          xterm */

	case 'j': case 'k': case 'l': case 'm': /* *+,-             xterm */
	case 'n': case 'o': case 'p': case 'q': /* ./01             xterm */
	case 'r': case 's': case 't': case 'u': /* 2345             xterm */
	case 'v': case 'w': case 'x': case 'y': /* 6789             xterm */
		kbd.key = c - 'p' + '0'; break;
	}
	if (kbd.key != KBD_UNDEF)
		copy_struct(&ev->info.keyboard, &kbd);

	return 3;		/* even if we didn't recognize it */
}


/** Initialize @a *ev to match the byte @a key received from the terminal.
 * @a key must not be a value from enum term_event_special_key.  */
static void
set_kbd_event(const struct itrm *itrm, struct interlink_event *ev,
	      int key, term_event_modifier_T modifier)
{
	ELOG
	if (key == itrm->verase)
		key = KBD_BS;
	else switch (key) {
	case ASCII_TAB:
		key = KBD_TAB;
		break;
	case ASCII_DEL:	/* often overridden by itrm->verase above */
		key = KBD_BS;
		break;
	case ASCII_LF:
	case ASCII_CR:
		key = KBD_ENTER;
		break;

	case ASCII_ESC:
		key = KBD_ESC;
		break;

	case ASCII_BS:	/* often overridden by itrm->verase above */
	default:
		if (key < ' ') {
			key += 'A' - 1;
			modifier |= KBD_MOD_CTRL;
		}
	}

	set_kbd_interlink_event(ev, key, modifier);
}

/** Timer callback for itrm.timer.  As explained in install_timer(),
 * this function must erase the expired timer ID from all variables.  */
static void
kbd_timeout(struct itrm *itrm)
{
	ELOG
	struct interlink_event ev;
	int el;

	itrm->timer = TIMER_ID_UNDEF;
	/* The expired timer ID has now been erased.  */

	assertm(itrm->in.queue.len, "timeout on empty queue");
	assert(!itrm->blocked);	/* block_itrm should have killed itrm->timer */
	if_assert_failed return;

	if (can_read(itrm->in.std)) {
		in_kbd(itrm);
		return;
	}

	if (itrm->in.queue.len >= 2 && itrm->in.queue.data[0] == ASCII_ESC) {
		/* This is used for ESC [ and ESC O.  */
		set_kbd_event(itrm, &ev, itrm->in.queue.data[1], KBD_MOD_ALT);
		el = 2;
	} else {
		set_kbd_event(itrm, &ev, itrm->in.queue.data[0], KBD_MOD_NONE);
		el = 1;
	}
	itrm->bracketed_pasting = 0;
	itrm_queue_event(itrm, (char *) &ev, sizeof(ev));

	itrm->in.queue.len -= el;
	if (itrm->in.queue.len)
		memmove(itrm->in.queue.data, itrm->in.queue.data + el, itrm->in.queue.len);

	while (process_queue(itrm));
}

/** Parse one event from itrm_in.queue and append to itrm_out.queue.
 * @pre On entry, @a *itrm must not be blocked.
 * @returns the number of bytes removed from itrm->in.queue; at least 0.
 * @post If this function leaves the queue not full, it also reenables
 * reading from itrm->in.std.  (Because it does not add to the queue,
 * it never need disable reading.)  */
static int
process_queue(struct itrm *itrm)
{
	ELOG
	struct interlink_event ev;
	int el = 0;

	if (!itrm->in.queue.len) goto return_without_event;
	assert(!itrm->blocked);
	if_assert_failed return 0; /* unlike goto, don't enable reading */

	set_kbd_interlink_event(&ev, KBD_UNDEF, KBD_MOD_NONE);

#ifdef DEBUG_ITRM_QUEUE
	{
		int i;

		/* Dump current queue in a readable form to stderr. */
		for (i = 0; i < itrm->in.queue.len; i++)
			if (itrm->in.queue.data[i] == ASCII_ESC)
				fprintf(stderr, "ESC ");
			else if (isprint(itrm->in.queue.data[i]) && !isspace((unsigned char)itrm->in.queue.data[i]))
				fprintf(stderr, "%c ", itrm->in.queue.data[i]);
			else
				fprintf(stderr, "0x%02x ", itrm->in.queue.data[i]);

		fprintf(stderr, "\n");
		fflush(stderr);
	}
#endif /* DEBUG_ITRM_QUEUE */

	/* el == -1 means itrm->in.queue appears to be the beginning of an
	 *          escape sequence but it is not yet complete.  Set a timer;
	 *          if it times out, then assume it wasn't an escape sequence
	 *          after all.
	 * el == 0 means this function has not yet figured out what the data
	 *         in itrm->in.queue is, but some possibilities remain.
	 *         One of them will be chosen before returning.
	 * el > 0 means some bytes were successfully parsed from the beginning
	 *        of itrm->in.queue and should now be removed from there.
	 *        However, this does not always imply an event will be queued.
	 */

	/* ELinks should also recognize U+009B CONTROL SEQUENCE INTRODUCER
	 * as meaning the same as ESC 0x5B, and U+008F SINGLE SHIFT THREE as
	 * meaning the same as ESC 0x4F, but those cannot yet be implemented
	 * because of bug 777: the UTF-8 decoder is run too late.  */
	if (itrm->in.queue.data[0] == ASCII_ESC) {
		if (itrm->in.queue.len < 2) {
			el = -1;
		} else if (itrm->in.queue.data[1] == 0x5B /* CSI */) {
			el = decode_terminal_escape_sequence(itrm, &ev);
		} else if (itrm->in.queue.data[1] == 0x4F /* SS3 */) {
			el = decode_terminal_application_key(itrm, &ev);
#ifdef CONFIG_KITTY
		} else if (itrm->in.queue.data[1] == '_') {
			el = decode_kitty_escape_sequence(itrm);
#endif
		} else if (itrm->in.queue.data[1] == ASCII_ESC) {
			/* ESC ESC can be either Alt-Esc or the
			 * beginning of e.g. ESC ESC 0x5B 0x41,
			 * which we should parse as Esc Up.  */
			if (itrm->in.queue.len < 3) {
				if (get_ui_double_esc()) {
					el = decode_terminal_escape_sequence(itrm, &ev);
				} else {
					/* Need more data to figure it out.  */
					el = -1;
				}
			} else if (itrm->in.queue.data[2] == 0x5B
				   || itrm->in.queue.data[2] == 0x4F) {
				/* The first ESC appears to be followed
				 * by an escape sequence.  Treat it as
				 * a standalone Esc.  */
				el = 1;
				set_kbd_event(itrm, &ev,
					      itrm->in.queue.data[0],
					      KBD_MOD_NONE);
			} else {
				/* The second ESC of ESC ESC is not the
				 * beginning of any known escape sequence.
				 * This must be Alt-Esc, then.  */
				el = 2;
				set_kbd_event(itrm, &ev,
					      itrm->in.queue.data[1],
					      KBD_MOD_ALT);
			}
		}
		if (el == 0) {	/* Begins with ESC, but none of the above */
			el = 2;
			set_kbd_event(itrm, &ev, itrm->in.queue.data[1],
				      KBD_MOD_ALT);
		}

	} else if (itrm->in.queue.data[0] == 0) {
		static const struct term_event_keyboard os2xtd[256] = {
#include "terminal/key.inc"
		};

		if (itrm->in.queue.len < 2)
			el = -1;
		else {
			el = 2;
			set_kbd_interlink_event(&ev,
						os2xtd[(unsigned char)itrm->in.queue.data[1]].key,
						os2xtd[(unsigned char)itrm->in.queue.data[1]].modifier);
		}
	}

	if (el == 0) {
		el = 1;
		set_kbd_event(itrm, &ev, itrm->in.queue.data[0],
			      itrm->bracketed_pasting ? KBD_MOD_PASTE : KBD_MOD_NONE);
	}

	/* The call to decode_terminal_escape_sequence() might have changed the
	 * keyboard event to a mouse event. */
	if (ev.ev == EVENT_MOUSE || ev.info.keyboard.key != KBD_UNDEF) {
		itrm_queue_event(itrm, (char *) &ev, sizeof(ev));
		itrm->bracketed_pasting = 
			(ev.ev == EVENT_KBD
			 && (ev.info.keyboard.modifier & KBD_MOD_PASTE));
	}

return_without_event:
	if (el == -1) {
		install_timer(&itrm->timer, ESC_TIMEOUT, (void (*)(void *)) kbd_timeout,
			      itrm);
		return 0;
	} else {
		assertm(itrm->in.queue.len >= el, "event queue underflow");
		if_assert_failed { itrm->in.queue.len = el; }

		itrm->in.queue.len -= el;
		if (itrm->in.queue.len)
			memmove(itrm->in.queue.data, itrm->in.queue.data + el, itrm->in.queue.len);

		if (itrm->in.queue.len < itrm->in.queue.size)
			handle_itrm_stdin(itrm);

		return el;
	}
}


/** A select_handler_T read_func for itrm_in.std.  This is called when
 * characters typed by the user arrive from the terminal. */
void
in_kbd(struct itrm *itrm)
{
	ELOG
	int r;

	if (!can_read(itrm->in.std)) return;

	kill_timer(&itrm->timer);

	if (itrm->in.queue.len >= itrm->in.queue.size) {
		unhandle_itrm_stdin(itrm);
		while (process_queue(itrm));
		return;
	}

	r = safe_read(itrm->in.std, itrm->in.queue.data + itrm->in.queue.len,
		      itrm->in.queue.size - itrm->in.queue.len);
	if (r < 0) {
		free_itrm(itrm);
		return;
	}

	itrm->in.queue.len += r;
	if (itrm->in.queue.len > itrm->in.queue.size) {
		EL_ERROR(gettext("Too many bytes read from the itrm!"));
		itrm->in.queue.len = itrm->in.queue.size;
	}

	while (process_queue(itrm));
}


/** Enable reading from itrm_in.std.  ELinks will read any available
 * bytes from the tty into itrm->in.queue and then parse them.
 * Reading should be enabled whenever itrm->in.queue is not full and
 * itrm->blocked is 0.  */
static void
handle_itrm_stdin(struct itrm *itrm)
{
	ELOG
	assert(program.testjs || itrm->in.std >= 0);
	if_assert_failed return;

	set_handlers(itrm->in.std, (select_handler_T) in_kbd, NULL,
		     (select_handler_T) free_itrm, itrm);
}

/** Disable reading from itrm_in.std.  Reading should be disabled
 * whenever itrm->in.queue is full (there is no room for the data)
 * or itrm->blocked is 1 (other processes may read the data).  */
static void
unhandle_itrm_stdin(struct itrm *itrm)
{
	ELOG
	assert(itrm->in.std >= 0);
	if_assert_failed return;

	set_handlers(itrm->in.std, (select_handler_T) NULL, NULL,
		     (select_handler_T) free_itrm, itrm);
}
