/*
 * ascii.c -- quick crossreference for ASCII character aliases
 *
 * Tries to interpret arguments as names or aliases of ascii characters
 * and dumps out *all* the aliases of each. Accepts literal characters,
 * standard mnemonics, C-style backslash escapes, caret notation for control
 * characters, numbers in hex/decimal/octal/binary, English names.
 *
 * The slang names used are selected from the 2.2 version of the USENET ascii
 * pronunciation guide.  Some additional ones were merged in from the Jargon
 * File.
 *
 * SPDX-FileCopyrightText: (C) Eric S. Raymond <esr@thyrsus.com>
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * For license terms, see the file COPYING.
 */

#include <ctype.h>
#include <getopt.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MODE_HELP (1 << 0)
#define MODE_VERSION (1 << 1)
#define MODE_BTABLE (1 << 2)
#define MODE_OTABLE (1 << 3)
#define MODE_DTABLE (1 << 4)
#define MODE_XTABLE (1 << 5)
#define MODE_CHARS (1 << 6)

#define MASK_MARKED (1u << 0)

#define MASK_CNTRL (1u << 1)
#define MASK_SPACE (1u << 2)
#define MASK_PUNCT (1u << 3)
#define MASK_UPPER (1u << 4)
#define MASK_LOWER (1u << 5)
#define MASK_DIGIT (1u << 6)
#define MASK_XALPHA (1u << 7)
#define MASK_WORD (1u << 8)

#define MASK_ALPHA (MASK_UPPER | MASK_LOWER)
#define MASK_ALNUM (MASK_UPPER | MASK_LOWER | MASK_DIGIT)
#define MASK_PRINT (MASK_UPPER | MASK_LOWER | MASK_DIGIT | MASK_PUNCT)
#define MASK_GRAPH (MASK_PRINT & ~MASK_SPACE)
#define MASK_XDIGIT (MASK_DIGIT | MASK_XALPHA)

struct char_class {
	const char *name;
	unsigned int mask;
};

static char *progname;
static bool vertical = false;
static bool terse = false;
static bool line = false;
static unsigned int forced_radix = 0;

static const char *cnames[128][16] = {{
#include "nametable.h"
}};

static const char splashscreen[] =
#include "splashscreen.h"
    ;

/* The character class names are from POSIX. */
const struct char_class char_classes[] = {
    {":alnum:", MASK_ALNUM}, {":alpha:", MASK_ALPHA}, {":cntrl:", MASK_CNTRL},
    {":digit:", MASK_DIGIT}, {":lower:", MASK_LOWER}, {":print:", MASK_PRINT},
    {":punct:", MASK_PUNCT}, {":upper:", MASK_UPPER}, {":xdigit:", MASK_XDIGIT},
    {":graph:", MASK_GRAPH}, {":word:", MASK_WORD},   {NULL, 0},
};

static unsigned int char_mask_table[256];

/* Case-blind string compare */
static int stricmp(const char *s, const char *t) {
	while (*s != '\0' &&
	       (tolower((unsigned char)*s) == tolower((unsigned char)*t))) {
		s++, t++;
	}
	return (int)(*t - *s);
}

static unsigned int char_class_mask(const char *name) {
	const struct char_class *cc;

	for (cc = char_classes; cc->name; cc++) {
		if (!strcmp(cc->name, name)) {
			return cc->mask;
		}
	}
	return 0;
}

static void init_char_mask_table(void) {
	unsigned int ch;

	ch = 0;
	while (ch < 0x20) {
		char_mask_table[ch++] = MASK_CNTRL;
	}
	char_mask_table[ch++] = MASK_SPACE;
	while (ch < 0x30) {
		char_mask_table[ch++] = MASK_PUNCT;
	}
	while (ch < 0x3a) {
		char_mask_table[ch++] = MASK_DIGIT | MASK_WORD;
	}
	while (ch < 0x41) {
		char_mask_table[ch++] = MASK_PUNCT;
	}
	while (ch < 0x47) {
		char_mask_table[ch++] = MASK_UPPER | MASK_XALPHA | MASK_WORD;
	}
	while (ch < 0x5b) {
		char_mask_table[ch++] = MASK_UPPER | MASK_WORD;
	}
	while (ch < 0x61) {
		char_mask_table[ch++] = MASK_PUNCT;
	}
	while (ch < 0x67) {
		char_mask_table[ch++] = MASK_LOWER | MASK_XALPHA | MASK_WORD;
	}
	while (ch < 0x7b) {
		char_mask_table[ch++] = MASK_LOWER | MASK_WORD;
	}
	while (ch < 0x7f) {
		char_mask_table[ch++] = MASK_PUNCT;
	}
	char_mask_table[ch] = MASK_CNTRL; /* Delete */
	char_mask_table[0x5F] = MASK_WORD;
}

static void mark_mask(unsigned int mask) {
	unsigned int ch;

	for (ch = 0; ch < 128; ch++) {
		if (char_mask_table[ch] & mask) {
			char_mask_table[ch] |= MASK_MARKED;
		}
	}
}

static void mark(unsigned int ch) {
	if (ch != UINT_MAX && ch < 256) {
		char_mask_table[ch] |= MASK_MARKED;
	}
}

/* Binary-to-ASCII conversion */
static const char *btoa(unsigned int val) {
	static char rep[7 + 1];
	char *p = rep + sizeof(rep);

	*--p = '\0';
	do {
		*--p = (val & 1) ? '1' : '0';
		val >>= 1;
	} while (p > rep);
	return p;
}

static unsigned int parse_partial_digits(const char **sp, unsigned int radix) {
	const char digits[] = "0123456789abcdef";
	const char *digitp;
	const char *s;
	unsigned int digit, val;

	s = *sp;
	if (*s == '\0') {
		return UINT_MAX;
	}
	val = 0;
	for (; *s != '\0'; s++) {
		if ((digitp = strchr(digits, tolower((unsigned char)*s))) ==
		    NULL) {
			break;
		}
		if ((digit = digitp - digits) >= radix) {
			break;
		}
		val = (val * radix) + digit;
		if (val >= 256) {
			val = UINT_MAX;
			break;
		}
	}
	*sp = s;
	return val;
}

static unsigned int parse_digits(const char *s, unsigned int radix) {
	unsigned int val;

	val = parse_partial_digits(&s, radix);
	return (*s == '\0') ? val : UINT_MAX;
}

static unsigned int parse_prefixed(const char *s) {
	switch (s[0]) {
	case 'b':
		return parse_digits(s + 1, 2);
	case 'o':
		return parse_digits(s + 1, 8);
	case 'd':
		return parse_digits(s + 1, 10);
	case 'x':
		return parse_digits(s + 1, 16);
	default:
		return UINT_MAX;
	}
}

static unsigned int parse_hi_lo(const char *s) {
	unsigned int hi, lo;

	if ((hi = parse_partial_digits(&s, 10)) == UINT_MAX) {
		return UINT_MAX;
	}
	if ((hi >= 16) || (*s != '/')) {
		return UINT_MAX;
	}
	if ((lo = parse_digits(s + 1, 10)) == UINT_MAX) {
		return UINT_MAX;
	}
	if (lo >= 16) {
		return UINT_MAX;
	}
	return 16 * hi + lo;
}

static void parse_and_mark_number(const char *s) {
	if (s[0] == '\\') {
		mark(parse_prefixed(s + 1));
		mark(parse_digits(s + 1, 8));
	} else {
		if (s[0] == '0') {
			mark(parse_prefixed(s + 1));
		} else {
			mark(parse_prefixed(s));
		}
		mark(parse_digits(s, 2));
		mark(parse_digits(s, 8));
		mark(parse_digits(s, 10));
		mark(parse_digits(s, 16));
	}
}

static void parse_and_mark_radix(const char *s, unsigned int radix) {
	unsigned int val;

	if (s[0] == '\\') {
		s++;
	}

	if ((radix == 16) && (s[0] == '0') && (s[1] == 'x' || s[1] == 'X')) {
		s += 2;
	} else if ((radix == 8) && (s[0] == '0') && (s[1] == 'o' || s[1] == 'O')) {
		s += 2;
	} else if ((radix == 10) && (s[0] == '0') && (s[1] == 'd' || s[1] == 'D')) {
		s += 2;
	} else if ((radix == 2) && (s[0] == '0') && (s[1] == 'b' || s[1] == 'B')) {
		s += 2;
	} else {
		switch (radix) {
		case 16:
			if (s[0] == 'x' || s[0] == 'X' || s[0] == 'h' ||
			    s[0] == 'H') {
				s++;
			}
			break;
		case 8:
			if (s[0] == 'o' || s[0] == 'O') {
				s++;
			}
			break;
		case 10:
			if (s[0] == 'd' || s[0] == 'D') {
				s++;
			}
			break;
		case 2:
			if (s[0] == 'b' || s[0] == 'B') {
				s++;
			}
			break;
		}
	}

	val = parse_digits(s, radix);
	mark(val);
}

static void parse_and_mark(const char *str) {
	int ch;
	unsigned int mask;
	const char **ptr;
	size_t len, i;

	len = strlen(str);

	/* interpret single characters as themselves */
	if (len == 1) {
		mark((unsigned char)str[0]);
	}

	/* process multiple letters */
	if (line == 1) {
		for (i = 0; i < len; i++) {
			mark((unsigned char)str[i]);
		}
		return;
	}

	if ((mask = char_class_mask(str))) {
		mark_mask(mask);
		return;
	}

	/* interpret ^-escapes as control-character notation */
	if ((str[0] == '^') && (len == 2)) {
		if ((str[1] >= '@') && (str[1] <= '_')) {
			mark(str[1] - '@');
		} else if (str[1] == '?') {
			mark(0x7f);
		}
		return;
	}

	/* interpret C-style backslash escapes */
	if (*str == '\\' && len == 2 && strchr("abfnrtv0", str[1])) {
		for (ch = 7; ch < 14; ch++) {
			for (ptr = &cnames[ch][1]; *ptr; ptr++) {
				if (**ptr == '\\' && strcmp(str, *ptr) == 0) {
					mark(ch);
					return;
				}
			}
		}
	}

	/* interpret 2 and 3-character ASCII control mnemonics */
	if (len == 2 || len == 3) {
		/* first check for standard mnemonics */
		if (stricmp(str, "DEL") == 0) {
			mark(0x7f);
			return;
		}
		if (stricmp(str, "BL") == 0) {
			mark(' ');
			return;
		} else if (isalpha((unsigned char)str[0])) {
			for (ch = 0; ch <= 32; ch++) {
				if (!stricmp(str, cnames[ch][0]) ||
				    !strcmp(str, cnames[ch][1])) {
					mark(ch);
					return;
				}
			}
		}
	}

	/* OK, now try to interpret the string as a numeric */
	parse_and_mark_number(str);
	mark(parse_hi_lo(str));

	if ((len > 1) && isalpha((unsigned char)str[0])) {
		/* try to match long names */
		char canbuf[BUFSIZ], *ep;
		size_t canbuf_len;
		size_t copy_len;

		/* map dashes and other junk to spaces */
		copy_len = len;
		if (copy_len >= sizeof(canbuf)) {
			copy_len = sizeof(canbuf) - 1;
		}
		for (i = 0; i < copy_len; i++) {
			if (str[i] == '-' ||
			    isspace((unsigned char)str[i])) {
				canbuf[i] = ' ';
			} else {
				canbuf[i] = str[i];
			}
		}
		canbuf[copy_len] = '\0';

		/* strip `sign' or `Sign' suffix */
		canbuf_len = strlen(canbuf);
		if (canbuf_len >= 4) {
			ep = canbuf + canbuf_len - 4;
			if (!strcmp(ep, "sign") || !strcmp(ep, "Sign")) {
				*ep = '\0';
				canbuf_len = strlen(canbuf);
			}
		}

		/* remove any trailing whitespace */
		while (canbuf_len > 0 && canbuf[canbuf_len - 1] == ' ') {
			canbuf[--canbuf_len] = '\0';
		}

		/* look through all long names for a match */
		for (ch = 0; ch < 128; ch++) {
			for (ptr = &cnames[ch][1]; *ptr; ptr++) {
				if (!stricmp(*ptr, canbuf)) {
					mark(ch);
				}
			}
		}
	}
}

/* List all the names for a given character */
static void print_character(unsigned int ch) {
	const char **ptr = &cnames[ch][0];

	if (terse) {
		printf("%u/%u   %u   0x%02X   0o%o   %s\n", ch / 16, ch % 16,
		       ch, ch, ch, btoa(ch));
		return;
	}

	printf("ASCII %u/%u is decimal %03u, hex %02x, octal %03o, bits %s: ",
	       ch / 16, ch % 16, ch, ch, ch, btoa(ch));

	/* display high-half characters */
	if (ch & 0x80) {
		ch &= ~0x80;
		if (ch == 0x7f) {
			printf("meta-^?\n");
		} else if (isprint((unsigned char)ch)) {
			printf("meta-%c\n", (char)ch);
		} else {
			printf("meta-^%c\n", '@' + (ch & 0x1f));
		}
		return;
	}

	if (isprint((unsigned char)ch)) {
		printf("prints as `%s'\n", *ptr++);
	} else if (iscntrl((unsigned char)ch) || ch == 0x7f) {
		if (ch == 0x7f) {
			printf("called ^?");
		} else {
			printf("called ^%c", '@' + (ch & 0x1f));
		}
		for (; strlen(*ptr) < 4 &&
		       isupper((unsigned char)**ptr);
		     ptr++) {
			printf(", %s", *ptr);
		}
		putchar('\n');
	}

	printf("Official name: %s\n", *ptr++);

	if (*ptr) {
		const char *commentary = (char *)NULL;

		if (**ptr == '\\') {
			printf("C escape: '%s'\n", *ptr);
			ptr++;
		}

		printf("Other names: ");
		for (; *ptr; ptr++) {
			if (**ptr == '#') {
				commentary = *ptr;
			} else {
				printf("%s%s ", *ptr,
				       (ptr[1] != NULL && *ptr[1] != '#') ? ","
				                                          : "");
			}
		}
		putchar('\n');
		if (commentary) {
			printf("Note: %s\n", commentary + 2);
		}
	}

	putchar('\n');
}

static void print_table(unsigned int radix) {
	unsigned int i, j, len, rows, cols;
	const char separator[] = "   ";
	const char *tail = separator + 3;
	const char *space;
	const char *name;

	if (vertical) {
		cols = 4;
	} else {
		cols = 8;
	}
	rows = 128 / cols;
	for (i = 0; i < rows; i++) {
		for (j = 0; j < cols; j++) {
			name = *cnames[i + (j * rows)];
			len = strlen(name);
			space = tail - (len % 3);
			switch (radix) {
			case 10:
				printf("%5d %1s%1s", i + (j * rows), name,
				       space);
				break;
			case 8:
				printf("  %03o %1s%1s", i + (j * rows), name,
				       space);
				break;
			case 16:
				printf("   %02X %1s%1s", i + (j * rows), name,
				       space);
				break;
			case 2:
				printf("   %s %1s%1s", btoa(i + (j * rows)),
				       name, space);
				break;
			}
		}
		printf("\n");
	}
}

static void generic_usage(FILE *out, int status) {
	fprintf(out, "Usage: %s [-adxohv] [-t] [char-alias...]\n", progname);
	fputs(splashscreen, out);
	exit(status);
}

static void usage(void) { generic_usage(stderr, 2); }

static unsigned int parse_command_line(int argc, char **argv) {
	unsigned int mode;
	int op;

	mode = 0;
	progname = argv[0];
	while ((op = getopt(argc, argv, "abdhostvx")) != -1) {
		switch (op) {
		case 'a':
			vertical = true;
			break;
		case 'b':
			mode |= MODE_BTABLE;
			break;
		case 'd':
			mode |= MODE_DTABLE;
			break;
		case 'h':
			mode |= MODE_HELP;
			break;
		case 'o':
			mode |= MODE_OTABLE;
			break;
		case 's':
			terse = line = true;
			break;
		case 't':
			terse = true;
			break;
		case 'v':
			mode |= MODE_VERSION;
			break;
		case 'x':
			mode |= MODE_XTABLE;
			break;
		case '?':
			usage();
			break;
		default:
			usage();
			break;
		}
	}
	if (mode & MODE_HELP) {
		return MODE_HELP;
	}
	if (mode & MODE_VERSION) {
		return MODE_VERSION;
	}
	switch (mode) {
	case 0:
		break;
	case MODE_BTABLE:
		forced_radix = 2;
		break;
	case MODE_OTABLE:
		forced_radix = 8;
		break;
	case MODE_DTABLE:
		forced_radix = 10;
		break;
	case MODE_XTABLE:
		forced_radix = 16;
		break;
	default:
		usage();
	}
	if (forced_radix != 0 && optind < argc) {
		if (vertical) {
			fprintf(stderr, "ascii: -a option only applies to table output.\n");
			exit(1);
		}
		while (optind < argc) {
			parse_and_mark_radix(argv[optind++], forced_radix);
		}
		return MODE_CHARS;
	}
	if (mode && (terse || line)) {
		usage();
	}
	if (mode != 0 && optind < argc) {
		fprintf(stderr, "ascii: table or help request doesn't mix with arguments.\n");
		exit(1);
	}
	if (vertical && mode == 0) {
		fprintf(stderr, "ascii: -a option must be followed by one of -b -d -o -x.\n");
		exit(1);
	}
	if (forced_radix != 0) {
		return mode;
	}
	if (argc == optind) {
		return MODE_HELP;
	}
	while (optind < argc) {
		if (forced_radix != 0) {
			parse_and_mark_radix(argv[optind++], forced_radix);
		} else {
			parse_and_mark(argv[optind++]);
		}
	}
	return MODE_CHARS;
}

int main(int argc, char **argv) {
	unsigned int ch;

	init_char_mask_table();
	switch (parse_command_line(argc, argv)) {
	case MODE_CHARS:
		for (ch = 0; ch < 128; ch++) {
			if (char_mask_table[ch] & MASK_MARKED) {
				print_character(ch);
			}
		}
		break;
	case MODE_BTABLE:
		print_table(2);
		break;
	case MODE_OTABLE:
		print_table(8);
		break;
	case MODE_DTABLE:
		print_table(10);
		break;
	case MODE_XTABLE:
		print_table(16);
		break;
	case MODE_VERSION:
		printf("ascii %s\n", REVISION);
		break;
	case MODE_HELP:
		generic_usage(stdout, 0);
		break;
	default:
		usage();
	}
	return 0;
}
