/**
 * @file sink_akgl.c
 * @brief The libakgl-backed text sink: where PRINT goes when a host is drawing.
 *
 * Implements the section 1.5 vtable over akgl_text_measure() and
 * akgl_text_rendertextat(). Owns the cursor, the wrap and the scroll --
 * everything in the reference's basicruntime_graphics.go except Write and
 * Println, which are the sink interface itself.
 *
 * The measurement call is new in libakgl 42b60f7 and is the reason this file can
 * exist at all: the reference derives its character grid from
 * font.SizeUTF8("A") (basicruntime.go:96) and there was no akgl_text_*
 * equivalent until then.
 */

#include <ctype.h>
#include <stdio.h>
#include <akerror.h>
#include <akstdlib.h>

#include <akgl/controller.h>
#include <akgl/draw.h>
#include <akgl/error.h>
#include <akgl/text.h>

#include <akbasic/akgl.h>
#include <akbasic/error.h>

/** @brief Rows of scrollback the sink keeps. Matches the `text` array in akgl.h. */
#define SINK_MAX_ROWS		64
/** @brief Bytes per row, including the terminator. */
#define SINK_MAX_COLUMNS	256

/**
 * @brief Scroll the grid up by one row, dropping the top one.
 *
 * Returns a context rather than `void` because the row moves can fail and there
 * was nowhere to say so. Everything on this chain -- scroll, newline,
 * putchar_at, echo_line, edit_key -- returns one for the same reason, and the
 * chain already ends at sink_write, sink_writeln and sink_readline, which
 * returned one all along. See libakstdlib #38.
 */
static akerr_ErrorContext *scroll(akbasic_AkglSink *state)
{
    PREPARE_ERROR(errctx);
    int row = 0;

    for ( row = 0; row < SINK_MAX_ROWS - 1; row++ ) {
	PASS(errctx, aksl_memcpy(state->text[row], state->text[row + 1], SINK_MAX_COLUMNS));
    }
    PASS(errctx, aksl_memset(state->text[SINK_MAX_ROWS - 1], 0, SINK_MAX_COLUMNS));
    if ( state->cursorrow > 0 ) {
	state->cursorrow -= 1;
    }
    /*
     * A line being typed is anchored to a row, and that row just moved. Follow
     * it, or a backspace erases somebody else's text. Clamped at zero: a line
     * long enough to scroll its own start off the top redraws from the top row,
     * which is cosmetically wrong and is the only place this can be seen.
     */
    if ( state->editing ) {
	state->editrow -= 1;
	if ( state->editrow < 0 ) {
	    state->editrow = 0;
	}
    }
}

/** @brief Move to the start of the next row, scrolling if that runs off the end. */
static akerr_ErrorContext *newline(akbasic_AkglSink *state)
{
    PREPARE_ERROR(errctx);

    state->cursorcol = 0;
    state->cursorrow += 1;
    if ( state->cursorrow >= state->rows || state->cursorrow >= SINK_MAX_ROWS ) {
	PASS(errctx, scroll(state));
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Put one character at the cursor, wrapping and scrolling as needed.
 *
 * Wrapping happens here, on the character grid, rather than being left to
 * SDL_ttf's own wraplength. The cursor has to end up somewhere definite: a
 * program that PRINTs a long string and then PRINTs again expects the second one
 * to start on the row after the first one ended, and only the code that placed
 * the characters knows which row that is.
 *
 * **A row is a NUL-terminated string, so a write past its end pads.** Without
 * that, `CHAR 1, 40, 1, "#"` on an otherwise empty row stored the `#` at column
 * 40 with `text[1][0]` still `'\0'` -- and the render loop, which stops at the
 * terminator, drew nothing at all. The write succeeded, the cursor moved, the
 * stdout mirror showed the character, and the window stayed blank. That silent
 * nothing is the trap; the documented truncation on the way *back* is fine and
 * is unaffected. TODO.md section 6 item 32.
 *
 * The padding is spaces rather than whatever was there: the buffer is not
 * cleared between rows, so the gap holds the tail of some longer row that used
 * to be here.
 */
static akerr_ErrorContext *putchar_at(akbasic_AkglSink *state, char c)
{
    PREPARE_ERROR(errctx);
    int col = 0;

    if ( c == '\n' ) {
	PASS(errctx, newline(state));
	SUCCEED_RETURN(errctx);
    }
    if ( state->cursorcol >= state->columns || state->cursorcol >= SINK_MAX_COLUMNS - 1 ) {
	PASS(errctx, newline(state));
    }
    for ( col = 0; col < state->cursorcol; col++ ) {
	if ( state->text[state->cursorrow][col] == '\0' ) {
	    break;
	}
    }
    for ( ; col < state->cursorcol; col++ ) {
	state->text[state->cursorrow][col] = ' ';
    }
    state->text[state->cursorrow][state->cursorcol] = c;
    state->cursorcol += 1;
    state->text[state->cursorrow][state->cursorcol] = '\0';
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *sink_write(akbasic_TextSink *self, const char *text)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;
    size_t i = 0;

    FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
		     "NULL argument in akgl sink write");
    state = (akbasic_AkglSink *)self->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
		     "akgl sink has no state");
    for ( i = 0; text[i] != '\0'; i++ ) {
	PASS(errctx, putchar_at(state, text[i]));
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *sink_writeln(akbasic_TextSink *self, const char *text)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;

    PASS(errctx, sink_write(self, text));
    state = (akbasic_AkglSink *)self->self;
    PASS(errctx, newline(state));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Redraw the line being typed, erasing whatever a longer one left behind.
 *
 * Two passes over the grid rather than one clever one. The first blanks what is
 * already drawn and the second draws the current text, which leaves the cursor
 * exactly where the text ends without anybody having to reproduce putchar_at's
 * wrapping arithmetic a second time. Nothing is rendered here -- the grid is
 * memory, and the host draws it when it draws its frame -- so the cost is a
 * couple of memcpy-sized loops per keystroke.
 */
static akerr_ErrorContext *echo_line(akbasic_AkglSink *state)
{
    PREPARE_ERROR(errctx);
    int i = 0;
    int erasedto = 0;
    int endedat = 0;

    state->cursorrow = state->editrow;
    state->cursorcol = state->editcol;
    for ( i = 0; i < state->echolen; i++ ) {
	PASS(errctx, putchar_at(state, ' '));
    }
    erasedto = state->cursorrow;

    state->cursorrow = state->editrow;
    state->cursorcol = state->editcol;
    for ( i = 0; i < state->editlen; i++ ) {
	PASS(errctx, putchar_at(state, state->editline[i]));
    }
    endedat = state->cursorrow;
    state->echolen = state->editlen;

    /*
     * A line that got shorter can leave whole rows holding nothing but the
     * spaces the erase pass just wrote -- which is what backspacing back across
     * a wrap does to the row below. Truncate them, so such a row reports itself
     * *empty* rather than merely blank-looking.
     *
     * That distinction is load-bearing rather than tidiness: render() decides
     * what to erase and redraw from whether a row has text, and a row of spaces
     * is text. Left alone it would be repainted forever, and anything the
     * program had drawn underneath it would stay erased.
     */
    for ( i = endedat + 1; i <= erasedto && i < SINK_MAX_ROWS; i++ ) {
	state->text[i][0] = '\0';
    }
    SUCCEED_RETURN(errctx);
}

/** @brief Append one byte to the line being typed, if there is room for it. */
static void edit_append(akbasic_AkglSink *state, char c)
{
    if ( state->editlen >= (int)sizeof(state->editline) - 1 ) {
	/* Full. Dropped silently, exactly as a C128's 80-character limit does. */
	return;
    }
    state->editline[state->editlen] = c;
    state->editlen += 1;
    state->editline[state->editlen] = '\0';
}

/**
 * @brief Fold one keystroke into the line being typed.
 *
 * **The composed text is preferred over the keycode wherever there is any**, and
 * that is the whole reason libakgl 0.3.0's akgl_Keystroke exists. A keycode
 * cannot express a shifted character, a keyboard layout, a compose key or a dead
 * key; SDL has already worked all of that out by the time the ring sees it, and
 * `text` is the answer. Before 0.3.0 this took a bare keycode, folded letters to
 * upper case, and could not type a double quote -- which meant a BASIC string
 * literal could not be typed at the window at all.
 *
 * The keycode is still what identifies the editing keys, because Return,
 * Backspace and Escape are keys rather than characters and several of them
 * compose to text SDL would otherwise hand straight through.
 */
static akerr_ErrorContext *edit_key(akbasic_AkglSink *state, const akgl_Keystroke *key, bool *submitted)
{
    PREPARE_ERROR(errctx);
    size_t i = 0;

    if ( key->key == SDLK_RETURN || key->key == SDLK_KP_ENTER ||
	 key->key == '\r' || key->key == '\n' ) {
	*submitted = true;
	SUCCEED_RETURN(errctx);
    }
    if ( key->key == SDLK_BACKSPACE || key->key == '\b' || key->key == 0x7f ) {
	if ( state->editlen > 0 ) {
	    /*
	     * One *byte* at a time, which is wrong for a multi-byte character and
	     * is left that way deliberately: every character a BASIC program can
	     * hold is one byte (section 1.2's inline string), so a multi-byte
	     * character cannot survive being submitted anyway. Erasing what was
	     * accepted is consistent with that.
	     */
	    state->editlen -= 1;
	    state->editline[state->editlen] = '\0';
	    PASS(errctx, echo_line(state));
	}
	SUCCEED_RETURN(errctx);
    }
    if ( key->key == SDLK_ESCAPE || key->key == 0x1b ) {
	state->editlen = 0;
	state->editline[0] = '\0';
	PASS(errctx, echo_line(state));
	SUCCEED_RETURN(errctx);
    }

    if ( key->text[0] != '\0' ) {
	for ( i = 0; key->text[i] != '\0' && i < sizeof(key->text); i++ ) {
	    /*
	     * Printable ASCII only. The grid is a byte per cell and a value's
	     * string is a fixed 256 bytes, so a multi-byte character has nowhere
	     * to go -- dropping it is honest where storing half of it is not.
	     */
	    if ( (unsigned char)key->text[i] >= 0x20 && (unsigned char)key->text[i] < 0x7f ) {
		edit_append(state, key->text[i]);
	    }
	}
	PASS(errctx, echo_line(state));
	SUCCEED_RETURN(errctx);
    }

    /*
     * No composed text, but a printable keycode: type it anyway.
     *
     * **This fallback is why the editor still works on a host that never called
     * SDL_StartTextInput().** SDL emits no SDL_EVENT_TEXT_INPUT until text input
     * is started, so without it every keystroke arrives here with an empty
     * `text` -- and treating that as "not a character" makes the entire keyboard
     * dead, silently, which is exactly what happened once. A worse keyboard is a
     * great deal better than no keyboard.
     *
     * Upper case, because that is all a bare keycode can offer and it is what a
     * C128 does. When text input *is* running a printable key always carries
     * text, so this never fires and lower case survives.
     */
    if ( key->key >= 0x20 && key->key < 0x7f ) {
	edit_append(state, (char)toupper((unsigned char)key->key));
	PASS(errctx, echo_line(state));
	SUCCEED_RETURN(errctx);
    }

    /*
     * Neither: a cursor key, a function key or a bare modifier. Not an editing
     * command here -- a script's own GET loop is what wants those.
     */
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Collect keystrokes until a line is submitted or the host stops.
 *
 * Its own function because it is a loop: CATCH and the _BREAK macros expand to a
 * C break, which inside a loop would escape only the loop and leave the rest of
 * an ATTEMPT running with an error pending. PASS only in here, and the caller
 * wraps this one call in the ATTEMPT that owns the cleanup.
 */
static akerr_ErrorContext AKERR_NOIGNORE *edit_loop(akbasic_AkglSink *state, bool *eof)
{
    PREPARE_ERROR(errctx);
    akgl_Keystroke key;
    bool submitted = false;
    bool available = false;
    bool running = true;

    while ( !submitted ) {
	/*
	 * poll_keystroke rather than poll_key: the same ring, with the modifier
	 * state and the composed text still attached. poll_key is the reduced
	 * view, and it is what GET and GETKEY still use -- a script asking "was
	 * the up arrow pressed" wants a keycode, not the empty string.
	 */
	PASS(errctx, akgl_controller_poll_keystroke(&key, &available));
	if ( available ) {
	    PASS(errctx, edit_key(state, &key, &submitted));
	    continue;
	}
	/*
	 * Nothing waiting: hand the frame back to the host, which pumps the
	 * events that fill the ring this loop is reading. Skipping the pump
	 * while keys are available is what keeps a paste or a fast typist from
	 * costing one frame per character.
	 */
	PASS(errctx, state->pump(state->pumpself, &running));
	if ( !running ) {
	    *eof = true;
	    SUCCEED_RETURN(errctx);
	}
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *sink_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;

    FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
		     "NULL argument in akgl sink readline");
    FAIL_ZERO_RETURN(errctx, (len > 1), AKBASIC_ERR_BOUNDS,
		     "Read buffer of %zu bytes is too small", len);
    state = (akbasic_AkglSink *)self->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
		     "akgl sink has no state");

    *eof = false;
    dest[0] = '\0';

    /*
     * No pump means no host loop to borrow, and a sink that sat on the keyboard
     * without one would deadlock the process on its first INPUT. Report end of
     * input instead -- the contract sink.h states, and what INPUT already
     * handles.
     */
    if ( state->pump == NULL ) {
	*eof = true;
	SUCCEED_RETURN(errctx);
    }

    state->editing = true;
    state->editrow = state->cursorrow;
    state->editcol = state->cursorcol;
    state->editlen = 0;
    state->echolen = 0;
    state->editline[0] = '\0';

    ATTEMPT {
	CATCH(errctx, edit_loop(state, eof));
    } CLEANUP {
	/* Whatever happened, stop drawing a cursor over a line nobody is typing. */
	state->editing = false;
    } PROCESS(errctx) {
    } FINISH(errctx, true);

    if ( *eof ) {
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, aksl_strncpy(dest, len, state->editline, len - 1));
    PASS(errctx, newline(state));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *sink_clear(akbasic_TextSink *self)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;

    FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER,
		     "NULL sink in akgl sink clear");
    state = (akbasic_AkglSink *)self->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
		     "akgl sink has no state");
    PASS(errctx, aksl_memset(state->text, 0, sizeof(state->text)));
    state->cursorcol = 0;
    state->cursorrow = 0;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Put the cursor at a character cell, for CHAR.
 *
 * Clamped rather than refused: a program that asks for a column past the edge of
 * a window it cannot measure has made an ordinary mistake, and a C128 clamps too.
 */
static akerr_ErrorContext *sink_moveto(akbasic_TextSink *self, int col, int row)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;

    FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in moveto");
    state = (akbasic_AkglSink *)self->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");

    if ( col < 0 ) { col = 0; }
    if ( row < 0 ) { row = 0; }
    if ( col >= state->columns ) { col = state->columns - 1; }
    if ( row >= state->rows ) { row = state->rows - 1; }
    state->cursorcol = col;
    state->cursorrow = row;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Constrain the text area to a rectangle of cells, for WINDOW.
 *
 * The sink already drives everything -- wrap, scroll, cursor -- from `x`, `y`,
 * `columns` and `rows`, so a window is those four fields and nothing else. The
 * cell size does not change, which is what keeps a windowed program's text the
 * same size as an unwindowed one's.
 *
 * The full-screen geometry is remembered so a later WINDOW can grow back out;
 * without it each call could only ever shrink.
 */
/**
 * @brief Report the character grid, for RWINDOW and RGR.
 *
 * The four numbers a program cannot otherwise learn. Columns and rows are the
 * *current* window rather than the whole screen, which is what RWINDOW means on
 * a C128 and what a program placing text actually needs; the cell size is
 * unaffected by windowing, which is what keeps a windowed program's text the
 * same size as an unwindowed one's.
 */
static akerr_ErrorContext *sink_grid(akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;

    FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in grid");
    FAIL_ZERO_RETURN(errctx, (columns != NULL && rows != NULL && cellw != NULL && cellh != NULL),
		     AKERR_NULLPOINTER, "NULL destination in grid");
    state = (akbasic_AkglSink *)self->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
    FAIL_ZERO_RETURN(errctx, (state->cellw > 0 && state->cellh > 0), AKBASIC_ERR_STATE,
		     "The sink has no character grid to measure");

    *columns = state->columns;
    *rows = state->rows;
    *cellw = state->cellw;
    *cellh = state->cellh;
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *sink_window(akbasic_TextSink *self, int left, int top, int right, int bottom)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;
    int maxcols = 0;
    int maxrows = 0;

    FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in window");
    state = (akbasic_AkglSink *)self->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
    FAIL_ZERO_RETURN(errctx, (state->cellw > 0 && state->cellh > 0), AKBASIC_ERR_STATE,
		     "The sink has no character grid to window");

    maxcols = state->fullwidth / state->cellw;
    maxrows = (state->fullheight / state->cellh) - state->texttop;
    if ( left < 0 ) { left = 0; }
    if ( top < 0 ) { top = 0; }
    if ( right >= maxcols ) { right = maxcols - 1; }
    if ( bottom >= maxrows ) { bottom = maxrows - 1; }
    FAIL_ZERO_RETURN(errctx, (right >= left && bottom >= top), AKBASIC_ERR_VALUE,
		     "WINDOW asks for no cells at all");

    state->x = state->fullx + (left * state->cellw);
    state->y = state->fully + ((state->texttop + top) * state->cellh);
    state->columns = (right - left) + 1;
    state->rows = (bottom - top) + 1;
    state->width = state->columns * state->cellw;
    state->height = state->rows * state->cellh;
    state->cursorcol = 0;
    state->cursorrow = 0;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Apply GRAPHIC's text-plane half without teaching the runtime about SDL.
 *
 * Modes 1 and 3 are full bitmaps, so the retained text grid is hidden. Modes 2
 * and 4 have a bitmap above and text below. A C128 gives an omitted split its
 * bottom six rows; an explicit zero means all text, and the host clamps a C128
 * row number to however many measured cells its own window has.
 */
static akerr_ErrorContext *sink_graphic(akbasic_TextSink *self, int mode, int split)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;
    int maxcols = 0;
    int maxrows = 0;

    FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in graphic");
    state = (akbasic_AkglSink *)self->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "akgl sink has no state");
    maxcols = state->fullwidth / state->cellw;
    maxrows = state->fullheight / state->cellh;

    state->graphicmode = mode;
    if ( mode == 0 ) {
	/* Text mode owns the whole screen again, not the last split's rows. */
	state->texttop = 0;
	state->x = state->fullx;
	state->y = state->fully;
	state->width = state->fullwidth;
	state->height = state->fullheight;
	state->columns = maxcols;
	state->rows = maxrows;
	if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
	    state->columns = SINK_MAX_COLUMNS - 1;
	}
	if ( state->rows > SINK_MAX_ROWS ) {
	    state->rows = SINK_MAX_ROWS;
	}
	state->cursorcol = 0;
	state->cursorrow = 0;
	SUCCEED_RETURN(errctx);
    }
    if ( mode != 2 && mode != 4 ) {
	SUCCEED_RETURN(errctx);
    }
    if ( split < 0 ) {
	split = maxrows - 6;
    }
    if ( split < 0 ) { split = 0; }
    if ( split >= maxrows ) {
	/* A split below the final row is C128 all-bitmap mode, not a zero-row grid. */
	state->graphicmode = 1;
	SUCCEED_RETURN(errctx);
    }

    state->texttop = split;
    state->x = state->fullx;
    state->y = state->fully + (split * state->cellh);
    state->width = state->fullwidth;
    state->height = (maxrows - split) * state->cellh;
    state->columns = maxcols;
    state->rows = maxrows - split;
    if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
	state->columns = SINK_MAX_COLUMNS - 1;
    }
    if ( state->rows > SINK_MAX_ROWS ) {
	state->rows = SINK_MAX_ROWS;
    }
    state->cursorcol = 0;
    state->cursorrow = 0;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_sink_init_akgl(akbasic_TextSink *obj, akbasic_AkglSink *state, akgl_RenderBackend *renderer, TTF_Font *font, int w, int h)
{
    PREPARE_ERROR(errctx);
    int cellw = 0;
    int cellh = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
		     "NULL argument in sink_init_akgl");
    FAIL_ZERO_RETURN(errctx, (renderer != NULL), AKERR_NULLPOINTER,
		     "NULL renderer in sink_init_akgl: the host creates it, not the sink");
    FAIL_ZERO_RETURN(errctx, (font != NULL), AKERR_NULLPOINTER,
		     "NULL font in sink_init_akgl");

    /*
     * Before anything else in libakgl. akgl_game_init() would have done it, but
     * an embedded interpreter drives subsystems directly and never calls that --
     * and a code raised before this runs carries no name into its stack trace.
     * Idempotent, so a host that already called it loses nothing.
     */
    PASS(errctx, akgl_error_init());

    /*
     * The character grid, measured rather than assumed. Direct equivalent of the
     * reference's font.SizeUTF8("A"), and it needs no renderer -- which is why
     * the sink can size itself before a frame has ever been drawn.
     */
    PASS(errctx, akgl_text_measure(font, "A", &cellw, &cellh));
    FAIL_ZERO_RETURN(errctx, (cellw > 0 && cellh > 0), AKBASIC_ERR_VALUE,
		     "Font measures a %dx%d character cell, which cannot be a grid",
		     cellw, cellh);
    FAIL_ZERO_RETURN(errctx, (w >= cellw && h >= cellh), AKBASIC_ERR_BOUNDS,
		     "A %dx%d text area has no room for a %dx%d character",
		     w, h, cellw, cellh);

    PASS(errctx, aksl_memset(state, 0, sizeof(*state)));
    state->renderer = renderer;
    state->font = font;
    state->color.r = 0xff;
    state->color.g = 0xff;
    state->color.b = 0xff;
    state->color.a = 0xff;
    /* Opaque, not transparent: the text layer owns the rows it draws. */
    state->cursorperiodms = AKBASIC_SINK_CURSOR_BLINK_MS;
    state->background.r = 0x00;
    state->background.g = 0x00;
    state->background.b = 0x00;
    state->background.a = 0xff;
    state->x = 0;
    state->y = 0;
    state->width = w;
    state->height = h;
    /* Remembered so WINDOW can grow back out to the whole area. */
    state->fullx = state->x;
    state->fully = state->y;
    state->fullwidth = w;
    state->fullheight = h;
    state->cellw = cellw;
    state->cellh = cellh;
    state->columns = w / cellw;
    state->rows = h / cellh;
    state->graphicmode = 0;
    state->texttop = 0;
    if ( state->columns > SINK_MAX_COLUMNS - 1 ) {
	state->columns = SINK_MAX_COLUMNS - 1;
    }
    if ( state->rows > SINK_MAX_ROWS ) {
	state->rows = SINK_MAX_ROWS;
    }

    obj->self = state;
    obj->write = sink_write;
    obj->writeln = sink_writeln;
    obj->readline = sink_readline;
    obj->clear = sink_clear;
    obj->moveto = sink_moveto;
    obj->window = sink_window;
    obj->grid = sink_grid;
    obj->graphic = sink_graphic;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_sink_akgl_render(akbasic_TextSink *obj)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;
    int row = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
		     "NULL sink in sink_akgl_render");
    state = (akbasic_AkglSink *)obj->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
		     "akgl sink has no state");

    /* A full bitmap owns every display row; the text buffer remains intact. */
    if ( state->graphicmode == 1 || state->graphicmode == 3 ) {
	SUCCEED_RETURN(errctx);
    }

    /*
     * **Repaint every row of the text area, every frame.** Not just the rows
     * that changed -- that was tried, with a `drawn[]` array marking which rows
     * had carried glyphs, and it is wrong on real hardware.
     *
     * SDL_RenderPresent swaps buffers. What a frame inherits is not the frame
     * before it, it is the frame *two* before it, and on some backends it is
     * undefined. So a row erased once is erased in one buffer and still dirty in
     * the other, and presenting alternates between them: the row blinks in and
     * out forever. That is exactly what a backspace across a line wrap looked
     * like -- the first character of the wrapped tail flickering after the rest
     * had gone. It reproduces on X11 and never under the dummy driver or a
     * software renderer, which is why the suite was green.
     *
     * There is no cheaper correct answer available here. A frame either owns
     * every pixel it presents or it inherits pixels it cannot reason about.
     *
     * Row by row rather than one fill over the whole area, still: the two are
     * equivalent in cost here and the row loop keeps the text layer's ownership
     * of exactly `rows * cellh` pixels obvious, which matters when the area is
     * smaller than the window.
     *
     * A loop, so no CATCH and no _BREAK macros: they expand to a C break, which
     * would escape the loop with an error still pending. PASS only.
     *
     * Wrapping is off at the draw -- a zero wraplength -- because the grid above
     * already decided where every line ends. Letting SDL_ttf wrap again would
     * put characters somewhere the cursor does not think they are.
     */
    for ( row = 0; row < state->rows; row++ ) {
	SDL_FRect cells;

	cells.x = (float)state->x;
	cells.y = (float)(state->y + (row * state->cellh));
	cells.w = (float)state->width;
	cells.h = (float)state->cellh;
	PASS(errctx, akgl_draw_filled_rect(state->renderer, &cells, state->background));

	if ( state->text[row][0] == '\0' ) {
	    continue;
	}
	PASS(errctx, akgl_text_rendertextat(state->font, state->text[row],
					    state->color, 0,
					    state->x,
					    state->y + (row * state->cellh)));
    }

    /*
     * The cursor: a filled block at the cursor cell, blinking.
     *
     * A block rather than the reference's underscore glyph (drawCursor,
     * basicruntime_graphics.go:33). The underscore sat *under* the text being
     * typed and made it hard to read, which is what got it removed; a block
     * occupies the cell after the text instead of the space beneath it.
     *
     * The clock is SDL's rather than the host's. Everywhere else in this
     * interpreter the host owns the clock, because the library owns no loop and
     * must not block -- but this is an adaptor that already links SDL, the
     * blink is cosmetic, and threading a timestamp through the sink interface to
     * animate a cursor would be a poor trade. `cursorperiodms` of zero holds it
     * solid, which is what makes a frame deterministic for a test.
     */
    if ( state->editing ) {
	SDL_FRect cell;
	bool visible = true;

	if ( state->cursorperiodms > 0 ) {
	    visible = ((SDL_GetTicks() % state->cursorperiodms) <
		       (state->cursorperiodms / 2));
	}
	if ( visible ) {
	    int curcol = state->cursorcol;
	    int currow = state->cursorrow;

	    /*
	     * A line that exactly fills a row leaves the cursor one past the last
	     * column, because putchar_at only wraps when the *next* character
	     * arrives. Drawn there it would be off the right edge and invisible.
	     * Show it where the next character will actually land instead, which
	     * is also where a C128 puts it.
	     */
	    if ( curcol >= state->columns ) {
		curcol = 0;
		currow += 1;
	    }
	    if ( currow < state->rows ) {
		cell.x = (float)(state->x + (curcol * state->cellw));
		cell.y = (float)(state->y + (currow * state->cellh));
		cell.w = (float)state->cellw;
		cell.h = (float)state->cellh;
		PASS(errctx, akgl_draw_filled_rect(state->renderer, &cell, state->color));
	    }
	}
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_sink_akgl_set_pump(akbasic_TextSink *obj, akbasic_AkglPump pump, void *self)
{
    PREPARE_ERROR(errctx);
    akbasic_AkglSink *state = NULL;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
		     "NULL sink in sink_akgl_set_pump");
    state = (akbasic_AkglSink *)obj->self;
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER,
		     "akgl sink has no state");

    state->pump = pump;
    state->pumpself = self;
    SUCCEED_RETURN(errctx);
}
