/*
 * Copyright (c) 2017, 2022 Jonas 'Sortie' Termansen.
 * Copyright (c) 2023 Juhani 'nortti' Krekelä.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF7
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * terminal.c
 * Terminal emulator.
 */

#include <sys/ioctl.h>
#include <sys/keycodes.h>
#include <sys/wait.h>

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <pty.h>
#include <pwd.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <wchar.h>

#if !defined(TTY_NAME_MAX)
#include <sortix/limits.h>
#endif

#include <display.h>
#include <framebuffer.h>
#include <pixel.h>
#include <vgafont.h>

#include "palette.h"

struct kbkey_sequence
{
	const char* sequence;
	int kbkey;
	int flags;
};

#define MODIFIER_ALT (1 << 0)
#define MODIFIER_LSHIFT (1 << 1)
#define MODIFIER_RSHIFT (1 << 2)
#define MODIFIER_LCONTROL (1 << 3)
#define MODIFIER_RCONTROL (1 << 4)

#define SEQUENCE_1IFMOD (1 << 0)
#define SEQUENCE_OSHORT (1 << 1)

static const struct kbkey_sequence kbkey_sequences[] =
{
	{ "\e[A", KBKEY_UP, SEQUENCE_1IFMOD },
	{ "\e[B", KBKEY_DOWN, SEQUENCE_1IFMOD},
	{ "\e[C", KBKEY_RIGHT, SEQUENCE_1IFMOD },
	{ "\e[D", KBKEY_LEFT, SEQUENCE_1IFMOD },
	{ "\e[F", KBKEY_END, SEQUENCE_1IFMOD },
	{ "\e[H", KBKEY_HOME, SEQUENCE_1IFMOD },
	{ "\e[2~", KBKEY_INSERT, 0 },
	{ "\e[3~", KBKEY_DELETE, 0 },
	{ "\e[5~", KBKEY_PGUP, 0 },
	{ "\e[6~", KBKEY_PGDOWN, 0 },
	{ "\e[1P", KBKEY_F1, SEQUENCE_OSHORT },
	{ "\e[1Q", KBKEY_F2, SEQUENCE_OSHORT },
	{ "\e[1R", KBKEY_F3, SEQUENCE_OSHORT },
	{ "\e[1S", KBKEY_F4, SEQUENCE_OSHORT },
	{ "\e[15~", KBKEY_F5, 0 },
	{ "\e[17~", KBKEY_F6, 0 },
	{ "\e[18~", KBKEY_F7, 0 },
	{ "\e[19~", KBKEY_F8, 0 },
	{ "\e[20~", KBKEY_F9, 0 },
	{ "\e[21~", KBKEY_F10, 0 },
	{ "\e[23~", KBKEY_F11, 0 },
	{ "\e[24~", KBKEY_F12, 0 },
};

static inline const struct kbkey_sequence* lookup_keystroke_sequence(int kbkey)
{
	size_t count = sizeof(kbkey_sequences) / sizeof(kbkey_sequences[0]);
	for ( size_t i = 0; i < count; i++ )
		if ( kbkey_sequences[i].kbkey == kbkey )
			return &kbkey_sequences[i];
	return NULL;
}

static uint32_t WINDOW_ID = 0;
static uint32_t WINDOW_WIDTH = 0;
static uint32_t WINDOW_HEIGHT = 0;

static bool need_redraw = true;
static bool need_show = true;
static bool need_exit = false;
static bool redraw_pipe_written = false;
static int redraw_pipe[2];

struct entry
{
	uint32_t attr;
	uint32_t fgcolor;
	uint32_t bgcolor;
	wchar_t wc;
};

#define ATTR_INVERSE (1 << 0)
#define ATTR_BOLD (1 << 1)
#define ATTR_UNDERLINE (1 << 2)

static struct entry* scrollback;
static size_t column = 0;
static size_t row = 0;
static size_t columns = 0;
static size_t rows = 0;
static int modifiers = 0;
static mbstate_t in_ps;
static mbstate_t out_ps;
static uint32_t default_fgcolor;
static uint32_t default_bgcolor;
static uint32_t current_fgcolor;
static uint32_t current_bgcolor;
static uint32_t attr;
static uint32_t next_attr;
static unsigned ansisavedposx;
static unsigned ansisavedposy;
static enum { NONE = 0, CSI, CHARSET, COMMAND, GREATERTHAN, } ansimode;
#define ANSI_NUM_PARAMS 16
static unsigned ansiusedparams;
static unsigned ansiparams[ANSI_NUM_PARAMS];
static bool ignore_sequence;
static bool draw_cursor = true;

static void scrollback_resize(size_t new_rows, size_t new_columns)
{
	// TODO: Recover gracefully if the scrollback fails.
	// TODO: Overflow.
	struct entry* new_scrollback =
		calloc(new_rows * new_columns, sizeof(struct entry));
	if ( !new_scrollback )
		err(1, "malloc");
	size_t src_y_after_cursor = rows ? row + 1 : 0;
	size_t src_y_count =
		new_rows < src_y_after_cursor ? new_rows : src_y_after_cursor;
	size_t src_y_from = src_y_after_cursor - src_y_count;
	size_t new_row = row;
	size_t new_column = column;
	new_row += src_y_from;
	for ( size_t dst_y = 0; dst_y < new_rows; dst_y++ )
	{
		size_t src_y = src_y_from + dst_y;
		for ( size_t dst_x = 0; dst_x < new_columns; dst_x++ )
		{
			size_t src_x = dst_x;
			struct entry tc;
			if ( src_x < columns && src_y < rows )
				tc = scrollback[src_y * columns + src_x];
			else if ( columns && rows )
			{
				size_t templ_x = src_y < columns ? src_y : src_x - 1;
				size_t templ_y = src_y < rows ? src_y : rows - 1;
				tc = scrollback[templ_y * columns + templ_x];
				tc.wc = 0;
				tc.attr = 0;
			}
			else
				tc = (struct entry) { 0 };
			new_scrollback[dst_y * new_columns + dst_x] = tc;
			if ( src_x == column && src_y == row )
			{
				new_row = dst_y;
				new_column = dst_x;
			}
		}
	}
	if ( new_columns <= new_column )
		new_column = new_columns ? new_column - 1 : 0;
	if ( new_rows <= new_row )
		new_row = new_rows ? new_row - 1 : 0;
	free(scrollback);
	scrollback = new_scrollback;
	rows = new_rows;
	columns = new_columns;
	row = new_row;
	column = new_column;
}

static void fill(size_t from_x, size_t from_y, size_t to_x, size_t to_y,
                 struct entry with)
{
	// TODO: Assert within bounds?
	size_t from = from_y * columns + from_x;
	size_t to = to_y * columns + to_x;
	for ( size_t i = from; i <= to; i++ )
		scrollback[i] = with;
}

static void move(size_t to_x, size_t to_y, size_t from_x, size_t from_y,
                 size_t num_chars)
{
	// TODO: Assert within bounds?
	size_t from = from_y * columns + from_x;
	size_t to = to_y * columns + to_x;
	if ( to < from )
		for ( size_t i = 0; i < num_chars; i++ )
			scrollback[to + i] = scrollback[from + i];
	else if ( from < to )
		for ( size_t i = 0; i < num_chars; i++ )
			scrollback[to + num_chars - 1 - i] =
				scrollback[from + num_chars - 1 - i];
}

static void scroll(ssize_t offsigned, struct entry with)
{
	if ( 0 < offsigned )
	{
		size_t off = offsigned;
		if ( rows < off )
			off = rows;
		size_t dist = off * columns;
		size_t end = rows * columns - dist;
		for ( size_t i = 0; i < end; i++ )
			scrollback[i] = scrollback[i + dist];
		for ( size_t i = end; i < end + dist; i++ )
			scrollback[i] = with;
	}
	else if ( offsigned < 0 )
	{
		size_t off = -offsigned; // TODO: Negation overflow.
		if ( rows < off )
			off = rows;
		size_t dist = off * columns;
		size_t end = rows * columns;
		for ( size_t i = end-1; dist <= i; i-- )
			scrollback[i] = scrollback[i - dist];
		for ( size_t i = 0; i < dist; i++ )
			scrollback[i] = with;
	}
}

static void newline(void)
{
	if ( row + 1 < rows )
	{
		row++;
		return;
	}
	struct entry entry;
	entry.attr = 0;
	entry.fgcolor = current_fgcolor;
	entry.bgcolor = current_bgcolor;
	entry.wc = 0;
	scroll(1, entry);
}

static void run_ansi_command(char c)
{
	switch ( c )
	{
	case 'A': // Cursor up
	{
		unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
		if ( row < dist )
			row = 0;
		else
			row -= dist;
	} break;
	case 'B': // Cursor down
	{
		unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
		if ( rows <= row + dist )
			row = rows-1;
		else
			row += dist;
	} break;
	case 'C': // Cursor forward
	{
		unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
		if ( columns <= column + dist )
			column = columns-1;
		else
			column += dist;
	} break;
	case 'D': // Cursor backward
	{
		unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
		if ( column < dist )
			column = 0;
		else
			column -= dist;
	} break;
	case 'E': // Move to beginning of line N lines down.
	{
		column = 0;
		unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
		if ( rows <= row + dist )
			row = rows-1;
		else
			row += dist;
	} break;
	case 'F': // Move to beginning of line N lines up.
	{
		column = 0;
		unsigned dist = 0 < ansiusedparams ? ansiparams[0] : 1;
		if ( row < dist )
			row = 0;
		else
			row -= dist;
	} break;
	case 'G': // Move the cursor to column N.
	{
		unsigned pos = 0 < ansiusedparams ? ansiparams[0]-1 : 0;
		if ( columns <= pos )
			pos = columns-1;
		column = pos;
	} break;
	case 'H': // Move the cursor to line Y, column X.
	case 'f':
	{
		unsigned posy = 0 < ansiusedparams ? ansiparams[0]-1 : 0;
		unsigned posx = 1 < ansiusedparams ? ansiparams[1]-1 : 0;
		if ( columns <= posx )
			posx = columns-1;
		if ( rows <= posy )
			posy = rows-1;
		column = posx;
		row = posy;
	} break;
	case 'J': // Erase parts of the screen.
	{
		unsigned mode = 0 < ansiusedparams ? ansiparams[0] : 0;
		size_t from_x = 0, from_y = 0;
		size_t to_x = 0, to_y = 0;

		if ( mode == 0 ) // From cursor to end.
		{
			from_x = column;
			from_y = row;
			// TODO: Ensure the number of rows and columns are always non-zero.
			to_x = columns - 1;
			to_y = rows - 1;
		}

		if ( mode == 1 ) // From start to cursor.
		{
			from_x = 0;
			from_y = 0;
			to_x = columns - 1;
			to_y = rows - 1;
		}

		if ( mode == 2 ) // Everything.
		{
			from_x = 0;
			from_y = 0;
			to_x = columns - 1;
			to_y = rows - 1;
		}

		struct entry with;
		with.attr = 0;
		with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
		with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
		with.wc = 0;
		fill(from_x, from_y, to_x, to_y, with);
	} break;
	case 'K': // Erase parts of the current line.
	{
		unsigned mode = 0 < ansiusedparams ? ansiparams[0] : 0;
		size_t from_x = 0, from_y = row;
		size_t to_x = 0, to_y = row;

		if ( mode == 0 ) // From cursor to end.
		{
			from_x = column;
			to_x = columns - 1;
		}

		if ( mode == 1 ) // From start to cursor.
		{
			from_x = 0;
			to_x = column;
		}

		if ( mode == 2 ) // Everything.
		{
			from_x = 0;
			to_x = columns - 1;
		}

		struct entry with;
		with.attr = 0;
		with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
		with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
		with.wc = 0;
		fill(from_x, from_y, to_x, to_y, with);
	} break;
	case 'L': // Append lines before current line.
	{
		column = 0;
		unsigned count = 0 < ansiusedparams ? ansiparams[0] : 1;
		if ( rows - row < count )
			count = rows - row;
		unsigned move_lines = rows - (row + count);
		move(0, row + count, 0, row, move_lines * columns);
		struct entry with;
		with.attr = 0;
		with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
		with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
		with.wc = 0;
		if ( 0 < count )
			fill(0, row, columns - 1, row + count - 1, with);
	} break;
	case 'M': // Delete lines starting from beginning of current line.
	{
		column = 0;
		unsigned count = 0 < ansiusedparams ? ansiparams[0] : 1;
		if ( rows - row < count )
			count = rows - row;
		unsigned move_lines = rows - (row + count);
		move(0, row, 0, row + count, move_lines * columns);
		struct entry with;
		with.attr = 0;
		with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
		with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
		with.wc = 0;
		if ( 0 < count )
			fill(0, rows - count, columns - 1, rows - 1, with);
	} break;
	// TODO: CSI Ps P  Delete Ps Character(s) (default = 1) (DCH).
	//       (delete those characters and move the rest of the line leftward).
	case 'S': // Scroll a line up and place a new line at the buttom.
	{
		struct entry with;
		with.attr = 0;
		with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
		with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
		with.wc = 0;
		scroll(1, with);
		row = rows - 1;
	} break;
	case 'T': // Scroll a line up and place a new line at the top.
	{
		struct entry with;
		with.attr = 0;
		with.fgcolor = attr & ATTR_INVERSE ? current_bgcolor : current_fgcolor;
		with.bgcolor = attr & ATTR_INVERSE ? current_fgcolor : current_bgcolor;
		with.wc = 0;
		scroll(-1, with);
		row = 0;
	} break;
	case 'd': // Move the cursor to line N.
	{
		unsigned posy = 0 < ansiusedparams ? ansiparams[0]-1 : 0;
		if ( rows <= posy )
			posy = rows-1;
		row = posy;
	} break;
	case 'm': // Change how the text is rendered.
	{
		if ( ansiusedparams == 0 )
		{
			ansiparams[0] = 0;
			ansiusedparams++;
		}

		for ( size_t i = 0; i < ansiusedparams; i++ )
		{
			unsigned cmd = ansiparams[i];
			// Turn all attributes off.
			if ( cmd == 0 )
			{
				attr = 0;
				current_fgcolor = default_fgcolor;
				current_bgcolor = default_bgcolor;
			}
			// Boldness.
			else if ( cmd == 1 )
				attr |= ATTR_BOLD;
			// TODO: 2, Faint
			// TODO: 3, Italicized
			// Underline.
			else if ( cmd == 4 )
				attr |= ATTR_UNDERLINE;
			// TODO: 5, Blink (appears as Bold)
			// Inverse.
			else if ( cmd == 7 )
				attr |= ATTR_INVERSE;
			// TODO: 8, Invisible
			// TODO: 9, Crossed-out
			// TODO: 21, Doubly-underlined
			// Normal (neither bold nor faint).
			else if ( cmd == 22 )
				attr &= ~ATTR_BOLD;
			// TODO: 23, Not italicized
			// Not underlined.
			else if ( cmd == 24 )
				attr &= ~ATTR_UNDERLINE;
			// TODO: 25, Steady (not blinking)
			// Positive (not inverse).
			else if ( cmd == 27 )
				attr &= ~ATTR_INVERSE;
			// TODO: 28, Visible (not hidden)
			// Set text color.
			else if ( 30 <= cmd && cmd <= 37 )
			{
				unsigned val = cmd - 30;
				current_fgcolor = palette[val] | 0xFF000000;
			}
			// Set text color.
			else if ( cmd == 38 )
			{
				if ( 5 <= ansiusedparams - i && ansiparams[i+1] == 2 )
				{
					uint8_t r = ansiparams[i+2];
					uint8_t g = ansiparams[i+3];
					uint8_t b = ansiparams[i+4];
					i += 5 - 1;
					current_fgcolor = make_color(r, g, b);
				}
				else if ( 3 <= ansiusedparams - i && ansiparams[i+1] == 5 )
				{
					uint8_t index = ansiparams[i+2];
					i += 3 - 1;
					current_fgcolor = palette[index] | 0xFF000000;
				}
			}
			// Set default text color.
			else if ( cmd == 39 )
			{
				current_fgcolor = default_fgcolor;
			}
			// Set background color.
			else if ( 40 <= cmd && cmd <= 47 )
			{
				unsigned val = cmd - 40;
				current_bgcolor = palette[val] | 0xFF000000;
			}
			// Set background color.
			else if ( cmd == 48 )
			{
				if ( 5 <= ansiusedparams - i && ansiparams[i+1] == 2 )
				{
					uint8_t r = ansiparams[i+2];
					uint8_t g = ansiparams[i+3];
					uint8_t b = ansiparams[i+4];
					i += 5 - 1;
					current_bgcolor = make_color(r, g, b);
				}
				else if ( 3 <= ansiusedparams - i && ansiparams[i+1] == 5 )
				{
					uint8_t index = ansiparams[i+2];
					i += 3 - 1;
					current_bgcolor = palette[index] | 0xFF000000;
				}
			}
			// Set default background color.
			else if ( cmd == 49 )
			{
				current_bgcolor = default_bgcolor;
			}
			// Set text color.
			else if ( 90 <= cmd && cmd <= 97 )
			{
				unsigned val = cmd - 90 + 8;
				current_fgcolor = palette[val] | 0xFF000000;
			}
			// Set background color.
			else if ( 100 <= cmd && cmd <= 107 )
			{
				unsigned val = cmd - 100 + 8;
				current_bgcolor = palette[val] | 0xFF000000;
			}
			else
			{
				ansimode = NONE;
			}
		}
	} break;
	case 'n': // Request special information from terminal.
	{
		ansimode = NONE;
		// TODO: Handle this code.
	} break;
	case 's': // Save cursor position.
	{
		ansisavedposx = column;
		ansisavedposy = row;
	} break;
	case 'u': // Restore cursor position.
	{
		column = ansisavedposx;
		row = ansisavedposy;
		if ( columns <= column )
			column = columns-1;
		if ( rows <= row )
			row = rows-1;
	} break;
	case 'l': // Hide cursor.
	{
		// TODO: This is somehow related to the special char '?'.
		if ( 0 < ansiusedparams && ansiparams[0] == 25 )
			draw_cursor = false;
		if ( 0 < ansiusedparams && ansiparams[0] == 1049 )
			{}; // TODO: Save scrollback.
	} break;
	case 'h': // Show cursor.
	{
		// TODO: This is somehow related to the special char '?'.
		if ( 0 < ansiusedparams && ansiparams[0] == 25 )
			draw_cursor = true;
		if ( 0 < ansiusedparams && ansiparams[0] == 1049 )
			{}; // TODO: Restore scrollback.
	} break;
	default:
	{
		ansimode = NONE;
	}
	// TODO: Handle other cases.
	}

	ansimode = NONE;
}

static void put_ansi_escaped(char c)
{
	// Check the proper prefixes are used.
	if ( ansimode == CSI )
	{
		if ( c == '[' )
			ansimode = COMMAND;
		else if ( c == '(' || c == ')' || c == '*' || c == '+' ||
		          c == '-' || c == '.' || c == '/' )
			ansimode = CHARSET;
		// TODO: Enter and exit alternatve keypad mode.
		else if ( c == '=' || c == '>' )
			ansimode = NONE;
		else
		{
			ansimode = NONE;
		}
		return;
	}

	if ( ansimode == CHARSET )
	{
		ansimode = NONE;
		return;
	}

	// Read part of a parameter.
	if ( '0' <= c && c <= '9' )
	{
		if ( ansiusedparams == 0 )
			ansiusedparams++;
		unsigned val = c - '0';
		ansiparams[ansiusedparams-1] *= 10;
		ansiparams[ansiusedparams-1] += val;
	}

	// Parameter delimiter.
	else if ( c == ';' )
	{
		if ( ansiusedparams == ANSI_NUM_PARAMS )
		{
			ansimode = NONE;
			return;
		}
		ansiparams[ansiusedparams++] = 0;
	}

	// Left for future standardization, so discard this sequence.
	else if ( c == ':' )
	{
		ignore_sequence = true;
	}

	else if ( c == '>' )
	{
		ansimode = GREATERTHAN;
	}

	// Run a command.
	else if ( 64 <= c && c <= 126 )
	{
		if ( !ignore_sequence )
		{
			if ( ansimode == COMMAND )
				run_ansi_command(c);
			else if ( ansimode == GREATERTHAN )
			{
				// Send Device Attributes
				if ( c == 'c' )
				{
					// TODO: Send an appropriate response through the terminal.
				}
				else
				{
					ansimode = NONE;
					return;
				}
				ansimode = NONE;
			}
		}
		else
			ansimode = NONE;
	}

	// Something I don't understand, and ignore intentionally.
	else if ( c == '?' )
	{
		//ansimode = NONE;
	}

	// TODO: There are some rare things that should be supported here.

	// Ignore unknown input.
	else
	{
		ansimode = NONE;
	}
}

static uint32_t boldify(uint32_t color)
{
	int b = color >>  0 & 0xFF;
	int g = color >>  8 & 0xFF;
	int r = color >> 16 & 0xFF;
	int a = color >> 24 & 0xFF;
	b += 63;
	if ( 255 < b )
		b = 255;
	g += 63;
	if ( 255 < g )
		g = 255;
	r += 63;
	if ( 255 < r )
		r = 255;
	return make_color_a(r, g, b, a);
}

static void outwc(wchar_t wc)
{
	if ( wc == L'\a' )
	{
	}
	else if ( wc == L'\n' )
	{
		newline();
	}
	else if ( wc == L'\r' )
	{
		column = 0;
	}
	else if ( wc == L'\b' )
	{
		if ( column )
		{
			column--;
			struct entry* entry = &scrollback[row * columns + column];
			next_attr = entry->attr & (ATTR_BOLD | ATTR_UNDERLINE);
			if ( entry->wc == L'_' )
				next_attr |= ATTR_UNDERLINE;
			else if ( entry->wc == L' ' )
				next_attr &= ~ATTR_BOLD;
			else
				next_attr |= ATTR_BOLD;
		}
	}
	else if ( wc == L'\t' )
	{
		if ( column == columns )
		{
			newline();
			column = 0;
		}
		column++;
		column = -(-column & ~((size_t)0x7));
		if ( columns <= column )
			column = columns;
	}
	else if ( wc == L'\e' )
	{
		next_attr = 0;
		ansiusedparams = 0;
		ansiparams[0] = 0;
		ignore_sequence = false;
		ansimode = CSI;
	}
	else
	{
		if ( column == columns )
		{
			newline();
			column = 0;
		}
		struct entry* entry = &scrollback[row * columns + column++];
		entry->attr = attr | next_attr;
		if ( !(entry->attr & ATTR_INVERSE) )
		{
			entry->fgcolor = current_fgcolor;
			entry->bgcolor = current_bgcolor;
		}
		else
		{
			entry->fgcolor = current_bgcolor;
			entry->bgcolor = current_fgcolor;
		}
		if ( entry->attr & ATTR_BOLD )
			entry->fgcolor = boldify(entry->fgcolor);
		entry->wc = wc;
		next_attr = 0;
	}
}

static void outc(char c)
{
	if ( ansimode != NONE )
	{
		put_ansi_escaped(c);
		return;
	}
	wchar_t wc;
	size_t amount = mbrtowc(&wc, &c, 1, &out_ps);
	if ( amount == (size_t) -2 )
		return;
	if ( amount == (size_t) -1 )
	{
		memset(&out_ps, 0, sizeof(out_ps));
		wc = 0xFFFD; /* REPLACEMENT CHARACTER */;
	}
	if ( amount == (size_t) 0 )
		wc = L' ';
	outwc(wc);
}

static pthread_mutex_t scrollback_mutex = PTHREAD_MUTEX_INITIALIZER;
static int master_fd;

static void* outgoing_thread(void* ctx)
{
	(void) ctx;
	const char* getcursor = "\e[6n";
	size_t i = 0;
	char c;
	ssize_t amount = 0;
	while ( 0 < (amount = read(master_fd, &c, 1)) )
	{
		// TODO: Do escape code handling in the escape code parsing.
		if ( c == getcursor[i] )
		{
			i++;
			if ( !getcursor[i] )
			{
				i = 0;
				char buf[64];
				snprintf(buf, sizeof(buf), "\e[%zu;%zuR",
				         (size_t) (row + 1), (size_t) (column + 1));
				for ( size_t n = 0; buf[n]; n++ )
				{
					if ( write(master_fd, &buf[n], 1) <= 0 )
					{
						warn("incoming write");
						break; // TODO: This break is incorrect.
					}
				}
			}
			continue;
		}
		pthread_mutex_lock(&scrollback_mutex);
		for ( size_t j = 0; j < i; j++ )
			outc(getcursor[j]);
		i = 0;
		outc(c);
		if ( !redraw_pipe_written )
		{
			char c = 'X';
			if ( write(redraw_pipe[1], &c, 1) < 0 )
				warn("write: redraw_pipe");
			else
				redraw_pipe_written = true;
		}
		pthread_mutex_unlock(&scrollback_mutex);
	}
	if ( amount < 0 )
		warn("outgoing read");
	return NULL;
}

void on_disconnect(void* ctx)
{
	(void) ctx;
	need_exit = true;
}

void on_quit(void* ctx, uint32_t window_id)
{
	(void) ctx;
	if ( window_id != WINDOW_ID )
		return;
	need_exit = true;
}

void on_resize(void* ctx, uint32_t window_id, uint32_t width, uint32_t height)
{
	(void) ctx;
	if ( window_id != WINDOW_ID )
		return;
	// TODO: The resolution 0x0 gets sent for newly created windows that hasn't
	//       been shown yet, fix the compositor to not do this.
	if ( width == 0 && height == 0 )
		return;
	pthread_mutex_lock(&scrollback_mutex);
	size_t new_rows = height / FONT_HEIGHT;
	size_t new_columns = width / FONT_WIDTH;
	scrollback_resize(new_rows, new_columns);
	struct winsize ws;
	ws.ws_row = rows;
	ws.ws_col = columns;
	if ( ioctl(master_fd, TIOCSWINSZ, &ws) < 0 )
		warn("TIOCSWINSZ");
	pthread_mutex_unlock(&scrollback_mutex);
	need_redraw = true;
	WINDOW_WIDTH = width;
	WINDOW_HEIGHT = height;
}

void inuc(unsigned char uc)
{
	write(master_fd, &uc, 1);
}

void on_keycode(int kbkey)
{
	if ( kbkey < 0 )
		return;

	if ( kbkey == KBKEY_ESC )
	{
		inuc('\e');
		return;
	}

	const struct kbkey_sequence* seq = lookup_keystroke_sequence(kbkey);
	if ( !seq )
		return;

	const char* str = seq->sequence;
	size_t len = strlen(str);

	int mods = 0;
	if ( modifiers & (MODIFIER_LSHIFT | MODIFIER_RSHIFT) )
		mods |= 1;
	if ( modifiers & MODIFIER_ALT )
		mods |= 2;
	if ( modifiers & (MODIFIER_LCONTROL | MODIFIER_RCONTROL) )
		mods |= 4;

	if ( (seq->flags & SEQUENCE_OSHORT) && mods == 0 )
	{
		inuc('\e');
		inuc('O');
		inuc((unsigned char) str[len-1]);
		return;
	}

	for ( size_t i = 0; i < len - 1; i++ )
		inuc((unsigned char) str[i]);
	if ( seq->flags & SEQUENCE_1IFMOD && mods != 0 )
		inuc('1');
	if ( mods )
	{
		inuc(';');
		inuc('1' + mods);
	}
	inuc(str[len-1]);
}

void on_keyboard(void* ctx, uint32_t window_id, uint32_t codepoint)
{
	(void) ctx;
	if ( window_id != WINDOW_ID )
		return;
	int kbkey = KBKEY_DECODE(codepoint);
	if ( kbkey != 0 )
	{
		// TODO: Don't do this here, let the compositor do this.
		if ( kbkey == KBKEY_LALT )
			modifiers |= MODIFIER_ALT;
		else if ( kbkey == -KBKEY_LALT )
			modifiers &= ~MODIFIER_ALT;
		else if ( kbkey == KBKEY_LSHIFT )
			modifiers |= MODIFIER_LSHIFT;
		else if ( kbkey == -KBKEY_LSHIFT )
			modifiers &= ~MODIFIER_LSHIFT;
		else if ( kbkey == KBKEY_RSHIFT )
			modifiers |= MODIFIER_RSHIFT;
		else if ( kbkey == -KBKEY_RSHIFT )
			modifiers &= ~MODIFIER_RSHIFT;
		else if ( kbkey == KBKEY_LCTRL )
			modifiers |= MODIFIER_LCONTROL;
		else if ( kbkey == -KBKEY_LCTRL )
			modifiers &= ~MODIFIER_LCONTROL;
		else if ( kbkey == KBKEY_RCTRL )
			modifiers |= MODIFIER_RCONTROL;
		else if ( kbkey == -KBKEY_RCTRL )
			modifiers &= ~MODIFIER_RCONTROL;
		on_keycode(kbkey);
		return;
	}
	if ( codepoint == '\n' )
		codepoint = '\r';
	bool control = modifiers & (MODIFIER_LCONTROL | MODIFIER_RCONTROL);
	if ( codepoint == '\b' )
		codepoint = 127;
	if ( modifiers & MODIFIER_ALT )
		inuc('\e');
	if ( control && codepoint == L' ' )
		inuc(0);
	else if ( control && (L'`' <= codepoint && codepoint <= L'}') )
		inuc(codepoint - L'`');
	else if ( control && (L'@' <= codepoint && codepoint <= L'_') )
		inuc(codepoint - L'@');
	else if ( control && codepoint == L'?' )
		inuc(127);
	else
	{
		wchar_t wc = codepoint;
		char mb[MB_CUR_MAX];
		size_t amount = wcrtomb(mb, wc, &in_ps);
		if ( amount == (size_t) -1 )
			memset(&in_ps, 0, sizeof(in_ps));
		else
		{
			for ( size_t i = 0; i < amount; i++ )
				inuc((unsigned char) mb[i]);
		}
	}
}

void draw(struct display_connection* connection)
{
	uint32_t* framebuffer =
		calloc(WINDOW_WIDTH * WINDOW_HEIGHT, sizeof(uint32_t));
	assert(framebuffer);

	struct framebuffer fb;
	fb.pitch = WINDOW_WIDTH;
	fb.buffer = framebuffer;
	fb.xres = WINDOW_WIDTH;
	fb.yres = WINDOW_HEIGHT;

	pthread_mutex_lock(&scrollback_mutex);
	size_t draw_rows = WINDOW_HEIGHT / FONT_HEIGHT;
	size_t draw_columns = WINDOW_WIDTH / FONT_WIDTH;
	if ( rows < draw_rows )
		draw_rows = rows;
	if ( columns < draw_columns )
		draw_rows = columns;
	for ( size_t y = 0; y < rows; y++ )
	{
		size_t yoff = FONT_HEIGHT * y;
		for ( size_t x = 0; x < columns; x++ )
		{
			size_t xoff = FONT_WIDTH * x;
			size_t cell_width = FONT_WIDTH;
			size_t cell_height = FONT_HEIGHT;
			if ( x + 1 == columns )
				cell_width = WINDOW_WIDTH - xoff;
			if ( y + 1 == rows )
				cell_height = WINDOW_HEIGHT - yoff;
			struct framebuffer charfb =
				framebuffer_crop(fb, xoff, yoff, cell_width, cell_height);
			struct entry* entry = &scrollback[y * columns + x];
			for ( size_t py = 0; py < charfb.yres; py++ )
			{
				uint32_t* line = charfb.buffer + py * charfb.pitch;
				for ( size_t px = 0; px < charfb.xres; px++ )
					line[px] = entry->bgcolor;
			}
			render_char(charfb, entry->wc, entry->fgcolor);
			size_t entry_width = FONT_WIDTH;
			size_t entry_height = FONT_HEIGHT;
			if ( charfb.xres < entry_width )
				entry_width = charfb.xres;
			if ( charfb.yres < entry_height )
				entry_height = charfb.yres;
			size_t underlines = 0;
			if ( draw_cursor && y == row && x == column )
				underlines = 2;
			else if ( entry->attr & ATTR_UNDERLINE )
				underlines = 1;
			size_t start_underlines = FONT_HEIGHT - underlines;
			for ( size_t py = start_underlines; py < entry_height; py++ )
			{
				uint32_t* line = charfb.buffer + py * charfb.pitch;
				for ( size_t x = 0; x < entry_width; x++ )
					line[x] = blend_pixel(line[x], entry->fgcolor);
			}
		}
	}
	pthread_mutex_unlock(&scrollback_mutex);

	display_render_window(connection, WINDOW_ID, 0, 0, WINDOW_WIDTH,
	                      WINDOW_HEIGHT, framebuffer);

	free(framebuffer);
}

static void signal_handler(int signum)
{
	(void) signum;
}

int main(int argc, char* argv[])
{
	struct display_connection* connection = display_connect_default();
	if ( !connection && errno == ECONNREFUSED )
		display_spawn(argc, argv);
	if ( !connection )
		err(1, "Could not connect to display server");

	int opt;
	while ( (opt = getopt(argc, argv, "")) != -1 )
	{
		switch ( opt )
		{
		default: return 1;
		}
	}

	load_font();

	if ( pipe(redraw_pipe) < 0 )
		err(1, "pipe");

	rows = 25;
	columns = 80;

	// TODO: Overflow.
	scrollback = calloc(rows * columns, sizeof(struct entry));
	if ( !scrollback )
		err(1, "malloc");

	WINDOW_WIDTH = columns * FONT_WIDTH;
	WINDOW_HEIGHT = rows * FONT_HEIGHT;

	default_bgcolor = make_color_a(0, 0, 0, 220);
	default_fgcolor = palette[7] | 0xFF000000;
	current_bgcolor = default_bgcolor;
	current_fgcolor = default_fgcolor;
	for ( size_t y = 0; y < rows; y++ )
	{
		for ( size_t x = 0; x < columns; x++ )
		{
			struct entry* entry = &scrollback[y * columns + x];
			entry->attr = 0;
			entry->fgcolor = current_fgcolor;
			entry->bgcolor = current_bgcolor;
			entry->wc = 0;
		}
	}

	display_create_window(connection, WINDOW_ID);
	display_resize_window(connection, WINDOW_ID, WINDOW_WIDTH, WINDOW_HEIGHT);
	display_title_window(connection, WINDOW_ID, "Terminal");

	struct winsize ws;
	ws.ws_row = rows;
	ws.ws_col = columns;
	char path[TTY_NAME_MAX + 1];
	int slave_fd;
	if ( openpty(&master_fd, &slave_fd, path, NULL, &ws) < 0 )
		err(1, "openpty");

	sigset_t saved_mask, sigchld_mask;
	sigemptyset(&sigchld_mask);
	sigaddset(&sigchld_mask, SIGCHLD);
	sigprocmask(SIG_BLOCK, &sigchld_mask, &saved_mask);
	struct sigaction sa = { .sa_handler = signal_handler };
	struct sigaction old_sa;
	sigaction(SIGCHLD, &sa, &old_sa);

	pid_t child_pid = fork();
	if ( child_pid < 0 )
		err(1, "fork");

	if ( !child_pid )
	{
		sigprocmask(SIG_SETMASK, &saved_mask, NULL);
		if ( setsid() < 0 )
		{
			warn("setsid");
			_exit(1);
		}
		if ( ioctl(slave_fd, TIOCSCTTY, 0) < 0 )
		{
			warn("ioctl: TIOCSCTTY");
			_exit(1);
		}
		if ( close(0) < 0 || close(1) < 0 || close(2) < 0 )
		{
			warn("close");
			_exit(1);
		}
		if ( dup2(slave_fd, 0) != 0 ||
		     dup2(slave_fd, 1) != 1 ||
		     dup2(slave_fd, 2) != 2 )
		{
			warn("dup");
			_exit(1);
		}
		if ( closefrom(3) < 0 )
		{
			warn("closefrom");
			_exit(1);
		}
		const char* program;
		if ( argc <= optind )
		{
			program = "sh";
			uid_t uid = getuid();
			struct passwd* pwd = getpwuid(uid);
			if ( !pwd )
				warn("getpwuid: %ju", (uintmax_t) uid);
			else
				program = pwd->pw_shell;
			char* login_program;
			if ( asprintf(&login_program, "-%s", program) < 0 )
			{
				warn("malloc");
				_exit(1);
			}
			execlp(program, login_program, (const char*) NULL);
		}
		else
		{
			// Support explicit login shells using a leading dash.
			program = argv[optind];
			if ( program[0] == '-' )
				program++;
			execvp(program, argv + optind);
		}
		warn("%s", program);
		_exit(127);
	}

	int errnum;
	pthread_t outthread;
	if ( (errnum = pthread_create(&outthread, NULL, outgoing_thread, NULL)) )
	{
		errno = errnum;
		err(1, "pthread_create");
	}

	struct display_event_handlers handlers;
	memset(&handlers, 0, sizeof(handlers));
	handlers.disconnect_handler = on_disconnect;
	handlers.quit_handler = on_quit;
	handlers.resize_handler = on_resize;
	handlers.keyboard_handler = on_keyboard;

	const nfds_t nfds = 2;
	struct pollfd pfds[nfds];
	pfds[0].fd = redraw_pipe[0];
	pfds[0].events = POLLIN;
	pfds[0].revents = 0;
	pfds[1].fd = display_connection_fd(connection);
	pfds[1].events = POLLIN;
	pfds[1].revents = 0;

	while ( !need_exit )
	{
		int status;
		if ( 0 < waitpid(child_pid, &status, WNOHANG) )
			break;

		if ( need_redraw )
		{
			draw(connection);
			need_redraw = false;
		}

		if ( need_show )
		{
			display_show_window(connection, WINDOW_ID);
			need_show = false;
		}

		if ( ppoll(pfds, nfds, NULL, &saved_mask) < 0 )
		{
			if ( errno == EINTR )
				continue;
			err(1, "poll");
		}

		if ( pfds[0].revents )
		{
			pthread_mutex_lock(&scrollback_mutex);
			char c;
			ssize_t amount = read(redraw_pipe[0], &c, 1);
			if ( amount < 0 )
				err(1, "read: redraw_pipe");
			if ( 0 < amount )
			{
				redraw_pipe_written = false;
				need_redraw = true;
			}
			pthread_mutex_unlock(&scrollback_mutex);
		}

		if ( pfds[1].revents )
		{
			while ( display_poll_event(connection, &handlers) == 0 );
		}
	}

	display_disconnect(connection);

	return 0;
}
