/* watch -- execute a program repeatedly, displaying output fullscreen
 *
 * Based on the original 1991 'watch' by Tony Rems <rembo@unisoft.com>
 * (with mods and corrections by Francois Pinard).
 *
 * Substantially reworked, new features (differences option, SIGWINCH
 * handling, unlimited command length, long line handling) added Apr 1999 by
 * Mike Coleman <mkc@acm.org>.
 *
 * Changes by Albert Cahalan, 2002-2003.
 *
 * Changes by Todd LaWall <bitreaper [at] n357 [dot] com>, 2007 -- added 
 * save-history functionality, bumped version
 *
 */

#define VERSION "0.3.0"

#include <ctype.h>
#include <getopt.h>
#include <signal.h>
#include <ncurses.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
#include <termios.h>
#include <locale.h>
#include "proc/procps.h"

#ifdef FORCE_8BIT
#undef isprint
#define isprint(x) ( (x>=' '&&x<='~') || (x>=0xa0) )
#endif

static struct option longopts[] = {
	{"differences", optional_argument, 0, 'd'},
	{"help", no_argument, 0, 'h'},
	{"interval", required_argument, 0, 'n'},
	{"save-history", required_argument, 0, 's'},
	{"multifile", no_argument, 0, 'm'},
	{"no-title", no_argument, 0, 't'},
	{"version", no_argument, 0, 'v'},
	{0, 0, 0, 0}
};

static char usage[] =
    "Usage: %s [-dhsmntv] [--differences[=cumulative]] [--help] [--interval=<n>] [--save-history=<filename> [--multifile]] [--no-title] [--version] <command>\n";

static char *progname;

static int curses_started = 0;
static int height = 24, width = 80;
static int screen_size_changed = 0;
static int first_screen = 1;
static int show_title = 2;  // number of lines used, 2 or 0
static FILE* hist_file_fp = NULL;
static char* hist_filename = NULL; 
static char* hist_cmd = NULL;
static int option_multifile = 0; /* yes, this technically should be in main, but we need it outside of main too */

#define min(x,y) ((x) > (y) ? (y) : (x))

static void do_usage(void) NORETURN;
static void do_usage(void)
{
	fprintf(stderr, usage, progname);
	exit(1);
}

static void do_help(void)
{
  fprintf(stderr,"  -d, --differences[=cumulative]\thighlight changes between updates\n");
  fprintf(stderr,"\t\t(cumulative means highlighting is cumulative)\n");
  fprintf(stderr,"  -h, --help\t\t\t\tprint a summary of the options\n");
  fprintf(stderr,"  -n, --interval=<seconds>\t\tseconds to wait between updates\n");
  fprintf(stderr,"  -s, --save-history=<filename>\t\tsave off screen dump when differences show up.\n");
  fprintf(stderr,"\t\t\t\t\tRequires --differences\n");
  fprintf(stderr,"  -m, --multifile\t\t\t\tdump each change to an individual timestamped file\n");
  fprintf(stderr,"  -v, --version\t\t\t\tprint the version number\n");
  fprintf(stderr,"  -t, --no-title\t\t\tturns off showing the header\n");
}
static void do_exit(int status) NORETURN;
static void do_exit(int status)
{
	if (curses_started)
		endwin();
	if (hist_file_fp)
		fclose( hist_file_fp );
	exit(status);
}

/* signal handler */
static void die(int notused) NORETURN;
static void die(int notused)
{
	(void) notused;
	do_exit(0);
}

static void
winch_handler(int notused)
{
	(void) notused;
	screen_size_changed = 1;
}

static char env_col_buf[24];
static char env_row_buf[24];
static int incoming_cols;
static int incoming_rows;

static void
get_terminal_size(void)
{
	struct winsize w;
	if(!incoming_cols){  // have we checked COLUMNS?
		const char *s = getenv("COLUMNS");
		incoming_cols = -1;
		if(s && *s){
			long t;
			char *endptr;
			t = strtol(s, &endptr, 0);
			if(!*endptr && (t>0) && (t<(long)666)) incoming_cols = (int)t;
			width = incoming_cols;
			snprintf(env_col_buf, sizeof env_col_buf, "COLUMNS=%d", width);
			putenv(env_col_buf);
		}
	}
	if(!incoming_rows){  // have we checked LINES?
		const char *s = getenv("LINES");
		incoming_rows = -1;
		if(s && *s){
			long t;
			char *endptr;
			t = strtol(s, &endptr, 0);
			if(!*endptr && (t>0) && (t<(long)666)) incoming_rows = (int)t;
			height = incoming_rows;
			snprintf(env_row_buf, sizeof env_row_buf, "LINES=%d", height);
			putenv(env_row_buf);
		}
	}
	if (incoming_cols<0 || incoming_rows<0){
		if (ioctl(2, TIOCGWINSZ, &w) == 0) {
			if (incoming_rows<0 && w.ws_row > 0){
				height = w.ws_row;
				snprintf(env_row_buf, sizeof env_row_buf, "LINES=%d", height);
				putenv(env_row_buf);
			}
			if (incoming_cols<0 && w.ws_col > 0){
				width = w.ws_col;
				snprintf(env_col_buf, sizeof env_col_buf, "COLUMNS=%d", width);
				putenv(env_col_buf);
			}
		}
	}
}

static char* get_new_file_name( void ){
	time_t t;
	struct tm *now = NULL;
	char formatted_time[20];  /* 19 datetime chars and the \0 */
	static char newname[129]; /* 128 chars plus that \0 */
	t = time( NULL );
	now = localtime( &t );

	if( !hist_filename ){
		fprintf( stderr, "Cannot operate without a filename!  Don't know how we got here...\n" );
		exit( -1 );
	}
	strftime( formatted_time, 20, "%F_%T", now );
	snprintf( newname, 129, "%s_%s", hist_filename, formatted_time );
	return newname;
}

static void dump_screen_contents( void )
{
	int x = 0, y = 0;
	time_t t = time(NULL);
	int cols = 0;
	struct winsize w;

	if( !hist_file_fp ){
		fprintf( stderr, "dump_screen_contents(): attempted to dump screen contents to a null file pointer!\n" );
		exit( -1 );
	}

	/* print banner -- should we respect --no-title here? I don't think we should,
	   because if the files are lying about, wouldn't you like to know what command
	   generated them?  Now I could see leaving out the "Change seen at" banner, 
	   if only because the timestamp in the file name is good enough.
	*/
	fprintf( hist_file_fp, "================================================================================\n");
	fprintf( hist_file_fp, "Command run: %s\n", hist_cmd );
	fprintf( hist_file_fp, "Change seen at: %s", ctime(&t));
	fprintf( hist_file_fp, "================================================================================\n");

	/* get columns, so we can put newlines at the end of the lines in the screen */
	/* this makes it so diff can actually show you where things changed in a screen */

	if (ioctl(2, TIOCGWINSZ, &w) == 0) {
		cols = w.ws_col - 1;
	}

	/* now cycle through the screen and print it out */
	for( y = 0; y < height; y++ ){
		for( x = 0; x < width; x++ ){
			move( y, x );
			fputc( (inch() & A_CHARTEXT), hist_file_fp );
			if( x == cols ){
				fputc( '\n', hist_file_fp );
			}
		}
	}
	move( 0, 0 );
}

static void change_occured( void )
{
	char *temp_name = NULL;

	if( option_multifile ){
		if( hist_file_fp ){
			fclose( hist_file_fp );
		}
		temp_name = get_new_file_name();
		if (!(hist_file_fp = fopen( temp_name, "w" ) ) ){
			fprintf( stderr, "Could not open history file %s: %s\n", temp_name, strerror(errno) );
			 exit( -1 );
		}
	}

	dump_screen_contents( );
}

int
main(int argc, char *argv[])
{
	int optc;
	int option_differences = 0,
	    option_differences_cumulative = 0,
	    option_help = 0, option_version = 0,
	  option_savehistory = 0;
	double interval = 2;
	char *command;
	int command_length = 0;	/* not including final \0 */
	int difference_seen = 1; /* set to 1 for dumping the initial/starting screen */

	setlocale(LC_ALL, "");
	progname = argv[0];

	while ((optc = getopt_long(argc, argv, "+d::hn:s:mvt", longopts, (int *) 0))
	       != EOF) {
		switch (optc) {
		case 'd':
			option_differences = 1;
			if (optarg)
				option_differences_cumulative = 1;
			break;
		case 'h':
			option_help = 1;
			break;
		case 't':
			show_title = 0;
			break;
		case 'n':
			{
				char *str;
				interval = strtod(optarg, &str);
				if (!*optarg || *str)
					do_usage();
				if(interval < 0.1)
					interval = 0.1;
				if(interval > ~0u/1000000)
					interval = ~0u/1000000;
			}
			break;
		case 's':
			option_savehistory = 1;

			/* if we don't get optarg, then we're supposed to die anyway, getopt_long handles that, */
			/* yet I feel the compelling urge to check before copying anyway. */
			if( optarg ){

				/* silly getopt_long doesn't know if the next arg to a required arg is
				 * another dash or not, so we've got to police that, otherwise
				 * we'd have filenames starting with dashes if we forgot to put
				 * one on the command line before the next option.
				 * An example of this bug would be using "-s -m", which wouldn't turn
				 * on multifile, it'd create a file called "-m".  not what we expected...
				 */
				if( optarg[0] == '-' ){
					fprintf( stderr, "save-history file names cannot start with dashes, sorry!\n\n" );
					do_usage( );
					exit( -1 );
				}
				hist_filename = strdup( optarg );
			}else{
				fprintf(stderr,"Strange, no option was given to --save-history, and getopt_long didn't catch it...n");
				do_help();
				exit(-1);
			}
			break;
		case 'm':
			option_multifile = 1;
			break;
		case 'v':
			option_version = 1;
			break;
		default:
			do_usage();
			break;
		}
	}
	if (option_version) {
		fprintf(stderr, "%s\n", VERSION);
		if (!option_help)
			exit(0);
	}

	if (option_help) {
		fprintf(stderr, usage, progname);
		do_help();
		exit(0);
	}

	if (optind >= argc)
		do_usage();

	command = strdup(argv[optind++]);
	command_length = strlen(command);
	for (; optind < argc; optind++) {
		char *endp;
		int s = strlen(argv[optind]);
		command = realloc(command, command_length + s + 2);	/* space and \0 */
		endp = command + command_length;
		*endp = ' ';
		memcpy(endp + 1, argv[optind], s);
		command_length += 1 + s;	/* space then string length */
		command[command_length] = '\0';
	}

	/* this section must come after the command string init, so we can print the command if we are saving history */
	if( option_savehistory ){
		if( !option_differences ){
			fprintf(stderr,"Differences option must be specified in order to save history of differences\n");
			do_help();
			exit(-1);
		}

		hist_cmd = command; /* no copy necessary, just use main's copy but make it visible to functions outside main */

		/* Here's a Gotcha, or not so obvious thing:
		  the way we're doing multifile is to close the hist_file_fp pointer on entry to change_occured().
		  this means if we open a file up here, it won't get used, it will be opened, then closed, leaving
		  the first file of the set to be empty.  If we leave the file pointer null for the first pass, 
		  change_occured() will do the right thing, and skip closing the null handle, and open up a good 
		  file handle before dumping the screen to it.

		  If it's a single history file, then here's where we need to open it. 
		*/
		if( !option_multifile ){
			if (!(hist_file_fp = fopen( hist_filename, "w" ) ) ){
				fprintf( stderr, "Could not open history file %s: %s\n", hist_filename, strerror(errno) );
				exit( -1 );
			}
		}
	}

	get_terminal_size();

	/* Catch keyboard interrupts so we can put tty back in a sane state.  */
	signal(SIGINT, die);
	signal(SIGTERM, die);
	signal(SIGHUP, die);
	signal(SIGWINCH, winch_handler);

	/* Set up tty for curses use.  */
	curses_started = 1;
	initscr();
	nonl();
	noecho();
	cbreak();

	for (;;) {
		time_t t = time(NULL);
		char *ts = ctime(&t);
		int tsl = strlen(ts);
		char *header;
		FILE *p;
		int x, y;
		int oldeolseen = 1;

		if (screen_size_changed) {
			get_terminal_size();
			resizeterm(height, width);
			clear();
			/* redrawwin(stdscr); */
			screen_size_changed = 0;
			first_screen = 1;
		}

		if (show_title) {
			// left justify interval and command,
			// right justify time, clipping all to fit window width
			asprintf(&header, "Every %.1fs: %.*s",
				interval, min(width - 1, command_length), command);
			mvaddstr(0, 0, header);
			if (strlen(header) > (size_t) (width - tsl - 1))
				mvaddstr(0, width - tsl - 4, "...  ");
			mvaddstr(0, width - tsl + 1, ts);
			free(header);
		}

		if (!(p = popen(command, "r"))) {
			perror("popen");
			do_exit(2);
		}

		for (y = show_title; y < height; y++) {
			int eolseen = 0, tabpending = 0;
			for (x = 0; x < width; x++) {
				int c = ' ';
				int attr = 0;

				if (!eolseen) {
					/* if there is a tab pending, just spit spaces until the
					   next stop instead of reading characters */
					if (!tabpending)
						do
							c = getc(p);
						while (c != EOF && !isprint(c)
						       && c != '\n'
						       && c != '\t');
					if (c == '\n')
						if (!oldeolseen && x == 0) {
							x = -1;
							continue;
						} else
							eolseen = 1;
					else if (c == '\t')
						tabpending = 1;
					if (c == EOF || c == '\n' || c == '\t')
						c = ' ';
					if (tabpending && (((x + 1) % 8) == 0))
						tabpending = 0;
				}
				move(y, x);
				if (option_differences) {
					int oldch = inch();
					char oldc = oldch & A_CHARTEXT;
					attr = !first_screen
					    && (c != oldc
						||
						(option_differences_cumulative
						 && (oldch & A_ATTRIBUTES)));
					if( c != oldc ){
						difference_seen = 1;
					}
				}
				if (attr)
					standout();
				addch(c);
				if (attr)
					standend();
			}
			oldeolseen = eolseen;
		}

		pclose(p);

		if( option_savehistory && difference_seen ){
			change_occured();
			difference_seen = 0;
		}

		first_screen = 0;
		refresh();
		usleep(interval * 1000000);
	}

	endwin();

	return 0;
}
