/*
 * manfilt.c		-- replace backspace sequences with attribute
 *			   information for vile
 *
 * Author: Kevin A. Buettner
 * Creation: 4/17/94
 *
 * This program filters backspace sequences often found in manual pages
 * for vile/xvile.  Backspace sequences representing italicized or bold
 * text are fixed up by removing the backspaces, underlines, and duplicate
 * characters (leaving just the text as it should appear on the screen).
 * Attributed text is so indicated by inserting a Cntrl-A sequence in front
 * of the text to be attributed.  These Cntrl-A sequences take the following
 * form:
 *   	^A<Count><Attr>:
 *
 * <Count> is a sequence of digits representing the number of characters
 * following the ':' to be attributed.
 *
 * <Attr> is a sequence of characters which indicates how to attribute the
 * characters following the ':'.  The following characters are presently
 * recognized by vile:
 *
 *	'I'	-- italic
 *	'B'	-- bold
 *	'U'	-- underline
 *	'R'	-- reverse video
 *
 * Examples:
 *	Before					After
 *	------					-----
 *	_^Hi_^Ht_^Ha_^Hl_^Hi_^Hc		^A6I:italic
 *
 *	b^Hbo^Hol^Hld^Hd			^A4B:bold
 *
 *	_^HB^HB_^Ho^Ho_^Ht^Ht_^Hh^Hh		^A4IB:Both
 *
 * On many system, bold sequences are actually quite a bit longer.  On
 * some systems, the repeated character is repeated as many as four times.
 * Thus the letter "B" would be represented as B^HB^HB^HB.
 *
 * For comparison, see the description of the BSD 'col' program (for
 * information about the types of escape sequences that might be emitted by
 * nroff).
 *
 * vile will choose some appropriate fallback (such as underlining) if
 * italics are not available.
 *
 * $Header: /usr/build/vile/vile/filters/RCS/manfilt.c,v 1.23 1999/11/09 23:23:31 tom Exp $
 *
 */

#include <filters.h>

#if !HAVE_STDLIB_H
# if !defined(HAVE_CONFIG_H) || MISSING_EXTERN_CALLOC
extern	char *	calloc	( size_t nmemb, size_t size );
# endif
# if !defined(HAVE_CONFIG_H) || MISSING_EXTERN_REALLOC
extern	char *	realloc	( char *ptr, size_t size );
# endif
#endif

#ifdef lint
#define	typecallocn(cast,ntypes)	(((cast *)0)+(ntypes))
#define	typereallocn(cast,ptr,ntypes)	((ptr)+(ntypes))
#else
#define	typecallocn(cast,ntypes)	(cast *)calloc(sizeof(cast),ntypes)
#define	typereallocn(cast,ptr,ntypes)	(cast *)realloc((char *)(ptr),\
							(ntypes)*sizeof(cast))
#endif

#define backspace() \
		if (cur_line != 0 \
		 && cur_line->l_this > 0) \
		    cur_line->l_this -= 1;

#define MAX_LINES 200

	/* SGR codes that we also use as mask values */
#define ATR_NORMAL 0
#define ATR_BOLD   1
#define ATR_UNDER  4

	/* character names */
#define ESCAPE    '\033'
#define CNTL_A    '\001'
#define SHIFT_OUT '\016'
#define SHIFT_IN  '\017'

#define SPACE     ' '
#define UNDERLINE '_'

#define CS_NORMAL    0
#define CS_ALTERNATE 1
/*
 * Each column of a line is represented by a linked list of the characters that
 * are printed to that position.  When storing items in this list, we keep a
 * canonical order to simplify analysis when dumping the line.
 */
typedef	struct	CharCell {
	struct	CharCell *link;
	char	c_ident;	/* CS_NORMAL/CS_ALTERNATE */
	char	c_level;	/* 0=base, 1=up halfline, -1=down halfline */
	char	c_value;	/* the actual value */
	} CHARCELL;

typedef struct	LineData {
	struct	LineData *l_next;
	struct	LineData *l_prev;
	size_t	l_last;		/* the number of cells allocated in l_cells[] */
	size_t	l_used;		/* the number of cells used in l_cells[] */
	size_t	l_this;		/* the current cell within the line */
	CHARCELL *l_cell;
	} LINEDATA;

extern int main ( int argc, char **argv );

static CHARCELL * allocate_cell ( void );
static LINEDATA * allocate_line ( void );
static int ansi_escape ( FILE *ifp );
static int cell_code ( LINEDATA *line, size_t col);
static int half_down ( int level );
static int half_up ( int level );
static void ManFilter ( FILE *ifp );
static void extend_line ( void );
static void failed ( const char *s );
static void flush_line ( void );
static void next_line ( void );
static void prev_line ( void );
static void put_cell ( int c, int level, int ident );

static LINEDATA *all_lines;
static LINEDATA *cur_line;
static long	total_lines;

static void
failed(const char *s)
{
	perror(s);
	exit(BADEXIT);
}

/*
 * Allocate a CHARCELL struct
 */
static CHARCELL *
allocate_cell(void)
{
	CHARCELL *p = typecallocn(CHARCELL,1);
	if (p == 0)
		failed("allocate_cell");
	return p;
}

/*
 * Allocate a LINEDATA struct
 */
static LINEDATA *
allocate_line(void)
{
	LINEDATA *p = typecallocn(LINEDATA,1);
	if (p == 0)
		failed("allocate_line");

	if (all_lines == 0)
		all_lines = p;

	if (total_lines++ > MAX_LINES)
		flush_line();

	return p;
}

/*
 * (Re)allocate the l_cell[] array for the current line
 */
static void
extend_line(void)
{
	size_t have = cur_line->l_last;
	size_t want = cur_line->l_this;
	if (want >= have) {
		CHARCELL *c = cur_line->l_cell;
		want += 80;
		if (c == 0) {
			c = typecallocn(CHARCELL,want);
		} else {
			c = typereallocn(CHARCELL,c,want);
		}
		if (c == 0)
			failed("extend_line");
		while (have < want) {
			c[have].link    = 0;
			c[have].c_value = SPACE;
			c[have].c_level = 0;
			c[have].c_ident = CS_NORMAL;
			have++;
		}
		cur_line->l_last = want;
		cur_line->l_cell = c;
	}
}

/*
 * Store a character at the current position, updating the current position.
 * We expect (but do not require) that an underscore precedes a nonblank
 * character that will overstrike it.  (Some programs produce the underscore
 * second, rather than first).
 */
static void
put_cell(int c, int level, int ident)
{
	int	col;
	int	len;
	CHARCELL *p, *q;

	if (cur_line == 0)
		cur_line = allocate_line();

	len = cur_line->l_used;
	col = cur_line->l_this++;
	extend_line();

	p = &(cur_line->l_cell[col]);

	if (len > col) {	/* some type of overstrike */
		if (c == UNDERLINE) {
			while ((q = p->link) != 0)
				p = q;
			q = allocate_cell();
			p->link = q;
			p = q;
		} else {
			if ((c != SPACE)
			 || (p->c_value == UNDERLINE)) {
				q = allocate_cell();
				*q = *p;
				p->link = q;
			} else if (c == SPACE)
				return;
		}
	}

	p->c_value = c;
	p->c_level = level;
	p->c_ident = ident;

	if (cur_line->l_used < cur_line->l_this)
		cur_line->l_used = cur_line->l_this;
}

/*
 * Interpret equivalent overstrike/underline for an ANSI escape sequence.
 */
static int
ansi_escape(FILE *ifp)
{
	int	code = ATR_NORMAL;
	int	c;

	while ((c = fgetc(ifp)) != EOF) {
		if (isalpha(c))
			break;
		else if (isdigit(c))
			code = (code * 10) + (c - '0');
		else
			code = 0;
	}

	if (c == 'm') {
		if (code != ATR_BOLD && code != ATR_UNDER)
			code = ATR_NORMAL;
	} else {
		code = ATR_NORMAL;
	}
	return code;
}

/*
 * Set the current pointer to the previous line, allocating it if necessary
 */
static void
prev_line(void)
{
	LINEDATA *old_line;

	if (cur_line == 0)
		cur_line = allocate_line();

	if (cur_line->l_prev == 0) {
		cur_line->l_prev = allocate_line();
		if (cur_line == all_lines)
			all_lines = cur_line->l_prev;
	}
	old_line = cur_line;
	cur_line = cur_line->l_prev;
	cur_line->l_next = old_line;
	cur_line->l_this = old_line->l_this;
}

/*
 * Set the current pointer to the next line, allocating it if necessary
 */
static void
next_line(void)
{
	LINEDATA *old_line;

	if (cur_line == 0)
		cur_line = allocate_line();

	if (cur_line->l_next == 0)
		cur_line->l_next = allocate_line();

	old_line = cur_line;
	cur_line = cur_line->l_next;
	cur_line->l_prev = old_line;
	cur_line->l_this = old_line->l_this;
}

/*
 * If we've got a blank line to write onto, fake half-lines that way.
 * Otherwise, eat them.  We assume that half-line controls occur in pairs.
 */
static int
half_up(int level)
{
	prev_line();
	if (cur_line->l_this < cur_line->l_used) {
		next_line();
		return level+1;
	}
	return 0;
}

static int
half_down(int level)
{
	if (level == 0) {
		next_line();
		return 0;
	}
	return level-1;
}

static int
cell_code(LINEDATA *line, size_t col)
{
	CHARCELL *p = &(line->l_cell[col]);
	CHARCELL *q;
	int code = ATR_NORMAL;
	while ((q = p->link) != 0) {
		if (q->c_value == UNDERLINE
		 && q->c_value != p->c_value) {
			code |= ATR_UNDER;
		} else
			code |= ATR_BOLD;
		p = q;
	}
	return code;
}

/*
 * Write the oldest line from memory to standard output and deallocate its
 * storage.
 */
static void
flush_line(void)
{
	size_t	col;
	int	ref_code;
	int	tst_code;
	int	counter;
	LINEDATA *l = all_lines;
	CHARCELL *p;

	if (l != 0) {
		all_lines = l->l_next;
		if (cur_line == l)
			cur_line = all_lines;

		ref_code = ATR_NORMAL;
		counter  = 0;
		for (col = 0; col < l->l_used; col++) {
			if (--counter <= 0) {
				tst_code = cell_code(l,col);
				if (tst_code != ref_code) {
					ref_code = tst_code;
					for (counter = 1; counter+col < l->l_used; counter++) {
						tst_code = cell_code(l, col+counter);
						if (tst_code != ref_code)
							break;
					}
					if (ref_code != ATR_NORMAL) {
						printf("%c%d", CNTL_A, counter);
						if (ref_code & ATR_BOLD)
							putchar('B');
						if (ref_code & ATR_UNDER)
							putchar('I');
						putchar(':');
					}
				}
			}
			putchar(l->l_cell[col].c_value);

			while ((p = l->l_cell[col].link) != 0) {
				l->l_cell[col].link = p->link;
				free((char *)p);
			}
		}
		putchar('\n');

		if (l != 0) {
			if (l->l_cell != 0)
				free((char *)l->l_cell);
			free((char *)l);
		}
	}
}

/*
 * Filter an entire file, writing the result to the standard output.
 */
static void
ManFilter(FILE *ifp)
{
	int	c;
	int	level = 0;
	int	ident = CS_NORMAL;
	int	esc_mode = ATR_NORMAL;

	while ((c = fgetc(ifp)) != EOF) {
		switch (c) {
		case '\b':
			backspace();
			break;

		case '\r':
			if (cur_line != 0)
				cur_line->l_this = 0;
			break;

		case '\n':
			next_line();
			cur_line->l_this = 0;
			break;

		case '\t':
			do {
				put_cell(SPACE, level, ident);
			} while (cur_line->l_this & 7);
			break;

		case '\v':
			prev_line();
			break;

		case SHIFT_IN:
			ident = CS_NORMAL;
			break;

		case SHIFT_OUT:
			ident = CS_ALTERNATE;
			break;

		case ESCAPE:
			switch (fgetc(ifp)) {
			case '[':
				esc_mode = ansi_escape(ifp);
				break;
			case '\007':
			case '7':
				prev_line();
				break;
			case '\010':
			case '8':
				level = half_up(level);
				break;
			case '\011':
			case '9':
				level = half_down(level);
				break;
			default: /* ignore everything else */
				break;
			}
			break;

		default: /* ignore other nonprinting characters */
			if (isprint(c)) {
				put_cell(c, level, ident);
				if (c != SPACE) {
					if (esc_mode & ATR_BOLD) {
						backspace();
						put_cell(c, level, ident);
					}
					if (esc_mode & ATR_UNDER) {
						backspace();
						put_cell('_', level, ident);
					}
				}
			}
			break;
		}
	}

	while (all_lines != 0)
		flush_line();

	total_lines = 0;
}

int
main(int argc, char **argv)
{
	int n;

#if OPT_LOCALE
	setlocale(LC_CTYPE, "");
#endif

	if (argc > 1) {
		for (n = 1; n < argc; n++) {
			FILE *fp = fopen(argv[n], "r");
			if (fp == 0)
				failed(argv[n]);
			ManFilter(fp);
			(void)fclose(fp);
		}
	} else {
		ManFilter(stdin);
	}
	fflush(stdout);
	fclose(stdout);
	exit(GOODEXIT);	/* successful exit */
	/*NOTREACHED*/
}
