/* $NetBSD: infocmp.c,v 1.17 2020/03/31 12:44:15 roy Exp $ */

/*
 * Copyright (c) 2009, 2010, 2020 The NetBSD Foundation, Inc.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Roy Marples.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__RCSID("$NetBSD: infocmp.c,v 1.17 2020/03/31 12:44:15 roy Exp $");

#include <sys/ioctl.h>

#include <ctype.h>
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <term_private.h>
#include <term.h>
#include <unistd.h>
#include <util.h>

#define SW 8

typedef struct tient {
	char type;
	const char *id;
	signed char flag;
	int num;
	const char *str;
} TIENT;

static size_t cols;
static int aflag, cflag, nflag, qflag, xflag;

static size_t
outstr(FILE *f, const char *str)
{
	unsigned char ch;
	size_t r, l;

	r = 0;
	l = strlen(str);
	while ((ch = (unsigned char)(*str++)) != '\0') {
		switch (ch) {
		case 128:
			ch = '0';
			break;
		case '\033':
			ch = 'E';
			break;
		case '\014':
			ch = 'f';
			break;
		case '^': /* FALLTHROUGH */
		case ',': /* escape these */
			break;
		case ' ':
			ch = 's';
			break;
		default:
			if (ch == '\177') {
				if (f != NULL)
					fputc('^', f);
				ch = '?';
				r++;
			} else if (iscntrl(ch) &&
			    ch < 128 &&
			    ch != '\\' &&
			    (l < 4 || isdigit((unsigned char)*str)))
			{
				if (f != NULL)
					fputc('^', f);
				ch += '@';
				r++;
			} else if (!isprint(ch)) {
				if (f != NULL)
					fprintf(f, "\\%03o", ch);
				r += 4;
				continue;
			}
			goto prnt;
		}

		if (f != NULL)
			fputc('\\', f);
		r++;
prnt:
		if (f != NULL)
			fputc(ch, f);
		r++;
	}
	return r;
}

static int
ent_compare(const void *a, const void *b)
{
	const TIENT *ta, *tb;

	ta = (const TIENT *)a;
	tb = (const TIENT *)b;
	return strcmp(ta->id, tb->id);
}

static void
setdb(char *db)
{
	static const char *ext[] = { ".cdb", ".db" };

	for (size_t i = 0; i < __arraycount(ext); i++) {
		char *ptr = strstr(db, ext[i]);
		if (ptr == NULL || ptr[strlen(ext[i])] != '\0')
			continue;
		*ptr = '\0';
		break;
	}
	setenv("TERMINFO", db, 1);
}

static void
print_ent(const TIENT *ents, size_t nents)
{
	size_t col, i, l;
	char nbuf[64];

	if (nents == 0)
		return;

	col = SW;
	printf("\t");
	for (i = 0; i < nents; i++) {
		if (*ents[i].id == '.' && aflag == 0)
			continue;
		switch (ents[i].type) {
		case 'f':
			if (ents[i].flag == ABSENT_BOOLEAN)
				continue;
			l = strlen(ents[i].id) + 2;
			if (ents[i].flag == CANCELLED_BOOLEAN)
				l++;
			break;
		case 'n':
			if (ents[i].num == ABSENT_NUMERIC)
				continue;
			if (VALID_NUMERIC(ents[i].num))
				l = snprintf(nbuf, sizeof(nbuf), "%s#%d,",
				    ents[i].id, ents[i].num);
			else
				l = snprintf(nbuf, sizeof(nbuf), "%s@,",
				    ents[i].id);
			break;
		case 's':
			if (ents[i].str == ABSENT_STRING)
				continue;
			if (VALID_STRING(ents[i].str))
				l = strlen(ents[i].id) +
				    outstr(NULL, ents[i].str) + 7;
			else
				l = strlen(ents[i].id) + 3;
			break;
		default:
			errx(EXIT_FAILURE, "invalid type");
		}
		if (col != SW) {
			if (col + l > cols) {
				printf("\n\t");
				col = SW;
			} else
				col += printf(" ");
		}
		switch (ents[i].type) {
		case 'f':
			col += printf("%s", ents[i].id);
			if (ents[i].flag == ABSENT_BOOLEAN ||
			    ents[i].flag == CANCELLED_BOOLEAN)
				col += printf("@");
			col += printf(",");
			break;
		case 'n':
			col += printf("%s", nbuf);
			break;
		case 's':
			col += printf("%s", ents[i].id);
			if (VALID_STRING(ents[i].str)) {
				col += printf("=");
				col += outstr(stdout, ents[i].str);
			} else
				col += printf("@");
			col += printf(",");
			break;
		}
	}
	printf("\n");
}

static size_t
load_ents(TIENT *ents, TERMINAL *t, char type)
{
	size_t i, n, max;
	TERMUSERDEF *ud;

	switch (type) {
	case 'f':
		max = TIFLAGMAX;
		break;
	case 'n':
		max = TINUMMAX;
		break;
	default:
		max = TISTRMAX;
	}

	n = 0;
	for (i = 0; i <= max; i++) {
		switch (type) {
		case 'f':
			if (t->flags[i] == 1 ||
			    (aflag && t->flags[i] == CANCELLED_BOOLEAN))
			{
				ents[n].id = _ti_flagid(i);
				ents[n].type = 'f';
				ents[n++].flag = t->flags[i];
			}
			break;
		case 'n':
			if (VALID_NUMERIC(t->nums[i]) ||
			    (aflag && t->nums[i] == CANCELLED_NUMERIC))
			{
				ents[n].id = _ti_numid(i);
				ents[n].type = 'n';
				ents[n++].num = t->nums[i];
			}
			break;
		default:
			if (VALID_STRING(t->strs[i]) ||
			    (aflag && t->strs[i] == CANCELLED_STRING))
			{
				ents[n].id = _ti_strid(i);
				ents[n].type = 's';
				ents[n++].str = t->strs[i];
			}
			break;
		}
	}

	if (xflag != 0 && t->_nuserdefs != 0) {
		for (i = 0; i < t->_nuserdefs; i++) {
			ud = &t->_userdefs[i];
			if (ud->type == type) {
				switch (type) {
				case 'f':
					if (!aflag &&
					    !VALID_BOOLEAN(ud->flag))
						continue;
					break;
				case 'n':
					if (!aflag &&
					    !VALID_NUMERIC(ud->num))
						continue;
					break;
				case 's':
					if (!aflag &&
					    !VALID_STRING(ud->str))
						continue;
					break;
				}
				ents[n].id = ud->id;
				ents[n].type = ud->type;
				ents[n].flag = ud->flag;
				ents[n].num = ud->num;
				ents[n++].str = ud->str;
			}
		}
	}

	qsort(ents, n, sizeof(TIENT), ent_compare);
	return n;
}

static void
cprint_ent(TIENT *ent)
{

	if (ent == NULL) {
		if (qflag == 0)
			printf("NULL");
		else
			printf("-");
	}

	switch (ent->type) {
	case 'f':
		if (VALID_BOOLEAN(ent->flag))
			printf(ent->flag == 1 ? "T" : "F");
		else if (qflag == 0)
			printf("F");
		else if (ent->flag == CANCELLED_BOOLEAN)
			printf("@");
		else
			printf("-");
		break;
	case 'n':
		if (VALID_NUMERIC(ent->num))
			printf("%d", ent->num);
		else if (qflag == 0)
			printf("NULL");
		else if (ent->num == CANCELLED_NUMERIC)
			printf("@");
		else
			printf("-");
		break;
	case 's':
		if (VALID_STRING(ent->str)) {
			printf("'");
			outstr(stdout, ent->str);
			printf("'");
		} else if (qflag == 0)
			printf("NULL");
		else if (ent->str == CANCELLED_STRING)
			printf("@");
		else
			printf("-");
		break;
	}
}

static void
compare_ents(TIENT *ents1, size_t n1, TIENT *ents2, size_t n2)
{
	size_t i1, i2;
	TIENT *e1, *e2, ee;
	int c;

	i1 = i2 = 0;
	ee.type = 'f';
	ee.flag = ABSENT_BOOLEAN;
	ee.num = ABSENT_NUMERIC;
	ee.str = ABSENT_STRING;
	while (i1 != n1 || i2 != n2) {
		if (i1 == n1)
			c = 1;
		else if (i2 == n2)
			c = -1;
		else
			c = strcmp(ents1[i1].id, ents2[i2].id);
		if (c == 0) {
			e1 = &ents1[i1++];
			e2 = &ents2[i2++];
		} else if (c < 0) {
			e1 = &ents1[i1++];
			e2 = &ee;
			ee.id = e1->id;
			ee.type = e1->type;
		} else {
			e1 = &ee;
			e2 = &ents2[i2++];
			ee.id = e2->id;
			ee.type = e2->type;
		}
		switch (e1->type) {
		case 'f':
			if (cflag != 0) {
				if (e1->flag == e2->flag)
					printf("\t%s\n", ents1[i1].id);
				continue;
			}
			if (e1->flag == e2->flag)
				continue;
			break;
		case 'n':
			if (cflag != 0) {
				if (e1->num == e2->num)
					printf("\t%s#%d\n",
					    ents1[i1].id, ents1[i1].num);
				continue;
			}
			if (e1->num == e2->num)
				continue;
			break;
		case 's':
			if (cflag != 0) {
				if (VALID_STRING(e1->str) &&
				    VALID_STRING(e2->str) &&
				    strcmp(e1->str, e2->str) == 0) {
					printf("\t%s=", ents1[i1].id);
					outstr(stdout, ents1[i1].str);
					printf("\n");
				}
				continue;
			}
			if (VALID_STRING(e1->str) &&
			    VALID_STRING(e2->str) &&
			    strcmp(e1->str, e2->str) == 0)
				continue;
			break;
		}
		printf("\t%s: ", e1->id);
		cprint_ent(e1);
		if (e1->type == 'f')
			printf(":");
		else
			printf(", ");
		cprint_ent(e2);
		printf(".\n");
	}
}

static TERMINAL *
load_term(const char *name)
{
	TERMINAL *t;

	t = ecalloc(1, sizeof(*t));
	if (name == NULL)
		name = getenv("TERM");
	if (name == NULL)
		name = "dumb";
	if (_ti_getterm(t, name, 1) == 1)
		return t;

	if (_ti_database == NULL)
		errx(EXIT_FAILURE,
		    "no terminal definition found in internal database");
	else
		errx(EXIT_FAILURE,
		    "no terminal definition found in %s.db", _ti_database);
}

static void
show_missing(TERMINAL *t1, TERMINAL *t2, char type)
{
	ssize_t i, max;
	const char *id;

	switch (type) {
	case 'f':
		max = TIFLAGMAX;
		break;
	case 'n':
		max = TINUMMAX;
		break;
	default:
		max = TISTRMAX;
	}

	for (i = 0; i <= max; i++) {
		switch (type) {
		case 'f':
			if (t1->flags[i] != ABSENT_BOOLEAN ||
			    t2->flags[i] != ABSENT_BOOLEAN)
				continue;
			id = _ti_flagid(i);
			break;
		case 'n':
			if (t1->nums[i] != ABSENT_NUMERIC ||
			    t2->nums[i] != ABSENT_NUMERIC)
				continue;
			id = _ti_numid(i);
			break;
		default:
			if (t1->strs[i] != ABSENT_STRING ||
			    t2->strs[i] != ABSENT_STRING)
				continue;
			id = _ti_strid(i);
			break;
		}
		printf("\t!%s.\n", id);
	}
}

static TERMUSERDEF *
find_userdef(TERMINAL *term, const char *id)
{
	size_t i;

	for (i = 0; i < term->_nuserdefs; i++)
		if (strcmp(term->_userdefs[i].id, id) == 0)
			return &term->_userdefs[i];
	return NULL;
}

static void
use_terms(TERMINAL *term, size_t nuse, char **uterms)
{
	TERMINAL **terms;
	TERMUSERDEF *ud, *tud;
	size_t i, j, agree, absent, data;

	terms = ecalloc(nuse, sizeof(*terms));
	for (i = 0; i < nuse; i++) {
		if (strcmp(term->name, *uterms) == 0)
			errx(EXIT_FAILURE, "cannot use same terminal");
		for (j = 0; j < i; j++)
			if (strcmp(terms[j]->name, *uterms) == 0)
				errx(EXIT_FAILURE, "cannot use same terminal");
		terms[i] = load_term(*uterms++);
	}

	for (i = 0; i < TIFLAGMAX + 1; i++) {
		agree = absent = data = 0;
		for (j = 0; j < nuse; j++) {
			if (terms[j]->flags[i] == ABSENT_BOOLEAN ||
			    terms[j]->flags[i] == CANCELLED_BOOLEAN)
				absent++;
			else {
				data++;
				if (term->flags[i] == terms[j]->flags[i])
					agree++;
			}
		}
		if (data == 0)
			continue;
		if (agree > 0 && agree + absent == nuse)
			term->flags[i] = ABSENT_BOOLEAN;
		else if (term->flags[i] == ABSENT_BOOLEAN)
			term->flags[i] = CANCELLED_BOOLEAN;
	}

	for (i = 0; i < TINUMMAX + 1; i++) {
		agree = absent = data = 0;
		for (j = 0; j < nuse; j++) {
			if (terms[j]->nums[i] == ABSENT_NUMERIC ||
			    terms[j]->nums[i] == CANCELLED_NUMERIC)
				absent++;
			else {
				data++;
				if (term->nums[i] == terms[j]->nums[i])
					agree++;
			}
		}
		if (data == 0)
			continue;
		if (agree > 0 && agree + absent == nuse)
			term->nums[i] = ABSENT_NUMERIC;
		else if (term->nums[i] == ABSENT_NUMERIC)
			term->nums[i] = CANCELLED_NUMERIC;
	}

	for (i = 0; i < TISTRMAX + 1; i++) {
		agree = absent = data = 0;
		for (j = 0; j < nuse; j++) {
			if (terms[j]->strs[i] == ABSENT_STRING ||
			    terms[j]->strs[i] == CANCELLED_STRING)
				absent++;
			else {
				data++;
				if (VALID_STRING(term->strs[i]) &&
				    strcmp(term->strs[i],
					terms[j]->strs[i]) == 0)
					agree++;
			}
		}
		if (data == 0)
			continue;
		if (agree > 0 && agree + absent == nuse)
			term->strs[i] = ABSENT_STRING;
		else if (term->strs[i] == ABSENT_STRING)
			term->strs[i] = CANCELLED_STRING;
	}

	/* User defined caps are more tricky.
	   First we set any to absent that agree. */
	for (i = 0; i < term->_nuserdefs; i++) {
		agree = absent = data = 0;
		ud = &term->_userdefs[i];
		for (j = 0; j < nuse; j++) {
			tud = find_userdef(terms[j], ud->id);
			if (tud == NULL)
				absent++;
			else {
				data++;
				switch (ud->type) {
				case 'f':
					if (tud->type == 'f' &&
					    tud->flag == ud->flag)
						agree++;
					break;
				case 'n':
					if (tud->type == 'n' &&
					    tud->num == ud->num)
						agree++;
					break;
				case 's':
					if (tud->type == 's' &&
					    VALID_STRING(tud->str) &&
					    VALID_STRING(ud->str) &&
					    strcmp(ud->str, tud->str) == 0)
						agree++;
					break;
				}
			}
		}
		if (data == 0)
			continue;
		if (agree > 0 && agree + absent == nuse) {
			ud->flag = ABSENT_BOOLEAN;
			ud->num = ABSENT_NUMERIC;
			ud->str = ABSENT_STRING;
		}
	}

	/* Now add any that we don't have as cancelled */
	for (i = 0; i < nuse; i++) {
		for (j = 0; j < terms[i]->_nuserdefs; j++) {
			ud = find_userdef(term, terms[i]->_userdefs[j].id);
			if (ud != NULL)
				continue; /* We have handled this */
			term->_userdefs = erealloc(term->_userdefs,
			    sizeof(*term->_userdefs) * (term->_nuserdefs + 1));
			tud = &term->_userdefs[term->_nuserdefs++];
			tud->id = terms[i]->_userdefs[j].id;
			tud->type = terms[i]->_userdefs[j].flag;
			tud->flag = CANCELLED_BOOLEAN;
			tud->num = CANCELLED_NUMERIC;
			tud->str = CANCELLED_STRING;
		}
	}
}

int
main(int argc, char **argv)
{
	char *term, *Barg;
	int ch, uflag;
	TERMINAL *t, *t2;
	size_t n, n2;
	struct winsize ws;
	TIENT ents[TISTRMAX + 1], ents2[TISTRMAX + 1];

	cols = 80; /* default */
	term = getenv("COLUMNS");
	if (term != NULL)
		cols = strtoul(term, NULL, 10);
	else if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0)
		cols = ws.ws_col;

	uflag = xflag = 0;
	Barg = NULL;
	while ((ch = getopt(argc, argv, "1A:B:acnquw:x")) != -1)
		switch (ch) {
		case '1':
			cols = 1;
			break;
		case 'A':
			setdb(optarg);
			break;
		case 'B':
			Barg = optarg;
			break;
		case 'a':
			aflag = 1;
			break;
		case 'c':
			cflag = 1;
			break;
		case 'n':
			nflag = 1;
			break;
		case 'q':
			qflag = 1;
			break;
		case 'u':
			uflag = 1;
			aflag = 1;
			break;
		case 'w':
			cols = strtoul(optarg, NULL, 10);
			break;
		case 'x':
			xflag = 1;
			break;
		case '?':
		default:
			fprintf(stderr,
			    "usage: %s [-1acnqux] [-A database] [-B database] "
			    "[-w cols] [term]\n",
			    getprogname());
			return EXIT_FAILURE;
		}
	cols--;

	if (optind + 1 < argc)
		aflag = 1;

	if (optind < argc)
		term = argv[optind++];
	else
		term = NULL;
	t = load_term(term);

	if (uflag != 0)
		use_terms(t, argc - optind, argv + optind);

	if ((optind + 1 != argc && nflag == 0) || uflag != 0) {
		if (uflag == 0)
			printf("# Reconstructed from %s\n",
			     _ti_database == NULL ?
			     "internal database" : _ti_database);
		/* Strip internal versioning */
		term = strchr(t->name, TERMINFO_VDELIM);
		if (term != NULL)
			*term = '\0';
		printf("%s", t->name);
		if (t->_alias != NULL) {
			char *alias, *aliascpy, *delim;

			alias = aliascpy = estrdup(t->_alias);
			while (alias != NULL && *alias != '\0') {
				putchar('|');
				delim = strchr(alias, TERMINFO_VDELIM);
				if (delim != NULL)
					*delim++ = '\0';
				printf("%s", alias);
				if (delim != NULL) {
					while (*delim != '\0' && *delim != '|')
						delim++;
					if (*delim == '\0')
						alias = NULL;
					else
						alias = delim + 1;
				} else
					alias = NULL;
			}
			free(aliascpy);
		}
		if (t->desc != NULL && *t->desc != '\0')
			printf("|%s", t->desc);
		printf(",\n");

		n = load_ents(ents, t, 'f');
		print_ent(ents, n);
		n = load_ents(ents, t, 'n');
		print_ent(ents, n);
		n = load_ents(ents, t, 's');
		print_ent(ents, n);

		if (uflag != 0) {
			printf("\t");
			n = SW;
			for (; optind < argc; optind++) {
				n2 = 5 + strlen(argv[optind]);
				if (n != SW) {
					if (n + n2 > cols) {
						printf("\n\t");
						n = SW;
					} else
						n += printf(" ");
				}
				n += printf("use=%s,", argv[optind]);
			}
			printf("\n");
		}
		return EXIT_SUCCESS;
	}

	if (Barg == NULL)
		unsetenv("TERMINFO");
	else
		setdb(Barg);
	t2 = load_term(argv[optind++]);
	printf("comparing %s to %s.\n", t->name, t2->name);
	if (qflag == 0)
		printf("    comparing booleans.\n");
	if (nflag == 0) {
		n = load_ents(ents, t, 'f');
		n2 = load_ents(ents2, t2, 'f');
		compare_ents(ents, n, ents2, n2);
	} else
		show_missing(t, t2, 'f');
	if (qflag == 0)
		printf("    comparing numbers.\n");
	if (nflag == 0) {
		n = load_ents(ents, t, 'n');
		n2 = load_ents(ents2, t2, 'n');
		compare_ents(ents, n, ents2, n2);
	} else
		show_missing(t, t2, 'n');
	if (qflag == 0)
		printf("    comparing strings.\n");
	if (nflag == 0) {
		n = load_ents(ents, t, 's');
		n2 = load_ents(ents2, t2, 's');
		compare_ents(ents, n, ents2, n2);
	} else
		show_missing(t, t2, 's');
	return EXIT_SUCCESS;
}
