// Space Taxi -- taxi physics.
//
// Faithful translation of the C64's $6032 per-frame physics routine
// (see MECHANICS.md "Per-frame algorithm" and "Velocity model
// (CORRECTED)"). Acceleration-based: stick deflection sets an INSTANT
// per-frame acceleration; gravity is a constant acceleration always
// added (Y, signed -- level K is anti-gravity); both feed a persistent
// velocity accumulator that integrates into position each frame.
// Release the stick and velocity persists -- the cab drifts.
//
// Edge handling MATCHES the C64's $6AED behaviour: vx reflects at
// column 23 going left or column 65 going right. The cab BOUNCES off
// the screen edges rather than stopping.
//
// Crash detection: any contact with a solid non-pad tile crashes the
// cab. Any contact with a pad-surface tile is a successful landing.
// The C64 game does the same via sprite-bg hardware collision plus
// the $7D75 trampoline predicate; we substitute explicit pad-vs-wall
// tile classification.
//
// Per-level physics templates ($7D8F-$7D96 in the C64; xAccel/yAccel/
// xGrav/yGrav fields on StLevelT in the port) make every level feel
// different -- accel ranges $0E..$40, Y-gravity ranges $F9 (-7,
// anti-gravity on level K) to $06 (heavy on levels D/E/H/M/etc.).

#include <string.h>

#include "spacetaxi.h"


// Velocity safety clamp. With ST_SUBPIXEL = 256, ST_MAX_VX = 512 caps
// the cab at ~2 px/frame which is in line with the C64 max effective
// velocity (the C64's int16 accumulator integrates accel=14..64 over
// many frames; left uncapped it'd wrap, but real-game effective speeds
// stay in the hundreds-of-sub-units range). Earlier 127 capped at
// 0.5 px/frame, making the cab feel glued to molasses.
#define ST_MAX_VX             512
#define ST_MAX_VY             512

// Initial downward velocity injected at crash. The C64 sets $714F =
// $03 and $714E = $03 at $6A4E-$6A53 -- a 16-bit vy of $0303 = 771
// sub-units (~3 px/frame). The port's ST_MAX_VY caps the effective
// max, so we just start at that ceiling for a strong opening fall;
// gravity (the level's own yGrav) then integrates normally during
// the death anim. On anti-gravity levels (K = -7) the initial high
// downward velocity dominates for many frames before being slowed
// or reversed -- matches the C64's behavior on those levels.
#define ST_CRASH_FALL_VY     ST_MAX_VY

#define ST_TAXI_W_PX           24
#define ST_TAXI_H_PX           24

// Death-animation duration, in GAME TICKS (the death countdown runs
// inside stEngineTick, now stepped at the fixed 30 Hz cadence). Matches
// the C64 sequence at $6A72/$6B24/$6B4C: phase-1 integrates Y velocity
// until the cab falls to the floor (row >= $DA), phase-2 walks cels
// $CC..$D1 at a slowing rate, phase-3 holds ~70 video frames (~35 game
// ticks). 60 game ticks (~2 s) comfortably covers fall + impact walk +
// hold. The cab keeps falling under gravity during the countdown so the
// visual matches the original "watch it drop".
#define ST_CRASH_ANIM_FRAMES  60u

// Edge reflection at the visible playfield edges. The C64's $6AED
// thresholds (col 23 left / col 65 in X-MSB high half) are in VIC
// sprite-X coordinates where X=24 is the left visible column -- so
// "col 23" means "cab leftedge at visible left edge". Our port has
// no VIC-X offset (X=0 IS the visible left edge), so the equivalent
// is "bounce when the cab tries to leave the visible playfield".


static bool isSolidAt(const StLevelT *level, int16_t px, int16_t py);
static bool onLandingPad(const StLevelT *level, int16_t px, int16_t py, uint8_t *outPad);
static void reflectAtEdges(StTaxiT *t);
static void respawnTaxi(StGameT *game);


static bool isSolidAt(const StLevelT *level, int16_t px, int16_t py) {
    int16_t  tx = (int16_t)(px / ST_TILE_PIXELS);
    int16_t  ty = (int16_t)(py / ST_TILE_PIXELS);
    uint8_t  tile;

    if (tx < 0 || tx >= (int16_t)ST_TILEMAP_W) {
        return true;        // off-screen sides treated as walls
    }
    if (ty < 0 || ty >= (int16_t)ST_PLAYFIELD_ROWS) {
        return true;        // top/bottom out-of-field treated as walls
    }
    tile = level->tilemap[ty * ST_TILEMAP_W + tx];
    // The tilemap holds raw C64 screen-RAM character codes (romToLevel.py
    // copies screen RAM directly; the empty cell is the space character
    // $20, BG=0x20 in the extractor -- confirmed by the .dat histogram
    // where char 32 dominates the tilemap). On the C64 a sprite-vs-
    // background collision ($D01F) latches on ANY non-blank background
    // cell the cab overlaps, so a cell is solid iff its char code is not
    // the space character. (The earlier "1..63 solid / 64..127 pad /
    // 128+ decorative" convention was invented and is wrong against the
    // real data -- it treated char 32, the empty playfield, as solid,
    // which crashes the cab the instant it descends into open air.)
    //
    // The transporter opening ($67, the 4-tile top-wall gap) is the one
    // non-space tile the cab may pass through: it's the screen exit. It
    // only ever occurs at row 0 cols 18..21, so treating it as passable
    // everywhere is safe.
    return tile != ST_TILE_EMPTY && tile != ST_TILE_TRANSPORTER;
}


static bool onLandingPad(const StLevelT *level, int16_t px, int16_t py, uint8_t *outPad) {
    uint8_t  i;
    uint8_t  tx;
    uint8_t  ty;

    tx = (uint8_t)(px / ST_TILE_PIXELS);
    ty = (uint8_t)(py / ST_TILE_PIXELS);
    for (i = 0u; i < level->padCount; i++) {
        const StPadT *p = &level->pads[i];
        if (ty == p->tileY && tx >= p->tileX && tx < (uint8_t)(p->tileX + p->tileW)) {
            if (outPad != NULL) {
                *outPad = i;
            }
            return true;
        }
    }
    return false;
}


static void reflectAtEdges(StTaxiT *t) {
    // C64 $6AED behavior translated to the port's 0..319 visible
    // playfield: when vx is negative and the cab leftedge reaches 0,
    // negate vx. Same on the right when leftedge reaches (320 - W).
    // The cab visually touches the screen edge and bounces back;
    // matches the C64's "screen-edge bumper" behavior. Y has no
    // bounce -- the cab just stops at top/bottom (no real level
    // throws the cab against those edges).
    int32_t  maxX = ((int32_t)ST_TILEMAP_W * ST_TILE_PIXELS - ST_TAXI_W_PX) * ST_SUBPIXEL;
    int32_t  maxY = ((int32_t)ST_PLAYFIELD_ROWS * ST_TILE_PIXELS - ST_TAXI_H_PX) * ST_SUBPIXEL;

    if (t->x < 0 && t->vx < 0) {
        t->vx = (int16_t)(-t->vx);
        t->x  = 0;
    }
    if (t->x > maxX && t->vx > 0) {
        t->vx = (int16_t)(-t->vx);
        t->x  = maxX;
    }
    if (t->y < 0)    { t->y = 0;    t->vy = 0; }
    if (t->y > maxY) { t->y = maxY; t->vy = 0; }
}


static void respawnTaxi(StGameT *game) {
    StTaxiT  *t = &game->taxi;
    int32_t   spawnX;
    int32_t   spawnY;
    uint8_t   i;

    spawnX = (int32_t)game->level.taxiSpawnTileX * ST_TILE_PIXELS * ST_SUBPIXEL;
    spawnY = (int32_t)game->level.taxiSpawnTileY * ST_TILE_PIXELS * ST_SUBPIXEL;
    t->x         = spawnX;
    t->y         = spawnY;
    t->vx        = 0;
    t->vy        = 0;
    t->landed    = false;
    t->onPad     = 0xFFu;
    t->thrusting = false;
    // $6892 forces #$C0 on every respawn: gear up, facing right.
    t->gearDown      = false;
    t->fireHeld      = false;
    t->facing        = ST_DIR_RIGHT;
    t->crashImpacted = false;
    // Boot any in-flight passenger out of the cab; a respawn doesn't
    // keep the fare. Waiting passengers (still on a pad) survive.
    for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
        if (game->passengers[i].active && game->passengers[i].onboard) {
            game->passengers[i].active  = false;
            game->passengers[i].onboard = false;
        }
    }
}


void stEngineReset(StGameT *game) {
    StTaxiT        *t = &game->taxi;
    const StLevelT *L = &game->level;

    memset(t, 0, sizeof(*t));
    t->x      = (int32_t)L->taxiSpawnTileX * ST_TILE_PIXELS * ST_SUBPIXEL;
    t->y      = (int32_t)L->taxiSpawnTileY * ST_TILE_PIXELS * ST_SUBPIXEL;
    t->facing = ST_DIR_RIGHT;
    t->onPad  = 0xFFu;

    // Delegated to stPassenger.c so the fare-cursor (gNextFareIdx) and
    // the seed-spawn share one code path. Previously stEngineReset
    // spawned fares[0] inline without advancing the cursor, which then
    // caused spawnNextFare to re-spawn fare 0 instead of fare 1.
    stPassengerReset(game);
}


void stEngineTick(StGameT *game) {
    StTaxiT        *t = &game->taxi;
    const StLevelT *L = &game->level;
    int32_t         nx;
    int32_t         ny;
    int16_t         ax;
    int16_t         ay;
    int16_t         centerX;
    int16_t         feetY;
    uint8_t         landingPad;

    // Death animation: mirror the C64 sequence -- the cab keeps
    // FALLING through phase 1 ($6A72) until it hits the floor, then
    // sits there through phases 2/3 before respawn. No input accepted,
    // no further crash checks (otherwise hitting the floor on the way
    // down would re-trigger). Mute thrust input so applyInput's
    // per-frame joystick read can't keep the SFX going.
    //
    // Uses the LEVEL's normal gravity during integration -- the C64
    // doesn't override gravity at crash, it just injects a high
    // downward vy at crash entry and then lets normal physics run.
    // On level K (anti-gravity = -7) the high initial vy dominates
    // for many frames before being slowed/reversed; matches C64.
    if (t->crashTicks > 0u) {
        int32_t  maxY = ((int32_t)ST_PLAYFIELD_ROWS * ST_TILE_PIXELS
                         - ST_TAXI_H_PX) * ST_SUBPIXEL;

        t->thrusting = false;
        t->vy = (int16_t)(t->vy + (int16_t)L->yGrav);
        if (t->vy >  ST_MAX_VY) { t->vy =  ST_MAX_VY; }
        if (t->vy < -ST_MAX_VY) { t->vy = -ST_MAX_VY; }
        t->y += t->vy;
        if (t->y > maxY) {
            // Floor hit: the C64's phase-1 -> phase-2 transition (row
            // $DA, $6ACC) -- the debris stops falling and the impact
            // cel walk plays out where it landed.
            t->y  = maxY;
            t->vy = 0;
            t->crashImpacted = true;
        }
        t->crashTicks--;
        if (t->crashTicks == 0u) {
            if (game->state == ST_STATE_DEMO) {
                // $6B57-$6B5E: a demo crash skips the life bookkeeping
                // entirely; the main loop returns to the title.
                game->demoEnded = true;
                return;
            }
            if (game->lives > 0u) {
                game->lives--;
            }
            if (game->lives == 0u) {
                game->state = ST_STATE_GAME_OVER;
            } else {
                respawnTaxi(game);
            }
        }
        return;
    }

    // Step 1: instant acceleration from stick.
    ax = (int16_t)((int16_t)t->thrustDx * (int16_t)L->xAccel);
    ay = (int16_t)((int16_t)t->thrustDy * (int16_t)L->yAccel);
    if (!t->thrusting) {
        ax = 0;
        ay = 0;
    }

    // Step 2: integrate accel + per-level gravity into velocity.
    // Gravity is int8 signed so a level can pull upward (level K).
    t->vx = (int16_t)(t->vx + ax + (int16_t)L->xGrav);
    t->vy = (int16_t)(t->vy + ay + (int16_t)L->yGrav);

    // Safety clamp on the velocity accumulator -- without this a long
    // fall under gravity (or sustained one-way thrust against no
    // collision) lets vx/vy wrap int16_t and reverse sign.
    if (t->vx >  ST_MAX_VX) { t->vx =  ST_MAX_VX; }
    if (t->vx < -ST_MAX_VX) { t->vx = -ST_MAX_VX; }
    if (t->vy >  ST_MAX_VY) { t->vy =  ST_MAX_VY; }
    if (t->vy < -ST_MAX_VY) { t->vy = -ST_MAX_VY; }

    nx = t->x + t->vx;
    ny = t->y + t->vy;

    // Wall collision (X-axis): sample the cab's leading edge at three
    // Y rows (top, middle, bottom). If any sample hits a solid cell,
    // undo X movement and zero vx so the cab stops against the wall.
    {
        int16_t leadX = (t->vx > 0)
                      ? (int16_t)((nx >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX - 1)
                      : (int16_t)(nx >> ST_SUBPIXEL_SHIFT);
        int16_t topY  = (int16_t)(t->y >> ST_SUBPIXEL_SHIFT);
        int16_t midY  = (int16_t)(topY + ST_TAXI_H_PX / 2);
        int16_t botY  = (int16_t)(topY + ST_TAXI_H_PX - 1);
        if (isSolidAt(L, leadX, topY) ||
            isSolidAt(L, leadX, midY) ||
            isSolidAt(L, leadX, botY)) {
            nx    = t->x;
            t->vx = 0;
        }
    }

    // Y-axis collision. The C64 game has no velocity threshold: any
    // sprite-vs-background contact crashes the cab unless the cab is
    // touching a pad surface (in which case it's a successful landing
    // regardless of descent speed). We do the same via tile lookup.
    feetY   = (int16_t)((ny >> ST_SUBPIXEL_SHIFT) + ST_TAXI_H_PX - 1);
    centerX = (int16_t)((nx >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX / 2);

    if (t->vy > 0) {
        if (isSolidAt(L, centerX, feetY)) {
            // The C64 pad detector ($645C) only registers a landing on a
            // SLOW descent: it gates on the Y-velocity high byte being
            // zero (vy < 1 px/frame). A fast descent fails the gate, so
            // the pad surface acts as solid background and the cab
            // crashes into it -- this is the "land gently or die"
            // mechanic. In the port's 8-bit sub-pixel scale that high-
            // byte-zero test is vy < ST_SUBPIXEL (256).
            // $6462 additionally requires the GEAR DOWN: with the gear
            // up the pad detector aborts and the pad surface is just
            // lethal background -- fast descent OR gear-up = crash.
            bool slowDescent = (t->vy < ST_SUBPIXEL);
            if (slowDescent && t->gearDown &&
                onLandingPad(L, centerX, feetY, &landingPad)) {
                ny = (int32_t)(feetY / ST_TILE_PIXELS) * ST_TILE_PIXELS * ST_SUBPIXEL
                     - (int32_t)ST_TAXI_H_PX * ST_SUBPIXEL;
                t->vx     = 0;
                t->vy     = 0;
                t->landed = true;
                t->onPad  = landingPad;
            } else {
                stAudioSfxCrash();
                // Enter the falling-death state. Zero vx so the cab
                // doesn't drift sideways during the fall, and inject
                // a high downward vy matching the C64's $6A4E-$6A53
                // setup (which writes $0303 = 771 sub-units into
                // $714E/$714F regardless of pre-impact state). The
                // crashTicks gate at the top of stEngineTick handles
                // gravity integration + respawn from here.
                t->vx = 0;
                t->vy = ST_CRASH_FALL_VY;
                t->thrusting = false;
                t->crashTicks = ST_CRASH_ANIM_FRAMES;
                t->crashImpacted = false;
            }
        } else {
            t->landed = false;
            t->onPad  = 0xFFu;
        }
    } else if (t->vy < 0) {
        // Ascending: head hits ceiling? Stop vertical motion, leave
        // horizontal alone so the cab can drift along the underside.
        int16_t headY = (int16_t)(ny >> ST_SUBPIXEL_SHIFT);
        if (isSolidAt(L, centerX, headY)) {
            ny    = t->y;
            t->vy = 0;
        }
        t->landed = false;
    }

    t->x = nx;
    t->y = ny;
    reflectAtEdges(t);
}


bool stEngineInTransporter(const StGameT *game) {
    const StTaxiT *t = &game->taxi;
    int16_t        topY;
    int16_t        centerCol;

    // The cab has exited when its top has risen into row 0 while its
    // center sits within the transporter columns (18..21). The opening
    // tile ($67) is passable (see isSolidAt), so the cab can ascend the
    // empty channel and reach row 0 only through the gap -- the walls
    // stop it everywhere else.
    topY      = (int16_t)(t->y >> ST_SUBPIXEL_SHIFT);
    centerCol = (int16_t)(((t->x >> ST_SUBPIXEL_SHIFT) + ST_TAXI_W_PX / 2)
                          / ST_TILE_PIXELS);
    // Fire once the cab has risen into the opening (top within rows 0-1).
    // The walls block every column but the 18-21 gap, so reaching here at
    // all means the cab is exiting through the transporter.
    return topY < (int16_t)(2 * ST_TILE_PIXELS) &&
           centerCol >= (int16_t)ST_TRANSPORTER_COL_LO &&
           centerCol <  (int16_t)ST_TRANSPORTER_COL_HI;
}


// Transporter shrink-warp ($5C91 takeoff + $5CB9 pad2pad). The C64 walks
// the cab sprite through shrink cels $E2..$E9 (8 cels / 7 steps) while
// nudging it +1px right / -1px up per step, with a cadence that slows as
// it recedes. $5CB9 INCs $5A66 and gates the step on (counter & mask):
// mask $03 (every 4) for the first steps, then $0F (every 16) from $E5.
//
// UNIT PROOF (from raw.bin, so this is not re-litigated): the animation
// runs in the loop at $5B85 -- JSR $5BBB (the animDispatcher, sole caller,
// which advances $5A66 via $5CB9) is called ONCE per iteration, while the
// single-frame raster wait $404B (LDA $D019 / AND #$01 / BEQ on line 252)
// runs TWICE per iteration ($5BAC + $5BB2), exactly like the gameplay
// loop's $5F94/$5FA3. So $5A66 advances once per 2 video frames = one
// GAME TICK. The boundaries 4,8,12,16,32,48,64 and the total 64 are
// therefore GAME TICKS: 64 ticks = 128 video frames = ~2.13 s NTSC. The
// values stay in their native C64 units (stEngineWarpTick runs once per
// game tick), unlike the port-invented crashTicks. The port has no shrink
// cels, so the renderer scales a block by stEngineWarpStep instead.
#define ST_WARP_TOTAL_FRAMES  64u

static const uint8_t kWarpStepFrame[8] = { 0u, 4u, 8u, 12u, 16u, 32u, 48u, 64u };


uint8_t stEngineWarpStep(const StGameT *game) {
    uint8_t  frame = game->taxi.warpFrame;
    uint8_t  step  = 0u;
    uint8_t  k;

    for (k = 0u; k < 8u; k++) {
        if (frame >= kWarpStepFrame[k]) {
            step = k;
        }
    }
    return step;
}


void stEngineStartWarp(StGameT *game) {
    game->taxi.warpFrame = 1u;
    game->taxi.vx        = 0;
    game->taxi.vy        = 0;
    game->taxi.thrusting = false;
    // $5CB0: the C64 gates SID voice-3 noise on ($D412 = $81) for the
    // warp. Voice 3's freq word is $0617 (set at the $4092 RNG init,
    // ~92 Hz shift rate) -- the darkest rumble, portable pitch 31.
    jlAudioNoise(31u, 4u);
}


bool stEngineWarpTick(StGameT *game) {
    StTaxiT  *t = &game->taxi;
    uint8_t   before;
    uint8_t   after;

    before = stEngineWarpStep(game);
    t->warpFrame++;
    after = stEngineWarpStep(game);
    // Nudge +1px right / -1px up on each step boundary (net +7/-7).
    if (after != before) {
        t->x += (int32_t)ST_SUBPIXEL;
        t->y -= (int32_t)ST_SUBPIXEL;
    }
    if (t->warpFrame >= (uint8_t)ST_WARP_TOTAL_FRAMES) {
        t->warpFrame = 0u;
        jlAudioNoise(0u, 15u);      // warp done: rumble off
        return true;
    }
    return false;
}
