/* zmatrix.c */

#include <curses.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>


/*********************************/
/* #DEFINEs and pseudo-constants */
/*********************************/

#define TRUE 1
#define FALSE 0

/* Character parameters */
int MIN_CHAR = '!';
int MAX_CHAR = '}';

/* Stream parameters */
int NUM_STREAMS = 12;		/* Number of streams in each column */
int MIN_HEIGHT = 6;		/* Shortest height of each stream */
int MAX_HEIGHT = 20;		/* Tallest height of each stream */
int MIN_GAP = 4;		/* Smallest vertical gap between streams */
int MAX_GAP = 8;		/* Largest vertical gap between streams */
int MIN_COLOR;			/* Lowest color (set to red) */
int MAX_COLOR;			/* Highest color (set to white) */
int USE_RAND_HEAD = TRUE;	/* Use random white head characters? */
int USE_RAND_BOLD = TRUE;	/* Use random bold characters? */
int USE_RAND_COLOR = FALSE;	/* Use different colors for each stream? */
int FIXED_COLOR;		/* Use if !USE_RAND_COLOR; green by default */

/* Screen parameters */
int SCR_RIGHT = 0;
int SCR_BOTTOM = 24;
int COL_GAP = 2;		/* Space in between columns + 1 */
int NUM_COLS = 0;

/* Speed params */
int TICK = 6250;
int MIN_CYCLE = 4;
int MAX_CYCLE = 16;

/* Returns a random number between min and max */
#define rand_num(min, max) ((int)(min) + (int)((float)((max) - (min) + 1) * \
		rand() / (float)(RAND_MAX)))

/* Returns the lesser of the two parameters */
#define min(a, b) ((a) < (b) ? (a) : (b))


/********************/
/* Type definitions */
/********************/

/* An individual cascade of gibberish */
typedef struct stream_t {
	int top;
	int bottom;
	int color;
	int head_char;
	int bold_head;
} stream_t;

/* A vertical column in which streams live */
typedef struct col_t {
	int cycle;
	int time;
	stream_t* streams;
} col_t;

/* Global pointer to an array of columns */
col_t* cols = NULL;


/**************/
/* Prototypes */
/**************/

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

/* Utility functions */
int	rand_char(void);
void	curs_init(void);
void	curs_plot(int ch, int x, int y, int color, int bold);
void	change_params(int ch);
void	update_stream_params(stream_t* stream);
void	resize_screen(int);
void	finish(int);

/* Primary functions */
void	init_col(col_t* col);
void	free_col(col_t* col);
void	init_screen(void);
void	reset_stream(col_t* column, int stream_id);
void	move_streams(int);


/*************/
/* Functions */
/*************/

/* Ye olde main function */
int main(int argc, char** argv) {
	int x;
	struct itimerval timer;
	struct timespec ts = {3600, 0}
	
	/* Set up timer delays */
	timer.it_interval.tv_usec = timer.it_value.tv_usec = TICK;
	timer.it_interval.tv_sec = timer.it_value.tv_sec = 0;

	srand(time(NULL));			/* Initialize the randomizer */
	curs_init();				/* Set up curses */

	resize_screen(0);			/* Initialize columns */
	
	init_screen();				/* Display everything */

	signal(SIGALRM, move_streams);		/* Every tick, do a loop */
	signal(SIGWINCH, resize_screen);	/* Change screen params */
	signal(SIGINT, finish);			/* On interrupt, quit */
	signal(SIGTERM, finish);		/* On Ctrl+C, quit */
	setitimer(ITIMER_REAL, &timer, NULL);	/* Start the ball rolling */
	while (TRUE) nanosleep(&ts);		/* And wait forever */
	
	/* This shouldn't happen, so... */
	return EXIT_FAILURE;
}

/* Return a random character */
int rand_char(void) {
	return rand_num(MIN_CHAR, MAX_CHAR);
}

/* Set up curses */
void curs_init(void) {	
	initscr();		/* Initialize the curses library */
	
	keypad(stdscr, TRUE);	/* Enable keyboard mapping */
	nonl();         	/* Tell curses not to do LF->CR+LF on output */
	cbreak();		/* Don't wait for \n on input */
	noecho();		/* Don't echo input */
	nodelay(stdscr, TRUE);	/* Don't wait for input before continuing */

	/* Simple color assignment */
	if (has_colors() ) {
		start_color();

		init_pair(COLOR_BLACK, COLOR_BLACK, COLOR_BLACK);
		init_pair(COLOR_GREEN, COLOR_GREEN, COLOR_BLACK);
		init_pair(COLOR_RED, COLOR_RED, COLOR_BLACK);
		init_pair(COLOR_CYAN, COLOR_CYAN, COLOR_BLACK);
		init_pair(COLOR_WHITE, COLOR_WHITE, COLOR_BLACK);
		init_pair(COLOR_MAGENTA, COLOR_MAGENTA, COLOR_BLACK);
		init_pair(COLOR_BLUE, COLOR_BLUE, COLOR_BLACK);
		init_pair(COLOR_YELLOW, COLOR_YELLOW, COLOR_BLACK);
	}
	
	/* Set color defaults */
	MIN_COLOR = COLOR_RED;
	MAX_COLOR = COLOR_WHITE;
	FIXED_COLOR = COLOR_GREEN;
}

/* Plot a character onscreen */
void curs_plot(int ch, int x, int y, int color, int bold) {
	if (x < 0 || x > SCR_RIGHT || y < 0 || y > SCR_BOTTOM)
		return;
	
	if (bold)
		attron(A_BOLD);
	attron(COLOR_PAIR(color));
	mvaddch(y, x, ch);
	attrset(A_NORMAL);
}

/* Change program params during runtime */
void change_params(int ch) {
	int x, y;
	switch (ch) {
		case 'q': finish(0); break;
		case 'b':
			USE_RAND_BOLD = !USE_RAND_BOLD;
			break;
		case 'n':
			USE_RAND_BOLD = FALSE;
			USE_RAND_HEAD = FALSE;
		case 'B':
			USE_RAND_HEAD = !USE_RAND_HEAD;
			break;
		case 'C':
			USE_RAND_COLOR = !USE_RAND_COLOR;
			break;
		case '!': FIXED_COLOR = COLOR_RED; break;
		case '@': FIXED_COLOR = COLOR_GREEN; break;
		case '#': FIXED_COLOR = COLOR_YELLOW; break;
		case '$': FIXED_COLOR = COLOR_BLUE; break;
		case '%': FIXED_COLOR = COLOR_MAGENTA; break;
		case '^': FIXED_COLOR = COLOR_CYAN; break;
		case '&': FIXED_COLOR = COLOR_WHITE; break;
	}
	
	/* Update all the streams with the new settings right away */
	for (x = 0; x < NUM_COLS; x++)
		for (y = 0; y < NUM_STREAMS; y++)
			update_stream_params(&cols[x].streams[y]);
}

/* Update a stream's parameters */
void update_stream_params(stream_t* stream) {
	if (USE_RAND_COLOR)
		stream->color = rand_num(MIN_COLOR, MAX_COLOR);
	else
		stream->color = FIXED_COLOR;
	if (USE_RAND_HEAD == FALSE)
		stream->bold_head = FALSE;		
}

/* Resize the screen during runtime, after a SIGWINCH */
void resize_screen(int u) {
	int x;
	int old_num_cols = NUM_COLS;

	endwin();
	initscr();
	
	/* Grab the new dimensions from stdscr */
	getmaxyx(stdscr, SCR_BOTTOM, SCR_RIGHT);

	/* Change the number of visible columns */
	NUM_COLS = SCR_RIGHT / COL_GAP;

	if (old_num_cols < NUM_COLS) {
		/* Allocate every new column */
		cols = (col_t*)realloc(cols, sizeof(col_t) * NUM_COLS);
		for (x = old_num_cols; x < NUM_COLS; x++)
			init_col(cols + x);
	} else if (old_num_cols > NUM_COLS) {
		/* Free every deleted column */
		for (x = NUM_COLS; x < old_num_cols; x++)
			free_col(cols + x);
		cols = (col_t*)realloc(cols, sizeof(col_t) * NUM_COLS);
	}

	/* Immediately update display */
	init_screen();
}

/* Cleanup and exit */
void finish(int u) {
	int x;	
	
	clear();			/* Clear the screen */
	refresh();			/* Update screen */
	endwin();			/* Shut down curses */

	/* Free streams and columns */
	for (x = 0; x < NUM_COLS; x++)
		free_col(cols + x);
	free(cols);

	exit(EXIT_SUCCESS);
}

/* Fill a given column structure with random starting data */
void init_col(col_t* col) {
	int bottom, height;
	stream_t* s;
	stream_t* s_end;
	
	/* TODO: adjust NUM_STREAMS to account for screen height */
	s = (stream_t*)malloc(sizeof(stream_t) * NUM_STREAMS);
	col->streams = s;

	col->cycle = rand_num(MIN_CYCLE, MAX_CYCLE);
	col->time = rand_num(0, col->cycle - 1);

	/* Initialize the streams bottom-to-top */
	bottom = -rand_num(MIN_GAP, MAX_HEIGHT);
	for (s_end = s + NUM_STREAMS; s < s_end; s++) {
		height = rand_num(MIN_HEIGHT, MAX_HEIGHT);
		s->bottom = bottom;
		s->top = bottom - height;
			
		if (USE_RAND_COLOR)
			s->color = rand_num(MIN_COLOR, MAX_COLOR);
		else
			s->color = FIXED_COLOR;
		s->bold_head = rand_num(FALSE, USE_RAND_HEAD);
		s->head_char = rand_char();

		/* Go up above this stream for the next one */
		bottom -= height + rand_num(MIN_GAP, MAX_GAP);
	}
}

/* Deallocate a column's internal data */
void free_col(col_t* col) {
	free(col->streams);
}

/* Show the streams in their initial positions onscreen */
void init_screen(void) {
	int x, y, z, bottom, top;
	stream_t* s;

	/* Clear the screen */
	clear();

	for (x = 0; x < NUM_COLS; x++)
		for (y = 0; y < NUM_STREAMS; y++) {
			s = &cols[x].streams[y];
			
			/* Draw each character individually */
			for (z = s->top; z <= s->bottom; z++) {
				if (z == s->bottom && s->bold_head)
					/* Draw white head character */
					curs_plot(s->head_char, x * COL_GAP,
							z, COLOR_WHITE, TRUE);
				else
					/* Draw ordinary character and maybe
					   make it bold */
					curs_plot(rand_char(), x * COL_GAP,
						z, s->color, rand_num(FALSE,
							USE_RAND_BOLD));
			}
		}
	/* Update screen */
	refresh();
}

/* Reset a stream at the top of its column */
void reset_stream(col_t* column, int stream_id) {
	int y, top, bottom;
	stream_t* s = &column->streams[stream_id];
	
	/* Find top of all streams */
	top = SCR_BOTTOM + 1;
	for (y = 0; y < NUM_STREAMS; y++)
		top = min(top, column->streams[y].top);

	/* Move stream up above previous top */
	bottom = s->bottom = top - rand_num(MIN_GAP, MAX_GAP);
	s->top = bottom - rand_num(MIN_HEIGHT, MAX_HEIGHT);
	if (USE_RAND_COLOR)
		s->color = rand_num(MIN_COLOR, MAX_COLOR);
	else
		s->color = FIXED_COLOR;
	s->bold_head = rand_num(FALSE, USE_RAND_HEAD);
	s->head_char = rand_char();
}

/* Move the streams one step down */
void move_streams(int u) {
	int x, y, keypress;
	stream_t* s;

	/* If the user pressed a key, process the keystroke */
	if ((keypress = wgetch(stdscr)) != ERR)
		change_params(keypress);
	
	for (x = 0; x < NUM_COLS; x++)
		/* Increment timer and move streams if necessary */
		if (++cols[x].time >= cols[x].cycle) {
			cols[x].time = 0;
			for (y = 0; y < NUM_STREAMS; y++) {
				s = &cols[x].streams[y];
				
				/* Erase top character */
				curs_plot(' ', x * COL_GAP, s->top,
						COLOR_BLACK, FALSE);

				/* If there's a white head, dim it */
				if (s->bold_head)
					curs_plot(s->head_char,
						x * COL_GAP, s->bottom,
						s->color, rand_num(FALSE,
							USE_RAND_BOLD));

				/* Move down */
				s->top++;
				s->bottom++;
				s->head_char = rand_char();
				
				/* Move to very top if necessary */
				if (s->top > SCR_BOTTOM)
					reset_stream(&cols[x], y);

				/* Draw bottom character */
				if (s->bold_head)
					/* Draw white head character */
					curs_plot(s->head_char,
						x * COL_GAP, s->bottom,
						COLOR_WHITE, TRUE);
				else
					/* Draw ordinary character and maybe
					   make it bold */
					curs_plot(s->head_char,
						x * COL_GAP, s->bottom,
						s->color, rand_num(FALSE,
							USE_RAND_BOLD));
			}
		}
	
	/* Update screen */
	refresh();
}

/* End of file */

