/* gmatrix.c */

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

#define FALSE 0
#define TRUE 1
#define ANSI_BLACK 0
#define ANSI_RED 1
#define ANSI_GREEN 2
#define ANSI_YELLOW 3
#define ANSI_BLUE 4
#define ANSI_PURPLE 5
#define ANSI_CYAN 6
#define ANSI_WHITE 7

/* EWWW!!!  GLOBALS! */

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

/* Stream parameters */
int NUM_STREAMS = 16;		/* Streams per column */
int MIN_STREAM_GAP = 16;	/* Minimum vertical space between streams */
int MAX_STREAM_GAP = 48;	/* Maximum vertical space between streams */
int MIN_STREAM_HEIGHT = 15;	/* Minimum stream height in characters */
int MAX_STREAM_HEIGHT = 35;	/* Maximum stream height in characters */
int MIN_COLOR = ANSI_GREEN;	/* Minimum color */
int MAX_COLOR = ANSI_GREEN;	/* Maximum color */
int USE_RANDOM_BOLD = TRUE;	/* Have random bold characters? */
int USE_RANDOM_HEAD = TRUE;	/* Have a random white head character? */

/* Speed */
int DELAY = 20000;		/* Length of a tick, in microseconds */
int MIN_PERIOD = 2;		/* Fastest sliding rate */
int MAX_PERIOD = 5;		/* Slowest sliding rate */

/* Screen size */
int NUM_COLUMNS = 40;		/* Onscreen column count */
int COLUMN_GAP = 2;		/* Space in characters between columns + 1*/
int SCREEN_TOP = 1;		/* Top row of screen */
int SCREEN_BOTTOM = 24;		/* Bottom row of screen */
int SCREEN_LEFT = 1;		/* Left column of the screen */
int SCREEN_RIGHT = 80;		/* Right column of the screen */

/* 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))

/* Returns the greater of the two parameters */
#define max(a, b) ((a) > (b) ? (a) : (b))

/* An individual cascade of gibberish */
typedef struct STREAM {
	int top;
	int bottom;
	int color;	/* What color is it? */
	int head;	/* Does it have a white head character? */
	int head_char;	/* Head character's value */
} STREAM;

/* A vertical column in which STREAMs live */
typedef struct COLUMN {
	int period;	/* Ticks per slide */
	int time;	/* Elapsed ticks since last slide */
	STREAM* streams;
} COLUMN;

COLUMN* columns;

/* Returns a random character in range */
int rand_char()
{	
	return rand_num(MIN_CHAR, MAX_CHAR);
}

/* Erase screen */
void ansi_clear()
{
	printf("[2J");
}

/* Plot character */
void ansi_plot(int x, int y, int ch, int color, int bold)
{
	if (x < SCREEN_LEFT || x > SCREEN_RIGHT || y < SCREEN_TOP || y >
			SCREEN_BOTTOM)
		return;
	printf("[%i;%iH[%i;3%im%c", y, x, bold, color, ch);
}

/* Fill all the columns with random starting data */
void initialize_columns()
{
	int x, y, bottom, height;
	STREAM* s;
	for (x = 0; x < NUM_COLUMNS; x++) {
		columns[x].period = rand_num(MIN_PERIOD, MAX_PERIOD);
		columns[x].time = rand_num(0, columns[x].period - 1);

		/* Initialize the streams bottom-to-top */
		bottom = rand_num(SCREEN_BOTTOM - MAX_STREAM_GAP,
				SCREEN_BOTTOM +	MIN_STREAM_HEIGHT - 1);
		for (y = 0; y < NUM_STREAMS; y++) {
			s = &columns[x].streams[y];
			
			height = rand_num(MIN_STREAM_HEIGHT, MAX_STREAM_HEIGHT);
			s->bottom = bottom;
			s->top = bottom - height;
			
			s->color = rand_num(MIN_COLOR, MAX_COLOR);
			s->head = rand_num(FALSE, USE_RANDOM_HEAD);
			s->head_char = rand_char();

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

/* Display all the streams */
void initialize_display()
{
	int x, y, z, bottom, top, color;
	STREAM* s;

	ansi_clear();
	for (x = 0; x < NUM_COLUMNS; x++)
		for (y = 0; y < NUM_STREAMS; y++) {
			s = &columns[x].streams[y];
			
			/* Draw each character individually */
			for (z = s->top; z <= s->bottom; z++) {
				if (z == s->bottom && s->head)
					/* Draw white head character */
					ansi_plot(x * COLUMN_GAP + SCREEN_LEFT,
							z, s->head_char,
							ANSI_WHITE, TRUE);
				else
					/* Draw ordinary character and maybe
					   make it bold */
					ansi_plot(x * COLUMN_GAP + SCREEN_LEFT,
							z, rand_char(),
							s->color,
							rand_num(FALSE,
							USE_RANDOM_BOLD));
			}
		}
	/* Flush output */
	fflush(stdout);
}

/* Move a stream to the top of its column */
void reset_stream(COLUMN* column, int stream_id)
{
	int y, top, bottom;
	STREAM* s = &column->streams[stream_id];
	
	/* Find top of all streams */
	top = SCREEN_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_STREAM_GAP, MAX_STREAM_GAP);
	s->top = bottom - rand_num(MIN_STREAM_HEIGHT, MAX_STREAM_HEIGHT);
	s->color = rand_num(MIN_COLOR, MAX_COLOR);
	s->head = rand_num(FALSE, USE_RANDOM_HEAD);
	s->head_char = rand_char();
}

/* Ye olde main loop */
void main_loop()
{
	static int count = 0;
	int x, y;
	STREAM* s;

	for (x = 0; x < NUM_COLUMNS; x++)
		/* Increment timer and move streams if necessary */
		if (++columns[x].time >= columns[x].period) {
			columns[x].time = 0;
			for (y = 0; y < NUM_STREAMS; y++) {
				s = &columns[x].streams[y];
				
				/* Erase top character */
				ansi_plot(x * COLUMN_GAP + SCREEN_LEFT,
						s->top, ' ', ANSI_BLACK, FALSE);

				/* If there's a white head, dim it */
				if (s->head) {
					/* Draw character above head */
					ansi_plot(x * COLUMN_GAP + SCREEN_LEFT,
							s->bottom, s->head_char,
							s->color,
							rand_num(FALSE,
							USE_RANDOM_BOLD));

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

				/* Draw bottom character */
				if (s->head)
					/* Draw white head character */
					ansi_plot(x * COLUMN_GAP + SCREEN_LEFT,
							s->bottom, s->head_char,
							ANSI_WHITE, TRUE);
				else
					/* Draw ordinary character and maybe
					   make it bold */
					ansi_plot(x * COLUMN_GAP + SCREEN_LEFT,
							s->bottom, s->head_char,
							s->color,
							rand_num(FALSE,
							USE_RANDOM_BOLD));
			}
		}
	/* Flush output */
	fflush(stdout);
}

/* Cleanup */
void quit()
{
	int x;
	for (x = 0; x < NUM_COLUMNS; x++)
		free(columns[x].streams);
	free(columns);

	exit(EXIT_SUCCESS);
}

/* Ye olde main function */
int main(int argc, char* argv[])
{
	int x;
	struct itimerval timer;
	
	timer.it_interval.tv_usec = timer.it_value.tv_usec = DELAY;
	timer.it_interval.tv_sec = timer.it_value.tv_sec = 0;

	/* Allocate columns and streams */
	columns = (COLUMN*)malloc(sizeof(COLUMN) * NUM_COLUMNS);
	for (x = 0; x < NUM_COLUMNS; x++)
		columns[x].streams = (STREAM*)malloc(sizeof(STREAM) *
				NUM_STREAMS);
	
	srand(time(NULL));			/* Initialize the randomizer */
	
	initialize_columns();			/* Fill all the columns */
	initialize_display();			/* And display them */

	signal(SIGALRM, main_loop);		/* Every tick, do a loop */
	signal(SIGTERM, quit);			/* On Ctrl+C, quit */
	setitimer(ITIMER_REAL, &timer, NULL);	/* Start the ball rolling */
	while (TRUE);				/* And wait forever */
	
	/* This shouldn't happen, so... */
	return EXIT_FAILURE;
}

