/**
 * @file runtime_sprite.c
 * @brief The group H verb implementations: SPRITE, MOVSPR, SPRCOLOR, SPRSAV,
 *        COLLISION, and the BUMP / RSPPOS / RSPRITE / RSPCOLOR readbacks.
 *
 * Nothing here includes an SDL or a libakgl header. Every device call goes
 * through the akbasic_SpriteBackend record the host attached, which is what lets
 * the whole group be tested in a build with no SDL on the machine.
 *
 * These verbs are not in the Go reference -- it lists all of them as
 * unimplemented -- so the semantics come from Commodore BASIC 7.0 rather than
 * from a port. Where 7.0 does something a modern renderer cannot, or where the
 * manual leaves a unit undefined, the decision is recorded in TODO.md section 5
 * rather than presented as fidelity.
 *
 * The verbs split three ways, and the split is what makes them testable:
 *
 *   - SPRCOLOR and the four readbacks touch only interpreter state and work with
 *     no device at all.
 *   - SPRITE and MOVSPR update interpreter state *and* tell the device. With no
 *     device they still update the state and then refuse, so a program that
 *     positions a sprite and asks RSPPOS where it is gets the right answer
 *     either way.
 *   - SPRSAV and COLLISION need the device outright.
 */

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

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

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

#include "verbs.h"

/* Most verbs answer "did something happen"; this is that answer. */
#define SUCCEED_TRUE(__obj, __dest)			\
    do {						\
	*(__dest) = &(__obj)->staticTrueValue;		\
    } while ( 0 )

/** @brief What SSHAPE writes into a string variable, ahead of the slot number. */
#define SHAPE_PREFIX		"SHAPE:"

/** @brief MOVSPR's four forms, as src/parser_commands.c encodes them. */
#define MOVSPR_ABSOLUTE		0
#define MOVSPR_RELATIVE		1
#define MOVSPR_POLAR		2
#define MOVSPR_CONTINUOUS	3

/**
 * @brief Refuse politely when the host lent us no sprite device.
 *
 * Names the verb, because "no sprite device" on its own tells a program author
 * nothing about which line to look at. The standalone stdio driver attaches no
 * backend at all, so this is a common path rather than an edge case.
 */
static akerr_ErrorContext *require_sprites(akbasic_Runtime *obj, const char *verb)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (obj != NULL && verb != NULL), AKERR_NULLPOINTER,
		     "NULL argument in require_sprites");
    FAIL_ZERO_RETURN(errctx, (obj->sprites != NULL), AKBASIC_ERR_DEVICE,
		     "%s needs a sprite device and this runtime has none", verb);
    SUCCEED_RETURN(errctx);
}

/** @brief Check a sprite number and turn it into a slot index. */
static akerr_ErrorContext *sprite_index(int n, const char *verb, int *dest)
{
    PREPARE_ERROR(errctx);

    FAIL_ZERO_RETURN(errctx, (n >= 1 && n <= AKBASIC_MAX_SPRITES), AKBASIC_ERR_BOUNDS,
		     "%s: sprite %d is outside 1..%d", verb, n, AKBASIC_MAX_SPRITES);
    *dest = n - 1;
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Push one sprite's colour, expansion and priority at the device.
 *
 * One call rather than four, matching the backend record: SPRITE sets them
 * together and a backend that has to rebuild a texture to change a colour should
 * not do it four times for one statement.
 */
static akerr_ErrorContext *push_configuration(akbasic_Runtime *obj, int index)
{
    PREPARE_ERROR(errctx);
    akbasic_Sprite *sprite = &obj->sprite_state.sprites[index];
    akbasic_Color color;

    if ( obj->sprites == NULL || obj->sprites->configure == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, akbasic_graphics_palette(sprite->colorindex, &color));
    PASS(errctx, obj->sprites->configure(obj->sprites, index + 1, color,
					 sprite->xexpand, sprite->yexpand, sprite->behind));
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- SPRITE -- */

akerr_ErrorContext *akbasic_cmd_sprite(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Sprite *sprite = NULL;
    double args[7];
    int count = 0;
    int index = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in SPRITE");
    PASS(errctx, akbasic_args_numbers(obj, expr, "SPRITE", args, 7, &count));
    FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX,
		     "SPRITE expected a sprite number");
    PASS(errctx, sprite_index((int)args[0], "SPRITE", &index));
    sprite = &obj->sprite_state.sprites[index];

    /*
     * Every argument after the number is optional and an omitted one leaves that
     * attribute alone -- which is BASIC 7.0's rule and is what makes
     * `SPRITE 1,1` and `SPRITE 1,,,,1` both useful.
     */
    if ( count >= 2 ) {
	sprite->enabled = (args[1] != 0.0);
    }
    if ( count >= 3 ) {
	FAIL_ZERO_RETURN(errctx, (args[2] >= 1.0 && args[2] <= 16.0), AKBASIC_ERR_BOUNDS,
			 "SPRITE: colour %d is outside 1..16", (int)args[2]);
	sprite->colorindex = (int)args[2];
    }
    if ( count >= 4 ) {
	sprite->behind = (args[3] != 0.0);
    }
    if ( count >= 5 ) {
	sprite->xexpand = (args[4] != 0.0);
    }
    if ( count >= 6 ) {
	sprite->yexpand = (args[5] != 0.0);
    }
    if ( count >= 7 ) {
	/*
	 * The multicolour bit is recorded and RSPRITE reads it back, but no
	 * pattern format here carries the second bit per pixel that multicolour
	 * selects with -- see TODO.md section 5. Recording it costs nothing and
	 * keeps the argument position honest for the day one does.
	 */
	sprite->multicolour = (args[6] != 0.0);
    }

    /*
     * State first, device second, and the refusal last of all. A program with no
     * sprite device still gets its state updated, so RSPRITE and RSPPOS answer
     * correctly -- and it still gets told, by name, that SPRITE could not reach
     * a device.
     */
    PASS(errctx, require_sprites(obj, "SPRITE"));
    PASS(errctx, push_configuration(obj, index));
    if ( obj->sprites->show != NULL ) {
	PASS(errctx, obj->sprites->show(obj->sprites, index + 1, sprite->enabled));
    }
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- MOVSPR -- */

/**
 * @brief Put a sprite where the state says it is, if there is a device to tell.
 *
 * Shared by MOVSPR and by the continuous-motion service, which are the only two
 * things that move a sprite.
 */
static akerr_ErrorContext *push_position(akbasic_Runtime *obj, int index)
{
    PREPARE_ERROR(errctx);
    akbasic_Sprite *sprite = &obj->sprite_state.sprites[index];

    if ( obj->sprites == NULL || obj->sprites->move == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, obj->sprites->move(obj->sprites, index + 1, sprite->x, sprite->y));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_cmd_movspr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Sprite *sprite = NULL;
    double args[4];
    int count = 0;
    int index = 0;
    int form = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in MOVSPR");
    PASS(errctx, akbasic_args_numbers(obj, expr, "MOVSPR", args, 4, &count));
    /* form, n, a, b -- the parse handler guarantees all four or it raised. */
    FAIL_ZERO_RETURN(errctx, (count == 4), AKBASIC_ERR_SYNTAX,
		     "MOVSPR expected a sprite number and two values");
    form = (int)args[0];
    PASS(errctx, sprite_index((int)args[1], "MOVSPR", &index));
    sprite = &obj->sprite_state.sprites[index];

    switch ( form ) {
    case MOVSPR_ABSOLUTE:
	sprite->x = args[2];
	sprite->y = args[3];
	break;
    case MOVSPR_RELATIVE:
	sprite->x += args[2];
	sprite->y += args[3];
	break;
    case MOVSPR_POLAR:
	/*
	 * Distance first, then angle -- that order is BASIC 7.0's and it is the
	 * opposite of the continuous form's, which is a trap worth naming. The
	 * angle is degrees clockwise from vertical, so 0 is straight up and 90 is
	 * to the right; that is a compass bearing, not the mathematical
	 * convention, and it is why the sine and cosine look swapped.
	 */
	sprite->x += args[2] * sin(args[3] * M_PI / 180.0);
	sprite->y -= args[2] * cos(args[3] * M_PI / 180.0);
	break;
    case MOVSPR_CONTINUOUS:
	FAIL_ZERO_RETURN(errctx,
			 (args[3] >= 0.0 && args[3] <= (double)AKBASIC_SPRITE_MAX_SPEED),
			 AKBASIC_ERR_BOUNDS,
			 "MOVSPR: speed %d is outside 0..%d",
			 (int)args[3], AKBASIC_SPRITE_MAX_SPEED);
	sprite->angle = args[2];
	sprite->speed = (int)args[3];
	/*
	 * Nothing moves here. The sprite starts moving on the next step, paced
	 * off the host's clock by akbasic_sprite_service() -- exactly the way the
	 * PLAY queue is, and for the same reason: this library owns no loop and
	 * must not block.
	 */
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    default:
	FAIL_RETURN(errctx, AKBASIC_ERR_SYNTAX, "MOVSPR: unknown argument form %d", form);
    }

    PASS(errctx, require_sprites(obj, "MOVSPR"));
    PASS(errctx, push_position(obj, index));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_sprite_service(akbasic_Runtime *obj)
{
    PREPARE_ERROR(errctx);
    double elapsed = 0.0;
    int i = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in sprite_service");

    if ( obj->sprite_state.lastservicems == 0 ) {
	obj->sprite_state.lastservicems = obj->timems;
	SUCCEED_RETURN(errctx);
    }
    elapsed = (double)(obj->timems - obj->sprite_state.lastservicems) / 1000.0;
    if ( elapsed <= 0.0 ) {
	/*
	 * A host that never calls akbasic_runtime_settime() leaves the clock at
	 * zero, and a host that stepped twice inside one millisecond has not
	 * moved. Neither is an error and neither should move a sprite by a
	 * garbage amount; a clock that went backwards lands here too.
	 */
	SUCCEED_RETURN(errctx);
    }
    obj->sprite_state.lastservicems = obj->timems;

    /*
     * A loop, so CATCH and the _BREAK macros are banned in here -- they expand to
     * a C break and would leave the loop with an error still pending.
     */
    for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	akbasic_Sprite *sprite = &obj->sprite_state.sprites[i];
	double distance = 0.0;

	if ( sprite->speed <= 0 ) {
	    continue;
	}
	distance = (double)sprite->speed * AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND * elapsed;
	sprite->x += distance * sin(sprite->angle * M_PI / 180.0);
	sprite->y -= distance * cos(sprite->angle * M_PI / 180.0);
	PASS(errctx, push_position(obj, i));
    }
    SUCCEED_RETURN(errctx);
}

/* -------------------------------------------------------------- SPRCOLOR -- */

akerr_ErrorContext *akbasic_cmd_sprcolor(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Color c1;
    akbasic_Color c2;
    double args[2];
    int count = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in SPRCOLOR");
    PASS(errctx, akbasic_args_numbers(obj, expr, "SPRCOLOR", args, 2, &count));
    FAIL_ZERO_RETURN(errctx, (count >= 1), AKBASIC_ERR_SYNTAX,
		     "SPRCOLOR expected at least one colour");
    FAIL_ZERO_RETURN(errctx, (args[0] >= 1.0 && args[0] <= 16.0), AKBASIC_ERR_BOUNDS,
		     "SPRCOLOR: colour %d is outside 1..16", (int)args[0]);
    obj->sprite_state.sharedcolor1 = (int)args[0];
    if ( count >= 2 ) {
	FAIL_ZERO_RETURN(errctx, (args[1] >= 1.0 && args[1] <= 16.0), AKBASIC_ERR_BOUNDS,
			 "SPRCOLOR: colour %d is outside 1..16", (int)args[1]);
	obj->sprite_state.sharedcolor2 = (int)args[1];
    }

    /*
     * No device required. The two registers are the program's state, RSPCOLOR
     * reads them back, and a runtime with no sprite device is still entitled to
     * remember what it was told -- the same rule GRAPHIC's mode follows.
     */
    if ( obj->sprites != NULL && obj->sprites->shared_colors != NULL ) {
	PASS(errctx, akbasic_graphics_palette(obj->sprite_state.sharedcolor1, &c1));
	PASS(errctx, akbasic_graphics_palette(obj->sprite_state.sharedcolor2, &c2));
	PASS(errctx, obj->sprites->shared_colors(obj->sprites, c1, c2));
    }
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- SPRSAV -- */

/**
 * @brief Read a DIMmed integer array out into packed sprite pattern bytes.
 *
 * The array form is ours rather than BASIC 7.0's, and it exists because a value
 * in this interpreter carries a NUL-terminated `char[256]`: a C128 puts the
 * 63 raw bytes of a pattern in a string, and a string that cannot hold a zero
 * byte cannot hold a sprite. An integer array can, and `DIM P#(63)` is exactly
 * the right size -- see TODO.md section 5.
 */
static akerr_ErrorContext *pattern_from_array(akbasic_Runtime *obj, akbasic_ASTLeaf *arg, uint8_t *dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Variable *variable = NULL;
    int64_t subscript[1];
    int64_t length = 0;
    int i = 0;

    PASS(errctx, akbasic_environment_get(obj->environment, arg->identifier, &variable));
    FAIL_ZERO_RETURN(errctx, (variable != NULL), AKBASIC_ERR_UNDEFINED,
		     "SPRSAV could not reach the array %s", arg->identifier);
    length = (int64_t)variable->valuecount;
    FAIL_ZERO_RETURN(errctx, (length >= AKBASIC_SPRITE_PATTERN_BYTES), AKBASIC_ERR_BOUNDS,
		     "SPRSAV: %s holds %" PRId64 " elements and a sprite pattern is %d",
		     arg->identifier, length, AKBASIC_SPRITE_PATTERN_BYTES);

    for ( i = 0; i < AKBASIC_SPRITE_PATTERN_BYTES; i++ ) {
	akbasic_Value *element = NULL;

	subscript[0] = (int64_t)i;
	PASS(errctx, akbasic_variable_get_subscript(variable, subscript, 1, &element));
	FAIL_NONZERO_RETURN(errctx, (element->valuetype == AKBASIC_TYPE_STRING),
			    AKBASIC_ERR_TYPE,
			    "SPRSAV: %s(%d) is a string, and a pattern is bytes",
			    arg->identifier, i);
	/*
	 * Masked rather than refused. A pattern byte is eight pixels and there
	 * are only eight to set, so anything outside 0..255 is a program that
	 * has miscounted -- but a DATA statement full of 256s should draw
	 * something recognisable rather than stop the program dead.
	 */
	dest[i] = (uint8_t)(((element->valuetype == AKBASIC_TYPE_FLOAT)
			     ? (int64_t)element->floatval : element->intval) & 0xff);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief Install sprite @p index from whatever the first argument turned out to be.
 *
 * Three shapes of source, told apart by what the leaf *is* rather than by an
 * argument position:
 *
 *   - an unsubscripted integer-array identifier -- 63 pattern bytes
 *   - a string beginning "SHAPE:" -- a region SSHAPE saved
 *   - any other string -- a path to an image file
 */
static akerr_ErrorContext *define_from_leaf(akbasic_Runtime *obj, akbasic_ASTLeaf *arg, int index)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *value = NULL;
    akbasic_Color fg;
    akbasic_Color bg;
    uint8_t pattern[AKBASIC_SPRITE_PATTERN_BYTES];
    size_t prefixlen = 0;
    int handle = 0;
    int cmp = 0;

    if ( arg->leaftype == AKBASIC_LEAF_IDENTIFIER_INT &&
	 akbasic_leaf_first_subscript(arg) == NULL ) {
	FAIL_ZERO_RETURN(errctx, (obj->sprites->define != NULL), AKBASIC_ERR_DEVICE,
			 "This sprite device cannot be given a pattern");
	PASS(errctx, pattern_from_array(obj, arg, pattern));
	PASS(errctx, akbasic_graphics_palette(obj->sprite_state.sprites[index].colorindex, &fg));
	/*
	 * A clear bit is transparent, not black. On a C128 the background shows
	 * through a sprite's clear pixels; drawing them opaque would put a
	 * 24x21 black rectangle behind every sprite.
	 */
	bg.r = 0;
	bg.g = 0;
	bg.b = 0;
	bg.a = 0;
	PASS(errctx, obj->sprites->define(obj->sprites, index + 1, pattern,
					  AKBASIC_SPRITE_PATTERN_BYTES, fg, bg));
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
    FAIL_ZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
		     "SPRSAV expected a sprite number, an image path, a saved shape or an integer array");

    PASS(errctx, aksl_strlen(SHAPE_PREFIX, &prefixlen));
    PASS(errctx, aksl_strncmp(value->stringval, SHAPE_PREFIX, prefixlen, &cmp));
    if ( cmp == 0 ) {
	FAIL_ZERO_RETURN(errctx, (obj->sprites->define_shape != NULL), AKBASIC_ERR_DEVICE,
			 "This sprite device cannot take a saved shape");
	/*
	 * aksl_atoi refuses a slot number that is not one, where atoi(3) answered
	 * a silent 0 -- so `SPRSAV "SHAPE:", 1` is now told what is wrong with it
	 * rather than being handed shape 0.
	 */
	PASS(errctx, aksl_atoi(value->stringval + prefixlen, &handle));
	PASS(errctx, obj->sprites->define_shape(obj->sprites, index + 1, handle));
	SUCCEED_RETURN(errctx);
    }

    FAIL_ZERO_RETURN(errctx, (obj->sprites->define_file != NULL), AKBASIC_ERR_DEVICE,
		     "This sprite device cannot load an image file");
    /*
     * The running program's directory goes with the path. The backend tries the
     * working directory first and this second, which is what makes a `.bas`
     * stored beside its art work from anywhere -- and it is exactly how libakgl
     * resolves a spritesheet named by a sprite document.
     */
    PASS(errctx, obj->sprites->define_file(obj->sprites, index + 1, value->stringval,
					   (obj->sourcepath[0] != '\0'
					    ? obj->sourcepath : NULL)));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_cmd_sprsav(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *source = NULL;
    akbasic_ASTLeaf *target = NULL;
    akbasic_Value *value = NULL;
    int index = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in SPRSAV");
    PASS(errctx, require_sprites(obj, "SPRSAV"));

    source = akbasic_leaf_first_argument(expr);
    target = (source != NULL ? source->next : NULL);
    FAIL_ZERO_RETURN(errctx, (source != NULL && target != NULL), AKBASIC_ERR_SYNTAX,
		     "SPRSAV expected a source and a destination");
    FAIL_NONZERO_RETURN(errctx, (target->next != NULL), AKBASIC_ERR_SYNTAX,
			"SPRSAV takes exactly two arguments");

    /*
     * Only one direction. A C128's SPRSAV also copies a sprite *out* into a
     * string, which here would mean writing an image file -- a disk operation,
     * and group F is unimplemented as a whole. Refused by name rather than
     * half-built; see TODO.md section 5.
     */
    PASS(errctx, akbasic_runtime_evaluate(obj, target, &value));
    FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_DEVICE,
			"SPRSAV cannot copy a sprite out to a string or a file; that is a disk operation and this interpreter has none");
    PASS(errctx, sprite_index((int)value->intval, "SPRSAV", &index));

    PASS(errctx, define_from_leaf(obj, source, index));
    obj->sprite_state.sprites[index].defined = true;
    PASS(errctx, push_configuration(obj, index));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------- COLLISION -- */

akerr_ErrorContext *akbasic_cmd_collision(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = NULL;
    akbasic_ASTLeaf *handler = NULL;
    akbasic_Value *value = NULL;
    int type = 0;
    int source = AKBASIC_INTERRUPT_SPRITE;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in COLLISION");
    arg = akbasic_leaf_first_argument(expr);
    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
		     "COLLISION expected a collision type");
    PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
    FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"COLLISION expected a number");
    type = (int)value->intval;

    FAIL_ZERO_RETURN(errctx, (type >= 1 && type <= 3), AKBASIC_ERR_BOUNDS,
		     "COLLISION: type %d is outside 1..3", type);
    /*
     * Types 1 and 2 both exist now. A C128's type 2 is a sprite against the set
     * pixels of the bitmap screen, which was refused here because reading the
     * render target back every frame is not affordable -- but SOLID gives this
     * interpreter a background made of rectangles instead of pixels, and that is
     * the same question asked in a form it can answer. The same move SPRSAV made
     * when it learned to take an image path.
     *
     * Type 3 stays refused. There is still no light pen.
     */
    FAIL_ZERO_RETURN(errctx, (type == 1 || type == 2), AKBASIC_ERR_DEVICE,
		     "COLLISION type %d is not implemented: there is no light pen", type);
    source = (type == 2 ? AKBASIC_INTERRUPT_BACKGROUND : AKBASIC_INTERRUPT_SPRITE);

    handler = arg->next;
    if ( handler == NULL ) {
	/* No target disarms it, which is how a C128 turns a handler off. */
	PASS(errctx, akbasic_runtime_disarm_interrupt(obj, source));
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }
    FAIL_NONZERO_RETURN(errctx, (handler->next != NULL), AKBASIC_ERR_SYNTAX,
			"COLLISION takes a type and at most one handler");

    /*
     * A label is taken by *name*, not by evaluating it. Evaluating would resolve
     * it here and now, and a handler is normally written on a line the program
     * has not reached yet -- so the name is what is stored and it is resolved
     * when the interrupt fires. A line number is an ordinary expression and is
     * evaluated, so `COLLISION 1, L#` works too.
     */
    if ( handler->leaftype == AKBASIC_LEAF_IDENTIFIER &&
	 akbasic_leaf_first_subscript(handler) == NULL ) {
	PASS(errctx, akbasic_runtime_arm_interrupt(obj, source,
						   0, handler->identifier));
	SUCCEED_TRUE(obj, dest);
	SUCCEED_RETURN(errctx);
    }

    PASS(errctx, akbasic_runtime_evaluate(obj, handler, &value));
    FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"COLLISION expected a line number or a label");
    PASS(errctx, akbasic_runtime_arm_interrupt(obj, source,
					       value->intval, NULL));
    SUCCEED_TRUE(obj, dest);
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_collision_service(akbasic_Runtime *obj)
{
    PREPARE_ERROR(errctx);
    uint16_t mask = 0;
    uint16_t solidmask = 0;

    FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
		     "NULL runtime in collision_service");
    if ( obj->sprites == NULL || obj->sprites->collisions == NULL ) {
	SUCCEED_RETURN(errctx);
    }
    PASS(errctx, obj->sprites->collisions(obj->sprites, &mask));
    /*
     * Static geometry is a second question with a second accumulator and a
     * second handler, answered from the same scan the device just ran. A backend
     * that cannot do it withholds the entry point and reports nothing, the same
     * way a runtime with no backend at all does.
     */
    if ( obj->sprites->solids != NULL ) {
	PASS(errctx, obj->sprites->solids(obj->sprites, &solidmask));
    }
    /*
     * Pull the detail for whatever collided, while the scan that produced it is
     * still the current one. RCOLLISION answers from here rather than from the
     * device, because by the time a handler runs the sprites have moved on --
     * akbasic_sprite_service() runs immediately before this.
     */
    if ( (mask | solidmask) != 0 && obj->sprites->contact != NULL ) {
	uint16_t both = (uint16_t)(mask | solidmask);
	int i = 0;

	for ( i = 0; i < AKBASIC_MAX_SPRITES; i++ ) {
	    bool found = false;

	    if ( (both & (uint16_t)(1u << i)) == 0 ) {
		continue;
	    }
	    /* PASS rather than CATCH: this is a loop. */
	    PASS(errctx, obj->sprites->contact(obj->sprites, i + 1,
					       &obj->sprite_state.contacts[i], &found));
	}
    }
    if ( solidmask != 0 ) {
	obj->sprite_state.bumpedsolid |= solidmask;
	PASS(errctx, akbasic_runtime_raise_interrupt(obj, AKBASIC_INTERRUPT_BACKGROUND));
    }
    if ( mask == 0 ) {
	SUCCEED_RETURN(errctx);
    }
    /*
     * Accumulated, not replaced. BUMP() answers "has this sprite hit anything
     * since I last looked", so a collision that starts and ends between two
     * polls is still news; BUMP clears what it reports.
     */
    obj->sprite_state.bumped |= mask;
    /*
     * Raised unconditionally. An unarmed source records nothing, so there is
     * nothing to ask before raising -- see akbasic_runtime_raise_interrupt.
     */
    PASS(errctx, akbasic_runtime_raise_interrupt(obj, AKBASIC_INTERRUPT_SPRITE));
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------- readbacks -- */

/**
 * @brief Evaluate a function's nth argument.
 *
 * The dispatch table hands a function handler `lval` and `rval` as NULL and its
 * whole argument list on the leaf, so every function in the interpreter digs its
 * arguments out this way. src/runtime_functions.c has the same helper and keeps
 * it static; a third copy would be the point at which it moved to args.h, and
 * this is the second.
 */
static akerr_ErrorContext *nth_number(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *fname, int n, int64_t *dest)
{
    PREPARE_ERROR(errctx);
    akbasic_ASTLeaf *arg = NULL;
    akbasic_Value *value = NULL;
    int i = 0;

    FAIL_ZERO_RETURN(errctx, (expr != NULL), AKERR_NULLPOINTER, "NIL leaf");
    arg = akbasic_leaf_first_argument(expr);
    for ( i = 0; i < n && arg != NULL; i++ ) {
	arg = arg->next;
    }
    FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX,
		     "%s is missing argument %d", fname, n + 1);
    PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
    FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
			"%s expected a number in argument %d", fname, n + 1);
    *dest = (value->valuetype == AKBASIC_TYPE_FLOAT)
	? (int64_t)value->floatval : value->intval;
    SUCCEED_RETURN(errctx);
}

/** @brief Take a scratch integer value from the per-line pool for a function result. */
static akerr_ErrorContext *integer_result(akbasic_Runtime *obj, int64_t value, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *out = NULL;

    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_INTEGER;
    out->intval = value;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_bump(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    uint16_t reported = 0;
    int64_t type = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in BUMP");
    PASS(errctx, nth_number(obj, expr, "BUMP", 0, &type));
    FAIL_ZERO_RETURN(errctx, (type == 1 || type == 2), AKBASIC_ERR_DEVICE,
		     "BUMP type %" PRId64 " is not implemented; sprite-to-sprite is 1 and static geometry is 2",
		     type);

    /*
     * Reading clears, which is what a C128 does and what makes the question
     * answerable at all: without it a program that polls in a loop would see the
     * same collision forever.
     */
    if ( type == 2 ) {
	reported = obj->sprite_state.bumpedsolid;
	obj->sprite_state.bumpedsolid = 0;
    } else {
	reported = obj->sprite_state.bumped;
	obj->sprite_state.bumped = 0;
    }
    PASS(errctx, integer_result(obj, (int64_t)reported, dest));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_rsppos(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    akbasic_Sprite *sprite = NULL;
    PREPARE_ERROR(errctx);
    int64_t n = 0;
    int64_t field = 0;
    int index = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in RSPPOS");
    PASS(errctx, nth_number(obj, expr, "RSPPOS", 0, &n));
    PASS(errctx, nth_number(obj, expr, "RSPPOS", 1, &field));
    PASS(errctx, sprite_index((int)n, "RSPPOS", &index));
    sprite = &obj->sprite_state.sprites[index];

    switch ( field ) {
    case 0:
	PASS(errctx, integer_result(obj, (int64_t)sprite->x, dest));
	break;
    case 1:
	PASS(errctx, integer_result(obj, (int64_t)sprite->y, dest));
	break;
    case 2:
	PASS(errctx, integer_result(obj, (int64_t)sprite->speed, dest));
	break;
    default:
	FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS,
		    "RSPPOS: field %" PRId64 " is outside 0..2 (x, y, speed)", field);
    }
    SUCCEED_RETURN(errctx);
}

/** @brief Take a scratch float value from the per-line pool for a function result. */
static akerr_ErrorContext *float_result(akbasic_Runtime *obj, double value, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Value *out = NULL;

    PASS(errctx, akbasic_environment_new_value(obj->environment, &out));
    PASS(errctx, akbasic_value_zero(out));
    out->valuetype = AKBASIC_TYPE_FLOAT;
    out->floatval = value;
    *dest = out;
    SUCCEED_RETURN(errctx);
}

/* ----------------------------------------------------------------- SOLID -- */

akerr_ErrorContext *akbasic_cmd_solid(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Solid *solid = NULL;
    double args[5];
    int count = 0;
    int id = 0;
    int i = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in SOLID");
    PASS(errctx, require_sprites(obj, "SOLID"));
    FAIL_ZERO_RETURN(errctx, (obj->sprites->solid != NULL), AKBASIC_ERR_DEVICE,
		     "SOLID needs a sprite device that can carry static collision geometry, and this one cannot");
    PASS(errctx, akbasic_args_numbers(obj, expr, "SOLID", args, 5, &count));
    FAIL_ZERO_RETURN(errctx, (count == 0 || count == 1 || count == 5), AKBASIC_ERR_SYNTAX,
		     "SOLID takes nothing, an id, or an id and four corners, not %d arguments",
		     count);

    /*
     * No arguments clears the lot, which is how `TRAP`, `COLLISION` and `DCLOSE`
     * all read absence -- and is the verb a level change wants.
     */
    if ( count == 0 ) {
	for ( i = 0; i < AKBASIC_MAX_SOLIDS; i++ ) {
	    if ( !obj->sprite_state.solids[i].active ) {
		continue;
	    }
	    /* PASS rather than CATCH: this is a loop. */
	    PASS(errctx, aksl_memset(&obj->sprite_state.solids[i], 0,
				     sizeof(obj->sprite_state.solids[i])));
	    PASS(errctx, obj->sprites->solid(obj->sprites, i + 1, false, 0.0, 0.0, 0.0, 0.0));
	}
	SUCCEED_RETURN(errctx);
    }

    id = (int)args[0];
    FAIL_ZERO_RETURN(errctx, (id >= 1 && id <= AKBASIC_MAX_SOLIDS), AKBASIC_ERR_BOUNDS,
		     "SOLID: %d is outside 1..%d", id, AKBASIC_MAX_SOLIDS);
    solid = &obj->sprite_state.solids[id - 1];

    if ( count == 1 ) {
	PASS(errctx, aksl_memset(solid, 0, sizeof(*solid)));
	PASS(errctx, obj->sprites->solid(obj->sprites, id, false, 0.0, 0.0, 0.0, 0.0));
	SUCCEED_RETURN(errctx);
    }

    FAIL_ZERO_RETURN(errctx, (args[3] > args[1] && args[4] > args[2]), AKBASIC_ERR_VALUE,
		     "SOLID: (%g, %g) to (%g, %g) is not a rectangle with a positive width and height",
		     args[1], args[2], args[3], args[4]);
    solid->active = true;
    solid->x1 = args[1];
    solid->y1 = args[2];
    solid->x2 = args[3];
    solid->y2 = args[4];
    PASS(errctx, obj->sprites->solid(obj->sprites, id, true,
				     solid->x1, solid->y1, solid->x2, solid->y2));
    SUCCEED_RETURN(errctx);
}

/* ---------------------------------------------------------------- SPRHIT -- */

akerr_ErrorContext *akbasic_cmd_sprhit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Sprite *sprite = NULL;
    double args[6];
    int count = 0;
    int index = 0;
    int kind = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in SPRHIT");
    PASS(errctx, require_sprites(obj, "SPRHIT"));
    FAIL_ZERO_RETURN(errctx, (obj->sprites->shape != NULL), AKBASIC_ERR_DEVICE,
		     "SPRHIT needs a sprite device that can carry a collision shape, and this one cannot");
    PASS(errctx, akbasic_args_numbers(obj, expr, "SPRHIT", args, 6, &count));
    FAIL_ZERO_RETURN(errctx, (count >= 2), AKBASIC_ERR_SYNTAX,
		     "SPRHIT expected a sprite number and a shape kind");
    FAIL_ZERO_RETURN(errctx, (count == 2 || count == 6), AKBASIC_ERR_SYNTAX,
		     "SPRHIT takes a kind alone or a kind and four corners, not %d arguments",
		     count);
    PASS(errctx, sprite_index((int)args[0], "SPRHIT", &index));
    sprite = &obj->sprite_state.sprites[index];

    kind = (int)args[1];
    FAIL_ZERO_RETURN(errctx, (kind >= AKBASIC_SHAPE_NONE && kind < AKBASIC_SHAPE_LAST),
		     AKBASIC_ERR_BOUNDS,
		     "SPRHIT: shape kind %d is outside 0..%d", kind, AKBASIC_SHAPE_LAST - 1);
    sprite->shapekind = kind;

    /*
     * Two arguments means "fit the frame", which is what a sprite loaded from a
     * file needs: `SPRSAV "ship.png", 1` gives it the image's own size and the
     * program never learns what that was. The device recomputes it from the
     * picture, so the rectangle recorded here stays zero and RSPHIT reports what
     * the device worked out rather than what was not said.
     */
    sprite->shapeexplicit = (count == 6);
    if ( count == 6 ) {
	FAIL_ZERO_RETURN(errctx, (args[4] > args[2] && args[5] > args[3]), AKBASIC_ERR_VALUE,
			 "SPRHIT: (%g, %g) to (%g, %g) is not a rectangle with a positive width and height",
			 args[2], args[3], args[4], args[5]);
	sprite->shapex1 = args[2];
	sprite->shapey1 = args[3];
	sprite->shapex2 = args[4];
	sprite->shapey2 = args[5];
    } else {
	sprite->shapex1 = 0.0;
	sprite->shapey1 = 0.0;
	sprite->shapex2 = 0.0;
	sprite->shapey2 = 0.0;
    }

    PASS(errctx, obj->sprites->shape(obj->sprites, index + 1, sprite->shapekind,
				     sprite->shapex1, sprite->shapey1,
				     sprite->shapex2, sprite->shapey2));
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_rsphit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Sprite *sprite = NULL;
    int64_t n = 0;
    int64_t field = 0;
    int index = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in RSPHIT");
    PASS(errctx, nth_number(obj, expr, "RSPHIT", 0, &n));
    PASS(errctx, nth_number(obj, expr, "RSPHIT", 1, &field));
    PASS(errctx, sprite_index((int)n, "RSPHIT", &index));
    sprite = &obj->sprite_state.sprites[index];

    /*
     * The five fields are SPRHIT's own arguments in SPRHIT's own order, which is
     * the rule RSPRITE and RSPPOS already follow. No device is needed: this
     * answers from interpreter state, because asking the device would be asking
     * it to repeat what it was told.
     */
    switch ( field ) {
    case 0:
	PASS(errctx, integer_result(obj, (int64_t)sprite->shapekind, dest));
	break;
    case 1:
	PASS(errctx, float_result(obj, sprite->shapex1, dest));
	break;
    case 2:
	PASS(errctx, float_result(obj, sprite->shapey1, dest));
	break;
    case 3:
	PASS(errctx, float_result(obj, sprite->shapex2, dest));
	break;
    case 4:
	PASS(errctx, float_result(obj, sprite->shapey2, dest));
	break;
    default:
	FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS,
		    "RSPHIT: field %" PRId64 " is outside 0..4", field);
    }
    SUCCEED_RETURN(errctx);
}

/**
 * @brief One field of the contact sprite n is in.
 *
 * Sticky: the record is replaced whenever that sprite is in a contact and
 * otherwise left alone, so `BUMP` remains the event -- "has anything hit me since
 * I last looked" -- and this is the detail of it. Making it clear itself when
 * nothing is touching would break the pairing, because `BUMP` accumulates across
 * steps and a once-a-frame poll would find the detail already gone.
 *
 * Reading `BUMP` clears it, so the two stay in step.
 */
akerr_ErrorContext *akbasic_fn_rcollision(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    akbasic_Contact *contact = NULL;
    int64_t n = 0;
    int64_t field = 0;
    int index = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in RCOLLISION");
    PASS(errctx, nth_number(obj, expr, "RCOLLISION", 0, &n));
    PASS(errctx, nth_number(obj, expr, "RCOLLISION", 1, &field));
    PASS(errctx, sprite_index((int)n, "RCOLLISION", &index));
    contact = &obj->sprite_state.contacts[index];

    switch ( field ) {
    case 0:
	PASS(errctx, integer_result(obj, (int64_t)contact->what, dest));
	break;
    case 1:
	PASS(errctx, integer_result(obj, (int64_t)contact->other, dest));
	break;
    case 2:
	PASS(errctx, float_result(obj, contact->nx, dest));
	break;
    case 3:
	PASS(errctx, float_result(obj, contact->ny, dest));
	break;
    case 4:
	PASS(errctx, float_result(obj, contact->depth, dest));
	break;
    case 5:
	PASS(errctx, float_result(obj, contact->px, dest));
	break;
    case 6:
	PASS(errctx, float_result(obj, contact->py, dest));
	break;
    case 7:
	PASS(errctx, integer_result(obj, (int64_t)contact->axis, dest));
	break;
    default:
	FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS,
		    "RCOLLISION: field %" PRId64 " is outside 0..7", field);
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_rsprite(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    akbasic_Sprite *sprite = NULL;
    PREPARE_ERROR(errctx);
    int64_t n = 0;
    int64_t field = 0;
    int index = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in RSPRITE");
    PASS(errctx, nth_number(obj, expr, "RSPRITE", 0, &n));
    PASS(errctx, nth_number(obj, expr, "RSPRITE", 1, &field));
    PASS(errctx, sprite_index((int)n, "RSPRITE", &index));
    sprite = &obj->sprite_state.sprites[index];

    /* The five fields are SPRITE's own arguments, in SPRITE's own order. */
    switch ( field ) {
    case 0:
	PASS(errctx, integer_result(obj, (sprite->enabled ? 1 : 0), dest));
	break;
    case 1:
	PASS(errctx, integer_result(obj, (int64_t)sprite->colorindex, dest));
	break;
    case 2:
	PASS(errctx, integer_result(obj, (sprite->behind ? 1 : 0), dest));
	break;
    case 3:
	PASS(errctx, integer_result(obj, (sprite->xexpand ? 1 : 0), dest));
	break;
    case 4:
	PASS(errctx, integer_result(obj, (sprite->yexpand ? 1 : 0), dest));
	break;
    case 5:
	PASS(errctx, integer_result(obj, (sprite->multicolour ? 1 : 0), dest));
	break;
    default:
	FAIL_RETURN(errctx, AKBASIC_ERR_BOUNDS,
		    "RSPRITE: field %" PRId64 " is outside 0..5", field);
    }
    SUCCEED_RETURN(errctx);
}

akerr_ErrorContext *akbasic_fn_rspcolor(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
{
    PREPARE_ERROR(errctx);
    int64_t field = 0;

    (void)lval; (void)rval;
    FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
		     "NULL argument in RSPCOLOR");
    PASS(errctx, nth_number(obj, expr, "RSPCOLOR", 0, &field));
    FAIL_ZERO_RETURN(errctx, (field == 1 || field == 2), AKBASIC_ERR_BOUNDS,
		     "RSPCOLOR: register %" PRId64 " is 1 or 2", field);
    PASS(errctx, integer_result(obj,
				(field == 1
				 ? (int64_t)obj->sprite_state.sharedcolor1
				 : (int64_t)obj->sprite_state.sharedcolor2),
				dest));
    SUCCEED_RETURN(errctx);
}
