#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <math.h>
#include <time.h>
#include <unistd.h>

#define DEFAULT_SCREEN_WIDTH 80
#define DEFAULT_INTERVAL 15
#define DEFAULT_AVERAGE_SECONDS 60

#define MIN(X, Y) (((X) < (Y)) ? X : Y)

/* deadline - a program for counting down the time to a deadline for which a
 * set number of words need to be written, and supplying information about the
 * work still to be done.
 * Written by Graeme Cole, January 2005. Placed in the public domain.
 */

void print_help(void) {
	printf("Usage:\n  deadline [OPTIONS] (-c command | -f file) -t target -d yyyy/mm/dd hh:mm\n");
	printf("-c\tthe command to run to get the word-countable plain-text.\n");
	printf("-C\tthe command to run that outputs the number of words.\n");
	printf("-f\tthe file to word-count. If -f is given in conjunction with -C or -c,"
"\n\tthen the command is only run when the file is modified.\n");
	printf("-t\tthe target number of words.\n");
	printf("-d\tthe deadline date.\n");
	printf("OPTIONAL ARGUMENTS\n");
	printf("-1\tone-shot; print information once, then exit. Implies -o\n");
	printf("-F\tif -f is used, force loading file even if I think it hasn't changed.\n");
	printf("-a avg\taverage words per minute required (used to calculate smiley) is taken by"
"\n\tlooking at the samples over the past `avg' seconds (default is %d).\n",
DEFAULT_AVERAGE_SECONDS);
	printf("-i\tinterval in seconds between checks (default is %d).\n", DEFAULT_INTERVAL);
	printf("-o\tprint on stdout rather than stderr.\n");
	printf("-u\t(for dark consoles) use bash colour codes (-U = bold colours)\n");
	printf("-w cols\tspecify screen width for cursor positioning purposes (default is %d)\n", DEFAULT_SCREEN_WIDTH);
	printf("-h\tprint help.\n");
	printf("One of -c, -C and -f must be given. -t and -d must be given.\n");
	printf("deadline will run until interrupted.\n");
}

int wc_dash_w(FILE *f) {
	int c;
	int in_a_word = 0;
	int wc = 0;

	while ((c = fgetc(f)) != EOF) {
		if (!in_a_word)	{
			if (!isspace(c)) {
				++wc;
				in_a_word = 1;
			}
		}
		else {
			if (isspace(c)) {
				in_a_word = 0;
			}
		}
	}

	return wc;
}

time_t get_modified_time(char *filename) {
	struct stat statbuf;

	if (stat(filename, &statbuf) == -1)
		return -1;

	return statbuf.st_mtime;
}

void start_colour(FILE *f, int bold, int colour) {
	fprintf(f, "\033[%d;%dm", bold ? 1 : 0, colour);
}

void stop_colour(FILE *f) {
	fprintf(f, "\033[0m");
}

enum errornums {
	CANT_FOPEN = 0,
	CANT_POPEN,
	CANT_STAT,
	NO_NUMBER,
	NEGATIVE_NUMBER
};

char *error_strings[] = {
	/* First %s will be filled with strerror(errno). */
	"Can't open file: %s", /* CANT_FOPEN */
	"Can't open pipe to command: %s", /* CANT_POPEN */
	"Can't stat file: %s", /* CANT_STAT */
	"Word-counting command produced a non-number.", /* NO_NUMBER */
	"Word-counting command produced a negative number. I'm not going near that.", /* NEGATIVE_NUMBER */
};

float absolute(float f) {
	if (f < 0)
		return -f;
	else
		return f;
}

int main (int argc, char **argv) {
	char *command = NULL;
	char *filename = NULL;
	int screen_width = 0;

	/* OPTIONS */
	char command_barfs_number = 0;
	char force_load = 0;
	char oneshot = 0;
	char colours = 0;
	char bold = 0;
	FILE *out = stderr;
	int seconds_in_avg = DEFAULT_AVERAGE_SECONDS;

	/* REQUIRED DATA */
	int target = -1;
	int interval = DEFAULT_INTERVAL;
	struct tm deadline_tm;
	time_t deadline = -1;
	
	/* I */
	int i;

	if (argc <= 1) {
		print_help();
		exit(1);
	}

	for (i = 1; i < argc; ++i) {
		if (!strcmp("-h", argv[i]) || !strcmp("--help", argv[i])) {
			print_help();
			exit(0);
		}
		else if (!strcmp("-1", argv[i])) {
			oneshot = 1;
			out = stdout;
		}
		else if (!strcmp("-F", argv[i])) {
			force_load = 1;
		}
		else if (!strcmp("-o", argv[i])) {
			out = stdout;
		}
		else if (!strcmp("-u", argv[i])) {
			colours = 1;
		}
		else if (!strcmp("-U", argv[i])) {
			colours = bold = 1;
		}
		else if (!strcmp("-c", argv[i])) {
			if (command != NULL) {
				fprintf(stderr, "One command only please.\n");
				exit(1);
			}
			if (++i >= argc) {
				fprintf(stderr, "-c requires an argument.\n");
				exit(1);
			}
			command = argv[i];
			command_barfs_number = 0;
		}
		else if (!strcmp("-C", argv[i])) {
			if (command != NULL) {
				fprintf(stderr, "One command only please.\n");
				exit(1);
			}
			else if (++i >= argc) {
				fprintf(stderr, "-C requires an argument.\n");
				exit(1);
			}
			command = argv[i];
			command_barfs_number = 1;
		}
		else if (!strcmp("-f", argv[i])) {
			if (filename != NULL) {
				fprintf(stderr, "One file only please.\n");
				exit(1);
			}
			if (++i >= argc) {
				fprintf(stderr, "-f requires an argument.\n");
				exit(1);
			}
			filename = argv[i];
		}
		else if (!strcmp("-t", argv[i])) {
			if (++i >= argc) {
				fprintf(stderr, "-t requires an argument.\n");
				exit(1);
			}
			else if (sscanf(argv[i], "%d", &target) < 1) {
				fprintf(stderr, "-t requires a numeric argument.\n");
				exit(1);
			}
			else if (target < 1) {
				fprintf(stderr, "Your assignment is too damn easy. Goodbye.\n");
				exit(1);
			}
		}
		else if (!strcmp("-d", argv[i])) {
			if (++i >= argc) {
				fprintf(stderr, "-d requires a date argument (yyyy/mm/dd) followed by a time argument (hh:mm).\n");
				exit(1);
			}
			else if (sscanf(argv[i], "%d/%d/%d",
						&deadline_tm.tm_year,
						&deadline_tm.tm_mon,
						&deadline_tm.tm_mday) < 3) {
				fprintf(stderr, "-d requires a date argument in the form yyyy/mm/dd\n");
				exit(1);
			}
			else {
				if (deadline_tm.tm_year < 70)
					deadline_tm.tm_year += 100;
				else if (deadline_tm.tm_year >= 200)
					deadline_tm.tm_year -= 1900;
				
				deadline_tm.tm_mon--;
			}

			if (++i >= argc) {
				fprintf(stderr, "-d requires not only a date argument, but also a time argument (hh:mm).\n");
				exit(1);
			}
			else if (sscanf(argv[i], "%d:%d",
						&deadline_tm.tm_hour,
						&deadline_tm.tm_min) < 2) {
				fprintf(stderr, "-d requires a time argument after the date argument, in the form hh:mm\n");
				exit(1);
			}

			deadline_tm.tm_sec = 0;
			deadline_tm.tm_wday = -1;
			deadline_tm.tm_yday = -1;
			deadline_tm.tm_isdst = -1;

			deadline = mktime(&deadline_tm);
		}
		else if (!strcmp("-i", argv[i])) {
			if (++i >= argc) {
				fprintf(stderr, "-i requires an argument.\n");
				exit(1);
			}
			else if (sscanf(argv[i], "%d", &interval) < 1) {
				fprintf(stderr, "-i requires a numeric argument.\n");
				exit(1);
			}
			else if (interval < 1) {
				fprintf(stderr, "Very funny. -i requires a *positive* numeric argument.\n");
				exit(1);
			}
			
		}
		else if (!strcmp("-a", argv[i])) {
			if (++i >= argc) {
				fprintf(stderr, "-a requires an argument.\n");
				exit(1);
			}
			else if (sscanf(argv[i], "%d", &seconds_in_avg) < 1) {
				fprintf(stderr, "-a requires a numeric argument.\n");
				exit(1);
			}
			else if (seconds_in_avg < 1) {
				fprintf(stderr, "-a requires a positive numeric argument.\n");
				exit(1);
			}
		}
		else if (!strcmp("-w", argv[i])) {
			if (++i >= argc) {
				fprintf(stderr, "-w requires an argument.\n");
				exit(1);
			}
			else if (sscanf(argv[i], "%d", &screen_width) < 1) {
				fprintf(stderr, "-w requires a numeric argument.\n");
				exit(1);
			}
			else if (screen_width < 1) {
				fprintf(stderr, "-w requires a positive numeric argument.\n");
				exit(1);
			}
		}
		else {
			fprintf(stderr, "Invalid argument %s\n", argv[i]);
			exit(1);
		}
	}


	if (deadline == -1) {
		fprintf(stderr, "Either you didn't give a deadline (-d) or the crap you gave defeated mktime().\n");
		exit(1);
	}

	if (target == -1) {
		fprintf(stderr, "No target word count given. (use -h for help)\n");
		exit(1);
	}

	if (command == NULL && filename == NULL) {
		fprintf(stderr, 
"You need to give either a command to run to produce the plain text (with -c)\n"
"or a file name which contains the plain text (with -f)\n");
		exit(1);
	}

	if (screen_width == 0) {
		screen_width = DEFAULT_SCREEN_WIDTH;
	}

	srand(time(NULL) + getpid());
	
	while (1) {
		static int words = -1;
		time_t localtimenow, gmtimenow, remain;
		static time_t mod_last = -1;
		static time_t last_update = -1;
		int chars;
		static int samples = 0;
		static unsigned int iterations = 0;
		static int error = -1;
		float required;
		int update_this_time = 0;
		
		/* Average words per minute required over the last
		 * `seconds_in_avg' samples */
		static float average = 0;


		chars = 0;
		required = -1;

		gmtimenow = time(NULL);
		if (gmtimenow > last_update + interval) {
			update_this_time = 1;
			last_update = gmtimenow;
		}
		
		if (update_this_time) {
			/* If updating, get the number of words if we have to */
			if (command == NULL) {
				FILE *f;
				time_t mod = -1;
	
				if (!force_load && (mod = get_modified_time(filename)) == -1) {
					error = CANT_STAT;
				}
				else if (force_load || mod > mod_last) {
					/* need to reload it */
					f = fopen(filename, "r");
					if (f == NULL) {
						error = CANT_FOPEN;
					}
					else {
						words = wc_dash_w(f);
						fclose(f);
						error = -1;
					}
					mod_last = mod;
				}
			}
			else {
				FILE *p;
				time_t mod = -1;
	
				/* If a file has been specified with -f, only */
				/* run the command when that file is modified.*/

				if (!force_load && filename != NULL && (mod = get_modified_time(filename)) == -1) {
					error = CANT_STAT;
				}
				else if (force_load || filename == NULL || mod > mod_last) {
					p = popen(command, "r");
					if (p == NULL) {
						error = CANT_POPEN;
					}
					else if (command_barfs_number) {
						if (fscanf(p, "%d", &words) < 1) {
							/* ... I am a free man! */
							error = NO_NUMBER;
						}
						else if (words < 0) {
							error = NEGATIVE_NUMBER;
						}
						pclose(p);
					}
					else {
						words = wc_dash_w(p);
						pclose(p);
						error = -1;
					}
					mod_last = mod;
				}
			}
		}
	
		if (error == -1) {
			int foo;
			
			/* Get local time */
			localtimenow = mktime(localtime(&gmtimenow));
			
			remain = deadline - localtimenow;
			if (remain <= 0) {
				if (colours)
					start_colour(out, bold, 31);
				chars += fprintf(out, "%4.2d:%0.2d:%0.2d+ ",
						-remain / 3600, (-remain/60)%60,
						-remain % 60);
				if (colours)
					stop_colour(out);
			}
			else {
				if (colours)
					start_colour(out, bold, 32);
				chars += fprintf(out, "%4.2d:%0.2d:%0.2d  ",
						remain / 3600, (remain/60) % 60,
						remain % 60);
				if (colours)
					stop_colour(out);
			}

			if (words < target) {
				if (remain <= 0) {
					if (colours)
						start_colour(out, bold, 31);
					foo = fprintf(out, "%5d/%5d  %5d word%s remaining", words, target, target - words, (target - words) == 1 ? "" : "s");
					if (colours)
						stop_colour(out);
				}
				else {
					required = (float)(target - words) /
						((float)remain / (float)60);
					if (colours)
						start_colour(out, bold, 36);
					foo = fprintf(out, "%5d/%5d  %5d word%s remaining   ",
							words, target,
							target-words,
							target-words==1?"":"s");
					if (colours) {
						stop_colour(out);
						if (required < 4) {
							/* 0-4: white */
							start_colour(out, bold, 37);
						}
						else if (required < 15) {
							/* 4-15: yellow */
							start_colour(out, bold, 33);
						}
						else {
							/* >=15: red */
							start_colour(out, bold, 31);
						}
					}

					foo += fprintf(out,"%7.3f wpm required",
							(double) required);

					if (colours)
						stop_colour(out);
				}
			}
			else {
				if (colours)
					start_colour(out, bold, 32);
			        foo = fprintf(out, "%5d/%5d  %5d word%s spare", words, target, words - target, (words - target) == 1 ? "" : "s");
				if (colours)
					stop_colour(out);
			}

			chars += foo;

			/* Pad with spaces up to a point, then put the smiley */
			for (i = 0; i < 61 - foo; ++i) {
				fputc(' ', out);
				++chars;
			}

			if (words >= target) {
				if (colours)
					start_colour(out, bold, 32);
				switch (iterations % 2) {
					case 0:
						chars += fprintf(out, "\\o/");
						break;
					case 1:
						chars += fprintf(out, "<o>");
						break;
				}
				if (colours)
					stop_colour(out);
				required = 0;
			} 
			else if (remain <= 0) {
				if (colours)
					start_colour(out, bold, 31);
				chars += fprintf(out, ":-P");
				if (colours)
					stop_colour(out);
			}
			
			/* If there's still time remaining, recompute the
			 * average. We won't display WPM remaining, however, if
			 * we've already passed the number of words required.*/
			if (remain > 0) {
				int divisor = MIN(seconds_in_avg, samples);
				/* If updating, recompute average */
				average = (average * divisor + required) / (float) (divisor + 1);
				++samples;
				
				if (required > 0) {
					if (samples >= seconds_in_avg) {
						/* If the current required words
						   per minute is within 0.001 of
						   the average, then use a
						   neutral face. :-|
						   Else, if the current words
						   per minute required is LOWER
						   than the average, then put a
						   smiley face. :-)
						   Otherwise, put a frowning
						   face. :-(
						*/

						if (absolute(required - average) < 0.001) {
							if (colours)
								start_colour(out, bold, 37);
							chars += fprintf(out, ":-|");
							if (colours)
								stop_colour(out);
						}
						else if (required < average) {
							if (colours)
								start_colour(out, bold, 32);
							chars += fprintf(out, "%c-)", (rand()%256)?':':';');
							if (colours)
								stop_colour(out);
						}
						else {
							if (colours)
								start_colour(out, bold, 33);
							chars += fprintf(out, ":-(");
							if (colours)
								stop_colour(out);
						}
					}
					else {
						/* We don't have enough samples;
						 * no smiley yet */
						chars += fprintf(out, ":-?");
					}
				}
			}
		}
		else {
			chars += fprintf(out, error_strings[error], strerror(errno));
		}

		if (out == stdout) {
			putchar('\n');
		}

		if (oneshot) {
			exit(0);
		}

		if (out == stderr) {
			for (i = chars; i < screen_width - 1; ++i)
				fputc(' ', stderr);
		}

		++iterations;
		sleep(1);
		
		if (out == stderr) {
			fputc('\r', stderr);
			for (i = 0; i < chars; ++i)
				fputc(' ', stderr);
			fputc('\r', stderr);
		}
	}
	
	/* Here be dragons */
	return 0;
}
