/**
 * @file renumber.c
 * @brief Walking a program's branch targets, and the two things that do it.
 *
 * **RENUMBER** moves every line and rewrites every reference to one. Moving the
 * lines is easy -- source is filed under its line number in `source[]`, so it is
 * a permutation. Rewriting every `GOTO`, `GOSUB`, `RUN`, `RESTORE`, `TRAP` and
 * `COLLISION` target to match is the job, and it is why this was deferred rather
 * than written early: a `RENUMBER` that moved lines without fixing their
 * references would silently break every branch in the program, which is worse
 * than not having the verb at all.
 *
 * **akbasic_runtime_check_targets()** walks the same targets without moving
 * anything, refusing a program that branches by number to a line the program
 * never numbered. It shares this file because it needs exactly the same walk,
 * and a second copy of it would drift.
 *
 * The walk is textual, and the only thing it has to understand about the rest of
 * the language is where a string literal starts and stops -- `PRINT "GOTO 10"`
 * must come through untouched. That is the same statement-walking the label and
 * DATA prescans do. What the two callers differ in is one function pointer:
 * given a target, say what text should stand in its place, and raise if it is not
 * acceptable.
 */

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

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

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

/**
 * @brief Verbs whose numeric arguments name a line.
 *
 * `THEN` and `ELSE` are deliberately absent. Commodore BASIC lets `IF X THEN 100`
 * stand for `THEN GOTO 100`; this dialect does not -- the corpus writes
 * `THEN GOTO 100` -- so a number after `THEN` here is an expression, not a target.
 *
 * `LIST` and `DELETE` are absent too, and for a different reason: their
 * arguments are ranges typed at a prompt, not references stored in a program.
 * Renumbering a `DELETE` inside a listing would be rewriting something nobody
 * meant as a branch.
 */
static const char *BRANCH_VERBS[] = { "GOTO", "GOSUB", "RUN", "RESTORE", "TRAP" };
/** @brief How many entries #BRANCH_VERBS has. */
#define BRANCH_VERB_COUNT ((int)(sizeof(BRANCH_VERBS) / sizeof(BRANCH_VERBS[0])))

/**
 * @brief What a walk does with each numeric branch target it finds.
 *
 * The one thing RENUMBER and the target check differ in. `visit` is handed a
 * target and writes into @p dest the text that should stand in its place --
 * the new number for RENUMBER, the same number for a check -- and may raise to
 * refuse the line outright.
 */
typedef struct akbasic_TargetWalk akbasic_TargetWalk;
struct akbasic_TargetWalk
{
    void *self;
    akerr_ErrorContext *(*visit)(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len);
};

/**
 * @brief Where a line moved to, or the line itself when it did not move.
 *
 * A target naming a line that does not exist is left alone rather than
 * rewritten: `GOTO 9999` in a program with no line 9999 is already broken, and
 * inventing a destination for it would hide that.
 */
static int64_t mapped(const int16_t *map, int64_t line)
{
    if ( line < 0 || line >= AKBASIC_MAX_SOURCE_LINES ) {
	return line;
    }
    return (map[line] >= 0 ? map[line] : line);
}

/**
 * @brief Copy a run of comma-separated line numbers, visiting each.
 *
 * `ON X GOTO 100, 200, 300` is why this takes a list rather than one number: the
 * targets follow the `GOTO`, so handling `GOTO` handles `ON` for free.
 *
 * @param walk What to substitute for each target, and whether to allow it.
 * @param src Reads from here; advanced past what was consumed.
 * @param dest Writes to here; advanced past what was written.
 * @param limit One past the last byte @p dest may write.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_BOUNDS When the rewritten line does not fit.
 */
static akerr_ErrorContext *rewrite_targets(akbasic_TargetWalk *walk, const char **src, char **dest, const char *limit)
{
    PREPARE_ERROR(errctx);
    const char *p = *src;
    char *out = *dest;

    for ( ;; ) {
	char replacement[32];
	int64_t line = 0;
	size_t written = 0;

	while ( *p == ' ' || *p == '\t' ) {
	    FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
			     "RENUMBER: a rewritten line does not fit");
	    *out = *p;
	    out += 1;
	    p += 1;
	}
	if ( !isdigit((unsigned char)*p) ) {
	    /*
	     * Not a number: a label, an expression, or nothing at all. Labels are
	     * the reason RENUMBER is survivable in the first place -- a program
	     * written with `GOTO DONE` needs no rewriting and cannot be broken by
	     * one.
	     */
	    break;
	}
	while ( isdigit((unsigned char)*p) ) {
	    line = (line * 10) + (*p - '0');
	    p += 1;
	}
	PASS(errctx, walk->visit(walk, line, replacement, sizeof(replacement)));
	/*
	 * The fit is decided before the copy rather than read back out of a
	 * return value afterwards: aksl_strcpy refuses rather than truncates,
	 * so the bounds check has to come first to keep the diagnosis this
	 * function already gives.
	 */
	PASS(errctx, aksl_strlen(replacement, &written));
	FAIL_ZERO_RETURN(errctx, (written > 0 && out + written < limit), AKBASIC_ERR_BOUNDS,
			 "RENUMBER: a rewritten line does not fit");
	PASS(errctx, aksl_strcpy(out, (size_t)(limit - out), replacement));
	out += written;

	/*
	 * A comma continues the list; anything else ends it. The spaces are
	 * skipped to find out which, and put back if the answer is "ends" --
	 * without that, `THEN GOTO 9 ELSE ...` came back as `THEN GOTO 30ELSE`.
	 */
	{
	    const char *afterdigits = p;

	    while ( *p == ' ' || *p == '\t' ) {
		p += 1;
	    }
	    if ( *p != ',' ) {
		p = afterdigits;
		break;
	    }
	}
	FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
			 "RENUMBER: a rewritten line does not fit");
	*out = ',';
	out += 1;
	p += 1;
    }
    *src = p;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Rewrite every branch target in one line of source.
 *
 * @param walk What to substitute for each target, and whether to allow it.
 * @param code The line to read.
 * @param dest Where the rewritten line goes.
 * @param len Capacity of @p dest.
 * @return `NULL` on success, otherwise an error context owned by the caller.
 * @throws AKBASIC_ERR_BOUNDS When the rewritten line does not fit.
 */
static akerr_ErrorContext *rewrite_line(akbasic_TargetWalk *walk, const char *code, char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    const char *p = code;
    char *out = dest;
    const char *limit = dest + len - 1;
    bool statementstart = true;
    bool instring = false;
    int cmp = 0;
    int i = 0;

    while ( *p != '\0' ) {
	size_t verblen = 0;
	bool matched = false;

	FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
			 "RENUMBER: a rewritten line does not fit");
	if ( instring ) {
	    instring = (*p != '"');
	    *out = *p;
	    out += 1;
	    p += 1;
	    continue;
	}
	if ( *p == '"' ) {
	    /* A string literal is copied through untouched: `PRINT "GOTO 10"`. */
	    instring = true;
	    statementstart = false;
	    *out = *p;
	    out += 1;
	    p += 1;
	    continue;
	}
	if ( *p == ':' ) {
	    statementstart = true;
	    *out = *p;
	    out += 1;
	    p += 1;
	    continue;
	}
	if ( *p == ' ' || *p == '\t' ) {
	    *out = *p;
	    out += 1;
	    p += 1;
	    continue;
	}

	/*
	 * **Both ends, not just the trailing one.** The check below used to look
	 * only at the character *after* a match, so a name ending in a branch
	 * verb matched inside itself: `RCOLLISION(1, 1)` found `COLLISION` at its
	 * second character, read the `(1,` that followed as a handler argument,
	 * and refused the whole program with "branch to line 1, which the program
	 * did not number" -- naming a line that contains no branch at all.
	 *
	 * The comment below is right that the trailing check is what protects
	 * `GOTOX#`. Nothing protected `XGOTO#`, and `RCOLLISION` is the first
	 * name in the language to reach it.
	 */
	for ( i = 0; i < BRANCH_VERB_COUNT; i++ ) {
	    PASS(errctx, aksl_strlen(BRANCH_VERBS[i], &verblen));
	    if ( p > code && isalnum((unsigned char)p[-1]) ) {
		break;
	    }
	    PASS(errctx, aksl_strncasecmp(p, BRANCH_VERBS[i], verblen, &cmp));
	    if ( cmp == 0 && !isalnum((unsigned char)p[verblen]) ) {
		matched = true;
		break;
	    }
	}
	/*
	 * A branch verb may appear anywhere a statement may, and `GOTO` also
	 * follows `THEN`, `ELSE` and `ON X` -- none of which is a statement
	 * start. So the match is not gated on statementstart; what protects
	 * `A$ = "GOTO"` is the string check above, and what protects a variable
	 * called `GOTOX#` is the alphanumeric check here.
	 */
	if ( matched ) {
	    FAIL_ZERO_RETURN(errctx, (out + verblen < (size_t)(limit - dest) + dest),
			     AKBASIC_ERR_BOUNDS, "RENUMBER: a rewritten line does not fit");
	    PASS(errctx, aksl_memcpy(out, p, verblen));
	    out += verblen;
	    p += verblen;
	    PASS(errctx, rewrite_targets(walk, &p, &out, limit));
	    statementstart = false;
	    continue;
	}

	/*
	 * COLLISION's handler is its *second* argument, so the list rewrite
	 * cannot be pointed at it directly. Copy the type and the comma, then
	 * rewrite what follows.
	 */
	PASS(errctx, aksl_strncasecmp(p, "COLLISION", 9, &cmp));
	if ( cmp == 0 && !isalnum((unsigned char)p[9])
	     && !(p > code && isalnum((unsigned char)p[-1])) ) {
	    PASS(errctx, aksl_memcpy(out, p, 9));
	    out += 9;
	    p += 9;
	    while ( *p != '\0' && *p != ',' && *p != ':' ) {
		FAIL_ZERO_RETURN(errctx, (out < limit), AKBASIC_ERR_BOUNDS,
				 "RENUMBER: a rewritten line does not fit");
		*out = *p;
		out += 1;
		p += 1;
	    }
	    if ( *p == ',' ) {
		*out = ',';
		out += 1;
		p += 1;
		PASS(errctx, rewrite_targets(walk, &p, &out, limit));
	    }
	    statementstart = false;
	    continue;
	}

	statementstart = false;
	*out = *p;
	out += 1;
	p += 1;
    }
    *out = '\0';
    (void)statementstart;
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------- RENUMBER -- */

/** @brief RENUMBER's visitor: substitute the number the line moved to. */
static akerr_ErrorContext *visit_renumber(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    const int16_t *map = (const int16_t *)walk->self;
    int written = 0;

    PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, mapped(map, target)));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_renumber(akbasic_Runtime *obj, int64_t newstart, int64_t increment, int64_t oldstart)
{
    PREPARE_ERROR(errctx);
    int16_t *map = obj == NULL ? NULL : obj->renumber_map;
    akbasic_TargetWalk walk = { map, visit_renumber };
    int64_t next = newstart;
    int64_t i = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in renumber");
    FAIL_ZERO_RETURN(errctx, (increment > 0), AKBASIC_ERR_VALUE,
		     "RENUMBER's increment must be positive, not %" PRId64, increment);
    FAIL_ZERO_RETURN(errctx, (newstart > 0), AKBASIC_ERR_VALUE,
		     "RENUMBER cannot start at line %" PRId64, newstart);

    for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
	map[i] = -1;
    }

    /*
     * Build the whole map before touching anything. A rewrite needs to know
     * where *every* line ended up, including ones it has not reached yet --
     * a backward `GOTO` is as common as a forward one.
     */
    for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
	if ( obj->source[i].code[0] == '\0' || i < oldstart ) {
	    continue;
	}
	FAIL_ZERO_RETURN(errctx, (next < AKBASIC_MAX_SOURCE_LINES), AKBASIC_ERR_BOUNDS,
			 "RENUMBER: line %" PRId64 " would become %" PRId64 ", past the %d line limit",
			 i, next, AKBASIC_MAX_SOURCE_LINES - 1);
	/*
	 * A renumbered line must not land on a kept one. Refused rather than
	 * overwritten: losing a line to a renumbering is not recoverable.
	 */
	FAIL_ZERO_RETURN(errctx, (next >= oldstart || obj->source[next].code[0] == '\0'),
			 AKBASIC_ERR_VALUE,
			 "RENUMBER: line %" PRId64 " would become %" PRId64 ", which already exists",
			 i, next);
	map[i] = next;
	next += increment;
    }

    PASS(errctx, aksl_memset(obj->renumber_visited, 0, sizeof(obj->renumber_visited)));
    for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
	if ( obj->source[i].code[0] == '\0' ) {
	    continue;
	}
	/*
	 * Every line is rewritten, not just the moved ones: a line before
	 * `oldstart` can branch into the region that moved.
	 */
	PASS(errctx, rewrite_line(&walk, obj->source[i].code,
				  obj->renumber_line.code, sizeof(obj->renumber_line.code)));
	obj->renumber_line.lineno = i;
	/*
	 * Every line comes out numbered, whether or not it went in that way.
	 * Asking for numbers is what RENUMBER is, and a program that has been
	 * through it can be branched into by number -- which is the whole point
	 * of running it over source that arrived without any.
	 */
	obj->renumber_line.numbered = true;
	PASS(errctx, aksl_strncpy(obj->source[i].code, AKBASIC_MAX_LINE_LENGTH,
				 obj->renumber_line.code, AKBASIC_MAX_LINE_LENGTH));
	obj->source[i].lineno = obj->renumber_line.lineno;
	obj->source[i].numbered = obj->renumber_line.numbered;
    }

    /* Move the already-rewritten lines in place. The map is a partial
     * permutation: a chain ends at an empty slot, while a cycle closes back
     * on its starting line. A single displaced line is sufficient for both. */
    for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
	int64_t current = i;
	akbasic_SourceLine displaced;
	akbasic_SourceLine next_line;

	if ( map[i] < 0 || obj->renumber_visited[i] ) {
	    continue;
	}
	PASS(errctx, aksl_strncpy(displaced.code, AKBASIC_MAX_LINE_LENGTH,
				 obj->source[i].code, AKBASIC_MAX_LINE_LENGTH));
	displaced.lineno = obj->source[i].lineno;
	displaced.numbered = obj->source[i].numbered;
	for ( ;; ) {
	    int64_t destination = map[current];

	    obj->renumber_visited[current] = 1;
	    if ( destination == i ) {
		displaced.lineno = destination;
		PASS(errctx, aksl_strncpy(obj->source[destination].code, AKBASIC_MAX_LINE_LENGTH,
					 displaced.code, AKBASIC_MAX_LINE_LENGTH));
		obj->source[destination].lineno = displaced.lineno;
		obj->source[destination].numbered = displaced.numbered;
		break;
	    }
	    if ( map[destination] < 0 ) {
		displaced.lineno = destination;
		PASS(errctx, aksl_strncpy(obj->source[destination].code, AKBASIC_MAX_LINE_LENGTH,
					 displaced.code, AKBASIC_MAX_LINE_LENGTH));
		obj->source[destination].lineno = displaced.lineno;
		obj->source[destination].numbered = displaced.numbered;
		PASS(errctx, aksl_memset(&obj->source[current], 0,
					 sizeof(obj->source[current])));
		break;
	    }
	    PASS(errctx, aksl_strncpy(next_line.code, AKBASIC_MAX_LINE_LENGTH,
				     obj->source[destination].code, AKBASIC_MAX_LINE_LENGTH));
	    next_line.lineno = obj->source[destination].lineno;
	    next_line.numbered = obj->source[destination].numbered;
	    displaced.lineno = destination;
	    PASS(errctx, aksl_strncpy(obj->source[destination].code, AKBASIC_MAX_LINE_LENGTH,
				     displaced.code, AKBASIC_MAX_LINE_LENGTH));
	    obj->source[destination].lineno = displaced.lineno;
	    obj->source[destination].numbered = displaced.numbered;
	    PASS(errctx, aksl_memset(&obj->source[current], 0,
				     sizeof(obj->source[current])));
	    PASS(errctx, aksl_strncpy(displaced.code, AKBASIC_MAX_LINE_LENGTH,
				     next_line.code, AKBASIC_MAX_LINE_LENGTH));
	    displaced.lineno = next_line.lineno;
	    displaced.numbered = next_line.numbered;
	    current = destination;
	}
    }
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------- the target prescan -- */

/** @brief What visit_check() needs to name the line it refused. */
typedef struct
{
    const akbasic_Runtime *runtime;
    int64_t lineno;
} CheckState;

/**
 * @brief The prescan's visitor: allow the target, or refuse the program.
 *
 * A number that names a line the program never numbered is refused. In a script
 * written without line numbers `GOTO 100` finds the hundredth line and branches
 * there -- plausible, silent, and wrong, which is the worst kind of defect to be
 * handed at run time.
 *
 * A target naming an *empty* slot is allowed through, for the same reason
 * mapped() leaves one alone: `GOTO 9999` in a program with no line 9999 is
 * already broken, and refusing it here would be inventing a rule about lines
 * that do not exist. The substitution is the number itself, because nothing is
 * being rewritten.
 */
static akerr_ErrorContext *visit_check(akbasic_TargetWalk *walk, int64_t target, char *dest, size_t len)
{
    PREPARE_ERROR(errctx);
    CheckState *state = (CheckState *)walk->self;
    int written = 0;

    PASS(errctx, aksl_snprintf(&written, dest, len, "%" PRId64, target));
    if ( target >= 0 && target < AKBASIC_MAX_SOURCE_LINES ) {
	FAIL_NONZERO_RETURN(errctx,
			    (state->runtime->source[target].code[0] != '\0'
			     && !state->runtime->source[target].numbered),
			    AKBASIC_ERR_SYNTAX,
			    "Line %" PRId64 ": branch to line %" PRId64
			    ", which the program did not number. Branch by LABEL, or RENUMBER first",
			    state->lineno, target);
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_runtime_check_targets(akbasic_Runtime *obj)
{
    PREPARE_ERROR(errctx);
    /*
     * Static for the same reason akbasic_renumber()'s buffers are: a line's worth
     * of rewrite does not belong on a default stack alongside the rest of a
     * step(). Nothing is read back out of it -- the walk needs somewhere to put
     * the text it would have written, and this is it.
     */
    CheckState state = { NULL, 0 };
    akbasic_TargetWalk walk = { &state, visit_check };
    int64_t entry = 0;
    int64_t i = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in check_targets");
    state.runtime = obj;
    entry = obj->environment->lineno;

    for ( i = 0; i < AKBASIC_MAX_SOURCE_LINES; i++ ) {
	if ( obj->source[i].code[0] == '\0' ) {
	    continue;
	}
	state.lineno = i;
	/*
	 * Point the runtime at the line being walked, so the `? N :` prefix
	 * akbasic_runtime_error() builds from environment->lineno names the
	 * offending line rather than whichever one the loader stopped on. The
	 * message repeats the number because the other three prescans do not do
	 * this and get it wrong -- see TODO.md.
	 *
	 * PASS inside the loop, never CATCH: CATCH expands to a break, which
	 * would leave this loop rather than an enclosing ATTEMPT. That also means
	 * the cursor is deliberately *not* restored on the failure path, which is
	 * the whole point of setting it.
	 */
	obj->environment->lineno = i;
	PASS(errctx, rewrite_line(&walk, obj->source[i].code,
				 obj->renumber_discard, sizeof(obj->renumber_discard)));
    }
    /* Nothing was refused, so leave the cursor as the caller had it. */
    obj->environment->lineno = entry;
    SUCCEED_RETURN(errctx);
}
