/* SPDX-License-Identifier: GPL-3.0-only */

#include <stdlib.h>
#include <inttypes.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <assert.h>
#include <sys/mman.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <limits.h>
#include <sys/time.h>
#include <lopsub.h>

#include "tf.h"
#include "tfortune.lsg.h"

#define TF_SEP "---- "

static struct lls_parse_result *lpr, *sublpr;
#define CMD_PTR(_cname) lls_cmd(LSG_TFORTUNE_CMD_ ## _cname, tfortune_suite)
#define OPT_RESULT(_cname, _oname) (lls_opt_result(\
	LSG_TFORTUNE_ ## _cname ## _OPT_ ## _oname, \
	(CMD_PTR(_cname) == CMD_PTR(TFORTUNE))? lpr : sublpr))
#define OPT_GIVEN(_cname, _oname) (lls_opt_given(OPT_RESULT(_cname, _oname)))
#define OPT_STRING_VAL(_cname, _oname) (lls_string_val(0, \
	OPT_RESULT(_cname, _oname)))
#define OPT_UINT32_VAL(_cname, _oname) (lls_uint32_val(0, \
		OPT_RESULT(_cname, _oname)))

int loglevel_arg_val;

struct tf_user_data {int (*handler)(void);};
#define EXPORT_CMD_HANDLER(_cmd) const struct tf_user_data \
	lsg_tfortune_com_ ## _cmd ## _user_data = { \
		.handler = com_ ## _cmd \
	};

struct map_chunk {
	const char *base;
	size_t len;
};

struct epigram {
	struct map_chunk epi, tags;
};

static int lopsub_error(int lopsub_ret, char **errctx)
{
	const char *msg = lls_strerror(-lopsub_ret);
	if (*errctx)
		ERROR_LOG("%s: %s\n", *errctx, msg);
	else
		ERROR_LOG("%s\n", msg);
	free(*errctx);
	*errctx = NULL;
	return -E_LOPSUB;
}

/* per epigram context for the tag expression parser */
struct epi_properties {
	const struct map_chunk *chunk; /* only the epigram */
	struct linhash_table *tagtab;
	unsigned num_tags;
};

unsigned epi_len(const struct epi_properties *props)
{
	return props->chunk->len;
}

char *epi_text(const struct epi_properties *props)
{
	const char *txt = props->chunk->base;
	char *result = malloc(props->chunk->len + 1);
	memcpy(result, txt, props->chunk->len);
	result[props->chunk->len] = '\0';
	return result;
}

bool epi_has_tag(const char *tag, const struct epi_properties *props)
{
	return linhash_lookup(tag, props->tagtab);
}

struct tag_iter {
	char *str;
	char *saveptr; /* for strtok_r(3) */
	char *token;
};

static struct tag_iter *tag_iter_new(const struct map_chunk *tags)
{
	struct tag_iter *titer = xmalloc(sizeof(*titer));

	titer->str = xmalloc(tags->len + 1);
	memcpy(titer->str, tags->base, tags->len);
	titer->str[tags->len] = '\0';
	titer->token = strtok_r(titer->str, ",", &titer->saveptr);
	return titer;
}

static const char *tag_iter_get(const struct tag_iter *titer)
{
	return titer->token;
}

static void tag_iter_next(struct tag_iter *titer)
{
	titer->token = strtok_r(NULL, ",", &titer->saveptr);
}

static void tag_iter_free(struct tag_iter *titer)
{
	if (!titer)
		return;
	free(titer->str);
	free(titer);
}

static bool epi_admissible(const struct epigram *epi,
		const struct txp_context *ast)
{
	bool admissible;
	const char *p;
	struct epi_properties props = {
		.chunk = &epi->epi,
		.tagtab = linhash_new(3),
		.num_tags = 0,
	};
	struct tag_iter *titer;


	for (
		titer = tag_iter_new(&epi->tags);
		(p = tag_iter_get(titer));
		tag_iter_next(titer)
	) {
		struct linhash_item item = {.key = p};
		linhash_insert(&item, props.tagtab, NULL);
		props.num_tags++;
	}

	admissible = txp_eval_ast(ast, &props);
	linhash_free(props.tagtab);
	tag_iter_free(titer);
	return admissible;
}

static void print_epigram(const struct epigram *epi, bool print_tags)
{
	printf("%.*s", (int)epi->epi.len, epi->epi.base);
	if (print_tags)
		printf(TF_SEP "%.*s\n", (int)epi->tags.len, epi->tags.base);
}

static void print_admissible_epigrams(const struct epigram *epis,
		unsigned num_epis)
{
	unsigned n;

	for (n = 0; n < num_epis; n++)
		print_epigram(epis + n, OPT_GIVEN(PRINT, TAGS));
}

static void print_random_epigram(const struct epigram *epis, unsigned num_epis)
{
	long unsigned r;
	const struct epigram *epi;
	struct timeval tv;

	if (num_epis == 0) {
		ERROR_LOG("no matching epigram\n");
		return;
	}
	gettimeofday(&tv, NULL);
	srandom((unsigned)tv.tv_usec);
	r = (num_epis + 0.0) * (random() / (RAND_MAX + 1.0));
	assert(r < num_epis);
	epi = epis + r;
	print_epigram(epi, OPT_GIVEN(PRINT, TAGS));
}

static int read_tag_expression(struct iovec *result)
{
	const char *tx = OPT_STRING_VAL(PRINT, EXPRESSION);
	int ret, fd;

	assert(tx);
	if (strcmp(tx, "-")) {
		char *filename;
		if (tx[0] != '/') {
			char *home = get_homedir();
			xasprintf(&filename, "%s/.tfortune/expressions/%s",
				home, tx);
			free(home);
		} else
			filename = xstrdup(tx);
		ret = open(filename, O_RDONLY);
		if (ret < 0) {
			ret = -ERRNO_TO_TF_ERROR(errno);
			ERROR_LOG("could not open %s\n", filename);
		}
		free(filename);
		if (ret < 0)
			return ret;
		fd = ret;
	} else
		fd = STDIN_FILENO;
	ret = fd2buf(fd, result);
	close(fd);
	return ret;
}

static int tx2ast(const struct iovec *tx, struct txp_context **ast)
{
	int ret;
	char *errmsg;

	ret = txp_init(tx, ast, &errmsg);
	if (ret < 0) {
		ERROR_LOG("could not parse tag expression: %s\n", errmsg);
		free(errmsg);
		return ret;
	}
	return 1;
}

struct epi_iter {
	struct iovec *maps;
	unsigned num_maps;
	unsigned map_num;
	struct epigram epi;
	unsigned num_epis;
};

static bool get_next_epi(struct epi_iter *eiter)
{
	const char *epi_start = NULL;

	for (; eiter->map_num < eiter->num_maps; eiter->map_num++) {
		struct iovec *iov = eiter->maps + eiter->map_num;
		const char *buf, *end = iov->iov_base + iov->iov_len;

		if (!epi_start && eiter->epi.tags.base)
			epi_start = eiter->epi.tags.base
				+ eiter->epi.tags.len + 1;
		else
			epi_start = iov->iov_base;
		buf = epi_start;
		while (buf < end) {
			const size_t sep_len = strlen(TF_SEP);
			const char *p, *cr, *tags;
			size_t tag_len;

			cr = memchr(buf, '\n', end - buf);
			if (!cr)
				break;
			p = cr + 1;
			if (p + sep_len >= end)
				break;
			if (strncmp(p, TF_SEP, sep_len) != 0) {
				buf = p;
				continue;
			}
			tags = p + sep_len;
			cr = memchr(tags, '\n', end - tags);
			if (cr)
				tag_len = cr - tags;
			else
				tag_len = end - tags;
			eiter->epi.epi.base = epi_start;
			eiter->epi.epi.len = p - epi_start;
			eiter->epi.tags.base = tags;
			eiter->epi.tags.len = tag_len;
			eiter->num_epis++;
			return true;
		}
	}
	eiter->epi.epi.base = NULL;
	eiter->epi.epi.len = 0;
	eiter->epi.tags.base = NULL;
	eiter->epi.tags.len = 0;
	return false;
}

static char *get_basedir(void)
{
	char *home, *basedir;
	if (OPT_GIVEN(TFORTUNE, BASEDIR))
		return xstrdup(OPT_STRING_VAL(TFORTUNE, BASEDIR));
	home = get_homedir();
	xasprintf(&basedir, "%s/.tfortune", home);
	free(home);
	return basedir;
}

static char *get_epidir(void)
{
	char *basedir, *epidir;
	struct stat s;

	basedir = get_basedir();
	xasprintf(&epidir, "%s/epigrams", basedir);
	free(basedir);
	if (!OPT_GIVEN(TFORTUNE, BASEDIR) && stat(epidir, &s) < 0) {
		free(epidir);
		epidir = xstrdup(DATADIR "/tfortunes/epigrams");
		INFO_LOG("falling back to system-wide epidir %s\n", epidir);
	}
	return epidir;
}

static char *get_xdir(void)
{
	char *basedir = get_basedir(), *xdir;
	xasprintf(&xdir, "%s/expressions", basedir);
	free(basedir);
	return xdir;
}

static struct epi_iter *epi_iter_new(void)
{
	struct epi_iter *eiter = xmalloc(sizeof(*eiter));
	unsigned num_inputs = lls_num_inputs(sublpr);

	if (num_inputs == 0) {
		struct regfile_iter *riter;
		struct iovec iov;
		char *epidir = get_epidir();

		regfile_iter_new(epidir, &riter);
		free(epidir);
		eiter->maps = NULL;
		eiter->num_maps = 0;
		for (;
			regfile_iter_map(riter, &iov);
			regfile_iter_next(riter)
		) {
			eiter->num_maps++;
			eiter->maps = xrealloc(eiter->maps,
				eiter->num_maps * sizeof(*eiter->maps));
			eiter->maps[eiter->num_maps - 1] = iov;
		}
		regfile_iter_free(riter);
	} else {
		unsigned n;
		eiter->maps = xmalloc(num_inputs * sizeof(*eiter->maps));
		for (n = 0; n < num_inputs; n++)
			mmap_file(lls_input(n, sublpr), eiter->maps + n);
		eiter->num_maps = num_inputs;
	}
	eiter->map_num = 0;
	eiter->epi.epi.base = NULL;
	eiter->epi.epi.len = 0;
	eiter->epi.tags.base = NULL;
	eiter->epi.tags.len = 0;
	eiter->num_epis = 0;
	get_next_epi(eiter);
	return eiter;
}

static const struct epigram *epi_iter_get(const struct epi_iter *eiter)
{
	return (eiter->epi.epi.base && eiter->epi.tags.base)?
		&eiter->epi : NULL;
}

static unsigned epi_iter_num_maps(const struct epi_iter *eiter)
{
	return eiter->num_maps;
}

static unsigned epi_iter_num_epis(const struct epi_iter *eiter)
{
	return eiter->num_epis;
}

static void epi_iter_next(struct epi_iter *eiter)
{
	get_next_epi(eiter);
}

static void epi_iter_free(struct epi_iter *eiter)
{
	unsigned n;

	if (!eiter)
		return;
	for (n = 0; n < eiter->num_maps; n++)
		munmap(eiter->maps[n].iov_base, eiter->maps[n].iov_len);
	free(eiter->maps);
	free(eiter);
}

static int com_print(void)
{
	int ret;
	struct epigram *epis = NULL;
	unsigned epis_sz = 0, nae = 0; /* number of admissible epis */
	struct iovec tx;
	struct txp_context *ast;
	struct epi_iter *eiter;
	const struct epigram *epi;

	ret = read_tag_expression(&tx);
	if (ret < 0)
		return ret;
	ret = tx2ast(&tx, &ast);
	if (ret < 0)
		goto free_tx;
	for (
		eiter = epi_iter_new();
		(epi = epi_iter_get(eiter));
		epi_iter_next(eiter)
	) {
		if (!epi_admissible(epi, ast))
			continue;
		if (nae >= epis_sz) {
			epis_sz = 2 * epis_sz + 1;
			epis = xrealloc(epis, epis_sz * sizeof(*epis));
		}
		epis[nae++] = *epi;
	}
	if (OPT_GIVEN(PRINT, ALL))
		print_admissible_epigrams(epis, nae);
	else
		print_random_epigram(epis, nae);
	epi_iter_free(eiter);
	free(epis);
	txp_free(ast);
	ret = 1;
free_tx:
	free(tx.iov_base);
	return ret;
}
EXPORT_CMD_HANDLER(print);

static char *get_editor(void)
{
	char *val = getenv("TFORTUNE_EDITOR");

	if (val && val[0])
		return xstrdup(val);
	val = getenv("EDITOR");
	if (val && val[0])
		return xstrdup(val);
	return xstrdup("vi");
}

static void open_editor(const char *dir)
{
	char *editor;
	char **argv;
	pid_t pid;
	unsigned n, num_inputs = lls_num_inputs(sublpr);

	if ((pid = fork()) < 0) {
		EMERG_LOG("fork error: %s\n", strerror(errno));
		exit(EXIT_FAILURE);
	}
	if (pid) { /* parent */
		wait(NULL);
		return;
	}
	editor = get_editor();
	argv = xmalloc((num_inputs + 2) * sizeof(*argv));
	argv[0] = editor;
	for (n = 0; n < num_inputs; n++)
		xasprintf(&argv[n + 1], "%s/%s", dir, lls_input(n, sublpr));
	argv[num_inputs + 1] = NULL;
	execvp(editor, argv);
	EMERG_LOG("execvp error: %s\n", strerror(errno));
	_exit(EXIT_FAILURE);
}

static int create_dir(const char *path)
{
	int ret;

	ret = mkdir(path, 0777); /* rely on umask */
	if (ret < 0) {
		if (errno == EEXIST)
			return 0;
		ERROR_LOG("could not create %s\n", path);
		return -ERRNO_TO_TF_ERROR(errno);
	}
	NOTICE_LOG("created directory %s\n", path);
	return 1;
}

static int create_basedir(void)
{
	char *basedir;
	int ret;

	basedir = get_basedir();
	ret = create_dir(basedir);
	free(basedir);
	return ret;
}

static int generic_edit(const char *dir)
{
	char *errctx;
	int ret;
	bool basedir_given = OPT_GIVEN(TFORTUNE, BASEDIR);

	ret = lls_check_arg_count(sublpr, 1, INT_MAX, &errctx);
	if (ret < 0) {
		ret = lopsub_error(ret, &errctx);
		return ret;
	}
	if (!basedir_given) {
		ret = create_basedir();
		if (ret < 0)
			return ret;
		ret = create_dir(dir);
		if (ret < 0)
			return ret;
	}
	open_editor(dir);
	ret = 1;
	return ret;
}

static int com_ede(void)
{
	int ret;
	char *epidir = get_epidir();
	ret = generic_edit(epidir);
	free(epidir);
	return ret;
}
EXPORT_CMD_HANDLER(ede);

static int com_edx(void)
{
	int ret;
	char *xdir = get_xdir();
	ret = generic_edit(xdir);
	free(xdir);
	return ret;
}
EXPORT_CMD_HANDLER(edx);

static int item_alpha_compare(const struct linhash_item **i1,
		const struct linhash_item **i2)
{
	return strcmp((*i1)->key, (*i2)->key);
}

static int item_num_compare(const struct linhash_item **i1,
		const struct linhash_item **i2)
{
	long unsigned v1 = (long unsigned)(*i1)->object;
	long unsigned v2 = (long unsigned)(*i2)->object;

	return v1 < v2? -1 : (v1 == v2? 0 : 1);
}

struct dentry {
	char mode[11];
	nlink_t nlink;
	char *user, *group;
	off_t size;
	uint64_t mtime;
	char *name;
};

static void make_dentry(const char *name, const struct stat *stat,
		struct dentry *d)
{
	mode_t m = stat->st_mode;
	struct group *g;
	struct passwd *pwentry;

	sprintf(d->mode, "----------");
	if (S_ISREG(m))
		d->mode[0] = '-';
	else if (S_ISDIR(m))
		d->mode[0] = 'd';
	else if (S_ISCHR(m))
		d->mode[0] = 'c';
	else if ((S_ISBLK(m)))
		d->mode[0] = 'b';
	else if (S_ISLNK(m))
		d->mode[0] = 'l';
	else if (S_ISFIFO(m))
		d->mode[0] = 'p';
	else if ((S_ISSOCK(m)))
		d->mode[0] = 's';
	else
		d->mode[0] = '?';

	if (m & S_IRUSR)
		d->mode[1] = 'r';
	if (m & S_IWUSR)
		d->mode[2] = 'w';
	if (m & S_IXUSR) {
		if (m & S_ISUID)
			d->mode[3] = 's';
		else
			d->mode[3] = 'x';
	} else if (m & S_ISUID)
		d->mode[3] = 'S';

	if (m & S_IRGRP)
		d->mode[4] = 'r';
	if (m & S_IWGRP)
		d->mode[5] = 'w';
	if (m & S_IXGRP) {
		if (m & S_ISGID)
			d->mode[6] = 's';
		else
			d->mode[6] = 'x';
	} else if (m & S_ISGID)
		d->mode[6] = 'S';

	if (m & S_IROTH)
		d->mode[7] = 'r';
	if (m & S_IWOTH)
		d->mode[8] = 'w';
	if (m & S_IXOTH) {
		if (m & S_ISVTX)
			d->mode[9] = 't';
		else
			d->mode[9] = 'x';
	} else if (m & S_ISVTX)
		d->mode[9] = 'T';

	d->nlink = stat->st_nlink;

	pwentry = getpwuid(stat->st_uid);
	if (pwentry && pwentry->pw_name)
		d->user = xstrdup(pwentry->pw_name);
	else
		xasprintf(&d->user, "%u", stat->st_uid);

	g = getgrgid(stat->st_gid);
	if (g && g->gr_name)
		d->group = xstrdup(g->gr_name);
	else
		xasprintf(&d->group, "%u", stat->st_gid);
	d->size = stat->st_size;
	d->mtime = stat->st_mtime;
	d->name = xstrdup(name);
}

static int num_digits(uint64_t x)
{
	unsigned n = 1;

	if (x != 0)
		while (x > 9) {
			x /= 10;
			n++;
		}
	return n;
}

enum var_length_dentry_fields {
	VLDF_NLINKS,
	VLDF_USER,
	VLDF_GROUP,
	VLDF_SIZE,
	NUM_VLDF
};

static void update_field_field_widths(int field_widths[NUM_VLDF],
		const struct dentry *d)
{
	int *w, n;

	w = field_widths + VLDF_NLINKS;
	n = num_digits(d->nlink);
	*w = MAX(*w, n);

	w = field_widths + VLDF_USER;
	n = strlen(d->user);
	*w = MAX(*w, n);

	w = field_widths + VLDF_GROUP;
	n = strlen(d->group);
	*w = MAX(*w, n);

	w = field_widths + VLDF_SIZE;
	n = num_digits(d->size);
	*w = MAX(*w, n);
}

static void format_time(uint64_t seconds, uint64_t now, struct iovec *result)
{
	struct tm *tm;
	const uint64_t m = 6 * 30 * 24 * 3600; /* six months */
	size_t nbytes;

	tm = localtime((time_t *)&seconds);
	assert(tm);

	if (seconds > now - m && seconds < now + m) {
		nbytes = strftime(result->iov_base, result->iov_len,
			"%b %e %k:%M", tm);
		assert(nbytes > 0);
	} else {
		nbytes = strftime(result->iov_base, result->iov_len,
			"%b %e  %Y", tm);
		assert(nbytes > 0);
	}
}

static int list_directory(const char *dir, bool long_listing)
{
	struct regfile_iter *riter;
	const char *basename;
	int ret, field_widths[NUM_VLDF] = {0};
	struct dentry *dentries = NULL;
	unsigned n, num_dentries = 0, dentries_size = 0;
	struct timespec now;

	for (
		regfile_iter_new(dir, &riter);
		(basename = regfile_iter_basename(riter));
		regfile_iter_next(riter)
	) {
		const struct stat *stat;
		struct dentry *dentry;
		if (!long_listing) {
			printf("%s\n", basename);
			continue;
		}
		num_dentries++;
		if (num_dentries > dentries_size) {
			dentries_size = 2 * dentries_size + 1;
			dentries = xrealloc(dentries,
				dentries_size * sizeof(*dentries));
		}
		dentry = dentries + num_dentries - 1;
		stat = regfile_iter_stat(riter);
		make_dentry(basename, stat, dentry);
		update_field_field_widths(field_widths, dentry);
	}
	regfile_iter_free(riter);
	if (!long_listing)
		return 0;
	ret = clock_gettime(CLOCK_REALTIME, &now);
	assert(ret == 0);
	for (n = 0; n < num_dentries; n++) {
		struct dentry *d = dentries + n;
		char buf[30];
		struct iovec iov = {.iov_base = buf, .iov_len = sizeof(buf)};

		format_time(d->mtime, now.tv_sec, &iov);
		printf("%s %*lu %*s %*s %*" PRIu64 " %s %s\n",
			d->mode,
			field_widths[VLDF_NLINKS], (long unsigned)d->nlink,
			field_widths[VLDF_USER], d->user,
			field_widths[VLDF_GROUP], d->group,
		 	field_widths[VLDF_SIZE], (uint64_t)d->size,
			buf,
			d->name
		);
		free(d->user);
		free(d->group);
		free(d->name);
	}
	free(dentries);
	return 1;
}

static int com_lse(void)
{
	int ret;
	char *dir = get_epidir();

	ret = list_directory(dir, OPT_GIVEN(LSE, LONG));
	free(dir);
	return ret;
}
EXPORT_CMD_HANDLER(lse);

static int com_lsx(void)
{
	int ret;
	char *dir = get_xdir();

	ret = list_directory(dir, OPT_GIVEN(LSE, LONG));
	free(dir);
	return ret;
}
EXPORT_CMD_HANDLER(lsx);

static struct linhash_table *hash_tags(unsigned *num_epi_files,
		unsigned *num_epis)
{
	struct linhash_table *tagtab = linhash_new(3);
	struct epi_iter *eiter;
	const struct epigram *epi;

	for (
		eiter = epi_iter_new();
		(epi = epi_iter_get(eiter));
		epi_iter_next(eiter)
	) {
		struct tag_iter *titer;
		const char *tag;
		for (
			titer = tag_iter_new(&epi->tags);
			(tag = tag_iter_get(titer));
			tag_iter_next(titer)
		) {
			struct linhash_item item = {
				.key = xstrdup(tag),
				.object = (void *)1LU
			};
			void **object;
			if (linhash_insert(&item, tagtab, &object) < 0) {
				long unsigned val = (long unsigned)*object;
				val++;
				*object = (void *)val;
				free((char *)item.key);
			}
		}
		tag_iter_free(titer);
	}
	if (num_epi_files)
		*num_epi_files = epi_iter_num_maps(eiter);
	if (num_epis)
		*num_epis = epi_iter_num_epis(eiter);
	epi_iter_free(eiter);
	return tagtab;
}

static int com_lst(void)
{
	struct linhash_table *tagtab;
	struct linhash_iterator *liter;
	struct linhash_item *itemp;
	linhash_comparator *comp = OPT_GIVEN(LST, SORT_BY_COUNT)?
		item_num_compare : item_alpha_compare;
	bool reverse = OPT_GIVEN(LST, REVERSE);

	tagtab = hash_tags(NULL, NULL);
	for (
		liter = linhash_iterator_new(tagtab, comp, reverse);
		(itemp = linhash_iterator_item(liter));
		linhash_iterator_next(liter)
	) {
		if (OPT_GIVEN(LST, LONG))
			printf("%lu\t%s\n", (long unsigned)itemp->object,
				itemp->key);
		else
			printf("%s\n", itemp->key);
		free((char *)itemp->key);
	}
	linhash_iterator_free(liter);
	linhash_free(tagtab);
	return 0;
}
EXPORT_CMD_HANDLER(lst);

static int com_stats(void)
{
	struct linhash_table *tagtab;
	struct linhash_iterator *liter;
	struct linhash_item *itemp;
	unsigned num_epi_files, num_epis, num_unique_tags, num_x = 0;
	long unsigned num_tags = 0;
	char *xdir;
	struct regfile_iter *riter;

	tagtab = hash_tags(&num_epi_files, &num_epis);
	for (
		liter = linhash_iterator_new(tagtab, NULL, false);
		(itemp = linhash_iterator_item(liter));
		linhash_iterator_next(liter)
	) {
		free((char *)itemp->key);
		num_tags += (long unsigned)itemp->object;
	}
	num_unique_tags = linhash_num_items(tagtab);
	linhash_iterator_free(liter);
	xdir = get_xdir();
	for (
		regfile_iter_new(xdir, &riter);
		regfile_iter_basename(riter);
		regfile_iter_next(riter)
	)
		num_x++;
	regfile_iter_free(riter);
	free(xdir);
	printf("number of tag expressions.......... %5u\n", num_x);
	printf("number of epigram files............ %5u\n", num_epi_files);
	printf("number of epigrams................. %5u\n", num_epis);
	printf("number of tags..................... %5lu\n", num_tags);
	printf("number of unique tags.............. %5u\n", num_unique_tags);
	printf("average number of epigrams per file %8.02f\n",
		num_epi_files > 0?  (float)num_epis / num_epi_files : 0);
	printf("average number of tags per epigram. %8.02f\n", num_epis > 0?
		(float)num_tags / num_epis : 0);
	printf("average number of tag recurrence... %8.02f\n", num_unique_tags > 0?
		(float)num_tags / num_unique_tags : 0);
	if (OPT_GIVEN(STATS, VERBOSE)) {
		char *lh_stats = linhash_statistics(tagtab);
		printf("\nlinear hashing statistics:\n%s\n", lh_stats);
		free(lh_stats);
	}
	linhash_free(tagtab);
	return 1;
}
EXPORT_CMD_HANDLER(stats);

#define LSG_TFORTUNE_CMD(_name) #_name
static const char * const subcommand_names[] = {LSG_TFORTUNE_SUBCOMMANDS NULL};
#undef LSG_TFORTUNE_CMD

static void show_subcommand_summary(bool verbose)
{
	int i;

	printf("Available subcommands:\n");
	if (verbose) {
		const struct lls_command *cmd;
		for (i = 1; (cmd = lls_cmd(i, tfortune_suite)); i++) {
			const char *purpose = lls_purpose(cmd);
			const char *name = lls_command_name(cmd);
			printf("%-11s%s\n", name, purpose);
		}
	} else {
		unsigned n = 8;
		printf("\t");
		for (i = 0; i < LSG_NUM_TFORTUNE_SUBCOMMANDS; i++) {
			if (i > 0)
				n += printf(", ");
			if (n > 70) {
				printf("\n\t");
				n = 8;
			}
			n += printf("%s", subcommand_names[i]);
		}
		printf("\n");
	}
}

static int com_help(void)
{
	int ret;
	char *errctx, *help;
	const char *arg;
	const struct lls_command *cmd;

	ret = lls_check_arg_count(sublpr, 0, 1, &errctx);
	if (ret < 0)
		return lopsub_error(ret, &errctx);
	if (lls_num_inputs(sublpr) == 0) {
		show_subcommand_summary(OPT_GIVEN(HELP, LONG));
		return 0;
	}
	arg = lls_input(0, sublpr);
	ret = lls_lookup_subcmd(arg, tfortune_suite, &errctx);
	if (ret < 0)
		return lopsub_error(ret, &errctx);
	cmd = lls_cmd(ret, tfortune_suite);
	if (OPT_GIVEN(HELP, LONG))
		help = lls_long_help(cmd);
	else
		help = lls_short_help(cmd);
	printf("%s\n", help);
	free(help);
	return 1;
}
EXPORT_CMD_HANDLER(help);

const char *GET_VERSION(void);
static void handle_help_and_version(void)
{
	int i;
	char *help;
	const struct lls_command *cmd;

	if (OPT_GIVEN(TFORTUNE, VERSION)) {
		printf(PACKAGE " %s\n"
			"Copyright (C) " COPYRIGHT_YEAR " " AUTHOR ".\n"
			"License: " LICENSE ": <" LICENSE_URL ">.\n"
			"This is free software: you are free to change and redistribute it.\n"
			"There is NO WARRANTY, to the extent permitted by law.\n"
			"\n"
			"Web page: " PACKAGE_HOMEPAGE "\n"
			"Clone URL: " CLONE_URL "\n"
			"Gitweb: " GITWEB_URL "\n"
			"Author's Home Page: " HOME_URL "\n"
			"Send feedback to: " AUTHOR " <" EMAIL ">\n"
			,
			GET_VERSION()
		);
		exit(EXIT_SUCCESS);
	}
	cmd = CMD_PTR(TFORTUNE);
	if (OPT_GIVEN(TFORTUNE, DETAILED_HELP))
		help = lls_long_help(cmd);
	else if (OPT_GIVEN(TFORTUNE, HELP))
		help = lls_short_help(cmd);
	else
		return;
	printf("%s\n", help);
	free(help);
	if (OPT_GIVEN(TFORTUNE, DETAILED_HELP))
		for (i = 1; (cmd = lls_cmd(i, tfortune_suite)); i++) {
			help = lls_short_help(cmd);
			printf("%s\n---\n", help);
			free(help);
		}
	else
		show_subcommand_summary(true /* verbose */);
	exit(EXIT_SUCCESS);
}

enum tf_word_type {
	WT_COMMAND_NAME,
	WT_DOUBLE_DASH, /* -- */
	WT_SHORT_OPT_WITH_ARG, /* -l */
	WT_SHORT_OPT_WITHOUT_ARG, /* -V, -abc=d */
	WT_LONG_OPT_WITH_ARG, /* --loglevel */
	WT_LONG_OPT_WITHOUT_ARG, /* --foo=bar --help */
	WT_OPTION_ARG,
	WT_NON_OPTION_ARG,
	WT_DUNNO,
};

static bool is_short_opt(const char *word)
{
	if (word[0] != '-')
		return false;
	if (word[1] == '-')
		return false;
	if (word[1] == '\0')
		return false;
	return true;
}

static bool is_long_opt(const char *word)
{
	if (word[0] != '-')
		return false;
	if (word[1] != '-')
		return false;
	if (word[2] == '\0')
		return false;
	return true;
}

/* whether the next word will be an arg to this short opt */
static int short_opt_needs_arg(const char *word,
		const char * const *short_opts)
{
	size_t n, len;

	if (strchr(word, '='))
		return false;
	len = strlen(word);
	for (n = 0; short_opts[n]; n++) {
		const char *opt = short_opts[n];
		if (word[len - 1] != opt[1])
			continue;
		if (opt[2] == '=')
			return true;
		else
			return false;
	}
	return -1;
}

/* whether the next word will be an arg to this long opt */
static int long_opt_needs_arg(const char *word,
		const char * const *long_opts)
{
	size_t n;

	if (strchr(word, '='))
		return false;
	for (n = 0; long_opts[n]; n++) {
		const char *opt = long_opts[n];
		size_t len = strlen(opt);

		if (opt[len - 1] == '=')
			len--;
		if (strncmp(word + 2, opt + 2, len - 2))
			continue;
		if (opt[len] == '=')
			return true;
		else
			return false;
	}
	return -1;
}

static bool get_word_types(unsigned cword, unsigned arg0,
		const char * const *short_opts, const char * const *long_opts,
		enum tf_word_type *result)
{
	const char *word;
	unsigned n;
	bool have_dd = false;
	int ret;

	/* index zero is always the command name */
	assert(cword > arg0);
	result[arg0] = WT_COMMAND_NAME;
	for (n = arg0 + 1; n < cword; n++) {
		enum tf_word_type prev_type = result[n - 1];

		if (have_dd) {
			result[n] = WT_NON_OPTION_ARG;
			continue;
		}
		if (prev_type == WT_SHORT_OPT_WITH_ARG) {
			result[n] = WT_OPTION_ARG;
			continue;
		}
		if (prev_type == WT_LONG_OPT_WITH_ARG) {
			result[n] = WT_OPTION_ARG;
			continue;
		}
		word = lls_input(n, sublpr);
		if (strcmp(word, "--") == 0) {
			result[n] = WT_DOUBLE_DASH;
			have_dd = true;
			continue;
		}
		if (is_short_opt(word)) {
			ret = short_opt_needs_arg(word, short_opts);
			if (ret < 0)
				goto dunno;
			if (ret > 0)
				result[n] = WT_SHORT_OPT_WITH_ARG;
			else
				result[n] = WT_SHORT_OPT_WITHOUT_ARG;
			continue;
		}
		if (is_long_opt(word)) {
			ret = long_opt_needs_arg(word, long_opts);
			if (ret < 0)
				goto dunno;
			if (ret > 0)
				result[n] = WT_LONG_OPT_WITH_ARG;
			else
				result[n] = WT_LONG_OPT_WITHOUT_ARG;
			continue;
		}
		result[n] = WT_NON_OPTION_ARG;
	}
	return have_dd;
dunno:
	for (; n <= cword; n++)
		result[n] = WT_DUNNO;
	return false;
}

#define DUMMY_COMPLETER(_name) static char **complete_ ## _name( \
	__attribute__ ((unused)) uint32_t cword, \
	__attribute__ ((unused)) unsigned arg0, \
	__attribute__ ((unused)) bool have_dd \
	) {return NULL;}

DUMMY_COMPLETER(tfortune)
DUMMY_COMPLETER(compgen)
DUMMY_COMPLETER(completer)

static const char * const supercmd_opts[] = {LSG_TFORTUNE_TFORTUNE_OPTS, NULL};

static void print_zero_terminated_list(const char * const *list)
{
	const char * const *c;
	for (c = list; *c; c++)
		printf("%s%c", *c, '\0');
}

static void print_option_list(const char * const *opts)
{
	const char * const *c;

	for (c = opts; *c; c++) {
		int len = strlen(*c);
		assert(len > 0);
		if ((*c)[len - 1] == '=')
			len--;
		printf("%.*s%c", len, *c, '\0');
	}
}

static void activate_dirname_completion(void)
{
	printf("%c", '\0');
	printf("-o dirnames%c", '\0');
}

static void complete_loglevels(void)
{
	unsigned n;
	const struct lls_option *opt = lls_opt(LSG_TFORTUNE_TFORTUNE_OPT_LOGLEVEL,
		CMD_PTR(TFORTUNE));

	for (n = 0; n < LSG_NUM_TFORTUNE_TFORTUNE_LOGLEVEL_VALUES; n++) {
		const char *v = lls_enum_string_val(n, opt);
		printf("%s%c", v, '\0');
	}
}

static char **complete_dentries(const char *dir)
{
	const char *bn;
	struct regfile_iter *riter;
	unsigned n;
	char **result = NULL;

	regfile_iter_new(dir, &riter);
	for (
		n = 0;
		(bn = regfile_iter_basename(riter));
		regfile_iter_next(riter), n++
	) {
		result = xrealloc(result, (n + 2) * sizeof(*result));
		result[n] = xstrdup(bn);
		result[n + 1] = NULL;
	}
	return result;
}

static char **complete_ede(__attribute__ ((unused)) uint32_t cword,
		__attribute__ ((unused)) unsigned arg0,
		__attribute__ ((unused)) bool have_dd)
{
	char **result, *epidir = get_epidir();

	result = complete_dentries(epidir);
	free(epidir);
	return result;
}

static char **complete_edx(__attribute__ ((unused)) uint32_t cword,
		__attribute__ ((unused)) unsigned arg0,
		__attribute__ ((unused)) bool have_dd)
{
	char **result, *xdir = get_xdir();

	result = complete_dentries(xdir);
	free(xdir);
	return result;
}

static char **complete_std_opts(bool have_dd, const char * const *opts)
{
	if (have_dd)
		print_option_list(opts);
	else
		print_option_list(supercmd_opts);
	return NULL;
}

static char **complete_stats(__attribute__ ((unused)) uint32_t cword,
		__attribute__ ((unused)) unsigned arg0, bool have_dd)
{
	const char * const opts[] = {LSG_TFORTUNE_STATS_OPTS, NULL};
	return complete_std_opts(have_dd, opts);
}

static char **complete_lse(__attribute__ ((unused)) uint32_t cword,
		__attribute__ ((unused)) unsigned arg0, bool have_dd)
{
	const char * const opts[] = {LSG_TFORTUNE_LSE_OPTS, NULL};
	return complete_std_opts(have_dd, opts);
}

static char **complete_lst(__attribute__ ((unused)) uint32_t cword,
		__attribute__ ((unused)) unsigned arg0, bool have_dd)
{
	const char * const opts[] = {LSG_TFORTUNE_LST_OPTS, NULL};
	return complete_std_opts(have_dd, opts);
}

static char **complete_lsx(__attribute__ ((unused)) uint32_t cword,
		__attribute__ ((unused)) unsigned arg0, bool have_dd)
{
	const char * const opts[] = {LSG_TFORTUNE_LSX_OPTS, NULL};
	return complete_std_opts(have_dd, opts);
}

static char **complete_help(__attribute__ ((unused)) uint32_t cword,
		__attribute__ ((unused)) unsigned arg0, bool have_dd)
{
	const char * const opts[] = {LSG_TFORTUNE_HELP_OPTS, NULL};

	if (!have_dd)
		print_option_list(supercmd_opts);
	else
		print_option_list(opts);
	print_zero_terminated_list(subcommand_names);
	return NULL;
}

static char **complete_print(uint32_t cword, unsigned arg0, bool have_dd)
{
	const char * const short_opts[] = {LSG_TFORTUNE_PRINT_SHORT_OPTS, NULL};
	const char * const long_opts[] = {LSG_TFORTUNE_PRINT_LONG_OPTS, NULL};
	const char * const opts[] = {LSG_TFORTUNE_PRINT_OPTS, NULL};
	enum tf_word_type *word_types, prev_type;
	const char *prev;
	char **result, *xdir;

	word_types = xmalloc(cword * sizeof(*word_types));
	get_word_types(cword, arg0, short_opts, long_opts, word_types);
	prev = lls_input(cword - 1, sublpr);
	prev_type = word_types[cword - 1];
	free(word_types);
	switch (prev_type) {
		case WT_COMMAND_NAME:
		case WT_SHORT_OPT_WITHOUT_ARG:
		case WT_OPTION_ARG:
		case WT_LONG_OPT_WITHOUT_ARG:
		case WT_DOUBLE_DASH:
			if (!have_dd)
				print_option_list(supercmd_opts);
			else
				print_option_list(opts);
			return NULL;
		case WT_SHORT_OPT_WITH_ARG:
			if (strcmp(prev, "-x") == 0)
				goto complete_expression;
			break;
		case WT_LONG_OPT_WITH_ARG:
			if (strcmp(prev, "--expression") == 0)
				goto complete_expression;
			break;
		default:
			return NULL;
	}
complete_expression:
	xdir = get_xdir();
	result = complete_dentries(xdir);
	free(xdir);
	return result;
}

typedef char **(*completer)(uint32_t cword, unsigned arg0, bool have_dd);

#define LSG_TFORTUNE_CMD(_name) complete_ ## _name
static const completer completers[] = {LSG_TFORTUNE_COMMANDS};
#undef LSG_TFORTUNE_CMD

static int call_subcmd_completer(unsigned cmd_num, int arg0, uint32_t cword,
		bool have_dd)
{
	char **c, **candidates = completers[cmd_num](cword, arg0, have_dd);

	if (!candidates)
		return 0;
	for (c = candidates; *c; c++) {
		printf("%s%c", *c, '\0');
		free(*c);
	}
	free(candidates);
	return 1;
}

static bool need_subcommand_completer(uint32_t cword, unsigned subcmd_idx,
		const enum tf_word_type *word_types, bool have_dd)
{
	enum tf_word_type prev_type;
	const char *word;

	if (subcmd_idx == 0)
		return false;
	if (have_dd)
		return true;
	prev_type = word_types[cword - 1];
	assert(prev_type != WT_COMMAND_NAME);
	switch (prev_type) {
		case WT_SHORT_OPT_WITH_ARG:
		case WT_LONG_OPT_WITH_ARG:
		case WT_DUNNO:
			return false;
		default:
			break;
	}
	word = lls_input(cword, sublpr);
	if (is_short_opt(word))
		return false;
	if (is_long_opt(word))
		return false;
	return true;
}

static int com_compgen(void)
{
	unsigned n;
	uint32_t cword = OPT_UINT32_VAL(COMPGEN, CURRENT_WORD_INDEX);
	int ret;
	unsigned subcmd_idx;
	const char *word, *prev;
	const char * const short_opts[] = {LSG_TFORTUNE_TFORTUNE_SHORT_OPTS, NULL};
	const char * const long_opts[] = {LSG_TFORTUNE_TFORTUNE_LONG_OPTS, NULL};
	enum tf_word_type *word_types, prev_type;
	bool have_dd;

	if (cword == 0 || cword > lls_num_inputs(sublpr)) {
		ERROR_LOG("current word index == %u!?\n", cword);
		return -ERRNO_TO_TF_ERROR(EINVAL);
	}
	word_types = xmalloc(cword * sizeof(*word_types));
	have_dd = get_word_types(cword, 0, short_opts, long_opts, word_types);
	/*
	 * Locate the subcommand argument, if present. It is always the first
	 * non-option argument.
	 */
	subcmd_idx = 0;
	for (n = 1; n < cword; n++) {
		if (word_types[n] != WT_NON_OPTION_ARG)
			continue;
		subcmd_idx = n;
		break;
	}
	if (need_subcommand_completer(cword, subcmd_idx, word_types, have_dd)) {
		free(word_types);
		word = lls_input(subcmd_idx, sublpr);
		ret = lls_lookup_subcmd(word, tfortune_suite, NULL);
		if (ret < 0) /* invalid subcommand */
			return 0;
		return call_subcmd_completer(ret, subcmd_idx, cword, have_dd);
	}
	/* no subcommand */
	prev_type = word_types[cword - 1];
	prev = lls_input(cword - 1, sublpr);
	free(word_types);
	switch (prev_type) {
		case WT_DUNNO:
			return 0;
		case WT_COMMAND_NAME:
		case WT_SHORT_OPT_WITHOUT_ARG:
		case WT_OPTION_ARG:
		case WT_NON_OPTION_ARG:
		case WT_LONG_OPT_WITHOUT_ARG:
			if (!have_dd)
				print_option_list(supercmd_opts);
			/* fall through */
		case WT_DOUBLE_DASH:
			print_zero_terminated_list(subcommand_names);
			break;
		case WT_SHORT_OPT_WITH_ARG:
			if (strcmp(prev, "-b") == 0) {
				activate_dirname_completion();
				return 1;
			}
			if (strcmp(prev, "-l") == 0) {
				complete_loglevels();
				return 1;
			}
			break;
		case WT_LONG_OPT_WITH_ARG:
			if (strcmp(prev, "--basedir") == 0) {
				activate_dirname_completion();
				return 1;
			}
			if (strcmp(prev, "--loglevel") == 0) {
				complete_loglevels();
				return 1;
			}
			break;
	}
	return 0;
}
EXPORT_CMD_HANDLER(compgen);

static int com_completer(void)
{
	printf("%s\n",
		"_tfortune() \n"
		"{ \n"
			"local -i i offset=${TF_OFFSET:-0} \n"
			"local w compopts= have_empty=false\n"
			"local cur=\"${COMP_WORDS[$COMP_CWORD]}\" \n"

			"i=0 \n"
			"COMPREPLY=() \n"
			"while read -d '' w; do \n"
				"[[ -z \"$w\" ]] && { have_empty=true; continue; }\n"
				"if [[ $have_empty == true ]]; then\n"
					"compopt $w\n"
				"else \n"
					"[[ \"$w\" != \"$cur\"* ]] && continue \n"
					"COMPREPLY[i]=\"$w\" \n"
					"let i++ \n"
				"fi \n"
			"done < <(tfortune -- compgen --current-word-index \\\n"
			"\"$((COMP_CWORD + offset))\" -- $TF_EXTRA \"${COMP_WORDS[@]}\")\n"
		"} \n"
		"complete -F _tfortune tfortune \n"
	);
	if (OPT_GIVEN(COMPLETER, ALIAS)) {
		const char *ali = OPT_STRING_VAL(PRINT, EXPRESSION);
		printf("alias %s=\"tfortune --\"\n", ali);
		printf("_%s() { \n"
				"COMP_WORDS[0]='--'\n"
				"TF_EXTRA='tf' \n"
				"TF_OFFSET=1 \n"
				"_tfortune \"$@\" \n"
				"unset TF_EXTRA TF_OFFSET\n"
			"}\n",
			ali
		);
		printf("complete -F _%s %s \n", ali, ali);
	}
	return 1;
}
EXPORT_CMD_HANDLER(completer);

int main(int argc, char **argv)
{
	char *errctx;
	int ret;
	const struct lls_command *cmd = CMD_PTR(TFORTUNE), *subcmd;
	const struct tf_user_data *ud;
	unsigned num_inputs;

	ret = lls_parse(argc, argv, cmd, &lpr, &errctx);
	if (ret < 0) {
		lopsub_error(ret, &errctx);
		exit(EXIT_FAILURE);
	}
	loglevel_arg_val = OPT_UINT32_VAL(TFORTUNE, LOGLEVEL);
	handle_help_and_version();
	num_inputs = lls_num_inputs(lpr);
	if (num_inputs == 0) {
		show_subcommand_summary(true /* verbose */);
		ret = 0;
		goto free_lpr;
	}
	ret = lls_lookup_subcmd(argv[argc - num_inputs], tfortune_suite, &errctx);
	if (ret < 0) {
		ret = lopsub_error(ret, &errctx);
		goto free_lpr;
	}
	subcmd = lls_cmd(ret, tfortune_suite);
	ret = lls_parse(num_inputs, argv + argc - num_inputs, subcmd,
		&sublpr, &errctx);
	if (ret < 0) {
		ret = lopsub_error(ret, &errctx);
		goto free_lpr;
	}
	ud = lls_user_data(subcmd);
	ret = ud->handler();
	lls_free_parse_result(sublpr, subcmd);
	if (ret < 0)
		ERROR_LOG("%s\n", tf_strerror(-ret));
free_lpr:
	lls_free_parse_result(lpr, cmd);
	exit(ret >= 0? EXIT_SUCCESS : EXIT_FAILURE);
}
