/*
 * watch - execute a program repeatedly, displaying the 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>.
*/

#include <ctype.h>
#include <getopt.h>
#include <signal.h>
#include <ncurses.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>

#include "proc/version.h"

static struct option longopts[] = {
	{"differences", optional_argument, 0, 'd'},
	{"help", no_argument, 0, 'h'},
	{"interval", required_argument, 0, 'n'},
	{"version", no_argument, 0, 'v'},
	{0, 0, 0, 0}
};

static int curses_started = 0;
static int height = 24, width = 80;
static int screen_size_changed = 0;
static int first_screen = 1;

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

static void usage(const char *cmd)
{
	fprintf(stderr, "usage: %s [options] command\n", cmd);
	fprintf(stderr, "    --differences[=cumulative], -d    "
			"highlight changes between updates\n");
	fprintf(stderr, "    --interval=<seconds>, -n          "
			"seconds to wait between updates\n");
	fprintf(stderr, "    --help                            "
			"print a summary of the options\n");
	fprintf(stderr, "    --version, -V                     "
			"print version information and quit\n");
}

static void do_exit(int status)
{
	if (curses_started)
		endwin();
	exit(status);
}

/*
 * die - signal handler for SIGINT, SIGTERM, and SIGUP
 */
static void die(int unused)
{
	(void) unused;
	do_exit(0);
}

/*
 * winch_handler - signal handler for SIGWINCH
 */
static void winch_handler(int unused)
{
	(void) unused;
	screen_size_changed = 1;
}

static void get_terminal_size(void)
{
	struct winsize w;
	if (ioctl(2, TIOCGWINSZ, &w) == 0) {
		if (w.ws_row > 0)
			height = w.ws_row;
		if (w.ws_col > 0)
			width = w.ws_col;
	}
}

int main(int argc, char *argv[])
{
	int opt;
	int option_differences = 0, option_differences_cumulative = 0;
	int interval = 2;
	char *command;
	int command_length = 0;	/* not including final \0 */

	while ((opt = getopt_long(argc, argv, "d::n:hV", longopts,
			NULL)) != EOF) {
		switch (opt) {
		case 'd':
			option_differences = 1;
			if (optarg)
				option_differences_cumulative = 1;
			break;
		case 'n':
			{
				char *s;
				interval = strtol(optarg, &s, 10);
				if (!*optarg || *s) {
					usage(argv[0]);
					return 1;
				}
			}
			break;
		case 'V':
			display_version();
			return 0;
		case 'h':
			usage(argv[0]);
			return 0;
		default:
			usage(argv[0]);
			return 1;
		}
	}

	if (optind >= argc) {
		usage(argv[0]);
		return 1;
	}

	command = strdup(argv[optind++]);
	command_length = strlen(command);
	for (; optind < argc; optind++) {
		int s = strlen(argv[optind]);
		char *endp;

		command = realloc(command, command_length + s + 2);
		endp = &command[command_length];
		*endp = ' ';
		command_length += s + 1;
		strcpy(endp + 1, argv[optind]);
	}

	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();

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

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

		/*
		 * left justify interval and command, right justify time,
		 * clipping all to fit window width
		 */
		asprintf(&header, "Every %ds: %.*s",
			 interval, max(width - 1, command_length), command);
		mvaddstr(0, 0, header);
		if (strlen(header) > (unsigned int) (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);
		}

		last_was_eol = 0;
		for (y = 2; 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
					 */
				      restart:
					if (!tabpending)
						do
							c = getc(p);
						while (c != EOF && !isprint(c)
						       && c != '\n'
						       && c != '\t');

					/* Avoid putting the newline first at the row if the last
					 * character on the last row was not a newline.
					 * This avoids empty lines when a newline gets pushed one char
					 * off the end of a row.
					 */
					if (x == 0 && c == '\n'
					    && !last_was_eol) {
						last_was_eol = 1;
						goto restart;
					}

					if (c == '\n')
						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 (attr)
					standout();
				addch(c);
				if (attr)
					standend();
				last_was_eol = eolseen;
			}
		}

		pclose(p);

		first_screen = 0;
		refresh();
		sleep(interval);
	}

	endwin();

	return 0;
}
