/**
 * @file sink_tee.c
 * @brief A text sink that writes to two others.
 *
 * The reference's Write() and Println() (basicruntime_graphics.go:140,148) put
 * every line on stdout *and* on an SDL surface. TODO.md section 1.5 turned that
 * pair of hardcoded calls into a vtable specifically so the interpreter would
 * never carry the second one, and section 3 item 5 says it again for the
 * standalone AKGL driver: do not put a hardcoded second write in the
 * interpreter. This is where the second write lives instead.
 *
 * No SDL and no libakgl. Composing two function-pointer records needs neither,
 * so this stays in the core library where the stdio-only suite can test it.
 */

#include <stddef.h>

#include <akerror.h>

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

/** @brief Fetch and validate the state behind a tee sink. */
static akerr_ErrorContext AKERR_NOIGNORE *tee_state(akbasic_TextSink *self, akbasic_TeeSink **dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (self != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in tee sink");
    *dest = (akbasic_TeeSink *)self->self;
    FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_NULLPOINTER,
		     "tee sink has no state");
    SUCCEED_RETURN(errctx);
}

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

    PASS(errctx, tee_state(self, &state));
    PASS(errctx, state->primary->write(state->primary, text));
    PASS(errctx, state->mirror->write(state->mirror, text));
    SUCCEED_RETURN(errctx);
}

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

    PASS(errctx, tee_state(self, &state));
    PASS(errctx, state->primary->writeln(state->primary, text));
    PASS(errctx, state->mirror->writeln(state->mirror, text));
    SUCCEED_RETURN(errctx);
}

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

    PASS(errctx, tee_state(self, &state));
    FAIL_ZERO_RETURN(errctx, (dest != NULL && eof != NULL), AKERR_NULLPOINTER,
		     "NULL argument in tee sink readline");
    if ( state->reader == NULL ) {
	/*
	 * A pair with no reader reports end of input rather than raising, which
	 * is the contract sink.h states: running off the end is how RUNSTREAM
	 * mode finishes normally and INPUT already handles it.
	 */
	if ( len > 0 ) {
	    dest[0] = '\0';
	}
	*eof = true;
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, state->reader->readline(state->reader, dest, len, eof));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *tee_clear(akbasic_TextSink *self)
{
    PREPARE_ERROR(errctx);
    akbasic_TeeSink *state = NULL;

    PASS(errctx, tee_state(self, &state));
    PASS(errctx, state->primary->clear(state->primary));
    PASS(errctx, state->mirror->clear(state->mirror));
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Forward a cursor move to whichever half has a cursor.
 *
 * Only one of them will: an akgl sink has a character grid and a stdio sink does
 * not. Forwarding to both would be wrong anyway -- there is one cursor, and it
 * belongs to the half that draws.
 */
static akerr_ErrorContext *tee_moveto(akbasic_TextSink *self, int col, int row)
{
    PREPARE_ERROR(errctx);
    akbasic_TeeSink *state = NULL;

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

    if ( state->primary != NULL && state->primary->moveto != NULL ) {
	PASS(errctx, state->primary->moveto(state->primary, col, row));
    }
    if ( state->mirror != NULL && state->mirror->moveto != NULL ) {
	PASS(errctx, state->mirror->moveto(state->mirror, col, row));
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Constrain whichever half has a grid, for WINDOW.
 *
 * Same shape as tee_moveto() above it, and for the same reason: a stdio half has
 * no grid to constrain and a windowed half does, so the call goes to whoever can
 * take it and the other is left alone.
 *
 * This was missing, and its absence made `WINDOW` unreachable in the one build
 * that has a character grid -- the standalone AKGL frontend runs the interpreter
 * against a tee, so the fully implemented sink_window() underneath answered
 * nothing. `WINDOW` refused with "needs a text device with a character grid" on
 * the build that had one. TODO.md section 6 item 31.
 */
static akerr_ErrorContext *tee_window(akbasic_TextSink *self, int left, int top, int right, int bottom)
{
    PREPARE_ERROR(errctx);
    akbasic_TeeSink *state = NULL;

    PASS(errctx, tee_state(self, &state));
    if ( state->primary != NULL && state->primary->window != NULL ) {
	PASS(errctx, state->primary->window(state->primary, left, top, right, bottom));
    }
    if ( state->mirror != NULL && state->mirror->window != NULL ) {
	PASS(errctx, state->mirror->window(state->mirror, left, top, right, bottom));
    }
    SUCCEED_RETURN(errctx);
}

/** @brief Forward GRAPHIC's display-mode decision to the half with a display. */
static akerr_ErrorContext *tee_graphic(akbasic_TextSink *self, int mode, int split)
{
    PREPARE_ERROR(errctx);
    akbasic_TeeSink *state = NULL;

    PASS(errctx, tee_state(self, &state));
    if ( state->primary != NULL && state->primary->graphic != NULL ) {
	PASS(errctx, state->primary->graphic(state->primary, mode, split));
    }
    if ( state->mirror != NULL && state->mirror->graphic != NULL ) {
	PASS(errctx, state->mirror->graphic(state->mirror, mode, split));
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Report the grid of whichever half has one.
 *
 * The first half that answers wins rather than both being consulted, because
 * there is one grid and only one half can have it -- the same reasoning
 * tee_moveto() gives about there being one cursor.
 */
static akerr_ErrorContext *tee_grid(akbasic_TextSink *self, int *columns, int *rows, int *cellw, int *cellh)
{
    PREPARE_ERROR(errctx);
    akbasic_TeeSink *state = NULL;

    PASS(errctx, tee_state(self, &state));
    if ( state->primary != NULL && state->primary->grid != NULL ) {
	PASS(errctx, state->primary->grid(state->primary, columns, rows, cellw, cellh));
	SUCCEED_RETURN(errctx);
    }
    if ( state->mirror != NULL && state->mirror->grid != NULL ) {
	PASS(errctx, state->mirror->grid(state->mirror, columns, rows, cellw, cellh));
	SUCCEED_RETURN(errctx);
    }
    FAIL_RETURN(errctx, AKBASIC_ERR_STATE, "neither half of this tee has a character grid");
}

akerr_ErrorContext *akbasic_sink_init_tee(akbasic_TextSink *obj, akbasic_TeeSink *state, akbasic_TextSink *primary, akbasic_TextSink *mirror, akbasic_TextSink *reader)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && state != NULL), AKERR_NULLPOINTER,
		     "NULL argument in sink_init_tee");
    FAIL_ZERO_RETURN(errctx, (primary != NULL && mirror != NULL), AKERR_NULLPOINTER,
		     "A tee sink needs two sinks to write to");
    /*
     * Refused rather than quietly accepted. A third sink here would read from
     * somewhere nothing writes to, which is the sort of wiring mistake that
     * shows up much later as a program that never sees its own input.
     */
    FAIL_ZERO_RETURN(errctx, (reader == NULL || reader == primary || reader == mirror),
		     AKBASIC_ERR_VALUE,
		     "A tee sink's reader must be one of the two sinks it writes to");

    state->primary = primary;
    state->mirror = mirror;
    state->reader = reader;

    obj->self = state;
    obj->write = tee_write;
    obj->writeln = tee_writeln;
    obj->readline = tee_readline;
    obj->clear = tee_clear;
    /*
     * Cleared first, because the two below are conditional and an initializer
     * that leaves a field alone is not an initializer: a caller with a sink on
     * the stack would get whatever was in that memory, and `CHAR` decides
     * whether it can move a cursor by testing this pointer for NULL.
     */
    obj->moveto = NULL;
    obj->window = NULL;
    obj->grid = NULL;
    obj->graphic = NULL;
    /*
     * Offered only when a half can actually do it, so CHAR's refusal against a
     * stdio-only driver still reads correctly through a tee.
     */
    if ( (primary != NULL && primary->moveto != NULL) ||
	 (mirror != NULL && mirror->moveto != NULL) ) {
	obj->moveto = tee_moveto;
    }
    /* Same rule for WINDOW, so its refusal against a grid-less pair still reads
       correctly through a tee. */
    if ( (primary != NULL && primary->window != NULL) ||
	 (mirror != NULL && mirror->window != NULL) ) {
	obj->window = tee_window;
    }
    /* And for the grid RWINDOW and RGR read. */
    if ( (primary != NULL && primary->grid != NULL) ||
	 (mirror != NULL && mirror->grid != NULL) ) {
	obj->grid = tee_grid;
    }
    if ( (primary != NULL && primary->graphic != NULL) ||
	 (mirror != NULL && mirror->graphic != NULL) ) {
	obj->graphic = tee_graphic;
    }
    SUCCEED_RETURN(errctx);
}
