/*
 * Configurable ps-like program.
 * Dumb terminal display device which just sends text to stdout.
 *
 * Copyright (c) 2010 David I. Bell
 * Permission is granted to use, distribute, or modify this source,
 * provided that this copyright notice remains intact.
 */

#include <signal.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#include "ips.h"


/*
 * The ANSI escape sequence for clearing the terminal screen.
 */
#define	CLEAR_SCREEN	"\033[H\033[2J"


/*
 * A structure holding a color name and its numeric foreground
 * and background codes in the ANSI escape sequence.
 */
typedef struct
{
	const char *	name;
	short		foreground;
	short		background;
}
COLOR_INFO;


/*
 * The table of color names and their numeric values for the
 * basic ANSI terminal escape sequence.
 */
static const COLOR_INFO	colorInfoTable[] =
{
	{"black",	30,	40},
	{"red",		31,	41},
	{"green",	32,	42},
	{"yellow",	33,	43},
	{"blue",	34,	44},
	{"magenta",	35,	45},
	{"cyan",	36,	46},
	{"white",	37,	47},
	{NULL,		0,	0}
};


/*
 * Maximum color indices for the basic terminal colors and the
 * extended terminal colors.
 */
#define	MAX_BASIC_INDEX		7
#define	MAX_EXTENDED_INDEX	255


static	BOOL	TtyOpen(DISPLAY *);
static	BOOL	TtyDefineColor(DISPLAY *, int, const char *, const char *, int);
static	void	TtyCreateWindow(DISPLAY *);
static	void	TtyClose(DISPLAY *);
static	void	TtySetColor(DISPLAY *, int);
static	void	TtyRefresh(DISPLAY *);
static	void	TtyBeginPage(DISPLAY *);
static	void	TtyPutChar(DISPLAY *, int);
static	void	TtyPutString(DISPLAY *, const char *);
static	void	TtyPutBuffer(DISPLAY *, const char *, int);
static	void	TtyEndPage(DISPLAY *);
static	BOOL	TtyEventWait(DISPLAY *, int);
static	BOOL	TtyInputReady(DISPLAY *);
static	int	TtyReadChar(DISPLAY *);
static	void	TtyRingBell(DISPLAY *);
static	int	TtyGetRows(DISPLAY *);
static	int	TtyGetCols(DISPLAY *);
static	BOOL	TtyDoesScroll(DISPLAY *);


static DISPLAY	ttyDisplay =
{
	TtyOpen, TtyDefineColor, TtyCreateWindow, TtyClose, TtySetColor,
	TtyRefresh, TtyBeginPage, TtyPutChar, TtyPutString, TtyPutBuffer,
	TtyEndPage, TtyEventWait, TtyInputReady, TtyReadChar,
	TtyRingBell, TtyGetRows, TtyGetCols, TtyDoesScroll
};


/*
 * The table of the indexes into the color table for the
 * foreground and background colors (indexed by the color id).
 * The value of -1 means to use the default color.
 */
static	int	foregroundColorTable[MAX_COLORS];
static	int	backgroundColorTable[MAX_COLORS];
static	int	flagsColorTable[MAX_COLORS];


/*
 * The current color in effect.
 */
static	int	currentColorId;
static	int	currentForegroundIndex;
static	int	currentBackgroundIndex;
static	int	currentColorFlags;


/*
 * Terminal size data.
 */
static	BOOL	shown;		/* whether output has been shown */
static	BOOL	sizeChanged;	/* terminal size has changed */
static	int	rows = 99999;	/* number of rows in terminal */
static	int	cols = 80;	/* number of columns in terminal */

static  void    HandleResize(int arg);
static  void    GetTerminalSize(void);
static	int	FindColorNameIndex(const char * name);


/*
 * Return the instance of the terminal display device.
 */
DISPLAY *
GetTtyDisplay(void)
{
	return &ttyDisplay;
}


/*
 * Open the display device.
 */
static BOOL
TtyOpen(DISPLAY * display)
{
	int	colorId;

	/*
	 * If output is to a terminal, then get its current size and
	 * set up to handle resize signals.
	 */
	if (isatty(STDOUT_FILENO))
	{
		signal(SIGWINCH, HandleResize);

		GetTerminalSize();
	}

	/*
	 * Set buffering for block mode for efficiency.
	 */
	setvbuf(stdout, NULL, _IOFBF, BUFSIZ);

	shown = FALSE;

	/*
	 * Initialize the color table.
	 */
	for (colorId = 0; colorId < MAX_COLORS; colorId++)
	{
		foregroundColorTable[colorId] = -1;
		backgroundColorTable[colorId] = -1;
		flagsColorTable[colorId] = 0;
	}

	currentColorId = DEFAULT_COLOR_ID;
	currentForegroundIndex = -1;
	currentBackgroundIndex = -1;
	currentColorFlags = 0;

	return TRUE;
}


/*
 * Create the output window.
 * This is a no-op for us.
 */
static void
TtyCreateWindow(DISPLAY * display)
{
}


/*
 * Close the display device.
 */
static void
TtyClose(DISPLAY * display)
{
	fflush(stdout);
}


/*
 * Define a color for the specified color id.
 */
static BOOL
TtyDefineColor(DISPLAY * display, int colorId,
	const char * foreground, const char * background,
	int colorFlags)
{
	int	foregroundIndex = -1;
	int	backgroundIndex = -1;

	/*
	 * Validate the color id.
	 */
	if ((colorId < 0) || (colorId >= MAX_COLORS))
		return FALSE;

	/*
	 * Validate that the flags are only the ones we know.
	 */
	if (colorFlags & ~(COLOR_FLAG_UNDERLINE|COLOR_FLAG_BOLD))
		return FALSE;

	/*
	 * If the foreground color name is non-empty then parse
	 * it to get the index.
	 */
	if (*foreground)
	{
		foregroundIndex = FindColorNameIndex(foreground);

		if (foregroundIndex < 0)
			return FALSE;				
	}

	/*
	 * If the background color name is non-empty then parse
	 * it to get the index.
	 */
	if (*background)
	{
		backgroundIndex = FindColorNameIndex(background);

		if (backgroundIndex < 0)
			return FALSE;
	}

	/*
	 * If a high color index is used with a bold attribute
	 * then return an error.
	 */
	if (((foregroundIndex > MAX_BASIC_INDEX) || (backgroundIndex > MAX_BASIC_INDEX)) &&
		(colorFlags & COLOR_FLAG_BOLD))
	{
		return FALSE;
	}

	/*
	 * Set the foreground and background color indexes and the
	 * flags for this color id.
	 */
	foregroundColorTable[colorId] = foregroundIndex;
	backgroundColorTable[colorId] = backgroundIndex;
	flagsColorTable[colorId] = colorFlags;


	return TRUE;
}


/*
 * Find the color name in the table of colors and return the
 * index value of the color, or else parse a numeric color index.
 * This is a value from 0 to 255.  Returns -1 if the color name
 * is not known and is not a valid number.
 */
static int
FindColorNameIndex(const char * name)
{
	int	index;

	if (*name == '\0')
		return -1;

	/*
	 * Look for a real name.
	 */
	for (index = 0; colorInfoTable[index].name; index++)
	{
		if (strcmp(name, colorInfoTable[index].name) == 0)
			return index;
	}

	/*
	 * The name wasn't known.
	 * Try parsing the string as a number.
	 */
	index = 0;

	while ((*name >= '0') && (*name <= '9'))
		index = index * 10 + (*name++ - '0');

	/*
	 * If the name wasn't numeric or the index is out of range then fail.
	 */
	if (*name || (index < 0) || (index > MAX_EXTENDED_INDEX))
		return -1;

	return index;
}


/*
 * Set the color for further output.
 */
static void
TtySetColor(DISPLAY * display, int colorId)
{
	int	foregroundIndex;
	int	backgroundIndex;
	int	colorFlags;
	char *	cp;
	char *	endIntroCp;
	char	buffer[64];

	if ((colorId < 0) || (colorId >= MAX_COLORS))
		return;

	if (colorId == currentColorId)
		return;

	currentColorId = colorId;

	foregroundIndex = foregroundColorTable[colorId];
	backgroundIndex = backgroundColorTable[colorId];
	colorFlags = flagsColorTable[colorId];

	if ((foregroundIndex == currentForegroundIndex) &&
		(backgroundIndex == currentBackgroundIndex) &&
		(colorFlags == currentColorFlags))
	{
		return;
	}

	currentForegroundIndex = foregroundIndex;
	currentBackgroundIndex = backgroundIndex;
	currentColorFlags = colorFlags;

	/*
	 * Store the beginning of the escape sequence.
	 */
	cp = buffer;
	*cp++ = '\033';
	*cp++ = '[';
	endIntroCp = cp;

	/*
	 * Reset the color if the defaults are used.
	 */
	if ((foregroundIndex < 0) || (backgroundIndex < 0))
		*cp++ = '0';

	/*
	 * Set the attributes.
	 */
	if (currentColorFlags & COLOR_FLAG_UNDERLINE)
	{
		if (cp != endIntroCp)
			*cp++ = ';';

		*cp++ = '4';
	}

	if (currentColorFlags & COLOR_FLAG_BOLD)
	{
		if (cp != endIntroCp)
			*cp++ = ';';

		*cp++ = '1';
	}

	/*
	 * Set the foreground color if needed.
	 */
	if (foregroundIndex >= 0)
	{
		if (cp != endIntroCp)
			*cp++ = ';';

		/*
		 * The string depends on whether the index is in the basic
		 * or the extended range.
		 */
		if (foregroundIndex > MAX_BASIC_INDEX)
			sprintf(cp, "38;5;%d", foregroundIndex);
		else
			sprintf(cp, "%d", colorInfoTable[foregroundIndex].foreground);

		cp += strlen(cp);
	}

	/*
	 * Set the background color if needed.
	 */
	if (backgroundIndex >= 0)
	{
		if (cp != endIntroCp)
			*cp++ = ';';

		/*
		 * The string depends on whether the index is in the basic
		 * or the extended range.
		 */
		if (backgroundIndex > MAX_BASIC_INDEX)
			sprintf(cp, "48;5;%d", backgroundIndex);
		else
			sprintf(cp, "%d", colorInfoTable[backgroundIndex].background);

		cp += strlen(cp);
	}

	/*
	 * Terminate the escape sequence and send it.
	 */
	*cp++ = 'm';
	*cp = '\0';

	fputs(buffer, stdout);
}


static void
TtyRefresh(DISPLAY * display)
{
}


static void
TtyBeginPage(DISPLAY * display)
{
	if (clearScreen)
		fputs(CLEAR_SCREEN, stdout);
	else if (shown)
		putchar('\n');
}


static void
TtyPutChar(DISPLAY * display, int ch)
{
	/*
	 * Before writing any newline clear any current coloring.
	 * This is necessary to prevent display bugs on the next line.
	 */
	if (ch == '\n')
		TtySetColor(display, DEFAULT_COLOR_ID);

	putchar(ch);

	shown = TRUE;
}


static void
TtyPutString(DISPLAY * display, const char * str)
{
	fputs(str, stdout);
}


static void
TtyPutBuffer(DISPLAY * display, const char * str, int len)
{
	while (len-- > 0)
	{
		putchar(*str);
		str++;
	}
}


static void
TtyEndPage(DISPLAY * display)
{
	fflush(stdout);
}


/*
 * Handle events for the display while waiting for the specified amount
 * of time.  There are no input events to handle except for a window
 * size change, which will cause a signal to terminate the select.
 */
static BOOL
TtyEventWait(DISPLAY * display, int milliSeconds)
{
	struct timeval	timeOut;

	if (milliSeconds <= 0)
		return FALSE;

	timeOut.tv_sec = milliSeconds / 1000;
	timeOut.tv_usec = (milliSeconds % 1000) * 1000;

	(void) select(0, NULL, NULL, NULL, &timeOut);

	return FALSE;
}


/*
 * See if input is ready from the terminal.
 * This display device never returns any input.
 */
static BOOL
TtyInputReady(DISPLAY * display)
{
	return FALSE;
}


/*
 * Read the next character from the terminal.
 * This display device always returns EOF.
 */
static int
TtyReadChar(DISPLAY * display)
{
	return EOF;
}


static void
TtyRingBell(DISPLAY * display)
{
	fflush(stdout);
	fputc('\007', stderr);
	fflush(stderr);
}


static int
TtyGetRows(DISPLAY * display)
{
	if (sizeChanged)
		GetTerminalSize();

	return rows;
}


static int
TtyGetCols(DISPLAY * display)
{
	if (sizeChanged)
		GetTerminalSize();

	return cols;
}


static BOOL
TtyDoesScroll(DISPLAY * display)
{
	return TRUE;
}


/*
 * Signal handler for resizing of window.
 * This only sets a flag so that we can handle the resize later.
 * (Changing the size at unpredictable times would be dangerous.)
 */
static void
HandleResize(int arg)
{
	sizeChanged = TRUE;

	signal(SIGWINCH, HandleResize);
}


/*
 * Routine called to get the new terminal size from the kernel.
 */
static void
GetTerminalSize(void)
{
	struct	winsize	size;

	sizeChanged = FALSE;

	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0)
		return;

	rows = size.ws_row;
	cols = size.ws_col;

	if (rows <= 0)
		rows = 1;

	if (cols <= 0)
		cols = 1;
}

/* END CODE */
