/**
 * @file data.c
 * @brief Implements the `DATA` item list and the `READ` cursor.
 */

#include <ctype.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

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

akerr_ErrorContext *akbasic_data_state_init(akbasic_DataState *obj)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL data state in init");
    obj->count = 0;
    obj->cursor = 0;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Collect the items of one `DATA` statement.
 *
 * @param obj The list to append to.
 * @param cursor Points just past the `DATA` keyword; advanced past what is read.
 * @param lineno The line these items were written on.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_BOUNDS When the list is full or an item is too long.
 */
static akerr_ErrorContext *collect_items(akbasic_DataState *obj, const char **cursor, int64_t lineno)
{
    PREPARE_ERROR(errctx);
    const char *p = *cursor;

    for ( ;; ) {
	akbasic_DataItem *item = NULL;
	size_t used = 0;

	while ( *p == ' ' || *p == '\t' ) {
	    p += 1;
	}
	if ( *p == '\0' || *p == ':' ) {
	    break;
	}
	FAIL_ZERO_RETURN(errctx, (obj->count < AKBASIC_MAX_DATA_ITEMS), AKBASIC_ERR_BOUNDS,
			 "The program holds more than %d DATA items", AKBASIC_MAX_DATA_ITEMS);
	item = &obj->items[obj->count];
	item->lineno = lineno;
	item->quoted = false;

	if ( *p == '"' ) {
	    /*
	     * A quoted item runs to its closing quote and may hold anything --
	     * commas, colons, spaces. That is the only way to put a comma in a
	     * DATA item, and it is why the scan has to understand quotes at all.
	     */
	    item->quoted = true;
	    p += 1;
	    while ( *p != '\0' && *p != '"' && used < sizeof(item->text) - 1 ) {
		item->text[used] = *p;
		used += 1;
		p += 1;
	    }
	    if ( *p == '"' ) {
		p += 1;
	    }
	} else {
	    while ( *p != '\0' && *p != ',' && *p != ':' && used < sizeof(item->text) - 1 ) {
		item->text[used] = *p;
		used += 1;
		p += 1;
	    }
	    /* Trailing spaces are not part of an unquoted item. */
	    while ( used > 0 && (item->text[used - 1] == ' ' || item->text[used - 1] == '\t') ) {
		used -= 1;
	    }
	}
	item->text[used] = '\0';
	obj->count += 1;

	while ( *p == ' ' || *p == '\t' ) {
	    p += 1;
	}
	if ( *p != ',' ) {
	    break;
	}
	p += 1;
    }
    *cursor = p;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Scan one source line for `DATA` statements.
 *
 * Walks statement by statement, the way scan_line_labels() does, because `DATA`
 * is a verb and a verb starts a statement -- and a string literal is the only
 * thing that can hide a separator.
 */
static akerr_ErrorContext *scan_line(akbasic_DataState *obj, const char *code, int64_t lineno)
{
    PREPARE_ERROR(errctx);
    const char *cursor = code;
    bool statementstart = true;
    bool instring = false;
    int cmp = 0;

    while ( *cursor != '\0' ) {
	if ( instring ) {
	    instring = (*cursor != '"');
	    cursor += 1;
	    continue;
	}
	if ( *cursor == '"' ) {
	    instring = true;
	    statementstart = false;
	    cursor += 1;
	    continue;
	}
	if ( *cursor == ':' ) {
	    statementstart = true;
	    cursor += 1;
	    continue;
	}
	if ( isspace((unsigned char)*cursor) ) {
	    cursor += 1;
	    continue;
	}
	/* A stored line may still carry its own number; see scan_line_labels(). */
	if ( statementstart && isdigit((unsigned char)*cursor) ) {
	    while ( isdigit((unsigned char)*cursor) ) {
		cursor += 1;
	    }
	    continue;
	}
	if ( statementstart ) {
	    PASS(errctx, aksl_strncasecmp(cursor, "DATA", 4, &cmp));
	    if ( cmp == 0 && !isalnum((unsigned char)cursor[4]) ) {
		cursor += 4;
		PASS(errctx, collect_items(obj, &cursor, lineno));
		statementstart = false;
		continue;
	    }
	}
	statementstart = false;
	cursor += 1;
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_data_scan(akbasic_Runtime *obj)
{
    PREPARE_ERROR(errctx);
    int64_t i = 0;
    int64_t entry = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in data_scan");
    FAIL_ZERO_RETURN(errctx, (obj->environment != NULL), AKERR_NULLPOINTER,
		     "Runtime has no environment; call akbasic_runtime_init() first");
    entry = obj->environment->lineno;
    PASS(errctx, akbasic_data_state_init(&obj->data_state));
    for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
	if ( obj->source[i].code[0] != '\0' ) {
	    /* Keep BASIC's error prefix on the source line being prescanned. */
	    obj->environment->lineno = i;
	    PASS(errctx, scan_line(&obj->data_state, obj->source[i].code, i));
	}
    }
    obj->environment->lineno = entry;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_data_next(akbasic_Runtime *obj, akbasic_Type type, akbasic_Value *dest)
{
    PREPARE_ERROR(errctx);
    akbasic_DataItem *item = NULL;

    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in data_next");
    FAIL_ZERO_RETURN(errctx, (obj->data_state.cursor < obj->data_state.count),
		     AKBASIC_ERR_STATE, "OUT OF DATA");
    item = &obj->data_state.items[obj->data_state.cursor];
    obj->data_state.cursor += 1;

    PASS(errctx, akbasic_value_zero(dest));
    if ( type == AKBASIC_TYPE_STRING ) {
	dest->valuetype = AKBASIC_TYPE_STRING;
	PASS(errctx, aksl_strcpy(dest->stringval, sizeof(dest->stringval), item->text));
	SUCCEED_RETURN(errctx);
    }

    /*
     * A quoted item is a string and reading it into a number is a mistake worth
     * reporting -- `READ A#` against `DATA "TEXT"` would otherwise quietly yield
     * zero. An *unquoted* item that is not a number is caught the same way, by
     * the conversion refusing it.
     */
    FAIL_NONZERO_RETURN(errctx, item->quoted, AKBASIC_ERR_TYPE,
			"READ: DATA item \"%s\" on line %" PRId64 " is a string",
			item->text, item->lineno);
    if ( type == AKBASIC_TYPE_FLOAT ) {
	dest->valuetype = AKBASIC_TYPE_FLOAT;
	PASS(errctx, aksl_atof(item->text, &dest->floatval));
    } else {
	long long converted = 0;

	dest->valuetype = AKBASIC_TYPE_INTEGER;
	PASS(errctx, aksl_atoll(item->text, &converted));
	dest->intval = (int64_t)converted;
    }
    SUCCEED_RETURN(errctx);
}
