/*
 * Configurable ps-like program.
 * Display device which uses curses for fancy output to the terminal.
 *
 * 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 <curses.h>

#undef	FALSE
#undef	TRUE

#include "ips.h"


/*
 * A structure holding a color name and its index.
 */
typedef struct
{
	const char *	name;
	short		index;
}
COLOR_INFO;


/*
 * The table of color names and their indexes.
 */
static const COLOR_INFO	colorInfoTable[] =
{
	{"black",	COLOR_BLACK},
	{"red",		COLOR_RED},
	{"green",	COLOR_GREEN},
	{"yellow",	COLOR_YELLOW},
	{"blue",	COLOR_BLUE},
	{"magenta",	COLOR_MAGENTA},
	{"cyan",	COLOR_CYAN},
	{"white",	COLOR_WHITE},
	{NULL,		0}
};



static	BOOL	CursesOpen(DISPLAY *);
static	BOOL	CursesDefineColor(DISPLAY *, int, const char *, const char *, int);
static	void	CursesCreateWindow(DISPLAY *);
static	void	CursesClose(DISPLAY *);
static	void	CursesSetColor(DISPLAY *, int);
static	void	CursesRefresh(DISPLAY *);
static	void	CursesBeginPage(DISPLAY *);
static	void	CursesPutChar(DISPLAY *, int);
static	void	CursesPutString(DISPLAY *, const char *);
static	void	CursesPutBuffer(DISPLAY *, const char *, int);
static	void	CursesEndPage(DISPLAY *);
static	BOOL	CursesEventWait(DISPLAY *, int);
static	BOOL	CursesInputReady(DISPLAY *);
static	int	CursesReadChar(DISPLAY *);
static	void	CursesRingBell(DISPLAY *);
static	int	CursesGetRows(DISPLAY *);
static	int	CursesGetCols(DISPLAY *);
static	BOOL	CursesDoesScroll(DISPLAY *);


static DISPLAY	cursesDisplay =
{
	CursesOpen, CursesDefineColor, CursesCreateWindow, CursesClose,
	CursesSetColor, CursesRefresh, CursesBeginPage, CursesPutChar,
	CursesPutString, CursesPutBuffer, CursesEndPage, CursesEventWait,
	CursesInputReady, CursesReadChar, CursesRingBell,
	CursesGetRows, CursesGetCols, CursesDoesScroll
};


/*
 * The main window.
 */
static	WINDOW *	mainWindow;


/*
 * Terminal size data.
 */
static	BOOL	sizeChanged;	/* terminal size has changed */


/*
 * The attributes for the colors.
 */
static	int	attributeTable[MAX_COLORS];


/*
 * Static routines.
 */
static  void    HandleResize(int arg);
static  void    GetTerminalSize(void);
static	int	FindColorNameIndex(const char * name);


/*
 * Return the instance of the curses display device.
 */
DISPLAY *
GetCursesDisplay(void)
{
	return &cursesDisplay;
}


/*
 * Open the display device.
 */
static BOOL
CursesOpen(DISPLAY * display)
{
	SCREEN *	screen;

	screen = newterm(NULL, stdout, stdin);

	if (screen == NULL)
		return FALSE;

	set_term(screen);

	cbreak();
	noecho();

	if (has_colors())
	{
		start_color();
		use_default_colors();
	}

	mainWindow = newwin(0, 0, 0, 0);

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

	return TRUE;
}


/*
 * Create the window.
 * We don't do anything here.
 */
static void
CursesCreateWindow(DISPLAY * display)
{
}


/*
 * Close the display device.
 */
static void
CursesClose(DISPLAY * display)
{
	refresh();
	endwin();
}


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

	if ((colorId < 0) || (colorId >= MAX_COLORS) ||
		!has_colors() || (colorId >= COLOR_PAIRS))
	{
		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;
	}

	/*
	 * Tell curses about the color.
	 * This differs depending on whether color index 0 is used.
	 */
	if (colorId == 0)
		status = assume_default_colors(foregroundIndex, backgroundIndex);
	else
		status = init_pair(colorId, foregroundIndex, backgroundIndex);

	if (status == ERR)
		return FALSE;

	/*
	 * Calculate and save the attributes for the color id.
	 */
	if (colorFlags & COLOR_FLAG_UNDERLINE)
		attribute |= A_UNDERLINE;

	if (colorFlags & COLOR_FLAG_BOLD)
		attribute |= A_BOLD;

	attribute |= COLOR_PAIR(colorId);

	attributeTable[colorId] = attribute;

	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)
		{
			index = colorInfoTable[index].index;

			if (index >= COLORS)
				return -1;

			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 >= COLORS))
		return -1;

	return index;
}


static void
CursesSetColor(DISPLAY * display, int colorId)
{
	if ((colorId < 0) || (colorId >= MAX_COLORS) ||
		(colorId >= COLOR_PAIRS))
	{
		return;
	}

	wattrset(mainWindow, attributeTable[colorId]);
}


static void
CursesRefresh(DISPLAY * display)
{
	wrefresh(curscr);
}


static void
CursesBeginPage(DISPLAY * display)
{
	wmove(mainWindow, 0, 0);
}


static void
CursesPutChar(DISPLAY * display, int ch)
{
	waddch(mainWindow, ch);
}


static void
CursesPutString(DISPLAY * display, const char * str)
{
	waddstr(mainWindow, str);
}


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


static void
CursesEndPage(DISPLAY * display)
{
	wclrtobot(mainWindow);
	wmove(mainWindow, 0, 0);
	wrefresh(mainWindow);
}


/*
 * Handle events for the display while waiting for the specified amount
 * of time.  Returns early if there are input characters to be read.
 * Returns TRUE if the window was resized and so needs to be updated soon.
 */
static BOOL
CursesEventWait(DISPLAY * display, int milliSeconds)
{
	struct	timeval	timeOut;
	fd_set		readFds;

	if (milliSeconds <= 0)
		return sizeChanged;

	FD_ZERO(&readFds);
	FD_SET(STDIN_FILENO, &readFds);

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

	(void) select(STDIN_FILENO + 1, &readFds, NULL, NULL, &timeOut);

	return sizeChanged;
}


/*
 * See if input is ready from the terminal.
 */
static BOOL
CursesInputReady(DISPLAY * display)
{
	struct	timeval	timeOut;
	fd_set		readFds;

	FD_ZERO(&readFds);
	FD_SET(STDIN_FILENO, &readFds);

	timeOut.tv_sec = 0;
	timeOut.tv_usec = 0;

	return (select(STDIN_FILENO + 1, &readFds, NULL, NULL, &timeOut) > 0);
}


/*
 * Read the next character from the terminal.
 */
static int
CursesReadChar(DISPLAY * display)
{
	char	data;

	if (read(STDIN_FILENO, &data, 1) < 1)
		return EOF;

	return data & 0xff;
}


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


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

	return LINES;
}


/*
 * Return the number of columns for display.
 * Note: We reduce curses's value by one since it will always
 * auto-line-wrap if the last column is written into, and handling
 * that misfeature for correct output is otherwise painful.
 */
static int
CursesGetCols(DISPLAY * display)
{
	if (sizeChanged)
		GetTerminalSize();

	return COLS - 1;
}


static BOOL
CursesDoesScroll(DISPLAY * display)
{
	return FALSE;
}


/*
 * 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.
 * We inform curses of this change and let it resize its window.
 */
static void
GetTerminalSize(void)
{
	struct	winsize	size;
	int		rows;
	int		cols;

	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;

	/*
	 * If the values have changed then inform curses.
	 */
	if ((rows != LINES) || (cols != COLS))
		resize_term(rows, cols);
}

/* END CODE */
