/**
 * @file sink_stdio.c
 * @brief The stdio-backed text sink.
 *
 * This is what makes the golden corpus runnable with no SDL on the machine. It
 * writes exactly what the reference's fmt.Printf/fmt.Println mirror wrote, byte
 * for byte, including the newline writeln appends -- an error line therefore
 * ends in two newlines, because the caller's message already carries one. That
 * is the acceptance contract, not an accident. See TODO.md section 1.8.
 */

#include <stdio.h>

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

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

static akerr_ErrorContext *stdio_write(akbasic_TextSink *self, const char *text)
{
    PREPARE_ERROR(errctx);
    akbasic_StdioSink *state = NULL;
    int count = 0;

    FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
		     "NULL argument in sink write");
    state = (akbasic_StdioSink *)self->self;
    PASS(errctx, aksl_fprintf(&count, state->out, "%s", text));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *stdio_writeln(akbasic_TextSink *self, const char *text)
{
    PREPARE_ERROR(errctx);
    akbasic_StdioSink *state = NULL;
    int count = 0;

    FAIL_ZERO_RETURN(errctx, (self != NULL && text != NULL), AKERR_NULLPOINTER,
		     "NULL argument in sink writeln");
    state = (akbasic_StdioSink *)self->self;
    PASS(errctx, aksl_fprintf(&count, state->out, "%s\n", text));
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *stdio_readline(akbasic_TextSink *self, char *dest, size_t len, bool *eof)
{
    PREPARE_ERROR(errctx);
    akbasic_StdioSink *state = NULL;
    size_t used = 0;

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

    state = (akbasic_StdioSink *)self->self;
    *eof = false;
    dest[0] = '\0';

    /*
     * The end of the stream is AKERR_EOF rather than a NULL return, so it is
     * handled here and turned back into the *eof flag this sink promises. A
     * genuine read error is left to propagate, which is what a NULL from
     * fgets(3) could never be told apart from.
     */
    ATTEMPT {
	CATCH(errctx, aksl_fgets(dest, len, state->in, &used));
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE(errctx, AKERR_EOF) {
	*eof = true;
    } FINISH(errctx, true);
    if ( *eof ) {
	SUCCEED_RETURN(errctx);
    }
    /*
     * aksl_fgets(3)'s own contract: a full buffer with no trailing newline is
     * how a caller spots a line longer than the buffer, because the rest of it
     * is still sitting unread in the stream. Refusing here is what makes that
     * true -- without it, the unread remainder is picked up by the *next*
     * readline() as if it were its own statement, which does not fail, it just
     * runs the wrong program. AKBASIC_MAX_LINE_LENGTH is small enough now that
     * this is not a hypothetical: examples/breakout's own longest line used to
     * clear the old 256-byte ceiling by more than half.
     */
    FAIL_NONZERO_RETURN(errctx, (used == len - 1 && dest[used - 1] != '\n' && dest[used - 1] != '\r'),
			AKBASIC_ERR_BOUNDS,
			"Source line exceeds the %zu character limit", len - 1);
    /*
     * Strip the line terminator. The scanner treats \r and \n as end-of-line
     * anyway, but leaving them on would make a stored source line differ from
     * the same line typed at the REPL.
     */
    while ( used > 0 && (dest[used - 1] == '\n' || dest[used - 1] == '\r') ) {
	dest[used - 1] = '\0';
	used -= 1;
    }
    SUCCEED_RETURN(errctx);
}

static akerr_ErrorContext *stdio_clear(akbasic_TextSink *self)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (self != NULL), AKERR_NULLPOINTER, "NULL sink in clear");
    /* A terminal has no screen to clear that the golden corpus would agree on. */
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_sink_init_stdio(akbasic_TextSink *obj, akbasic_StdioSink *state, FILE *out, FILE *in)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL sink in init");
    FAIL_ZERO_RETURN(errctx, (state != NULL), AKERR_NULLPOINTER, "NULL sink state in init");

    state->out = (out != NULL ? out : stdout);
    state->in = (in != NULL ? in : stdin);

    obj->self = state;
    obj->write = stdio_write;
    obj->writeln = stdio_writeln;
    obj->readline = stdio_readline;
    obj->clear = stdio_clear;
    /*
     * The optional entry points, cleared rather than left alone. A stream has no
     * character grid, so `CHAR` and `WINDOW` must refuse by name -- and they
     * decide that by testing the pointer for NULL. A caller with a sink on the
     * stack would otherwise hand them whatever was in that memory.
     */
    obj->moveto = NULL;
    obj->window = NULL;
    obj->grid = NULL;
    obj->graphic = NULL;
    SUCCEED_RETURN(errctx);
}
