/*
 * Configurable ps-like program.
 * Routines to display the results.
 * All output goes through the currently set DISPLAY object.
 *
 * Copyright (c) 2010 David I. Bell
 * Permission is granted to use, distribute, or modify this source,
 * provided that this copyright notice remains intact.
 */

#include "ips.h"


/*
 * Static variables.
 */
static	int		headerWidth;	/* width of longest header string */
static	BOOL		isShown;	/* whether anything has been shown */
static	BOOL		haveScrollTime;	/* have last scroll time */
static	struct timeval	lastScrollTime;	/* time auto scrolling was last done */


/*
 * Local procedures.
 */
static	void		ShowVerticalProcessStatus(const PROC * proc);
static	const char *	ReturnInfoString(void);
static	const char *	ReturnHeaderString(void);
static	const char *	ReturnProcessString(const PROC * proc);
static	BOOL		CanShowColumn(const COLUMN * column, int col);
static	int		GetSkipCountDelta(void);
static	int		GetUsefulRows(void);
static	int		GetPageCount(void);
static	int		GetShowCount(void);


/*
 * Initialize for displaying the process status.
 * This opens the display device.
 * Returns TRUE if successful.
 */
BOOL
InitializeDisplay(void)
{
	COLUMN **	columnPtr;
	const COLUMN *	column;
	int		len;

	/*
	 * Set the display devicetype.
	 */
	if (!DpySetDisplay(displayType))
	{
		fprintf(stderr, "Display type \"%s\" is not available\n",
			displayType);

		return FALSE;
	}

	/*
	 * Try to open the display device.
	 */
	if (!DpyOpen())
	{
		fprintf(stderr, "Failed to open display type \"%s\"\n",
			displayType);

		return FALSE;
	}

	/*
	 * Try to define the colors the user specified.
	 */
	if (!DefineColors())
	{
		DpyClose();

		fprintf(stderr, "Failed to define colors\n");

		return FALSE;
	}

	/*
 	 * Now create the window.
	 */
	DpyCreateWindow();

	headerWidth = 0;

	if (noHeader || !isVertical)
		return TRUE;

	/*
	 * Iterate over all of the desired columns to be displayed
	 * and find the longest column header string.
	 */
	for (columnPtr = showList; *columnPtr; columnPtr++)
	{
		column = *columnPtr;

		len = strlen(column->heading);

		if (len > headerWidth)
			headerWidth = len;
	}

	return TRUE;
}


/*
 * Show all of the selected processes for one iteration of the program.
 * This clears the screen or prints a blank line, prints the header string
 * if required, then prints the process status for each selected process.
 * The status can be printed horizontally or vertically.
 */
void
ShowSelectedProcesses(void)
{
	const PROC *	proc;
	int		seenCount;
	int		skippedCount;
	int		usefulRows;
	struct	timeval	newScrollTime;

	isShown = FALSE;
	seenCount = 0;
	skippedCount = 0;

	/*
	 * If auto scrolling is enabled then see if we need to do that now.
	 */
	if (!isFrozen && (scrollSeconds > 0) && !DpyDoesScroll())
	{
		GetTimeOfDay(&newScrollTime);

		if (haveScrollTime &&
			(ElapsedMilliSeconds(&lastScrollTime, &newScrollTime)
				/ 1000 > scrollSeconds))
		{
			NextPage();
		}

		if (!haveScrollTime)
		{
			haveScrollTime = TRUE;
			lastScrollTime = newScrollTime;
		}
	}

	/*
	 * If the display needs refreshing the do that.
	 */
	if (isRefreshNeeded)
	{
		DpyRefresh();

		isRefreshNeeded = FALSE;
	}

	/*
	 * Begin display of a new page with the default color.
	 */
	DpySetColor(DEFAULT_COLOR_ID);

	DpyBeginPage();

	/*
	 * Show the info line if needed.
	 */
	if (isLooping && isInfoShown)
	{
		DpySetColor(infoColorId);
		DpyString(ReturnInfoString());
		DpyChar('\n');
		DpySetColor(DEFAULT_COLOR_ID);
	}

	/*
	 * Get how many rows of data we can show.
	 * If the display device scrolls then there is no limit.
	 */
	if (DpyDoesScroll())
		usefulRows = 0;
	else
		usefulRows = GetUsefulRows();

	/*
	 * Loop over all processes in the list.
	 */
	for (proc = processList; proc; proc = proc->next)
	{
		/*
		 * If we have filled the lines of the display then stop.
		 */
		if (usefulRows && (seenCount >= usefulRows))
			break;

		/*
		 * If we are restricting the output to the top few process,
		 * then stop when we reach the limit.
		 */
		if (topCount && (seenCount >= topCount))
			break;

		/*
		 * If the process is not to be shown, then skip it.
		 */
		if (!proc->isShown)
			continue;

		/*
		 * If we haven't skipped enough processes yet then skip it.
		 */
		if (skippedCount < skipCount)
		{
			skippedCount++;

			continue;
		}

		/*
		 * We want to display this process's status.
		 * If we haven't displayed any processes yet this iteration,
		 * then print the header string if required.
		 */
		if (seenCount == 0)
		{
			if (!noHeader && !isVertical)
			{
				DpySetColor(headerColorId);
				DpyString(ReturnHeaderString());
				DpyChar('\n');
				DpySetColor(DEFAULT_COLOR_ID);
			}
		}

		/*
		 * Set the color to the one which the process gets due to any
		 * row conditions that have been specified.
		 */
		DpySetColor(EvaluateRowColor(proc));

		/*
		 * If we want a vertical display of the columns,
		 * then do that, otherwise do the normal horizontal output.
		 */
		if (isVertical)
			ShowVerticalProcessStatus(proc);
		else
		{
			DpyString(ReturnProcessString(proc));
			DpyChar('\n');
		}

		seenCount++;
		isShown = TRUE;
	}

	/*
	 * Reset the colors to the default and end the page to flush output.
	 */
	DpySetColor(DEFAULT_COLOR_ID);

	DpyEndPage();
}


/*
 * Return the information string which gives a quick overview.
 */
static const char *
ReturnInfoString(void)
{
	int		delta;
	static	char	buf[MAX_INFO_LEN + 1];

	delta = GetSkipCountDelta();

	sprintf(buf,
	"[Procs: %d  Threads: %d  Shown: %d  Sleep: %g  Scroll: %d  Overlap: %d  Page: %d/%d%s]",
		procTotalCount, threadTotalCount, GetShowCount(),
		((double) sleepTimeMs) / 1000.0, scrollSeconds,
		overlapLines, (skipCount / delta) + 1,
		GetPageCount(), (isFrozen ? "  FROZEN" : ""));

	return buf;
}


/*
 * Return the header line as determined by the currently defined columns.
 * This returns a pointer to a static buffer with the stored information.
 * The column headers are justified appropriately.  This is only used for
 * horizontal column displays.
 */
static const char *
ReturnHeaderString(void)
{
	COLUMN **	columnPtr;
	const COLUMN *	column;
	int		col;
	int		len;
	int		padding;
	int		tmp;
	char *		cp;
	char *		bufCp;
	static	char	buf[MAX_WIDTH + 1];

	bufCp = buf;
	col = 0;
	padding = 0;
	columnPtr = showList;
	column = *columnPtr++;

	while (CanShowColumn(column, col))
	{
		while (padding-- > 0)
			*bufCp++ = ' ';

		cp = column->heading;

		len = strlen(cp);

		if (len > column->width)
			len = column->width;

		padding = column->width - len;

		if (column->justify == RIGHT)
		{
			while (padding-- > 0)
				*bufCp++ = ' ';
		}

		if (column->justify == CENTER)
		{
			tmp = padding / 2;
			padding -= tmp;

			while (tmp-- > 0)
				*bufCp++ = ' ';
		}

		memcpy(bufCp, cp, len);
		bufCp += len;

		col += column->width + separation;

		if (padding < 0)
			padding = 0;

		padding += separation;
		column = *columnPtr++;
	}

	*bufCp = '\0';

	return buf;
}


/*
 * Return a line of information about the specified process based on the
 * currently defined columns.  This returns a pointer to a static buffer
 * with the stored information.  The columns are justified appropriately.
 * This is only used for horizontal column displays.
 */
static const char *
ReturnProcessString(const PROC * proc)
{
	COLUMN **	columnPtr;
	COLUMN *	column;
	BOOL		overflow;
	int		len;
	int		padding;
	int		tmp;
	int		col;
	int		goalCol;
	const char *	value;
	char *		bufCp;
	static	char	buf[MAX_WIDTH + 1];

	bufCp = buf;
	col = 0;
	goalCol = 0;
	columnPtr = showList;
	column = *columnPtr++;

	while (CanShowColumn(column, goalCol))
	{
		/*
		 * Generate the column's value.
		 */
		value = column->showFunc(proc);

		len = strlen(value);

		/*
		 * Calculate the required padding to make the returned
		 * value fill up the width of the column.
		 */
		padding = column->width - len;

		if (padding < 0)
			padding = 0;

		/*
		 * Assume the column's value hasn't overflowed the
		 * allowed width until we know otherwise.
		 */
		overflow = FALSE;

		/*
		 * If this is not the last column on the line then
		 * truncate its value if necessary to the width of the
		 * column minus one (to allow for the vertical bar).
		 */
		if (*columnPtr && (len > column->width))
		{
			len = column->width - 1;
			overflow = TRUE;
			padding = 0;
		}

		/*
		 * If the column won't fit within the remaining width
		 * of the line then truncate its value to the remaining
		 * width minus one (to allow for the vertical bar).
		 */
		if (goalCol + len > outputWidth)
		{
			len = outputWidth - goalCol - 1;
			overflow = TRUE;
			padding = 0;
		}

		if (len < 0)
			len = 0;

		/*
		 * If there is any padding to be applied before the
		 * column's value is printed then apply it.
		 */
		if (padding > 0)
		{
			if (column->justify == RIGHT)
			{
				goalCol += padding;
				padding = 0;
			}

			if (column->justify == CENTER)
			{
				tmp = padding / 2;
				padding -= tmp;
				goalCol += tmp;
			}
		}

		/*
		 * Now actually space over to the goal column where the
		 * value is printed.
		 */
		while (col < goalCol)
		{
			col++;
			*bufCp++ = ' ';
		}

		/*
		 * Copy as much of the value as we allow.
		 */
		memcpy(bufCp, value, len);

		bufCp += len;
		goalCol += len;
		col += len;

		/*
		 * If the value overflowed then append a vertical bar.
		 */
		if (overflow)
		{
			*bufCp++ = '|';
			goalCol++;
			col++;
		}

		/*
		 * If there is any remaining padding then add it to
		 * the goal column for the next iteration to use.
		 */
		if (padding > 0)
			goalCol += padding;

		/*
		 * Add in the separation value to the goal column.
		 */
		goalCol += separation;

		column = *columnPtr++;
	}

	*bufCp = '\0';

	return buf;
}


/*
 * Return whether or not a column can be displayed starting at the
 * specified column value.  A left justified column can be displayed
 * as long as its heading value will fit (the minimum width).  But a
 * right or center justified column must fit its defined width.
 * This returns FALSE if a NULL column is given.
 */
static BOOL
CanShowColumn(const COLUMN * column, int col)
{
	if (column == NULL)
		return FALSE;

	if (column->justify == LEFT)
		return (col + column->minWidth <= outputWidth);

	return (col + column->width <= outputWidth);
}


/*
 * Print the status of a process in a vertical format.
 * In this format, each process status requires multiple lines, one per
 * item to be displayed.  The beginning of each line can give the item name,
 * and the rest of each line contains the value.  No column justification
 * is done here so that all values are left justified.
 */
static void
ShowVerticalProcessStatus(const PROC * proc)
{
	COLUMN **	columnPtr;
	const COLUMN *	column;
	int		columnWidth;
	int		len;
	char *		cp;
	const char *	str;
	char		buf[128];

	/*
	 * Separate the process status from the previous process status.
	 */
	if (isShown)
		DpyChar('\n');

	/*
	 * Calculate how many columns are available for each value.
	 * Make sure there is at least one column available, even if
	 * that means we will overflow the line.
	 */
	if (noHeader)
		columnWidth = outputWidth;
	else
		columnWidth = outputWidth - headerWidth - separation - 1;

	if (columnWidth <= 0)
		columnWidth = 1;

	/*
	 * Iterate over all of the desired columns to be displayed.
	 */
	for (columnPtr = showList; *columnPtr; columnPtr++)
	{
		column = *columnPtr;

		/*
		 * Construct and output the header string for the line.
		 * The string contains the column header and a colon,
		 * followed by padding out to the maximum width of the
		 * header strings plus the column separation value.
		 */
		if (!noHeader)
		{
			cp = buf;

			len = strlen(column->heading);
			memcpy(cp, column->heading, len);
			cp += len;

			*cp++ = ':';

			len = headerWidth + separation - len;

			if (len > 0)
			{
				memset(cp, ' ', len);
				cp += len;
			}

			*cp = '\0';

			DpyString(buf);
		}

		/*
		 * Now get the value for the data item and get its length.
		 */
		str = column->showFunc(proc);

		len = strlen(str);

		/*
		 * If the column is too wide, then truncate it and change
		 * the last character into a vertical bar.  Otherwise print
		 * the whole value.
		 */
		if (len > columnWidth)
		{
			DpyBuffer(str, columnWidth - 1);
			DpyString("|\n");
		}
		else
		{
			DpyString(str);
			DpyChar('\n');
		}
	}
}


/*
 * Adjust variables so that on the next update of the process list the
 * next page of processes will be shown.  If there are no more pages
 * then the first page will be shown.
 */
void
NextPage(void)
{
	/*
	 * If the rest of the processes from the current skip count
	 * fit in the display then move to the first page.
	 */
	if (skipCount + GetUsefulRows() >= GetShowCount())
	{
		TopPage();

		return;
	}

	skipCount += GetSkipCountDelta();

	haveScrollTime = FALSE;
}


/*
 * Adjust variables so that on the next update of the process list the
 * previous page of processes will be shown.  If there are no more pages
 * then the last page will be shown.
 */
void
PreviousPage(void)
{
	skipCount -= GetSkipCountDelta();

	if (skipCount < 0)
		BottomPage();

	haveScrollTime = FALSE;
}


/*
 * Adjust variables so that on the next update of the process list the
 * first page of processes will be shown.
 */
void
TopPage(void)
{
	skipCount = 0;
	haveScrollTime = FALSE;
}


/*
 * Adjust variables so that on the next update of the process list the
 * last page of processes will be shown.
 */
void
BottomPage(void)
{
	int	delta;
	int	modulo;

	/*
	 * Calculate the number of lines to skip where the last page
	 * of status would completely fill the screen.
	 */
	skipCount = GetShowCount() - GetUsefulRows();

	/*
	 * If no lines would be skipped then the last page is the same
	 * as the first page.
	 */
	if (skipCount <= 0)
	{
		skipCount = 0;
		haveScrollTime = FALSE;

		return;
	}

	/*
	 * If the skip count is not an exact multiple of the skip delta
	 * value then round it up to the next exact multiple.
	 */
	delta = GetSkipCountDelta();

	modulo = skipCount % delta;

	if (modulo > 0)
		skipCount += (delta - modulo);

	haveScrollTime = FALSE;
}


/*
 * Reset the scroll time so that it won't happen for a while.
 * This is called, for example, when the display has been unfrozen.
 */
void
ResetScrollTime(void)
{
	haveScrollTime = FALSE;
}


/*
 * Get the delta value for changing the number of lines to skip for a page.
 * This takes into account whether a header or info line is being shown
 * and how many lines of overlap there are between pages.  This will always
 * return a value of at least 1.
 */
static int
GetSkipCountDelta(void)
{
	int	count;

	count = GetUsefulRows() - overlapLines;

	if (count <= 0)
		count = 1;

	return count;
}


/*
 * Return the number of useful rows for showing actual process status.
 * This accounts for the header and information lines at the top if present.
 * This will always return a value of at least 1.
 */
static int
GetUsefulRows(void)
{
	int	count;

	count = DpyGetRows();

	if (!noHeader)
		count--;

	if (isInfoShown)
		count--;

	if (count <= 0)
		count = 1;

	return count;
}


/*
 * Return the number of pages required for the display.
 */
static int
GetPageCount(void)
{
	int	skip;
	int	delta;

	/*
	 * Calculate the number of lines to skip where the last page
	 * of status would completely fill the screen.
	 */
	skip = GetShowCount() - GetUsefulRows();

	/*
	 * If no lines would be skipped then there is only one page.
	 */
	if (skip <= 0)
		return 1;

	/*
	 * Divide the skip value by the amount we skip by to get
	 * the number of pages (and while rounding up).
	 */
	delta = GetSkipCountDelta();

	return 1 + (skip + (delta - 1)) / delta;
}


/*
 * Get the number of processes or threads to actually be shown.
 * This is the number of processes or processes that want showing,
 * but limited to the topCount if such a limit is in effect.
 */
static int
GetShowCount(void)
{
	int	count;

	if (showThreads)
		count = threadShowCount;
	else
		count = procShowCount;

	if (topCount && (count > topCount))
		count = topCount;

	return count;
}

/* END CODE */
