/* $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;
}