/*
 * Copyright (C) 1984-2026  Mark Nudelman
 *
 * You may distribute under the terms of either the GNU General Public
 * License or the Less License, as specified in the README file.
 *
 * For more information, see the README file.
 */

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include "lt_types.h"
#include "wchar.h"

static const char version[] = "lt_screen|v=1";

#define ERROR_CHAR       ' '
#define WIDESHADOW_CHAR  ((wchar)0)
static char const* osc8_start[] = { "\033]8;", NULL };
static char const* osc8_end[] = { "\033\\", "\7", NULL };

#define NUM_LASTCH 4 // must be >= strlen(osc8_*[*])
static wchar lastch[NUM_LASTCH];
static int lastch_curr = 0;

int usage(void) {
	fprintf(stderr, "usage: lt_screen [-w width] [-h height] [-qv]\n");
	return 0;
}

// ------------------------------------------------------------------

#define MAX_PARAMS         3

typedef struct ScreenChar {
	wchar ch;
	Attr attr;
	Color fg_color;
	Color bg_color;
} ScreenChar;

typedef struct ScreenState {
	ScreenChar* chars;
	int w;
	int h;
	int cx;
	int cy;
	Attr curr_attr;
	Color curr_fg_color;
	Color curr_bg_color;
	int param_top;
	int params[MAX_PARAMS+1];
	int in_esc;
	int in_osc8;
} ScreenState;

static ScreenState screen;
static int ttyin; // input text and control sequences
static int ttyout; // output for screen dump
static int quiet = 0;
static int verbose = 0;

// ------------------------------------------------------------------

// Initialize ScreenState.
static void screen_init(void) {
	screen.w = 80;
	screen.h = 24;
	screen.cx = 0;
	screen.cy = 0;
	screen.in_esc = 0;
	screen.in_osc8 = 0;
	screen.curr_attr = 0;
	screen.curr_fg_color = screen.curr_bg_color = NULL_COLOR;
	screen.param_top = -1;
	screen.params[0] = 0;
}

static int num_params(void) {
	return screen.param_top+1;
}

static void param_print(void) {
	int i;
	fprintf(stderr, "(");
	for (i = 0; i < num_params(); ++i)
		fprintf(stderr, "%d ", screen.params[i]);
	fprintf(stderr, ")");
}

static void param_clear(void) {
	screen.param_top = -1;
}

static void param_push(int v) {
	if (screen.param_top >= (int) countof(screen.params)-1) {
		param_clear();
		return;
	}
	screen.params[++screen.param_top] = v;
}

static int param_pop(void){
	if (num_params() == 0)
		return -1; // missing param 
	return screen.params[screen.param_top--];
}

static int screen_x(int x) {
	if (x < 0) x = 0;
	if (x >= screen.w) x = screen.w-1;
	return x;
}

static int screen_y(int y) {
	if (y < 0) y = 0;
	if (y >= screen.h) y = screen.h-1;
	return y;
}

// Return the char at a given screen position.
static ScreenChar* screen_char(int x, int y) {
	x = screen_x(x);
	y = screen_y(y);
	return &screen.chars[y * screen.w + x];
}

// Step the cursor after printing a char.
static int screen_incr(int* px, int* py) {
	if (++(*px) >= screen.w) {
		*px = 0;
		if (++(*py) >= screen.h) {
			*py = 0;
			return 0;
		}
	}
	return 1;
}

// Set the value, attributes and colors of a char on the screen.
static void screen_char_set(int x, int y, wchar ch, Attr attr, Color fg_color, Color bg_color) {
	ScreenChar* sc = screen_char(x, y);
	sc->ch = ch;
	sc->attr = attr;
	sc->fg_color = fg_color;
	sc->bg_color = bg_color;
}

static int screen_clear(int x, int y, int count) {
	while (count-- > 0) {
		screen_char_set(x, y, '_', 0, NULL_COLOR, NULL_COLOR);
		screen_incr(&x, &y);
	}
	return 1;
}

static void store_hex(byte** pp, int val) {
	char hexchar[] = "0123456789ABCDEF";
	*(*pp)++ = hexchar[(val >> 4) & 0xf];
	*(*pp)++ = hexchar[val & 0xf];
}

// Print an encoded image of the current screen to ttyout.
// The LTS_CHAR_* metachars encode changes of color and attribute.
static int screen_read(int x, int y, int count) {
	Attr attr = 0;
	int fg_color = NULL_COLOR;
	int bg_color = NULL_COLOR;
	while (count-- > 0) {
		byte buf[32];
		byte* bufp = buf;
		ScreenChar* sc = screen_char(x, y);
		if (sc->attr != attr) {
			attr = sc->attr;
			*bufp++ = LTS_CHAR_ATTR;
			store_hex(&bufp, attr);
		}
		if (sc->fg_color != fg_color) {
			fg_color = sc->fg_color;
			*bufp++ = LTS_CHAR_FG_COLOR;
			store_hex(&bufp, fg_color);
		}
		if (sc->bg_color != bg_color) {
			bg_color = sc->bg_color;
			*bufp++ = LTS_CHAR_BG_COLOR;
			store_hex(&bufp, bg_color);
		}
		if (x == screen.cx && y == screen.cy)
			*bufp++ = LTS_CHAR_CURSOR;
		if (sc->ch == '\\' || sc->ch == LTS_CHAR_ATTR || sc->ch == LTS_CHAR_FG_COLOR || sc->ch == LTS_CHAR_BG_COLOR || sc->ch == LTS_CHAR_CURSOR)
			*bufp++ = '\\';
		store_wchar(&bufp, sc->ch);
		write(ttyout, buf, bufp-buf);
		screen_incr(&x, &y);
	}
	write(ttyout, "\n", 1);
	return 1;
}

static int screen_move(int x, int y) {
	screen.cx = screen_x(x);
	screen.cy = screen_y(y);
	return 1;
}

static int screen_cr(void) {
	screen.cx = 0;
	return 1;
}

static int screen_bs(void) {
	if (screen.cx <= 0) return 0;
	--screen.cx;
	return 1;
}

static int screen_scroll(void) {
	int len = screen.w * (screen.h-1);
	memmove(screen_char(0,0), screen_char(0,1), len * sizeof(ScreenChar));
	screen_clear(0, screen.h-1, screen.w);
	return 1;
}

static int screen_rscroll(void) {
	int len = screen.w * (screen.h-1);
	memmove(screen_char(0,1), screen_char(0,0), len * sizeof(ScreenChar));
	screen_clear(0, 0, screen.w);
	return 1;
}

static int screen_set_attr(int attr) {
	screen.curr_attr |= attr;
	if (verbose) fprintf(stderr, "[%d,%d] set_attr(%d)=%d\n", screen.cx, screen.cy, attr, screen.curr_attr);
	return 1;
}

static int screen_clear_attr(int attr) {
	screen.curr_attr &= ~attr;
	if (verbose) fprintf(stderr, "[%d,%d] clr_attr(%d)=%d\n", screen.cx, screen.cy, attr, screen.curr_attr);
	return 1;
}

// ------------------------------------------------------------------ 
// lt_screen supports certain ANSI color values.
// This simplifies testing SGR sequences with less -R 
// compared to inventing custom color sequences.
static int screen_set_color(int color) {
	int ret = 0;
	switch (color) {
	case 1:  ret = screen_set_attr(ATTR_BOLD); break;
	case 4:  ret = screen_set_attr(ATTR_UNDERLINE); break;
	case 5:
	case 6:  ret = screen_set_attr(ATTR_BLINK); break;
	case 7:  ret = screen_set_attr(ATTR_STANDOUT); break;
	case 21:
	case 22: ret = screen_clear_attr(ATTR_BOLD); break;
	case 24: ret = screen_clear_attr(ATTR_UNDERLINE); break;
	case 25: ret = screen_clear_attr(ATTR_BLINK); break;
	case 27: ret = screen_clear_attr(ATTR_STANDOUT); break;
	// case 38: break;
	// case 48: break;
	default: 
		if (color <= 0) {
			screen.curr_fg_color = screen.curr_bg_color = NULL_COLOR;
			screen.curr_attr = 0;
			ret = 1;
		} else if (color == 39) {
			screen.curr_fg_color = NULL_COLOR;
			ret = 1;
		} else if (color == 49) {
			screen.curr_bg_color = NULL_COLOR;
			ret = 1;
		} else if ((color >= 30 && color <= 37) || (color >= 90 && color <= 97)) {
			screen.curr_fg_color = color;
			ret = 1;
		} else if ((color >= 40 && color <= 47) || (color >= 100 && color <= 107)) {
			screen.curr_bg_color = color;
			ret = 1;
		} else {
			fprintf(stderr, "[%d,%d] unrecognized color %d\n", screen.cx, screen.cy, color);
		}
		if (verbose) fprintf(stderr, "[%d,%d] set_color(%d)=%d/%d\n", screen.cx, screen.cy, color, screen.curr_fg_color, screen.curr_bg_color);
		break;
	}
	return ret;
}

// ------------------------------------------------------------------ 

static void beep(void) {
	if (!quiet)
		fprintf(stderr, "\7");
}

// Execute an escape sequence ending with a given char.
static int exec_esc(wchar ch) {
	int x, y, count;
	if (verbose) {
		fprintf(stderr, "exec ESC-%c ", (char)ch);
		param_print();
		fprintf(stderr, "\n");
	}
	switch (ch) {
	case 'A': // clear all 
		return screen_clear(0, 0, screen.w * screen.h);
	case 'L': // clear from cursor to end of line 
		return screen_clear(screen.cx, screen.cy, screen.w - screen.cx);
	case 'S': // clear from cursor to end of screen 
		return screen_clear(screen.cx, screen.cy, 
			(screen.w - screen.cx) + (screen.h - screen.cy -1) * screen.w);
	case 'R': // read N3 chars starting at (N1,N2)
		count = param_pop();
		y = param_pop();
		x = param_pop();
		if (x < 0) x = 0;
		if (y < 0) y = 0;
		if (count < 0) count = 0;
		return screen_read(x, y, count);
	case 'j': // jump cursor to (N1,N2)
		y = param_pop();
		x = param_pop();
		return screen_move(x, y);
	case 'g': // visual bell 
		return 0;
	case 'h': // cursor home 
		return screen_move(0, 0);
	case 'l': // cursor lower left 
		return screen_move(0, screen.h-1);
	case 'r': // reverse scroll
		return screen_rscroll();
	case '<': // cursor left to start of line
		return screen_cr();
	case 'e': // exit bold
		return screen_clear_attr(ATTR_BOLD);
	case 'b': // enter blink
		return screen_set_attr(ATTR_BLINK);
	case 'c': // exit blink
		return screen_clear_attr(ATTR_BLINK);
	case 'm': // SGR (Select  Graphics Rendition)
		if (num_params() == 0) {
			screen_set_color(-1);
		} else {
			while (num_params() > 0)
				screen_set_color(param_pop());
		}
		return 0;
	case '?': // print version string
		write(ttyout, version, strlen(version));
		return 1;
	default:
		return 0;
	}
}

// Print a char on the screen.
// Handles cursor movement and scrolling.
static int add_char(wchar ch) {
	//if (verbose) fprintf(stderr, "add (%c) %lx at %d,%d\n", (char)ch, (long)ch, screen.cx, screen.cy);
	screen_char_set(screen.cx, screen.cy, ch, screen.curr_attr, screen.curr_fg_color, screen.curr_bg_color);
	int fits = 1;
	int zero_width = (is_composing_char(ch) || 
	    (screen.cx > 0 && is_combining_char(screen_char(screen.cx-1,screen.cy)->ch, ch)));
	if (!zero_width) {
		fits = screen_incr(&screen.cx, &screen.cy);
		if (fits) {
			if (is_wide_char(ch)) {
				// The "shadow" is the second column used by a wide char.
				screen_char_set(screen.cx, screen.cy, WIDESHADOW_CHAR, 0, NULL_COLOR, NULL_COLOR);
				fits = screen_incr(&screen.cx, &screen.cy);
			}
			if (screen_char(screen.cx, screen.cy)->ch == WIDESHADOW_CHAR) {
				// We overwrote the first half of a wide character.
				// Change the orphaned shadow to an error char.
				screen_char_set(screen.cx, screen.cy, ERROR_CHAR, screen.curr_attr, NULL_COLOR, NULL_COLOR);
			}
		}
	}
	if (!fits) { // Wrap at bottom of screen = scroll
		screen.cx = 0;
		screen.cy = screen.h-1;
		return screen_scroll();
	}
	return 1;
}

// Remember the last few chars sent to the screen.
static void add_last(wchar ch) {
	lastch[lastch_curr++] = ch;
	if (lastch_curr >= NUM_LASTCH) lastch_curr = 0;
}

// Do the last entered characters match a string?
static int last_matches_str(char const* str) {
	int ci = lastch_curr;
	int si;
	for (si = strlen(str)-1; si >= 0; --si) {
		ci = (ci > 0) ? ci-1 : NUM_LASTCH-1;
		if (str[si] != lastch[ci]) return 0;
	}
	return 1;
}

// Do the last entered characters match any one of a list of strings?
static int last_matches(const char* const* tbl) {
	int ti;
	for (ti = 0; tbl[ti] != NULL; ++ti) {
		if (last_matches_str(tbl[ti]))
			return 1;
	}
	return 0;
}

// Handle a char sent to the screen while it is receiving an escape sequence.
static int process_esc(wchar ch) {
	int ok = 1;
	if (screen.in_osc8) {
		if (last_matches(osc8_end)) {
			screen.in_osc8 = screen.in_esc = 0;
		} else {
			// Discard everything between osc8_start and osc8_end.
		}
	} else if (last_matches(osc8_start)) {
		param_pop(); // pop the '8'
		screen.in_osc8 = 1;
	} else if (ch >= '0' && ch <= '9') {
		int d = (num_params() == 0) ? 0 : screen.params[screen.param_top--];
		param_push(10 * d + ch - '0');
	} else if (ch == ';') {
		param_push(0);
	} else if (ch == '[' || ch == ']') {
		; // Ignore ANSI marker
	} else { // end of escape sequence
		screen.in_esc = 0;
		ok = exec_esc(ch);
		param_clear();
	}
	return ok;
}

// Handle a char sent to the screen.
// Normally it is just printed, but some control chars are handled specially.
static int process_char(wchar ch) {
	int ok = 1;
	add_last(ch);
	if (screen.in_esc) {
		ok = process_esc(ch);
	} else if (ch == ESC) {
		screen.in_esc = 1;
	} else if (ch == '\r') {
		screen_cr();
	} else if (ch == '\b') {
		screen_bs();
	} else if (ch == '\n') {
		if (screen.cy < screen.h-1)
			++screen.cy;
		else 
			screen_scroll();
		screen_cr(); // auto CR
	} else if (ch == '\7') {
		beep();
	} else if (ch == '\t') {
		ok = add_char(' '); // hardware tabs not supported
	} else if (ch >= '\40') { // printable char
		ok = add_char(ch);
	}
	return ok;
}

// ------------------------------------------------------------------ 

static int setup(int argc, char** argv) {
	int ch;
	screen_init();
	while ((ch = getopt(argc, argv, "h:qvw:")) != -1) {
		switch (ch) {
		case 'h':
			screen.h = atoi(optarg);
			break;
		case 'q':
			quiet = 1;
			break;
		case 'v':
			++verbose;
			break;
		case 'w':
			screen.w = atoi(optarg);
			break;
		default:
			return usage();
		}
	}
	int len = screen.w * screen.h;
	screen.chars = malloc(len * sizeof(ScreenChar));
	screen_clear(0, 0, len);
	if (optind >= argc) {
		ttyin = 0;
		ttyout = 1;
	} else {
		ttyin = ttyout = open(argv[optind], O_RDWR);
		if (ttyin < 0) {
			fprintf(stderr, "cannot open %s\n", argv[optind]);
			return 0;
		}
	}
	return 1;
}

static void set_signal(int signum, void (*handler)(int)) {
	struct sigaction sa;
	sa.sa_handler = handler;
	sa.sa_flags = 0;
	sigemptyset(&sa.sa_mask);
	sigaction(signum, &sa, NULL);
}

int main(int argc, char** argv) {
	set_signal(SIGINT,  SIG_IGN);
	set_signal(SIGQUIT, SIG_IGN);
	set_signal(SIGKILL, SIG_IGN);
	if (!setup(argc, argv))
		return RUN_ERR;
	for (;;) {
		wchar ch = read_wchar(ttyin);
		//if (verbose) fprintf(stderr, "screen read %c (%lx)\n", pr_ascii(ch), ch);
		if (ch == 0)
			break;
		if (!process_char(ch))
			beep();
	}
	return RUN_OK;
}
