/*
 * wdye.c: what did you expect: Lua-based Expect tool
 *
 * Copyright (c) 2025, Přemysl Eric Janouch <p@janouch.name>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted.
 *
 * 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.
 *
 */

#include "config.h"
#include "../../liberty.c"

#include <math.h>

#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>

#include <sys/ioctl.h>
#include <termios.h>
#ifdef SOLARIS
#include <stropts.h>
#endif

#ifdef WITH_CURSES
#include <curses.h>
#include <term.h>
#endif

static int64_t
clock_msec (void)
{
#ifdef _POSIX_TIMERS
	struct timespec tp;
	hard_assert (clock_gettime (CLOCK_BEST, &tp) != -1);
	return (int64_t) tp.tv_sec * 1000 + tp.tv_nsec / 1000000;
#else
	struct timeval tp;
	hard_assert (gettimeofday (&tp, NULL) != -1);
	return (int64_t) tp.tv_sec * 1000 + *msec = tp.tv_usec / 1000;
#endif
}

// execvpe is a GNU extension, reimplement it.
static int
execvpe (const char *file, char *const argv[], char *const envp[])
{
	const char *path = getenv ("PATH");
	if (strchr (file, '/') || !path)
		return execve (file, argv, envp);

	struct strv dirs = strv_make ();
	cstr_split (path, ":", false, &dirs);
	char *name = NULL;
	for (size_t i = 0; i < dirs.len; i++)
	{
		cstr_set (&name, xstrdup_printf ("%s/%s",
			*dirs.vector[i] ? dirs.vector[i] : ".", file));
		execve (name, argv, envp);
	}
	strv_free (&dirs);
	return -1;
}

// This is a particularly inefficient algorithm, but it can match binary data.
static const char *
str_memmem (const struct str *haystack, const struct str *needle, bool nocase)
{
	if (haystack->len < needle->len)
		return NULL;

	char *xhaystack = xmalloc (haystack->len + 1);
	char *xneedle = xmalloc (needle->len + 1);
	if (nocase)
	{
		for (size_t i = 0; i <= haystack->len; i++)
			xhaystack[i] = tolower ((uint8_t) haystack->str[i]);
		for (size_t i = 0; i <= needle->len; i++)
			xneedle[i] = tolower ((uint8_t) needle->str[i]);
	}
	else
	{
		memcpy (xhaystack, haystack->str, haystack->len + 1);
		memcpy (xneedle, needle->str, needle->len + 1);
	}

	const char *result = NULL;
	for (size_t i = 0, end = haystack->len - needle->len; i <= end; i++)
		if (!memcmp (xhaystack + i, xneedle, needle->len))
		{
			result = haystack->str + i;
			break;
		}

	free (xhaystack);
	free (xneedle);
	return result;
}

// --- Pseudoterminal ----------------------------------------------------------
// This is largely taken from Advanced Programming in the UNIX® Environment,
// just without a bunch of bugs.

static int
ptym_open (char **pts_name)
{
	int fdm = -1, err = 0;
	if ((fdm = posix_openpt (O_RDWR | O_NOCTTY)) < 0)
		return -1;
	if (grantpt (fdm) < 0
	 || unlockpt (fdm) < 0)
		goto errout;

	char *ptr = NULL;
	if ((ptr = ptsname (fdm)) == NULL)
		goto errout;

	cstr_set (pts_name, xstrdup (ptr));
	return fdm;

errout:
	err = errno;
	xclose (fdm);
	errno = err;
	return -1;
}

static int
ptys_open (const char *pts_name)
{
	int fds = -1;
#ifdef SOLARIS
	int err = 0, setup = 0;
#endif
	if ((fds = open (pts_name, O_RDWR)) < 0)
		return -1;
#ifdef SOLARIS
	if ((setup = ioctl (fds, I_FIND, "ldterm")) < 0)
		goto errout;
	if (setup == 0)
	{
		if (ioctl (fds, I_PUSH, "ptem") < 0
		 || ioctl (fds, I_PUSH, "ldterm") < 0)
			goto errout;

		if (ioctl (fds, I_PUSH, "ttcompat") < 0)
		{
errout:
			err = errno;
			xclose (fds);
			errno = err;
			return -1;
		}
	}
#endif
	return fds;
}

static pid_t
pty_fork (int *ptrfdm, char **slave_name,
	const struct termios *slave_termios, const struct winsize *slave_winsize,
	struct error **e)
{
	int fdm = -1, fds = -1;

	char *pts_name = NULL;
	if ((fdm = ptym_open (&pts_name)) < 0)
	{
		error_set (e, "can't open master pty: %s", strerror (errno));
		return -1;
	}
	if (slave_name != NULL)
		cstr_set (slave_name, xstrdup (pts_name));

	pid_t pid = fork ();
	if (pid < 0)
	{
		error_set (e, "fork: %s", strerror (errno));
		xclose (fdm);
	}
	else if (pid != 0)
		*ptrfdm = fdm;
	else
	{
		if (setsid () < 0)
			exit_fatal ("setsid: %s", strerror (errno));
		if ((fds = ptys_open (pts_name)) < 0)
			exit_fatal ("can't open slave pty: %s", strerror (errno));
		xclose (fdm);

#if defined BSD
		if (ioctl (fds, TIOCSCTTY, (char *) 0) < 0)
			exit_fatal ("TIOCSCTTY: %s", strerror (errno));
#endif

		if (slave_termios != NULL
		 && tcsetattr (fds, TCSANOW, slave_termios) < 0)
			exit_fatal ("tcsetattr error on slave pty: %s", strerror (errno));
		if (slave_winsize != NULL
		 && ioctl (fds, TIOCSWINSZ, slave_winsize) < 0)
			exit_fatal ("TIOCSWINSZ error on slave pty: %s", strerror (errno));

		if (dup2 (fds, STDIN_FILENO) != STDIN_FILENO)
			exit_fatal ("dup2 error to stdin");
		if (dup2 (fds, STDOUT_FILENO) != STDOUT_FILENO)
			exit_fatal ("dup2 error to stdout");
		if (dup2 (fds, STDERR_FILENO) != STDERR_FILENO)
			exit_fatal ("dup2 error to stderr");
		if (fds != STDIN_FILENO && fds != STDOUT_FILENO && fds != STDERR_FILENO)
			xclose (fds);
	}
	free (pts_name);
	return pid;
}

// --- JSON --------------------------------------------------------------------

static void
write_json_string (FILE *output, const char *s, size_t len)
{
	fputc ('"', output);
	for (const char *last = s, *end = s + len; s != end; last = s)
	{
		// Here is where you realize the asciicast format is retarded for using
		// JSON at all.  (Consider multibyte characters at read() boundaries.)
		int32_t codepoint = utf8_decode (&s, end - s);
		if (codepoint < 0)
		{
			s++;
			fprintf (output, "\\uFFFD");
			continue;
		}

		switch (codepoint)
		{
		break; case '"':  fprintf (output, "\\\"");
		break; case '\\': fprintf (output, "\\\\");
		break; case '\b': fprintf (output, "\\b");
		break; case '\f': fprintf (output, "\\f");
		break; case '\n': fprintf (output, "\\n");
		break; case '\r': fprintf (output, "\\r");
		break; case '\t': fprintf (output, "\\t");
		break; default:
			if (!utf8_validate_cp (codepoint))
				fprintf (output, "\\uFFFD");
			else if (codepoint < 32)
				fprintf (output, "\\u%04X", codepoint);
			else
				fwrite (last, 1, s - last, output);
		}
	}
	fputc ('"', output);
}

// --- Global state ------------------------------------------------------------

static struct
{
	lua_State *L;                       ///< Lua state
	lua_Number default_timeout;         ///< Default expect timeout (s)
}
g =
{
	.default_timeout = 10.,
};

static int
xlua_error_handler (lua_State *L)
{
	// Don't add tracebacks when there's already one, and pass nil through.
	const char *string = luaL_optstring (L, 1, NULL);
	if (string && !strchr (string, '\n'))
	{
		luaL_traceback (L, L, string, 1);
		lua_remove (L, 1);
	}
	return 1;
}

static bool
xlua_getfield (lua_State *L, int idx, const char *name,
	int expected, bool optional)
{
	int found = lua_getfield (L, idx, name);
	if (found == expected)
		return true;
	if (optional && found == LUA_TNIL)
		return false;

	const char *message = optional
		? "invalid field \"%s\" (found: %s, expected: %s or nil)"
		: "invalid or missing field \"%s\" (found: %s, expected: %s)";
	return luaL_error (L, message, name,
		lua_typename (L, found), lua_typename (L, expected));
}

static void
xlua_newtablecopy (lua_State *L, int idx, int first, int last)
{
	int len = last - first + 1;
	lua_createtable (L, len, 0);
	if (idx < 0)
		idx--;

	for (lua_Integer i = 0; i < len; i++)
	{
		lua_rawgeti (L, idx, first + i);
		lua_rawseti (L, -2, 1 + i);
	}
}

// --- Patterns ----------------------------------------------------------------

#define XLUA_PATTERN_METATABLE "pattern"

enum pattern_kind
{
	PATTERN_REGEX,                      ///< Regular expression match
	PATTERN_EXACT,                      ///< Literal string match
	PATTERN_TIMEOUT,                    ///< Timeout
	PATTERN_EOF,                        ///< EOF condition
	PATTERN_DEFAULT,                    ///< Either timeout or EOF condition
};

struct pattern
{
	enum pattern_kind kind;             ///< Tag
	int ref_process;                    ///< Process for all except TIMEOUT
	struct process *process;            ///< Weak pointer to the process
	regex_t *regex;                     ///< Regular expression for REGEX
	struct str exact;                   ///< Exact match literal for EXACT
	lua_Number timeout;                 ///< Timeout for TIMEOUT/DEFAULT (s)
	bool nocase;                        ///< Case insensitive search
	bool notransfer;                    ///< Do not consume process buffer
	int ref_values;                     ///< Return values as a table reference

	// Patterns are constructed in place, used once, and forgotten,
	// so we can just shove anything extra in here.

	struct error *e;                    ///< Error buffer
	struct str input;                   ///< Matched input
	regmatch_t *matches;                ///< Match indexes within the input
	bool eof;                           ///< End of file seen
};

static struct pattern *
pattern_new (lua_State *L, enum pattern_kind kind, int idx_process)
{
	struct pattern *self = lua_newuserdata (L, sizeof *self);
	luaL_setmetatable (L, XLUA_PATTERN_METATABLE);
	memset (self, 0, sizeof *self);

	self->kind = kind;
	self->ref_process = LUA_NOREF;
	self->exact = str_make ();
	self->timeout = -1.;
	self->ref_values = LUA_NOREF;
	self->input = str_make ();

	if (idx_process)
	{
		lua_pushvalue (L, idx_process);
		self->process = lua_touserdata (L, -1);
		self->ref_process = luaL_ref (L, LUA_REGISTRYINDEX);
	}
	return self;
}

static int
xlua_pattern_gc (lua_State *L)
{
	struct pattern *self = luaL_checkudata (L, 1, XLUA_PATTERN_METATABLE);
	luaL_unref (L, LUA_REGISTRYINDEX, self->ref_process);
	if (self->regex)
		regex_free (self->regex);
	str_free (&self->exact);
	luaL_unref (L, LUA_REGISTRYINDEX, self->ref_values);
	if (self->e)
		error_free (self->e);
	str_free (&self->input);
	free (self->matches);
	return 0;
}

static int
xlua_pattern_index (lua_State *L)
{
	struct pattern *self = luaL_checkudata (L, 1, XLUA_PATTERN_METATABLE);
	if (!lua_isinteger (L, 2))
	{
		const char *key = luaL_checkstring (L, 2);
		if (!strcmp (key, "process"))
			lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_process);
		else
			return luaL_error (L, "not a readable property: %s", key);
		return 1;
	}

	lua_Integer group = lua_tointeger (L, 2);
	switch (self->kind)
	{
	case PATTERN_REGEX:
	{
		const regmatch_t *m = self->matches + group;
		if (group < 0 || (size_t) group > self->regex->re_nsub
		 || m->rm_so < 0 || m->rm_eo < 0 || (size_t) m->rm_eo > self->input.len)
			lua_pushnil (L);
		else
			lua_pushlstring (L,
				self->input.str + m->rm_so, m->rm_eo - m->rm_so);
		return 1;
	}
	case PATTERN_EXACT:
	case PATTERN_EOF:
	case PATTERN_DEFAULT:
		if (group != 0)
			lua_pushnil (L);
		else
			lua_pushlstring (L, self->input.str, self->input.len);
		return 1;
	default:
		return luaL_argerror (L, 1, "indexing unavailable for this pattern");
	}
}

static bool
pattern_readtimeout (struct pattern *self, lua_State *L, int idx)
{
	lua_rawgeti (L, idx, 1);
	bool ok = lua_isnumber (L, -1);
	lua_Number v = lua_tonumber (L, -1);
	lua_pop (L, 1);
	if (v != v)
		luaL_error (L, "timeout is not a number");
	if (ok)
		self->timeout = v;
	return ok;
}

static void
pattern_readflags (struct pattern *self, lua_State *L, int idx)
{
	lua_getfield (L, idx, "nocase");
	self->nocase = lua_toboolean (L, -1);
	lua_getfield (L, idx, "notransfer");
	self->notransfer = lua_toboolean (L, -1);
	lua_pop (L, 2);
}

static luaL_Reg xlua_pattern_table[] =
{
	{ "__gc",       xlua_pattern_gc       },
	{ "__index",    xlua_pattern_index    },
	{ NULL,         NULL                  }
};

// --- Process -----------------------------------------------------------------

#define XLUA_PROCESS_METATABLE "process"

struct process
{
	int terminal_fd;                    ///< Process stdin/stdout/stderr
	pid_t pid;                          ///< Process ID or -1 if collected
	int ref_term;                       ///< Terminal information
	struct str buffer;                  ///< Terminal input buffer
	int status;                         ///< Process status iff pid is -1

	int64_t start;                      ///< Start timestamp (Unix msec)
	FILE *asciicast;                    ///< asciicast script dump
};

static struct process *
process_new (lua_State *L)
{
	struct process *self = lua_newuserdata (L, sizeof *self);
	luaL_setmetatable (L, XLUA_PROCESS_METATABLE);
	memset (self, 0, sizeof *self);

	self->terminal_fd = -1;
	self->pid = -1;
	self->ref_term = LUA_NOREF;
	self->buffer = str_make ();
	return self;
}

static int
xlua_process_gc (lua_State *L)
{
	struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
	if (self->terminal_fd != -1)
		xclose (self->terminal_fd);
	if (self->pid != -1)
		// The slave is in its own process group.
		kill (-self->pid, SIGKILL);
	luaL_unref (L, LUA_REGISTRYINDEX, self->ref_term);
	str_free (&self->buffer);
	if (self->asciicast)
		fclose (self->asciicast);
	return 0;
}

static int
xlua_process_index (lua_State *L)
{
	struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
	const char *key = luaL_checkstring (L, 2);
	if (*key != '_' && luaL_getmetafield (L, 1, key))
		return 1;

	if (!strcmp (key, "buffer"))
		lua_pushlstring (L, self->buffer.str, self->buffer.len);
	else if (!strcmp (key, "pid"))
		lua_pushinteger (L, self->pid);
	else if (!strcmp (key, "term"))
		lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_term);
	else
		return luaL_error (L, "not a readable property: %s", key);
	return 1;
}

static int
xlua_process_send (lua_State *L)
{
	struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
	int nargs = lua_gettop (L);
	for (int i = 2; i <= nargs; i++)
		if (!lua_isstring (L, i))
			return luaL_argerror (L, i, "need string arguments");

	for (int i = 2; i <= nargs; i++)
	{
		size_t len = 0;
		const char *arg = lua_tolstring (L, i, &len);
		ssize_t written = write (self->terminal_fd, arg, len);
		if (written == -1)
			return luaL_error (L, "write failed: %s", strerror (errno));
		else if (written != (ssize_t) len)
			return luaL_error (L, "write failed: %s", "short write");

		if (self->asciicast)
		{
			double timestamp = (clock_msec () - self->start) / 1000.;
			fprintf (self->asciicast, "[%f, \"i\", ", timestamp);
			write_json_string (self->asciicast, arg, len);
			fprintf (self->asciicast, "]\n");
		}
	}
	lua_pushvalue (L, 1);
	return 1;
}

static int
xlua_process_regex (lua_State *L)
{
	(void) luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
	luaL_checktype (L, 2, LUA_TTABLE);
	if (lua_gettop (L) != 2)
		return luaL_error (L, "too many arguments");

	struct pattern *pattern = pattern_new (L, PATTERN_REGEX, 1);
	pattern_readflags (pattern, L, 2);

	int flags = REG_EXTENDED;
	if (pattern->nocase)
		flags |= REG_ICASE;

	lua_rawgeti (L, 2, 1);
	if (!lua_isstring (L, -1))
		return luaL_error (L, "expected regular expression");

	size_t len = 0;
	const char *re = lua_tolstring (L, -1, &len);
	if (!(pattern->regex = regex_compile (re, flags, &pattern->e)))
		return luaL_error (L, "%s", pattern->e->message);
	lua_pop (L, 1);

	pattern->matches =
		xcalloc (pattern->regex->re_nsub + 1, sizeof *pattern->matches);

	xlua_newtablecopy (L, 2, 2, lua_rawlen (L, 2));
	pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
	return 1;
}

static int
xlua_process_exact (lua_State *L)
{
	(void) luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
	luaL_checktype (L, 2, LUA_TTABLE);
	if (lua_gettop (L) != 2)
		return luaL_error (L, "too many arguments");

	struct pattern *pattern = pattern_new (L, PATTERN_EXACT, 1);
	pattern_readflags (pattern, L, 2);

	lua_rawgeti (L, 2, 1);
	if (!lua_isstring (L, -1))
		return luaL_error (L, "expected string literal");

	size_t len = 0;
	const char *literal = lua_tolstring (L, -1, &len);
	str_append_data (&pattern->exact, literal, len);
	lua_pop (L, 1);

	xlua_newtablecopy (L, 2, 2, lua_rawlen (L, 2));
	pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
	return 1;
}

static int
xlua_process_eof (lua_State *L)
{
	(void) luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
	luaL_checktype (L, 2, LUA_TTABLE);
	if (lua_gettop (L) != 2)
		return luaL_error (L, "too many arguments");

	struct pattern *pattern = pattern_new (L, PATTERN_EOF, 1);
	pattern_readflags (pattern, L, 2);

	xlua_newtablecopy (L, 2, 1, lua_rawlen (L, 2));
	pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
	return 1;
}

static int
xlua_process_default (lua_State *L)
{
	(void) luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
	luaL_checktype (L, 2, LUA_TTABLE);
	if (lua_gettop (L) != 2)
		return luaL_error (L, "too many arguments");

	struct pattern *pattern = pattern_new (L, PATTERN_DEFAULT, 1);
	pattern_readflags (pattern, L, 2);

	int first = 1, last = lua_rawlen (L, 2);
	if (pattern_readtimeout (pattern, L, 2))
		first++;

	xlua_newtablecopy (L, 2, first, last);
	pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
	return 1;
}

static int
xlua_process_wait (lua_State *L)
{
	struct process *self = luaL_checkudata (L, 1, XLUA_PROCESS_METATABLE);
	bool nowait = luaL_opt(L, lua_toboolean, 2, false);
	if (lua_gettop (L) > 2)
		return luaL_error (L, "too many arguments");

	int status = self->status;
restart:
	if (self->pid != -1)
	{
		int options = 0;
		if (nowait)
			options |= WNOHANG;

		pid_t pid = waitpid (self->pid, &status, options);
		if (!pid)
			return 0;

		if (pid < 0)
		{
			if (errno == EINTR)
				goto restart;
			return luaL_error (L, "waitpid: %s", strerror (errno));
		}

		// We lose the ability to reliably kill the whole process group.
		self->status = status;
		self->pid = -1;
	}
	if (WIFEXITED (status))
	{
		lua_pushinteger (L, WEXITSTATUS (status));
		lua_pushinteger (L, WEXITSTATUS (status));
		lua_pushnil (L);
		return 3;
	}
	if (WIFSIGNALED (status))
	{
		lua_pushinteger (L, 128 + WTERMSIG (status));
		lua_pushnil (L);
		lua_pushinteger (L, WTERMSIG (status));
		return 3;
	}
	return 0;
}

static bool
process_feed (struct process *self)
{
	// Let's do this without O_NONBLOCK for now.
	char buf[BUFSIZ] = "";
	ssize_t n = read (self->terminal_fd, buf, sizeof buf);
	if (n < 0)
	{
		if (errno == EINTR)
			return true;
#ifdef __linux__
		// https://unix.stackexchange.com/a/538271
		if (errno == EIO)
			return false;
#endif

		print_warning ("read: %s", strerror (errno));
		return false;
	}

	if (self->asciicast)
	{
		double timestamp = (clock_msec () - self->start) / 1000.;
		fprintf (self->asciicast, "[%f, \"o\", ", timestamp);
		write_json_string (self->asciicast, buf, n);
		fprintf (self->asciicast, "]\n");
	}

	// TODO(p): Add match_max processing, limiting the buffer size.
	str_append_data (&self->buffer, buf, n);
	return n > 0;
}

static luaL_Reg xlua_process_table[] =
{
	{ "__gc",       xlua_process_gc       },
	{ "__index",    xlua_process_index    },
	{ "send",       xlua_process_send     },
	{ "regex",      xlua_process_regex    },
	{ "exact",      xlua_process_exact    },
	{ "eof",        xlua_process_eof      },
	{ "default",    xlua_process_default  },
	{ "wait",       xlua_process_wait     },
	{ NULL,         NULL                  }
};

// --- Terminal ----------------------------------------------------------------

struct terminfo_entry
{
	enum { TERMINFO_BOOLEAN, TERMINFO_NUMERIC, TERMINFO_STRING } kind;
	unsigned numeric;
	char string[];
};

#ifdef WITH_CURSES

static bool
load_terminfo (const char *term, struct str_map *strings)
{
	// Neither ncurses nor NetBSD curses need an actual terminal FD passed.
	// We don't want them to read out the winsize, we just read the database.
	int err = 0;
	TERMINAL *saved_term = set_curterm (NULL);
	if (setupterm ((char *) term, -1, &err) != OK)
	{
		set_curterm (saved_term);
		return false;
	}

	for (size_t i = 0; boolfnames[i]; i++)
	{
		int flag = tigetflag (boolnames[i]);
		if (flag <= 0)
			continue;

		struct terminfo_entry *entry = xcalloc (1, sizeof *entry + 1);
		*entry = (struct terminfo_entry) { TERMINFO_BOOLEAN, true };
		str_map_set (strings, boolfnames[i], entry);
	}
	for (size_t i = 0; numfnames[i]; i++)
	{
		int num = tigetnum (numnames[i]);
		if (num < 0)
			continue;

		struct terminfo_entry *entry = xcalloc (1, sizeof *entry + 1);
		*entry = (struct terminfo_entry) { TERMINFO_NUMERIC, num };
		str_map_set (strings, numfnames[i], entry);
	}
	for (size_t i = 0; strfnames[i]; i++)
	{
		const char *str = tigetstr (strnames[i]);
		if (!str || str == (const char *) -1)
			continue;

		size_t len = strlen (str) + 1;
		struct terminfo_entry *entry = xcalloc (1, sizeof *entry + len);
		*entry = (struct terminfo_entry) { TERMINFO_STRING, 0 };
		memcpy (entry + 1, str, len);
		str_map_set (strings, strfnames[i], entry);
	}
	del_curterm (set_curterm (saved_term));
	return true;
}

#endif

// --- Library -----------------------------------------------------------------

struct spawn_context
{
	struct str_map env;                 ///< Subprocess environment map
	struct str_map term;                ///< terminfo database
	struct strv envv;                   ///< Subprocess environment vector
	struct strv argv;                   ///< Subprocess argument vector

	struct error *error;                ///< Error
};

static struct spawn_context
spawn_context_make (void)
{
	struct spawn_context self = {};
	self.env = str_map_make (free);
	self.term = str_map_make (free);

	// XXX: It might make sense to enable starting from an empty environment.
	for (char **p = environ; *p; p++)
	{
		const char *equals = strchr (*p, '=');
		if (!equals)
			continue;

		char *key = xstrndup (*p, equals - *p);
		str_map_set (&self.env, key, xstrdup (equals + 1));
		free (key);
	}
	self.envv = strv_make ();
	self.argv = strv_make ();
	return self;
}

static void
spawn_context_free (struct spawn_context *self)
{
	str_map_free (&self->env);
	str_map_free (&self->term);
	strv_free (&self->envv);
	strv_free (&self->argv);

	if (self->error)
		error_free (self->error);
}

// -0, +0, e
static void
environ_map_update (struct str_map *env, lua_State *L)
{
	lua_pushnil (L);
	while (lua_next (L, -2))
	{
		if (lua_type (L, -2) != LUA_TSTRING)
			luaL_error (L, "environment maps must be keyed by strings");

		const char *value = lua_tostring (L, -1);
		str_map_set (env, lua_tostring (L, -2),
			value ? xstrdup (value) : NULL);
		lua_pop (L, 1);
	}
}

// The environment will get pseudo-randomly reordered,
// which is fine by POSIX.
static void
environ_map_serialize (struct str_map *env, struct strv *envv)
{
	struct str_map_iter iter = str_map_iter_make (env);
	const char *value;
	while ((value = str_map_iter_next (&iter)))
		strv_append_owned (envv,
			xstrdup_printf ("%s=%s", iter.link->key, value));
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static int
spawn_protected (lua_State *L)
{
	struct spawn_context *ctx = lua_touserdata (L, lua_upvalueindex (1));
	luaL_checktype (L, 1, LUA_TTABLE);

	// Step 1: Prepare process environment.
	if (xlua_getfield (L, 1, "environ", LUA_TTABLE, true))
	{
		environ_map_update (&ctx->env, L);
		lua_pop (L, 1);
	}
	char *term = str_map_find (&ctx->env, "TERM");
	if (!term)
	{
		print_debug ("setting a default TERM");
		str_map_set (&ctx->env, "TERM", (term = xstrdup ("dumb")));
	}
	environ_map_serialize (&ctx->env, &ctx->envv);

#ifdef WITH_CURSES
	// Step 2: Load terminal information.
	if (!load_terminfo (term, &ctx->term))
		luaL_error (L, "failed to initialize terminfo for %s", term);
#endif

	// Step 3: Prepare process command line.
	size_t argc = lua_rawlen (L, 1);
	for (size_t i = 1; i <= argc; i++)
	{
		lua_pushinteger (L, i);
		lua_rawget (L, 1);
		const char *arg = lua_tostring (L, -1);
		if (!arg)
			return luaL_error (L, "spawn arguments must be strings");

		strv_append (&ctx->argv, arg);
		lua_pop (L, 1);
	}
	if (ctx->argv.len < 1)
		return luaL_error (L, "missing argument");

	// Step 4: Create a process object.
	// This will get garbage collected as appropriate on failure.
	struct process *process = process_new (L);

	// This could be made into an object that can adjust winsize/termios.
	lua_createtable (L, 0, ctx->term.len);
	struct str_map_iter iter = str_map_iter_make (&ctx->term);
	const struct terminfo_entry *entry = NULL;
	while ((entry = str_map_iter_next (&iter)))
	{
		lua_pushstring (L, iter.link->key);
		switch (entry->kind)
		{
		break; case TERMINFO_BOOLEAN: lua_pushboolean (L, true);
		break; case TERMINFO_NUMERIC: lua_pushinteger (L, entry->numeric);
		break; case TERMINFO_STRING:  lua_pushstring (L, entry->string);
		break; default:               lua_pushnil (L);
		}
		lua_settable (L, -3);
	}
	process->ref_term = luaL_ref (L, LUA_REGISTRYINDEX);

	struct winsize ws = { .ws_row = 24, .ws_col = 80 };
	if ((entry = str_map_find (&ctx->term, "lines"))
	 && entry->kind == TERMINFO_NUMERIC)
		ws.ws_row = entry->numeric;
	if ((entry = str_map_find (&ctx->term, "columns"))
	 && entry->kind == TERMINFO_NUMERIC)
		ws.ws_col = entry->numeric;

	// Step 5: Spawn the process, which gets a new process group.
	process->pid =
		pty_fork (&process->terminal_fd, NULL, NULL, &ws, &ctx->error);
	if (process->pid < 0)
	{
		return luaL_error (L, "failed to spawn %s: %s",
			ctx->argv.vector[0], ctx->error->message);
	}
	if (!process->pid)
	{
		execvpe (ctx->argv.vector[0], ctx->argv.vector, ctx->envv.vector);
		print_error ("failed to spawn %s: %s",
			ctx->argv.vector[0], strerror (errno));
		// Or we could figure out when exactly to use statuses 126 and 127.
		_exit (EXIT_FAILURE);
	}

	// Step 6: Create a log file.
	if (getenv ("WDYE_LOGGING"))
	{
		const char *name = ctx->argv.vector[0];
		const char *last_slash = strrchr (name, '/');
		if (last_slash)
			name = last_slash + 1;

		char *path = xstrdup_printf ("%s-%s.%d.cast",
			PROGRAM_NAME, name, (int) process->pid);
		if (!(process->asciicast = fopen (path, "w")))
			print_warning ("%s: %s", path, strerror (errno));
		free (path);
	}
	process->start = clock_msec ();
	if (process->asciicast)
	{
		fprintf (process->asciicast, "{\"version\": 2, "
			"\"width\": %u, \"height\": %u, \"env\": {\"TERM\": \"%s\"}}\n",
			ws.ws_col, ws.ws_row, term);
	}

	set_cloexec (process->terminal_fd);
	return 1;
}

static int
xlua_spawn (lua_State *L)
{
	lua_pushcfunction (L, xlua_error_handler);
	lua_insert (L, 1);

	struct spawn_context ctx = {};
	lua_pushlightuserdata (L, &ctx);
	lua_pushcclosure (L, spawn_protected, 1);
	lua_insert (L, 2);

	// There are way too many opportunities for Lua to throw,
	// so maintain a context to clean up in one go.
	ctx = spawn_context_make ();
	int result = lua_pcall (L, lua_gettop (L) - 2, 1, 1);
	spawn_context_free (&ctx);
	if (result)
		return lua_error (L);

	// Remove the error handler ("good programming practice").
	lua_remove (L, 1);
	return 1;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

struct expect_context
{
	size_t patterns_len;                ///< Number of patterns
	struct pattern **patterns;          ///< Pattern array
	size_t pfds_len;                    ///< Number of distinct poll FDs
	struct pollfd *pfds;                ///< Distinct poll FDs

	lua_Number first_timeout;           ///< Nearest timeout value
	lua_Number timeout;                 ///< Actually used timeout value
};

static void
expect_context_free (struct expect_context *self)
{
	free (self->patterns);
	free (self->pfds);
}

static bool
expect_has_fd (struct expect_context *ctx, int fd)
{
	for (size_t i = 0; i < ctx->pfds_len; i++)
		if (ctx->pfds[i].fd == fd)
			return true;
	return false;
}

static struct process *
expect_fd_to_process (struct expect_context *ctx, int fd)
{
	for (size_t i = 0; i < ctx->patterns_len; i++)
	{
		struct pattern *p = ctx->patterns[i];
		if (p->process
		 && p->process->terminal_fd == fd)
			return p->process;
	}
	return NULL;
}

static void
expect_set_fd_eof (struct expect_context *ctx, int fd)
{
	for (size_t i = 0; i < ctx->patterns_len; i++)
	{
		struct pattern *p = ctx->patterns[i];
		if (p->process
		 && p->process->terminal_fd == fd)
			p->eof = true;
	}
}

static void
expect_prepare_pattern (struct expect_context *ctx, struct pattern *p)
{
	str_reset (&p->input);
	if (p->kind == PATTERN_REGEX)
		for (size_t i = 0; i <= p->regex->re_nsub; i++)
			p->matches[i] = (regmatch_t) { .rm_so = -1, .rm_eo = -1 };

	if (p->kind == PATTERN_REGEX
	 || p->kind == PATTERN_EXACT
	 || p->kind == PATTERN_EOF
	 || p->kind == PATTERN_DEFAULT)
	{
		p->eof = false;
		if (!expect_has_fd (ctx, p->process->terminal_fd))
			ctx->pfds[ctx->pfds_len++] = (struct pollfd)
				{ .fd = p->process->terminal_fd, .events = POLLIN };
	}
	if (p->kind == PATTERN_TIMEOUT
	 || p->kind == PATTERN_DEFAULT)
	{
		lua_Number v = p->timeout >= 0 ? p->timeout : g.default_timeout;
		if (ctx->first_timeout != ctx->first_timeout)
			ctx->first_timeout = v;
		else
			ctx->first_timeout = MIN (ctx->first_timeout, v);
	}
}

static void
expect_prepare (struct expect_context *ctx)
{
	// The liberty poller is not particularly appropriate for this use case.
	ctx->pfds_len = 0;
	ctx->pfds = xcalloc (ctx->patterns_len, sizeof *ctx->pfds);

	ctx->first_timeout = NAN;
	for (size_t i = 0; i < ctx->patterns_len; i++)
		expect_prepare_pattern (ctx, ctx->patterns[i]);

	// There is always at least a default timeout.
	ctx->timeout = g.default_timeout;
	if (ctx->first_timeout == ctx->first_timeout)
		ctx->timeout = ctx->first_timeout;
}

static struct pattern *
expect_match_timeout (struct expect_context *ctx)
{
	for (size_t i = 0; i < ctx->patterns_len; i++)
	{
		struct pattern *p = ctx->patterns[i];
		if (p->kind != PATTERN_TIMEOUT
		 && p->kind != PATTERN_DEFAULT)
			continue;

		if (p->timeout <= ctx->first_timeout)
			return p;
	}
	return NULL;
}

static bool
pattern_match (struct pattern *self)
{
	struct process *process = self->process;
	struct str *buffer = process ? &process->buffer : NULL;

	str_reset (&self->input);
	switch (self->kind)
	{
	case PATTERN_EOF:
	case PATTERN_DEFAULT:
	{
		if (!self->eof)
			return false;

		str_append_str (&self->input, &process->buffer);
		if (!self->notransfer)
			str_reset (&process->buffer);
		return true;
	}
	case PATTERN_REGEX:
	{
		int flags = 0;
#ifdef REG_STARTEND
		self->matches[0] = (regmatch_t) { .rm_so = 0, .rm_eo = buffer->len };
		flags |= REG_STARTEND;
#endif
		if (regexec (self->regex, buffer->str,
			self->regex->re_nsub + 1, self->matches, flags))
		{
			for (size_t i = 0; i <= self->regex->re_nsub; i++)
				self->matches[i] = (regmatch_t) { .rm_so = -1, .rm_eo = -1 };
			return false;
		}

		str_append_data (&self->input, buffer->str, self->matches[0].rm_eo);
		if (!self->notransfer)
			str_remove_slice (buffer, 0, self->matches[0].rm_eo);
		return true;
	}
	case PATTERN_EXACT:
	{
		const char *match = str_memmem (buffer, &self->exact, self->nocase);
		if (!match)
			return false;

		str_append_data (&self->input, match, self->exact.len);
		if (!self->notransfer)
			str_remove_slice (buffer, 0, match - buffer->str + self->exact.len);
		return true;
	}
	default:
		return false;
	}
}

static struct pattern *
expect_match_data (struct expect_context *ctx)
{
	for (size_t i = 0; i < ctx->patterns_len; i++)
	{
		struct pattern *p = ctx->patterns[i];
		if (pattern_match (p))
			return p;
	}
	return NULL;
}

static int
expect_protected (lua_State *L)
{
	struct expect_context *ctx = lua_touserdata (L, lua_upvalueindex (1));
	ctx->patterns_len = lua_gettop (L);
	ctx->patterns = xcalloc (ctx->patterns_len, sizeof *ctx->patterns);
	for (size_t i = 0; i < ctx->patterns_len; i++)
		ctx->patterns[i] = luaL_checkudata (L, i + 1, XLUA_PATTERN_METATABLE);
	expect_prepare (ctx);

	int64_t deadline = 0;
	struct pattern *match = NULL;
restart:
	// A "continue" statement means we start anew with a new timeout.
	// TODO(p): We should detect deadline > INT64_MAX, and wait indefinitely.
	deadline = clock_msec () + ctx->timeout * 1000;

	// First, check if anything matches already,
	// so that we don't need to wait for /even more/ data.
	match = expect_match_data (ctx);

	while (!match)
	{
		int64_t until_deadline = deadline - clock_msec ();
		int n = poll (ctx->pfds, ctx->pfds_len, MAX (0, until_deadline));
		if (n < 0)
			return luaL_error (L, "poll: %s", strerror (errno));

		for (int i = 0; i < n; i++)
		{
			struct pollfd *pfd = ctx->pfds + i;
			hard_assert (!(pfd->revents & POLLNVAL));
			if (!(pfd->revents & (POLLIN | POLLHUP | POLLERR)))
				continue;

			struct process *process = expect_fd_to_process (ctx, pfd->fd);
			hard_assert (process != NULL);
			if (!process_feed (process))
			{
				expect_set_fd_eof (ctx, pfd->fd);
				// Otherwise we would loop around this descriptor.
				pfd->fd = -1;
			}
		}

		if (n > 0)
			match = expect_match_data (ctx);
		else if (!(match = expect_match_timeout (ctx)))
			return 0;
	}

	// Resolve the matching pattern back to its Lua full userdata.
	int match_idx = 0;
	for (size_t i = 0; i < ctx->patterns_len; i++)
		if (ctx->patterns[i] == match)
			match_idx = i + 1;

	// Filter the values table by executing any functions with the pattern.
	lua_rawgeti (L, LUA_REGISTRYINDEX, match->ref_values);
	int values_idx = lua_gettop (L);
	int values_len = lua_rawlen (L, values_idx);
	lua_checkstack (L, values_len);

	lua_pushcfunction (L, xlua_error_handler);
	int handler_idx = lua_gettop (L);
	for (int i = 1; i <= values_len; i++)
	{
		lua_rawgeti (L, values_idx, i);
		if (!lua_isfunction (L, -1))
			continue;

		lua_pushvalue (L, match_idx);
		if (!lua_pcall (L, 1, LUA_MULTRET, handler_idx))
			continue;
		if (!lua_isnil (L, -1))
			return lua_error (L);

		lua_pop (L, lua_gettop (L) - values_idx + 1);
		goto restart;
	}
	return lua_gettop (L) - handler_idx;
}

static int
xlua_expect (lua_State *L)
{
	lua_pushcfunction (L, xlua_error_handler);
	lua_insert (L, 1);

	struct expect_context ctx = {};
	lua_pushlightuserdata (L, &ctx);
	lua_pushcclosure (L, expect_protected, 1);
	lua_insert (L, 2);

	int result = lua_pcall (L, lua_gettop (L) - 2, LUA_MULTRET, 1);
	expect_context_free (&ctx);
	if (result)
		return lua_error (L);

	// Remove the error handler ("good programming practice").
	lua_remove (L, 1);
	return lua_gettop (L);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

static int
xlua_timeout (lua_State *L)
{
	luaL_checktype (L, 1, LUA_TTABLE);
	if (lua_gettop (L) != 1)
		return luaL_error (L, "too many arguments");

	struct pattern *pattern = pattern_new (L, PATTERN_TIMEOUT, 0);

	int first = 1, last = lua_rawlen (L, 1);
	if (pattern_readtimeout (pattern, L, 1))
		first++;

	xlua_newtablecopy (L, 1, first, last);
	pattern->ref_values = luaL_ref (L, LUA_REGISTRYINDEX);
	return 1;
}

static int
xlua_continue (lua_State *L)
{
	// xlua_expect() handles this specially.
	lua_pushnil (L);
	return lua_error (L);
}

static luaL_Reg xlua_library[] =
{
	{ "spawn",    xlua_spawn    },
	{ "expect",   xlua_expect   },
	{ "timeout",  xlua_timeout  },
	{ "continue", xlua_continue },
	{ NULL,       NULL          }
};

// --- Initialisation, event handling ------------------------------------------

static void *
xlua_alloc (void *ud, void *ptr, size_t o_size, size_t n_size)
{
	(void) ud;
	(void) o_size;

	if (n_size)
		return realloc (ptr, n_size);

	free (ptr);
	return NULL;
}

static int
xlua_panic (lua_State *L)
{
	print_fatal ("Lua panicked: %s", lua_tostring (L, -1));
	lua_close (L);
	exit (EXIT_FAILURE);
	return 0;
}

int
main (int argc, char *argv[])
{
	if (argc < 2)
	{
		fprintf (stderr, "Usage: %s program.lua [args...]\n", argv[0]);
		return 1;
	}

	if (!(g.L = lua_newstate (xlua_alloc, NULL)))
		exit_fatal ("Lua initialization failed");
	lua_atpanic (g.L, xlua_panic);
	luaL_openlibs (g.L);
	luaL_checkversion (g.L);

	luaL_newlib (g.L, xlua_library);
	lua_setglobal (g.L, PROGRAM_NAME);

	luaL_newmetatable (g.L, XLUA_PROCESS_METATABLE);
	luaL_setfuncs (g.L, xlua_process_table, 0);
	lua_pop (g.L, 1);

	luaL_newmetatable (g.L, XLUA_PATTERN_METATABLE);
	luaL_setfuncs (g.L, xlua_pattern_table, 0);
	lua_pop (g.L, 1);

	luaL_checkstack (g.L, argc, NULL);

	lua_pushcfunction (g.L, xlua_error_handler);
	if (luaL_loadfile (g.L, strcmp (argv[1], "-") ? argv[1] : NULL))
		goto error;
	for (int i = 2; i < argc; i++)
		lua_pushstring (g.L, argv[i]);
	if (lua_pcall (g.L, argc - 2, 0, 1))
		goto error;
	lua_close (g.L);
	return 0;

error:
	print_error ("%s", lua_tostring (g.L, -1));
	lua_close (g.L);
	return 1;
}
