/**
 * @file main.c
 * @brief The standalone driver.
 *
 * Everything that belongs to a program rather than to a library lives here: argv
 * handling, which frontend to bring up, and a FINISH_NORETURN -- which belongs
 * only in a main(). The interpreter library itself never terminates the process.
 *
 * There are two drivers in here and the build option picks between them. Without
 * AKBASIC_HAVE_AKGL this is a terminal program: stdio in, stdout out, no window.
 * With it, the program is an SDL host -- window, font, event pump, frame loop --
 * and its output goes to the window *and* to stdout, so a piped program produces
 * the same bytes either way. The whole of that second driver is
 * akbasic/frontend.h; what is left here is argv and the choice.
 *
 * The runtime is static rather than automatic because it carries every pool the
 * interpreter owns -- several megabytes -- and that will not fit on a default
 * stack. An embedding game would place it in its own state for the same reason.
 */

/* clock_gettime, CLOCK_MONOTONIC and isatty. C99 alone declares none of them. */
#define _POSIX_C_SOURCE 199309L

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

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

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

static akbasic_Runtime RUNTIME;

#ifdef AKBASIC_HAVE_AKGL
#include <akbasic/frontend.h>

static akbasic_AkglFrontend FRONTEND;
#else

static akbasic_TextSink SINK;
static akbasic_StdioSink SINKSTATE;

/**
 * @brief Milliseconds off a monotonic clock, for akbasic_runtime_settime().
 *
 * The library reads no clock of its own -- it owns no loop and must not block --
 * so somebody has to tell it what time it is, and for a standalone program that
 * is this. A game does the same thing once a frame off whatever clock it already
 * keeps.
 *
 * Monotonic rather than wall-clock: the interpreter only ever compares these
 * values, and an NTP step backwards in the middle of a tune would hold a note
 * for however long the correction was.
 */
static int64_t monotonic_ms(void)
{
    struct timespec now;

    if ( clock_gettime(CLOCK_MONOTONIC, &now) != 0 ) {
	/*
	 * A clock that cannot be read leaves time frozen, which expires every
	 * duration immediately: the program still runs and the music simply
	 * rushes. Better than refusing to start over a note length.
	 */
	return 0;
    }
    return ((int64_t)now.tv_sec * 1000) + (now.tv_nsec / 1000000);
}

/**
 * @brief Step the interpreter to completion, refreshing the clock as it goes.
 *
 * One step at a time rather than a single unbounded run(), because the driver is
 * the thing that owns the clock: a PLAY string whose notes all measured
 * themselves against a frozen zero would rush the whole tune out at once.
 *
 * A function of its own rather than a loop inside main()'s ATTEMPT, for two
 * reasons that both come from the error protocol. CATCH expands to a `break`, so
 * inside a loop it would escape only the loop and leave the rest of the ATTEMPT
 * running with an error pending. And PASS expands to a `return` of the context,
 * which main() cannot do -- it returns an int. Wrapping the loop is what the
 * protocol prescribes for exactly this shape.
 */
static akerr_ErrorContext AKERR_NOIGNORE *drive(akbasic_Runtime *obj)
{
    PREPARE_ERROR(errctx);

    while ( obj->mode != AKBASIC_MODE_QUIT ) {
	PASS(errctx, akbasic_runtime_settime(obj, monotonic_ms()));
	PASS(errctx, akbasic_runtime_run(obj, 1));
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief The terminal driver: no window, no SDL, output on stdout.
 *
 * @param program The already-opened program file, or NULL for an interactive REPL.
 * @param path That file's path, so a relative asset path a program writes can be resolved
 *             against the program's own directory. NULL for an interactive REPL.
 */
static akerr_ErrorContext AKERR_NOIGNORE *run_stdio(FILE *program, const char *path)
{
    PREPARE_ERROR(errctx);

    if ( program != NULL ) {
	/*
	 * A file argument: read the program from it in RUNSTREAM mode, which
	 * files each line under its line number and then switches to RUN.
	 */
	PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, program));
	PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
	/*
	 * So an asset path a program writes -- `SPRSAV "ship.png", 1` -- can be
	 * tried against the program's own directory as well as against the
	 * working one. Without it a `.bas` stored beside its art only works when
	 * it is launched from its own directory.
	 */
	PASS(errctx, akbasic_runtime_set_source_path(&RUNTIME, path));
	PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_RUNSTREAM));
    } else {
	PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, stdin));
	PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK));
	PASS(errctx, akbasic_runtime_start(&RUNTIME, AKBASIC_MODE_REPL));
    }
    PASS(errctx, drive(&RUNTIME));
    SUCCEED_RETURN(errctx);
}

#endif /* !AKBASIC_HAVE_AKGL */

#ifdef AKBASIC_HAVE_AKGL
/**
 * @brief Where to find the Commodore font.
 *
 * Compiled in by CMake, because the reference's relative "./fonts/..." only ever
 * worked from its own source directory. AKBASIC_FONT overrides it at runtime,
 * which is what an installed copy or a different font needs.
 */
#ifndef AKBASIC_FONT_PATH
#define AKBASIC_FONT_PATH "fonts/C64_Pro_Mono-STYLE.ttf"
#endif

/**
 * @brief The SDL driver: an 800x600 window, and stdout still gets everything.
 *
 * @param program The already-opened program file, or NULL to type at the window.
 * @param path That file's path; see run_stdio(). NULL when there is no program file.
 */
static akerr_ErrorContext AKERR_NOIGNORE *run_akgl(FILE *program, const char *path)
{
    PREPARE_ERROR(errctx);
    const char *fontpath = getenv("AKBASIC_FONT");
    FILE *input = program;
    int mode = AKBASIC_MODE_RUNSTREAM;
    int fd = 0;

    if ( fontpath == NULL ) {
	fontpath = AKBASIC_FONT_PATH;
    }
    if ( program == NULL ) {
	/*
	 * With a terminal on the other end of stdin the window is the console
	 * and typed lines come from its line editor. Piped or redirected, they
	 * come from the pipe instead -- so `basic < program.bas` still behaves
	 * in an AKGL build, and the golden corpus can be driven through this
	 * binary as well as through the stdio one.
	 */
	PASS(errctx, aksl_fileno(stdin, &fd));
	input = (isatty(fd) ? NULL : stdin);
	mode = AKBASIC_MODE_REPL;
    }

    PASS(errctx, akbasic_frontend_akgl_init(&FRONTEND, "BASIC",
					    AKBASIC_FRONTEND_WIDTH,
					    AKBASIC_FRONTEND_HEIGHT,
					    fontpath, AKBASIC_FRONTEND_FONT_SIZE,
					    stdout, input));
    PASS(errctx, akbasic_frontend_akgl_attach(&FRONTEND, &RUNTIME));
    /* See the note in run_stdio(): this is what makes a relative asset path work. */
    PASS(errctx, akbasic_runtime_set_source_path(&RUNTIME, path));
    PASS(errctx, akbasic_runtime_start(&RUNTIME, mode));
    PASS(errctx, akbasic_frontend_akgl_drive(&FRONTEND, &RUNTIME));
    SUCCEED_RETURN(errctx);
}
#endif

int main(int argc, char **argv)
{
    PREPARE_ERROR(errctx);
    FILE *program = NULL;
    int rc = EXIT_SUCCESS;

    ATTEMPT {
	if ( argc > 1 ) {
	    CATCH(errctx, aksl_fopen(argv[1], "r", &program));
	}
#ifdef AKBASIC_HAVE_AKGL
	CATCH(errctx, run_akgl(program, (argc > 1 ? argv[1] : NULL)));
#else
	CATCH(errctx, run_stdio(program, (argc > 1 ? argv[1] : NULL)));
#endif
    } CLEANUP {
	if ( program != NULL ) {
	    IGNORE(aksl_fclose(program));
	}
#ifdef AKBASIC_HAVE_AKGL
	akbasic_frontend_akgl_shutdown(&FRONTEND);
#endif
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "akbasic terminated on an unhandled error");
	rc = EXIT_FAILURE;
    } FINISH_NORETURN(errctx);

    return rc;
}
