/**
 * @file interop_test.c
 * @brief Round-trip test for the galaga boundary, hoststruct.c-style.
 *
 * Links the real script.c and the real galaga.bas -- not copies -- so this
 * fails the moment the boundary and the script disagree. The four claims it
 * pins:
 *
 *  1. The script writes the engine's actor memory: a formation enemy's sway
 *     lands in akgl_Actor.x with no marshalling step.
 *  2. The outbox works: a diving enemy above the player raises FIRE# and the
 *     C side reads it.
 *  3. The boss flips actor state bit 13 at one hit point -- the boundary
 *     crossed engine-ward.
 *  4. Sustained calling holds: 24000 calls through the per-call
 *     akbasic_environment_zero() regime, the load a 40-enemy wave puts on
 *     the runtime in ten seconds.
 *
 * Exit status equals the number of failed claims.
 */

#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#include <akerror.h>

#include <akgl/actor.h>

#include "galaga.h"

#ifndef GALAGA_SCRIPT_PATH
#define GALAGA_SCRIPT_PATH "galaga.bas"
#endif

/* script.c reads these; main.c usually defines them. This test is the host. */
galaga_Game galaga_game;
galaga_Shared galaga_shared;

static int FAILURES = 0;

#define CLAIM(__cond, __text)						\
    if ( !(__cond) ) {							\
	fprintf(stderr, "FAILED: %s\n", __text);			\
	FAILURES += 1;							\
    } else {								\
	printf("ok: %s\n", __text);					\
    }

static akerr_ErrorContext *run_claims(void)
{
    galaga_Enemy enemy;
    akgl_Actor actor;
    int i = 0;
    PREPARE_ERROR(errctx);

    PASS(errctx, galaga_script_boot((char *)GALAGA_SCRIPT_PATH));

    /* --- 1: formation sway lands in the actor ---------------------------- */
    memset(&enemy, 0, sizeof(enemy));
    memset(&actor, 0, sizeof(actor));
    enemy.kind = GALAGA_ENEMY_BEE;
    enemy.state = GALAGA_ES_FORMATION;
    enemy.homex = 400.0f;
    enemy.homey = 300.0f;
    enemy.hp = 1;
    actor.x = 0.0f;
    actor.y = 0.0f;
    PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
    CLAIM((fabsf(actor.x - enemy.homex) <= 16.5f) && (actor.y == enemy.homey),
	  "a formation bee's sway is written into akgl_Actor.x/y by the script");

    /* --- 2: the fire outbox ---------------------------------------------- */
    memset(&enemy, 0, sizeof(enemy));
    memset(&actor, 0, sizeof(actor));
    enemy.kind = GALAGA_ENEMY_BEE;
    enemy.state = GALAGA_ES_DIVING;
    enemy.rnd = 0.0f;			/* 0.0 < DT% * 1.5: always willing */
    actor.x = 600.0f;
    actor.y = 200.0f;
    galaga_shared.playerx = 610.0f;	/* just off the shot's column      */
    galaga_shared.playery = 860.0f;	/* well below                      */
    PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
    CLAIM(enemy.fire == 1,
	  "a diving bee above the player raises FIRE# for the engine to consume");

    /* --- 3: the boss's hurt bit ------------------------------------------ */
    memset(&enemy, 0, sizeof(enemy));
    memset(&actor, 0, sizeof(actor));
    enemy.kind = GALAGA_ENEMY_BOSS;
    enemy.state = GALAGA_ES_FORMATION;
    enemy.homex = 500.0f;
    enemy.homey = 120.0f;
    enemy.hp = 1;
    actor.state = AKGL_ACTOR_STATE_ALIVE;
    PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
    CLAIM((actor.state & AKGL_ACTOR_STATE_UNDEFINED_13) != 0,
	  "a boss at one hit point raises actor state bit 13 from BASIC");

    /* --- 4: sustained calling --------------------------------------------- */
    memset(&enemy, 0, sizeof(enemy));
    memset(&actor, 0, sizeof(actor));
    enemy.kind = GALAGA_ENEMY_BEE;
    enemy.state = GALAGA_ES_FORMATION;
    enemy.homex = 400.0f;
    enemy.homey = 300.0f;
    for ( i = 0; i < 24000; i++ ) {
	enemy.rnd = 0.9f;		/* never dive: keep the state put */
	PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
    }
    CLAIM(galaga_game.script_errors == 0,
	  "24000 calls survive the per-call akbasic_environment_zero() regime");
    SUCCEED_RETURN(errctx);
}

/* ------------------------------------------------------------ benchmark --- */

/**
 * @brief UPDATEBEE's state machine, translated line for line into C.
 *
 * The native comparator for the benchmark below: the same guard, the same
 * three branches, the same arithmetic as galaga.bas's UPDATEBEE with its
 * helpers inlined. Nothing is simplified, so the timing difference is the
 * interpreter's, not the algorithm's.
 */
static void native_updatebee(galaga_Enemy *enemy, akgl_Actor *actor, float dt)
{
    float dx = 0.0f;
    float dy = 0.0f;
    float k = 0.0f;
    int32_t s = 0;

    enemy->t += dt;
    if ( enemy->t < 0.0f ) {
	return;
    }
    s = enemy->state;
    if ( (s & GALAGA_ES_ENTERING) != 0 ) {
	dx = enemy->homex - actor->x;
	dy = enemy->homey - actor->y;
	k = dt * 4.5f;
	if ( k > 1.0f ) {
	    k = 1.0f;
	}
	actor->x += dx * k + sinf(enemy->t * 6.0f) * 90.0f * dt;
	actor->y += dy * k;
	if ( fabsf(dx) < 3.0f && fabsf(dy) < 3.0f ) {
	    enemy->state = GALAGA_ES_FORMATION;
	    enemy->t = 0.0f;
	}
    }
    if ( (s & GALAGA_ES_FORMATION) != 0 ) {
	actor->x = enemy->homex + sinf(enemy->t * 1.7f) * 16.0f;
	actor->y = enemy->homey;
	if ( enemy->rnd < dt * 0.04f ) {
	    enemy->state = GALAGA_ES_DIVING;
	    enemy->t = 0.0f;
	}
    }
    if ( (s & GALAGA_ES_DIVING) != 0 ) {
	actor->y += (enemy->t * 150.0f + 260.0f) * dt;
	actor->x += sinf(enemy->t * 4.0f) * 130.0f * dt;
	dx = galaga_shared.playerx - actor->x;
	if ( dx > 220.0f ) {
	    dx = 220.0f;
	}
	if ( dx < -220.0f ) {
	    dx = -220.0f;
	}
	actor->x += dx * 0.2f * dt;
	if ( actor->y > 1040.0f ) {
	    actor->y = -90.0f;
	    enemy->state = GALAGA_ES_ENTERING;
	    enemy->t = 0.0f;
	}
	dx = galaga_shared.playerx - actor->x;
	if ( fabsf(dx) <= 140.0f && actor->y <= galaga_shared.playery
	     && enemy->rnd < dt * 1.5f ) {
	    enemy->fire = 1;
	}
    }
}

static double seconds_since(const struct timespec *t0)
{
    struct timespec t1;

    clock_gettime(CLOCK_MONOTONIC, &t1);
    return (double)(t1.tv_sec - t0->tv_sec) + (double)(t1.tv_nsec - t0->tv_nsec) / 1e9;
}

/**
 * @brief The cost of thinking in BASIC, measured against the same logic in C.
 *
 * Both loops run the identical formation-hold workload, 24,000 calls -- forty
 * enemies at sixty frames a second for ten seconds. Informational: nothing
 * asserts on the timing, because CI machines vary; the numbers print so the
 * tutorial can quote a real measurement.
 */
static akerr_ErrorContext *run_benchmark(void)
{
    galaga_Enemy enemy;
    akgl_Actor actor;
    struct timespec t0;
    double basic_s = 0.0;
    double native_s = 0.0;
    int i = 0;
    PREPARE_ERROR(errctx);

    memset(&enemy, 0, sizeof(enemy));
    memset(&actor, 0, sizeof(actor));
    enemy.kind = GALAGA_ENEMY_BEE;
    enemy.state = GALAGA_ES_FORMATION;
    enemy.homex = 400.0f;
    enemy.homey = 300.0f;
    enemy.rnd = 0.9f;

    clock_gettime(CLOCK_MONOTONIC, &t0);
    for ( i = 0; i < 24000; i++ ) {
	PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
    }
    basic_s = seconds_since(&t0);

    memset(&enemy, 0, sizeof(enemy));
    memset(&actor, 0, sizeof(actor));
    enemy.kind = GALAGA_ENEMY_BEE;
    enemy.state = GALAGA_ES_FORMATION;
    enemy.homex = 400.0f;
    enemy.homey = 300.0f;
    enemy.rnd = 0.9f;

    clock_gettime(CLOCK_MONOTONIC, &t0);
    for ( i = 0; i < 24000; i++ ) {
	native_updatebee(&enemy, &actor, 0.016f);
    }
    native_s = seconds_since(&t0);

    printf("benchmark: 24000 formation-hold updates, dt 0.016\n");
    printf("  BASIC through the boundary: %8.3f s  %7.2f us/call  %6.3f ms per 40-enemy frame\n",
	   basic_s, basic_s / 24000.0 * 1e6, basic_s / 24000.0 * 40.0 * 1e3);
    printf("  the same logic in C:        %8.3f s  %7.2f us/call  %6.3f ms per 40-enemy frame\n",
	   native_s, native_s / 24000.0 * 1e6, native_s / 24000.0 * 40.0 * 1e3);
    printf("  ratio: %.0fx\n", basic_s / native_s);
    SUCCEED_RETURN(errctx);
}

int main(void)
{
    PREPARE_ERROR(errctx);

    ATTEMPT {
	CATCH(errctx, run_claims());
	CATCH(errctx, run_benchmark());
    } CLEANUP {
    } PROCESS(errctx) {
    } HANDLE_DEFAULT(errctx) {
	LOG_ERROR_WITH_MESSAGE(errctx, "the interop test could not run");
	FAILURES += 1;
    } FINISH_NORETURN(errctx);

    return FAILURES;
}
