/*
 * Grid Layout Manager
 *
 * Purpose: To format text into columns and rows on the console.
 *
 * Author: Ryan Jennings <ryan78j@gmail.com>
 *
 * Date: Oct 28, 2006
 *
 * Note: Modified from C++ to C for the mudding community
 *       (email me if you want a C++ version).  Please notify me
 *       of any bugs and/or improvements!
 *
 * Credit: Got the idea from the Java UI.
 *
 * License: Code is public domain, provided you keep this header intact
 *          and it is not used for profit.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

typedef struct grid_layout
{
		char ***grid; // dynamic multi-dimensional array.
		size_t columnCount;	// number of columns
		size_t columnSize;	// character length of a single column
		size_t lineCount;	// number of lines
		char addBorder;	// draw border after block of text?
} 
GridLayout;

const size_t bufSize = bufSize;

void addTextToColumn(GridLayout *, size_t, const char *, ...) 
		__attribute__((format(printf, 3, 4)));

/*	Initializes the layout */
void init_output_format(GridLayout *fmt, size_t cols, size_t length)
{
	fmt->columnCount = cols;
	fmt->columnSize = (length / cols);
	fmt->lineCount = 0;
	fmt->addBorder = 0;
	fmt->grid = NULL;
}

/* replaces concurrent new lines with a space */
char *remove_new_lines(char *desc)
{
	static char buf[bufSize];
	char temp[bufSize];
	size_t i, t;

	if (!desc || !*desc)
		return "";

	t = 0;

	// first replace all new line characters with control character '\x20'.
	for (i = 0; i < strlen(desc); ++i) {
		if (desc[i] == '\n' || desc[i] == '\r') 
			temp[t++] = '\x20';
		else
			temp[t++] = desc[i];
	}

	temp[t] = '\0';

	// then replace all '\x20' characters in a row with a single space.
	for(t = 0, i = 0; i < strlen(temp); ) {
		if(temp[i] == '\x20') {
			buf[t++] = ' ';
			do { i++; } while (temp[i] == '\x20');
		}
		else
			buf[t++] = temp[i++];
	}
	buf[t] = '\0';

	return buf;
}


/*	Find the specified length of a number of printed characters in a string.
	Returns the actual length, stores the printed length in printLength.
*/
size_t get_line_len(char *str, size_t max_len, size_t *printLength)
{
	size_t sizeCount, rev;

	*printLength = 0;

	if (!str || !*str)
		return 0;

	for (sizeCount = 0; sizeCount < strlen(str); sizeCount++) {
		// skip colour codes
		if (str[sizeCount] == '{') {
			sizeCount++;
		} 
		else if (++(*printLength) >= max_len)
			break;
	}
	
	// if printed characters are less the the length, return the size.
	if(*printLength < max_len)
		return sizeCount;

	// otherwise be smart where we end the line
	for (rev = sizeCount; rev > 0; rev--)
		if (str[rev] == ' ')
			break;

	// no spaces? we'll have to split characters
	if(rev == 0) {
		rev = sizeCount;
	} else {
		*printLength -= sizeCount - rev;
	}

	return rev + 1;
}

/*	Add text to the layout.  Line and column can be specified. */
void addTextCore(GridLayout *fmt, char *buf, size_t lineSave, size_t columnSave)
{
	size_t index = 0, columnPos = 0;

	// loop while we have text in buf
	while(index < strlen(buf)) {
		char **columnBuf;

		// are we re-using a line?
		if(lineSave < fmt->lineCount) {
			columnBuf = fmt->grid[lineSave];
		} else {
			columnBuf = (char **) calloc(fmt->columnCount, sizeof(char *));
		}

		// try to add all columns
		for(columnPos = 0; columnPos < fmt->columnCount && index < strlen(buf); columnPos++) {
			size_t printLength, pos, fill;
			char tmp[fmt->columnSize], substr[strlen(&buf[index])+1];

			// if we are looking for a specific column, skip until its found
			if(columnSave < fmt->columnCount && columnSave != columnPos) {
				continue;
			}
		
			// Make the column is empty.
			if(columnBuf[columnPos]) {
					continue;
			}

			// skip spaces at begining of a new column.
			while(buf[index] == ' ') 
				index++;

			// get columnSize position in buf
			pos = get_line_len(&buf[index], fmt->columnSize-1, &printLength); 
			

			// no text
			if(pos == 0) 
				return;

			// always add a trailing space
			strcpy(tmp, " ");
			printLength++;

			// fill the rest of the column with spaces
			for(fill = printLength; fill < fmt->columnSize; fill++)
				strcat(tmp, " ");

			// make the string
			strcpy(substr, &buf[index]);
			strcpy(&substr[pos], tmp);

			// add it
			columnBuf[columnPos] = strdup(substr);

			index += pos;

			// save the column we added to
			columnSave = columnPos;
		} 

		// reassign saved line
		if(lineSave < fmt->lineCount) {
			fmt->grid[lineSave++] = columnBuf;
		} else {
			fmt->lineCount++;
			fmt->grid = (char ***) realloc(fmt->grid, fmt->lineCount * sizeof(char ***));
			fmt->grid[fmt->lineCount - 1] = columnBuf;
		}
	}

	// add a border if needed.
	if(fmt->addBorder) {
		char border[fmt->columnSize+1];

		for(columnPos = 0; columnPos < fmt->columnSize-1; columnPos++)
			border[columnPos] = '-';

		border[columnPos] = '\0';
	
		fmt->addBorder = 0;
		addTextToColumn(fmt, columnSave+1, border);
		fmt->addBorder = 1;
	}
}

/*	Adds text to the next available column. */
void addText(GridLayout *fmt, const char *text, ...) 
{
	char fmt_buf[bufSize];
	va_list args;
	size_t columnSave = 0, lineSave = 0;

	va_start(args, text);
	vsnprintf(fmt_buf, sizeof(fmt_buf), text, args);
	va_end(args);
		
	/* Find the next available column, passing lineCount and
	   columnCount to addTextCore if none are found. */
	for(lineSave = 0; lineSave < fmt->lineCount; lineSave++) {
		for(columnSave = 0; columnSave < fmt->columnCount; columnSave++) {
			if(!fmt->grid[lineSave][columnSave]) {
				break;
			}
		}
		// line is incomplete
		if(columnSave < fmt->columnCount) {
			break;
		}
	}
	
	addTextCore(fmt, remove_new_lines(fmt_buf), lineSave, columnSave);
}


/*	Adds text to a specified column. */
void addTextToColumn(GridLayout *fmt, size_t columnSave, const char *text, ...)
{
	char fmt_buf[bufSize];
	va_list args;
	size_t lineSave;

	if(--columnSave > fmt->columnCount)
		return;

	va_start(args, text);
	vsnprintf(fmt_buf, sizeof(fmt_buf), text, args);
	va_end(args);
		
	/* Find a line with an empty column */
	for(lineSave = 0; lineSave < fmt->lineCount; lineSave++) {
		if(!fmt->grid[lineSave][columnSave]) {
				break;
		}
	}
	
	addTextCore(fmt, remove_new_lines(fmt_buf), lineSave, columnSave);
}

/*	Adds text to every column in a row.  Useful for column headers. */
void addHeaders(GridLayout *fmt, const char *text, ...)
{
	va_list args;
	char fmt_buf[bufSize];
	size_t count = 0;

	if(!text || !*text)
		return;

	/* finish off any incomplete columns 
	   with empty columns so we have a new
	   line. 
	 */
	if(fmt->grid) {
		char emptyColumn[fmt->columnSize+1];
		int c, l;

		snprintf(emptyColumn, sizeof(emptyColumn), "%*s", fmt->columnSize, " ");

		for(l = 0; l < fmt->lineCount; l++) {
			for(c = 0; c < fmt->columnCount; c++) {
				// skip columns with text
				if(fmt->grid[l][c])
					continue;

				// add empty column
				fmt->grid[l][c] = strdup(emptyColumn);
			}
		}
	}

	va_start(args, text);
	vsnprintf(fmt_buf, sizeof(fmt_buf), text, args);
	va_end(args);

	// add text to every column
	while(count++ < fmt->columnCount) {
		addTextToColumn(fmt, count, fmt_buf);
	}
}


/* Prints the grid to stdout.  Can easily be
   modified to print elsewhere.
   Frees memory stored in the grid as it prints. 
 
   This function could also be modded to have borders
   around the text.
 */
void printText(GridLayout *fmt)
{
	size_t i, j;
	char emptyColumn[fmt->columnSize+1];

	// string of spaces for empty columns
	snprintf(emptyColumn, sizeof(emptyColumn), "%*s", fmt->columnSize, " ");

	for(i = 0; i < fmt->lineCount; i++) {
		for(j = 0; j < fmt->columnCount; j++) {
			if(fmt->grid[i][j]) {
				fprintf(stdout, fmt->grid[i][j]);
				free(fmt->grid[i][j]);
			}
			else
				fprintf(stdout, emptyColumn);
		}
		fprintf(stdout, "\n");
		free(fmt->grid[i]);
	}

	if(fmt->grid)
		free(fmt->grid);
}

/* Checks if any text has been added to the grid */
int hasText(GridLayout *fmt)
{
	return (fmt->lineCount && fmt->columnCount);
}

int main(int argc, char *argv[])
{
	GridLayout fmt;

	init_output_format(&fmt, 3, 60);

	fprintf(stdout, "\nThis is a %d column test with a line length of %d.\n", fmt.columnCount, fmt.columnSize * fmt.columnCount);

	fmt.addBorder = 1;

	addHeaders(&fmt, "COLUMN");

	addText(&fmt, "TEXT 1: This is the first block of text.");
	addText(&fmt, "TEXT %d: The second block of text is longer than the column size,"
				  " therefore wrapping to the next line in the same column.  New text "
				  " added will start on any columns skipped on the next line.", 2);
	addText(&fmt, "TEXT 3: The third block of text should fill columns left by text two.");
	addText(&fmt, "TEXT 4: This is the fourth block of text.  Cool eh?");
	addTextToColumn(&fmt, 3, "TEXT 5: This is fifth block of text.  It is set to be in column three no matter what.");
	addText(&fmt, "AUTHOR: Ryan Jennings <%s>", "ryan78j@gmail.com");
	addText(&fmt, "DATE: %s", "Oct 28, 2006");

	if(hasText(&fmt)) {
		fprintf(stdout, "\n");
		printText(&fmt);
		fprintf(stdout, "\n");
	}
}