/* ttym - countdown / stopwatch in a single C file.
 * See LICENSE file for copyright and license details.
 */
#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

#include "config.h"

/* ===== types & globals ===== */

typedef struct {
	int quiet;            /* -q: suppress completion bell */
	int silent;           /* -s: no bell, no extras */
} Config;

static struct termios oldtios;
static int ttyfd = -1;
static int rawset = 0;
static volatile sig_atomic_t interrupted = 0;

/* exposed for patches that read the final elapsed seconds at exit */
static long elapsed_s = 0;

/* nonzero once stdout is confirmed a tty; gates cursor/redraw escapes */
static int out_tty = 0;

/* ===== helpers ===== */

static void
die(const char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);
	fputc('\n', stderr);
	exit(1);
}

static void
sleep_ms(int ms)
{
	struct timespec ts = { ms / 1000, (long)(ms % 1000) * 1000000L };
	nanosleep(&ts, NULL);
}

static long
now_ms(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
}

static int
term_cols(void)
{
	struct winsize ws;
	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
		return ws.ws_col;
	return 80;
}

static void
on_signal(int sig)
{
	(void)sig;
	interrupted = 1;
}

static void
restore_tty(void)
{
	if (rawset && ttyfd >= 0)
		tcsetattr(ttyfd, TCSANOW, &oldtios);
	rawset = 0;
	if (out_tty)
		fputs("\033[?7h\033[?25h", stdout);  /* re-enable wrap, show cursor */
	fputc('\n', stdout);
	fflush(stdout);
}

/* ===== duration parsing =====
 * accepts: 25m | 90s | 2h | 1h30m | 01:30:00 | 25:00
 */

static int
parse_uint(const char *s, size_t n, long *out)
{
	long v = 0;
	if (n == 0) return -1;
	for (size_t i = 0; i < n; i++) {
		if (!isdigit((unsigned char)s[i])) return -1;
		v = v * 10 + (s[i] - '0');
		if (v > 1000000) return -1;
	}
	*out = v;
	return 0;
}

static long
parse_duration(const char *s)
{
	size_t len = strlen(s);
	if (len == 0) return -1;

	if (strchr(s, ':')) {
		/* Split on ':' into 2 or 3 fields, then parse each with
		 * parse_uint. That rejects empty fields, signs and
		 * whitespace, and bounds every component -- so unlike a
		 * raw sscanf("%ld") this cannot overflow on huge input.
		 */
		const char *fld[3];
		size_t flen[3];
		int nf = 0;
		size_t fstart = 0;
		for (size_t i = 0; i <= len; i++) {
			if (i != len && s[i] != ':') continue;
			if (nf == 3) return -1;
			fld[nf] = s + fstart;
			flen[nf] = i - fstart;
			nf++;
			fstart = i + 1;
		}
		long v[3] = { 0, 0, 0 };
		for (int k = 0; k < nf; k++)
			if (parse_uint(fld[k], flen[k], &v[k]) < 0)
				return -1;
		long total;
		if (nf == 3) {
			if (v[1] >= 60 || v[2] >= 60) return -1;
			total = v[0] * 3600 + v[1] * 60 + v[2];
		} else if (nf == 2) {
			if (v[1] >= 60) return -1;
			total = v[0] * 60 + v[1];
		} else {
			return -1;
		}
		return total > 0 ? total : -1;
	}

	long total = 0;
	size_t i = 0, start = 0;
	int have_any = 0;
	while (i < len) {
		if (isdigit((unsigned char)s[i])) { i++; continue; }
		if (s[i] != 'h' && s[i] != 'm' && s[i] != 's') return -1;
		long v;
		if (parse_uint(s + start, i - start, &v) < 0) return -1;
		switch (s[i]) {
		case 'h': total += v * 3600; break;
		case 'm': total += v * 60;   break;
		case 's': total += v;        break;
		}
		have_any = 1;
		i++;
		start = i;
	}
	if (!have_any || start != len) return -1;
	return total > 0 ? total : -1;
}

/* ===== formatting ===== */

static void
fmt_clock(long s, char *buf, size_t bufsz)
{
	long h = s / 3600; s %= 3600;
	long m = s / 60;  long sec = s % 60;
	if (h > 0) snprintf(buf, bufsz, "%02ld:%02ld:%02ld", h, m, sec);
	else       snprintf(buf, bufsz, "%02ld:%02ld", m, sec);
}

/* ===== drawing ===== */

static void
draw_countdown(long total, long elapsed_ms, int paused, int barw)
{
	if (total <= 0) return;
	long total_ms = total * 1000L;
	if (elapsed_ms < 0) elapsed_ms = 0;
	if (elapsed_ms > total_ms) elapsed_ms = total_ms;
	long es = elapsed_ms / 1000;
	long remaining = total - es;
	long pct = (elapsed_ms * 100) / total_ms;
	int fill = (int)((long)barw * elapsed_ms / total_ms);
	if (fill > barw) fill = barw;
	int empty = barw - fill;

	char rem[24], el[24];
	fmt_clock(remaining, rem, sizeof(rem));
	fmt_clock(es, el, sizeof(el));

	const char *color  = paused ? "\033[36m" : "\033[33m";
	const char *status = paused ? "PAUSED " : "       ";

	printf("\r%sT-%s\033[0m \033[2m+%s\033[0m [", color, rem, el);
	for (int i = 0; i < fill;  i++) fputs(BAR_FILL,  stdout);
	for (int i = 0; i < empty; i++) fputs(BAR_EMPTY, stdout);
	printf("] %3ld%% %s", pct, status);
	fflush(stdout);
}

static void
draw_stopwatch(long elapsed_ms, int paused)
{
	long es = elapsed_ms / 1000;
	char el[24];
	fmt_clock(es, el, sizeof(el));
	const char *color  = paused ? "\033[36m" : "\033[32m";
	const char *status = paused ? "PAUSED " : "       ";
	printf("\r%sT+%s\033[0m %s                          ",
	       color, el, status);
	fflush(stdout);
}

/* ===== usage =====
 * Patches MUST update this in lockstep with new flags so -h always
 * reflects the build.
 */

static void
usage(FILE *f)
{
	fprintf(f,
"usage:\n"
"  timer [FLAGS] DURATION    countdown\n"
"  timer [FLAGS]             stopwatch\n"
"\n"
"DURATION:  25m | 90s | 2h | 1h30m | 01:30:00 | 25:00\n"
"FLAGS:\n"
"  -q              no sound\n"
"  -s              silent\n"
"  --              end of options\n"
"  -h              help\n"
"\n"
"controls:  [space] pause/resume   [q | Ctrl+C] quit\n");
}

/* ===== run loop ===== */

static int
run(long total, int stopwatch, const Config *cfg)
{
	out_tty = isatty(STDOUT_FILENO);
	int cols = term_cols();
	int barw = cols - 40;
	if (barw < 10) barw = 10;

	ttyfd = open("/dev/tty", O_RDONLY);
	if (ttyfd >= 0 && isatty(ttyfd)) {
		if (tcgetattr(ttyfd, &oldtios) == 0) {
			struct termios raw = oldtios;
			raw.c_lflag &= ~(ICANON | ECHO);
			raw.c_cc[VMIN] = 0;
			raw.c_cc[VTIME] = 0;
			if (tcsetattr(ttyfd, TCSANOW, &raw) == 0)
				rawset = 1;
		}
	}

	atexit(restore_tty);
	struct sigaction sa = {0};
	sa.sa_handler = on_signal;
	sigaction(SIGINT,  &sa, NULL);
	sigaction(SIGTERM, &sa, NULL);

	if (out_tty)
		fputs("\033[?25l\033[?7l", stdout);

	long start_ms    = now_ms();
	long pause_accum = 0;
	long pause_start = 0;
	int  paused      = 0;
	int  user_quit   = 0;

	for (;;) {
		if (interrupted) { user_quit = 1; break; }

		long now = now_ms();
		long elapsed_ms = paused
			? pause_start - start_ms - pause_accum
			: now - start_ms - pause_accum;
		if (elapsed_ms < 0) elapsed_ms = 0;

		if (stopwatch) {
			if (out_tty)
				draw_stopwatch(elapsed_ms, paused);
		} else {
			if (out_tty)
				draw_countdown(total, elapsed_ms, paused, barw);
			if (!paused && elapsed_ms >= total * 1000L) break;
		}

		long frame_end = now_ms() + 100;
		for (;;) {
			if (interrupted) break;
			long rem_frame = frame_end - now_ms();
			if (rem_frame <= 0) break;
			if (ttyfd < 0) { sleep_ms((int)rem_frame); break; }

			struct pollfd pfd = { .fd = ttyfd, .events = POLLIN };
			int pr = poll(&pfd, 1, (int)rem_frame);
			if (pr > 0 && (pfd.revents & POLLIN)) {
				char c;
				ssize_t n = read(ttyfd, &c, 1);
				if (n == 1) {
					if (c == ' ') {
						if (paused) {
							pause_accum += now_ms() - pause_start;
							paused = 0;
						} else {
							pause_start = now_ms();
							paused = 1;
						}
					} else if (c == 'q' || c == 'Q') {
						user_quit = 1;
						goto done;
					}
				}
			} else if (pr == 0) {
				break;
			} else {
				sleep_ms((int)rem_frame);
				break;
			}
		}
	}
done:

	{
		long final_now = now_ms();
		long final_ms = paused
			? pause_start - start_ms - pause_accum
			: final_now  - start_ms - pause_accum;
		if (final_ms < 0) final_ms = 0;
		elapsed_s = final_ms / 1000;
	}

	if (stopwatch) {
		char el[24];
		fmt_clock(elapsed_s, el, sizeof(el));
		if (out_tty)
			printf("\r\033[32mSTOP %s%*s\033[0m\n", el, 30, "");
		else
			printf("STOP %s\n", el);
	} else if (user_quit) {
		char el[24];
		fmt_clock(elapsed_s, el, sizeof(el));
		if (out_tty)
			printf("\r\033[33mQUIT at %s%*s\033[0m\n", el, 30, "");
		else
			printf("QUIT at %s\n", el);
	} else {
		char tb[24];
		fmt_clock(total, tb, sizeof(tb));
		if (out_tty)
			printf("\r\033[32mDONE %s 100%%%*s\033[0m\n", tb, 20, "");
		else
			printf("DONE %s 100%%\n", tb);
		if (!cfg->silent && !cfg->quiet && out_tty) {
			fputc('\a', stdout);
			fflush(stdout);
			sleep_ms(50);
		}
	}

	(void)cfg;
	return user_quit ? 130 : 0;
}

/* ===== main ===== */

int
main(int argc, char **argv)
{
	Config cfg = {0};

	/* Pre-strip long-form flags before getopt. Stop at "--". */
	int new_argc = 0;
	char **new_argv = malloc(sizeof(char *) * (argc + 1));
	if (!new_argv) die("oom");
	int past_dd = 0;
	for (int i = 0; i < argc; i++) {
		if (!past_dd && !strcmp(argv[i], "--")) {
			past_dd = 1;
			new_argv[new_argc++] = argv[i];
			continue;
		}
		new_argv[new_argc++] = argv[i];
	}
	new_argv[new_argc] = NULL;

	int opt;
	while ((opt = getopt(new_argc, new_argv, "qsh")) != -1) {
		switch (opt) {
		case 'q': cfg.quiet  = 1; break;
		case 's': cfg.silent = 1; break;
		case 'h': usage(stdout); free(new_argv); return 0;
		default:  usage(stderr); free(new_argv); return 1;
		}
	}
	int rem_argc = new_argc - optind;
	char **rem_argv = new_argv + optind;

	int stopwatch = (rem_argc < 1);
	long total = 0;
	if (!stopwatch) {
		total = parse_duration(rem_argv[0]);
		if (total < 0) {
			fprintf(stderr, "bad duration: %s\n", rem_argv[0]);
			free(new_argv);
			return 1;
		}
	}

	int rc = run(total, stopwatch, &cfg);
	free(new_argv);
	return rc;
}
