// Space Taxi -- passenger AI (dynamic fare model).
//
// The C64 game has NO static per-level fare list (verified: the prior
// port's "circular A->B->..->A" fares were fabricated). Both the SPAWN
// pad and the DELIVERY destination are chosen at RUNTIME by RNG:
//
//   * padHoverSetup ($6537) picks a random free pad in [1..padCount] for
//     the passenger to appear on, and again for where they want to go.
//   * When all pads are consumed (single-pad screens, notably level A),
//     the destination becomes the "UP PLEASE!" sentinel ($0B, $6CB8):
//     the fare is delivered by flying UP through the top-wall
//     transporter opening.
//   * "PAD n PLEASE" is drawn from the runtime destination index+'0'
//     ($671E: gActiveSpriteIdx + $30); pads are numeric, not lettered.
//
// This port keeps ONE active fare at a time (spawn -> board -> deliver ->
// spawn next), a faithful reduction of the C64's multi-slot queue. The
// RNG here is a small port-side LCG (the exact C64 RNG sequence isn't
// observable and depends on live seed state); it only needs to pick a
// plausible free pad each fare.
//
// Scoring is flat-rate per the C64 original ($43B9 BCD blob added at
// success-stage 6 / $6742): a delivered fare adds ST_FARE_SCORE.

#include <stddef.h>

#include "spacetaxi.h"


#define ST_PASSENGER_H_PX     16
#define ST_PASSENGER_H_TILES  (ST_PASSENGER_H_PX / ST_TILE_PIXELS)
// The waiting passenger's in-place wave (and every beam/walk step)
// advances every 3 game ticks -- the C64's $715E == 3 reload. Since
// stPassengerTick is now called once per fixed game tick (~30 Hz),
// this is a plain tick counter, in lockstep with the physics.
#define ST_WAVE_STEP_TICKS    3u

// Verified via emulator trace of $4354 (BCD-add) with the $43B9 blob:
// a basic delivered fare adds '5' at the ones digit of the DDDD.DD HUD
// score. See stuff/spacetaxi/trace.py.
#define ST_FARE_SCORE         5u

// Spawn cadence (C64 idle ticker $660B): while no fare is active, each
// frame rolls rand%100 and spawns on roll < 3, else counts a 100-frame
// cap down to a forced spawn -- a fresh fare appears within ~1-2 seconds.
#define ST_SPAWN_ROLL_THRESH  3u
#define ST_SPAWN_CAP_FRAMES   100


static uint32_t gRng;
static uint8_t  gWaveTicks;   // game ticks since the last wave/step advance
// Frames until the next fare is forced to spawn (mirrors gDeathStage0Rng).
static int16_t  gSpawnCountdown;
// Pad deliveries completed on the current screen. Destination pads are
// "used up" as fares are delivered (C64 gSpriteSlots/gFareSlotCount); once
// only the last pad would remain free, the next fare wants "UP PLEASE!"
// (fly up to the next screen), which is what makes each screen finite.
static uint8_t  gDeliveredThisScreen;


static bool    arrivedAtTarget(const StPassengerT *p);
static void    deliverFare(StGameT *game, StPassengerT *p);
static void    pickDestination(StGameT *game, StPassengerT *p);
static void    placeOnPad(StPassengerT *p, const StLevelT *level, uint8_t padIdx);
static uint8_t rngPad(uint8_t n);
static void    spawnPassenger(StGameT *game);
static void    walkStride(StPassengerT *p);


// Walk arrival test. The C64 aligns the stride with AND #$FE and
// compares ((x ^ target) & ~1) == 0; the port's +/-2 window is the
// same idea, robust to an odd captured taxi X.
static bool arrivedAtTarget(const StPassengerT *p) {
    int16_t d = (int16_t)(p->x - p->targetX);
    return d >= -2 && d <= 2;
}


static void deliverFare(StGameT *game, StPassengerT *p) {
    game->score += ST_FARE_SCORE;
    stAudioSfxDropoff();
    p->active = false;
    gDeliveredThisScreen++;
    // The next fare appears after the spawn cadence (see stPassengerTick),
    // not instantly.
    gSpawnCountdown = ST_SPAWN_CAP_FRAMES;
}


// On boarding, choose where the fare wants to go. Destination pads get
// used up as the screen's fares are delivered: once delivering would leave
// no fresh pad (single-pad screens, or after padCount-1 deliveries), the
// fare wants "UP PLEASE!" -- the player flies up to the next screen.
static void pickDestination(StGameT *game, StPassengerT *p) {
    uint8_t  pc = game->level.padCount;
    uint8_t  d;

    if (pc <= 1u || gDeliveredThisScreen >= (uint8_t)(pc - 1u)) {
        p->destPad = ST_DEST_TRANSPORTER;
        return;
    }
    d = rngPad(pc);
    if (d == p->currentPad) {
        d = (uint8_t)((d + 1u) % pc);
    }
    p->destPad = d;
}


// Stand the passenger at the pad's real stand column (pad->standX, the
// C64 slot byte6 = gPassenger1Col). pad->tileY is the contact-surface
// row; the fare's feet sit on it, so the sprite top-left is two tiles up.
static void placeOnPad(StPassengerT *p, const StLevelT *level, uint8_t padIdx) {
    const StPadT *pad = &level->pads[padIdx];

    p->currentPad = padIdx;
    p->x          = (int16_t)pad->standX;
    p->y          = (int16_t)((pad->tileY - ST_PASSENGER_H_TILES) * ST_TILE_PIXELS);
}


// Small LCG -> uniform pad index in [0, n-1]. n==0 guarded to 0.
static uint8_t rngPad(uint8_t n) {
    if (n == 0u) {
        return 0u;
    }
    gRng = gRng * 1103515245u + 12345u;
    return (uint8_t)(((gRng >> 16) & 0x7FFFu) % n);
}


// One +/-2 px walk stride ($6D66 rightward, $6D68 leftward), with
// the stride cel alternation on waitPhase's low bit.
static void walkStride(StPassengerT *p) {
    if (p->targetX > p->x) {
        p->x = (int16_t)(p->x + 2);
    } else if (p->targetX < p->x) {
        p->x = (int16_t)(p->x - 2);
    }
    p->waitPhase ^= 1u;
}


static void spawnPassenger(StGameT *game) {
    StPassengerT *p = &game->passengers[0];
    uint8_t       padIdx;

    if (game->level.padCount == 0u) {
        return;
    }
    padIdx = rngPad(game->level.padCount);

    p->active    = true;
    p->onboard   = false;
    p->phase     = ST_PASS_BEAM_IN;
    p->waitPhase = 0u;
    p->destPad   = 0u;
    placeOnPad(p, &game->level, padIdx);
}


void stPassengerReset(StGameT *game) {
    uint8_t  i;

    gWaveTicks           = 0u;
    gDeliveredThisScreen = 0u;
    gSpawnCountdown      = ST_SPAWN_CAP_FRAMES;
    // Deterministic per-screen seed: varies the pad sequence level to
    // level without needing a wall-clock source.
    gRng = 0x2545F491u ^ ((uint32_t)game->levelIndex * 2654435761u);

    for (i = 0u; i < ST_MAX_PASSENGERS; i++) {
        game->passengers[i].active = false;
    }
    // The screen starts empty; the first fare appears after the spawn
    // cadence in stPassengerTick.
}


void stPassengerTick(StGameT *game) {
    StPassengerT *p = &game->passengers[0];
    StTaxiT      *t = &game->taxi;

    if (!p->active) {
        // Idle: a fresh fare appears after the C64 spawn cadence. Never
        // spawn mid-crash (the C64 gates the idle ticker on
        // gCollisionPhase == 0).
        if (t->crashTicks == 0u) {
            if (gSpawnCountdown > 0) {
                gSpawnCountdown--;
            }
            if (rngPad(100u) < ST_SPAWN_ROLL_THRESH || gSpawnCountdown <= 0) {
                spawnPassenger(game);
            }
        }
        return;
    }

    // One shared step clock (the C64's 3-game-tick cadence, $715E == 3)
    // drives every phase: beams, wave, and walk strides. Called once
    // per fixed game tick, so this is a plain tick count.
    {
        bool stepDue = (++gWaveTicks >= ST_WAVE_STEP_TICKS);
        if (stepDue) {
            gWaveTicks = 0u;
        }

        switch (p->phase) {
            case ST_PASS_BEAM_IN:
                // Materialize at standX: cels $CB..$C7 (5 steps).
                if (stepDue && ++p->waitPhase >= 5u) {
                    p->phase     = ST_PASS_WAIT;
                    p->waitPhase = 0u;
                }
                break;

            case ST_PASS_WAIT:
                // Stand still and wave through the $66D6 cycle.
                if (stepDue) {
                    p->waitPhase = (uint8_t)((p->waitPhase + 1u) & 3u);
                }
                if (t->landed && t->onPad == p->currentPad) {
                    // $6811 captures the taxi X ONCE at landing; the
                    // walk aims there even if the cab lifts off.
                    p->targetX   = (int16_t)(t->x >> ST_SUBPIXEL_SHIFT);
                    p->phase     = ST_PASS_WALK_TO_CAB;
                    p->waitPhase = 0u;
                }
                break;

            case ST_PASS_WALK_TO_CAB:
                if (stepDue) {
                    walkStride(p);
                    if (arrivedAtTarget(p)) {
                        if (t->landed && t->onPad == p->currentPad) {
                            p->phase     = ST_PASS_BOARD_OUT;
                            p->waitPhase = 0u;
                        } else {
                            // Cab left early: walk home and wait again.
                            // (Not byte-traced -- the C64 cab cannot
                            // normally leave mid-walk; this just keeps
                            // the state machine sane if ours does.)
                            p->targetX   = (int16_t)game->level.pads[p->currentPad].standX;
                            p->phase     = ST_PASS_WALK_TO_PAD;
                            p->waitPhase = 0u;
                        }
                    }
                }
                break;

            case ST_PASS_BOARD_OUT:
                // Shrink into the cab: cels $C7..$CB (5 steps).
                if (stepDue && ++p->waitPhase >= 5u) {
                    p->onboard   = true;
                    p->phase     = ST_PASS_RIDING;
                    p->waitPhase = 0u;
                    pickDestination(game, p);
                    stAudioSfxPickup();
                }
                break;

            case ST_PASS_RIDING:
                // Deliver by landing on the destination pad. (UP-PLEASE
                // fares deliver on the transporter fly-up instead; see
                // stPassengerTransporterExit.)
                if (p->destPad != ST_DEST_TRANSPORTER &&
                    t->landed &&
                    t->onPad < game->level.padCount &&
                    t->onPad == p->destPad) {
                    p->onboard   = false;
                    p->phase     = ST_PASS_DROP_IN;
                    p->waitPhase = 0u;
                    p->x         = (int16_t)(t->x >> ST_SUBPIXEL_SHIFT);
                    p->targetX   = (int16_t)game->level.pads[p->destPad].standX;
                    p->currentPad = p->destPad;
                    p->y = (int16_t)((game->level.pads[p->destPad].tileY -
                                      ST_PASSENGER_H_TILES) * ST_TILE_PIXELS);
                }
                break;

            case ST_PASS_DROP_IN:
                // Materialize beside the cab: cels $CB..$C7.
                if (stepDue && ++p->waitPhase >= 5u) {
                    p->phase     = ST_PASS_WALK_TO_PAD;
                    p->waitPhase = 0u;
                }
                break;

            case ST_PASS_WALK_TO_PAD:
                if (stepDue) {
                    walkStride(p);
                    if (arrivedAtTarget(p)) {
                        p->phase     = ST_PASS_DROP_OUT;
                        p->waitPhase = 0u;
                    }
                }
                break;

            case ST_PASS_DROP_OUT:
                // Shrink out at standX -- fare complete.
                if (stepDue && ++p->waitPhase >= 5u) {
                    deliverFare(game, p);
                }
                break;
        }
    }
}


void stPassengerTransporterExit(StGameT *game) {
    StPassengerT *p = &game->passengers[0];

    if (p->active && p->onboard && p->destPad == ST_DEST_TRANSPORTER) {
        game->score += ST_FARE_SCORE;
        stAudioSfxDropoff();
        p->active = false;
    }
}
