/***************************************************************************
 sound_stubs_openvms.c - OpenVMS Sound Stubs + null (cue-only) sound server

 Provides stub implementations for all sound-related symbols that
 are referenced by the engine but not needed on OpenVMS, PLUS a small
 self-contained "null" sound_server_t.

 NO_SOUND is defined, so no actual audio is produced. However, a lot of
 SCI game logic (intros, character-select, scripted cutscenes) does NOT
 advance until the sound object's `signal`/`prevSignal` selectors climb to
 specific cue values (e.g. 2 then 3). Those cues are normally posted by
 process_sound_events() when the sound server reports them. With no sound
 server at all (sound_server == NULL) those cues never fire and the game
 stalls (the QfG1 "choose your character" screen never animates or accepts
 clicks - see script 202 / selChar, gated on global_108).

 This null server produces no audio but DOES deliver the cue lifecycle
 (INITIALIZED -> PLAYING -> a run of ABSOLUTE cues -> FINISHED). The cues
 are paced by a HANDSHAKE with the game: the next cue (signal := N+1) is
 only emitted once the SCI Sound class's ::check method has consumed the
 previous one (it copies signal into prevSignal and zeroes signal). Reading
 the object's `signal` selector back as 0 is the "ready for next" signal.
 This guarantees prevSignal walks 1,2,3,... one value per GAME CYCLE, so
 cue-gated logic that compares prevSignal == 2 / == 3 (QfG1 char-select,
 script 202) sees every value in order and advances. See nullsnd_get_event.
***************************************************************************/

#ifdef OPENVMS

#include <sci_memory.h>
#include <sciresource.h>
#include <engine.h>
#include <sound.h>
#include <soundserver.h>
#include <stdio.h>
#include <stdlib.h>

/* TEMP diagnostic: shared probe log (defined in kevent.c, gated by -L).
 * Lets us confirm the null server is actually driven and what it emits.
 * Remove with the other probes once char-select is confirmed working. */
extern FILE *_openvms_probe_log(void);

/* Global sound variables */
int soundserver_dead = 1;  /* Kept 1: main.c then skips its get_msg_value()-based
                            ** TEST/volume init block (get_msg_value is a stub here
                            ** and would mismap to command 0). process_sound_events()
                            ** only checks sound_server != NULL, so cues still flow. */
sound_server_t *global_sound_server = NULL;

/* MIDI device stubs */
void *midi_device = NULL;
void *midi_device_null = NULL;
void *midi_devices = NULL;
void *pcmout_driver = NULL;
void *pcmout_drivers = NULL;
void *midiout_driver = NULL;
void *midiout_drivers = NULL;

int pcmout_buffer_size = 0;
int pcmout_sample_rate = 0;
int pcmout_stereo = 0;

/* MIDI_mapping must match the type in sound.h: MIDI_map_t MIDI_mapping[128] */
MIDI_map_t MIDI_mapping[128];
int MIDI_mappings_nr = 0;

/* Stub functions */
void *pcmout_find_driver(char *name) { return NULL; }
void *midi_find_device(char *name) { return NULL; }
void *midiout_find_driver(char *name) { return NULL; }

int midi_open(void *device) { return -1; }
int midi_event(void *device, int event, int data1, int data2) { return -1; }
int midi_event2(void *device, int event, int data1, int data2) { return -1; }
int midi_allstop(void *device) { return 0; }
int midi_reverb(void *device, int param) { return -1; }
int midi_volume(void *device, int volume) { return -1; }

/* map_MIDI_instruments takes resource_mgr_t * according to sound.h:250 */
int map_MIDI_instruments(resource_mgr_t *resmgr) { return 0; }

/* soundsrv_save_state and soundsrv_restore_state match soundserver.h:583,593 */
int soundsrv_save_state(FILE *debugstream, char *dir, sound_server_state_t *sss) { return 0; }
int soundsrv_restore_state(FILE *debugstream, char *dir, sound_server_state_t *sss) { return 0; }

/* Sound server array stub (sound.c:71) */
sound_server_t *sound_servers[] = { NULL };

/* Sound command stub (sound.c:137) */
int sound_command_default(state_t *s, unsigned int command, unsigned int handle, long value)
{
	return 0;  /* No-op on OpenVMS */
}

/* Get message value stub (sound.c:760) */
unsigned int get_msg_value(char *msg)
{
	return 0;  /* No sound messages on OpenVMS */
}

/* Sound server stub */
sound_server_t *sound_server_find_driver(char *name) { return NULL; }


/* ======================================================================
 * OpenVMS null (cue-only) sound server
 *
 * No audio. It only synthesizes the cue lifecycle the game logic waits on:
 *   INIT  -> INITIALIZED
 *   PLAY  -> PLAYING, then a run of ABSOLUTE cues, then FINISHED
 * ====================================================================== */

/* We emit ABSOLUTE cues (signal := 1,2,3,...), NOT cumulative (signal += 1).
 *
 * Bytecode ground truth (QfG1 script 202): selChar::doit gates on the sound
 * object global_108's `signal`/`prevSignal` with EXACT == comparisons (==2 to
 * commit the choice + re-play, ==3 to unlock stage 2), and selScript::doit
 * starts the fighter portrait animation only when prevSignal == 3.
 *
 * `prevSignal` is written ONLY by the SCI Sound class's ::check (script 989):
 *     if (signal) { client.cue(); prevSignal = signal; signal = 0; }
 * ::check runs once per GAME CYCLE. So we must (a) set the ABSOLUTE value (a
 * cumulative += 1 can never reach a literal 2/3 because ::check zeroes signal
 * every cycle) and (b) advance only one value per cycle so prevSignal latches
 * every value 1,2,3 in order.
 *
 * Pacing is a HANDSHAKE, not a frame/wall-clock counter: emit the next cue
 * only once the previous one was consumed, detected by reading the object's
 * live `signal` selector back as 0 (::check zeroed it after copying to
 * prevSignal). This is robust to kAnimate firing several times per game cycle
 * - which is exactly what broke the old one-cue-per-kAnimate-frame scheme
 * (signal raced 2->3->4 between two ::check calls, so prevSignal skipped past
 * 2 and 3 and the animation never started). See nullsnd_get_event. */
#define NULLSND_CUE_CAP  10      /* absolute cues emitted before FINISHED   */
#define NULLSND_MAX      16      /* max concurrent tracked handles          */

/* Per-handle playback state. handle == the SCI heap object address. */
typedef struct {
	int           active;     /* slot in use                              */
	unsigned int  handle;     /* sound object handle                      */
	int           looping;    /* non-zero if loop == -1 (endless music)   */
	int           playing;    /* PLAY received: only then do cues flow    */
	int           need_init;  /* INITIALIZED event pending                */
	int           need_play;  /* PLAYING event pending                    */
	int           cues_sent;  /* absolute cues emitted so far (==signal)  */
	int           done;       /* FINISHED already emitted / gone quiet    */
} nullsnd_track_t;

static nullsnd_track_t nullsnd_tracks[NULLSND_MAX];

/* Find the slot for a handle, or NULL. */
static nullsnd_track_t *
nullsnd_find(unsigned int handle)
{
	int i;
	for (i = 0; i < NULLSND_MAX; i++)
		if (nullsnd_tracks[i].active && nullsnd_tracks[i].handle == handle)
			return &nullsnd_tracks[i];
	return NULL;
}

/* Find the slot for a handle, allocating a free one if needed. */
static nullsnd_track_t *
nullsnd_alloc(unsigned int handle)
{
	int i;
	nullsnd_track_t *t = nullsnd_find(handle);
	if (t)
		return t;
	for (i = 0; i < NULLSND_MAX; i++)
		if (!nullsnd_tracks[i].active) {
			t = &nullsnd_tracks[i];
			t->active = 1;
			t->handle = handle;
			t->looping = 0;
			t->playing = 0;
			t->need_init = 0;
			t->need_play = 0;
			t->cues_sent = 0;
			t->done = 0;
			return t;
		}
	return NULL; /* table full: silently drop (>16 concurrent sounds) */
}

/* Allocate one sound_event_t (process_sound_events() free()s it). */
static sound_event_t *
nullsnd_make_event(unsigned int handle, unsigned int signal, int value)
{
	sound_event_t *ev = (sound_event_t *) malloc(sizeof(sound_event_t));
	if (!ev)
		return NULL;
	ev->handle = handle;
	ev->signal = signal;
	ev->value = value;
	return ev;
}

static int
nullsnd_init(struct _state *s, int flags)
{
	int i;
	for (i = 0; i < NULLSND_MAX; i++)
		nullsnd_tracks[i].active = 0;
	return 0; /* success */
}

static int
nullsnd_configure(struct _state *s, char *option, char *value)
{
	return 1; /* option not handled by this driver */
}

static void
nullsnd_exit(struct _state *s)
{
	int i;
	for (i = 0; i < NULLSND_MAX; i++)
		nullsnd_tracks[i].active = 0;
}

/* Return the next pending event, one per call, or NULL when nothing is due.
 * Called (drained in a loop) many times per second by process_sound_events(). */
static sound_event_t *
nullsnd_get_event(struct _state *s)
{
	int i;

	for (i = 0; i < NULLSND_MAX; i++) {
		nullsnd_track_t *t = &nullsnd_tracks[i];

		if (!t->active)
			continue;

		if (t->need_init) {
			t->need_init = 0;
			return nullsnd_make_event(t->handle, SOUND_SIGNAL_INITIALIZED, 0);
		}

		if (t->need_play) {
			t->need_play = 0;
			return nullsnd_make_event(t->handle, SOUND_SIGNAL_PLAYING, 0);
		}

		/* Only a handle that actually received PLAY emits cues. */
		if (!t->playing || t->done)
			continue;

		/* HANDSHAKE PACING - the real fix.
		 *
		 * The gates that start the portrait animation and unlock clicks
		 * (script 202: selScript::doit and selChar::doit stage 2) test the
		 * sound object's `prevSignal` selector with EXACT == comparisons
		 * (== 3), and stage 1 tests prevSignal == 2. `prevSignal` is written
		 * ONLY by the SCI Sound class's ::check method (script 989):
		 *     if (signal) { client.cue(); prevSignal = signal; signal = 0; }
		 * ::check runs once per GAME CYCLE (from Game::doit). So prevSignal
		 * only latches a value if `signal` still holds that value when ::check
		 * next runs, and it must latch EVERY value 1,2,3 in order.
		 *
		 * The previous scheme advanced the cue once per kAnimate FRAME, but
		 * kAnimate fires several times per game cycle, so signal raced
		 * 2->3->4 between two ::check calls and prevSignal skipped clean past
		 * 2 and 3 (confirmed in freesci_probe.log: signal walked 1..10 one per
		 * frame, but the animation never started and stage 1 never re-played).
		 *
		 * Correct pacing: emit the next absolute cue only after the previous
		 * one has been CONSUMED. ::check zeroes `signal` right after copying it
		 * into `prevSignal`, so reading the live `signal` selector back as 0
		 * means "prevSignal now holds my last cue; the game is ready for the
		 * next one." This makes prevSignal walk 1,2,3,... exactly one value per
		 * game cycle regardless of how many kAnimate frames a cycle spans, so
		 * it lands on 2 (stage 1: commit + re-play) then 3 (stage 2: animate +
		 * unlock input). It is self-clocking - no frame counter needed. */
		if (t->cues_sent > 0 && is_object(s, (heap_ptr) t->handle)) {
			int live = GET_SELECTOR((heap_ptr) t->handle, signal);
			if (live != 0)
				continue;   /* last cue not yet latched into prevSignal - wait */
		}

		/* Cue run complete? */
		if (t->cues_sent >= NULLSND_CUE_CAP) {
			if (!t->looping) {
				/* One-shot: report FINISHED and go quiet. */
				t->done = 1;
				{
					FILE *lf = _openvms_probe_log();
					if (lf) { fprintf(lf, "[NULLSND] emit FINISHED handle=%04x\n", t->handle); fflush(lf); }
				}
				return nullsnd_make_event(t->handle, SOUND_SIGNAL_FINISHED, 0);
			}
			/* LOOPING sound: WRAP the cue counter instead of freezing.
			 *
			 * A real looping SCI sound re-hits its embedded cue points every
			 * loop, so the sound object's prevSignal keeps revisiting every
			 * cue value. The game relies on that: selScript::doit only reaches
			 * its own state 3 (where it starts polling prevSignal == 3 to start
			 * the portrait animation) ~11 cycles after the (re)play, by which
			 * time a monotonic 1..N walk has already passed 3 and frozen at N -
			 * so prevSignal never returns to 3 and the animation never starts
			 * (confirmed in freesci_probe.log: signal walked 1..10 then stuck).
			 * Wrapping makes prevSignal cycle ...1,2,3,...,N,1,2,3... forever,
			 * so whenever selScript becomes ready it catches a 3 within one wrap
			 * period. Re-hitting signal==2 is harmless: selChar stage 1 cleared
			 * its arm (local_17) when it first fired, so it will not re-fire. */
			t->cues_sent = 0;
		}

		t->cues_sent++;
		/* ABSOLUTE cue: signal := cues_sent exactly. */
		{
			FILE *lf = _openvms_probe_log();
			if (lf) { fprintf(lf, "[NULLSND] emit ABSCUE signal=%d handle=%04x (handshake)\n",
				t->cues_sent, t->handle); fflush(lf); }
		}
		return nullsnd_make_event(t->handle, SOUND_SIGNAL_ABSOLUTE_CUE, t->cues_sent);
	}

	return NULL;
}

static int
nullsnd_command(struct _state *s, unsigned int command, unsigned int param1, long param2)
{
	nullsnd_track_t *t;

	{
		FILE *lf = _openvms_probe_log();
		if (lf) { fprintf(lf, "[NULLSND] command=%u handle=%04x val=%ld\n",
			command, param1, param2); fflush(lf); }
	}

	switch (command) {

	case SOUND_COMMAND_INIT_HANDLE:
		t = nullsnd_alloc(param1);
		if (t) {
			t->playing = 0;   /* INIT only: no cues until PLAY */
			t->cues_sent = 0;
			t->done = 0;
			t->need_init = 1;
			t->need_play = 0;
		}
		break;

	case SOUND_COMMAND_PLAY_HANDLE:
		t = nullsnd_alloc(param1);
		if (t) {
			t->playing = 1;
			t->cues_sent = 0;
			t->done = 0;
			t->need_play = 1;
		}
		break;

	case SOUND_COMMAND_LOOP_HANDLE:
		t = nullsnd_find(param1);
		if (t)
			/* loop selector: -1 (0xffff) means loop forever */
			t->looping = (param2 == -1 || param2 == 0xffff) ? 1 : 0;
		break;

	case SOUND_COMMAND_STOP_HANDLE:
	case SOUND_COMMAND_DISPOSE_HANDLE:
		t = nullsnd_find(param1);
		if (t)
			t->active = 0;
		break;

	case SOUND_COMMAND_STOP_ALL: {
		int i;
		for (i = 0; i < NULLSND_MAX; i++)
			nullsnd_tracks[i].active = 0;
		break;
	}

	case SOUND_COMMAND_TEST:
		return 1;    /* report polyphony 1 (a voice is available)         */

	case SOUND_COMMAND_GET_VOLUME:
		return 0xc;  /* a plausible default volume                        */

	case SOUND_COMMAND_GET_MUTE:
		return 0;    /* not muted                                         */

	default:
		break;       /* SET_VOLUME / SET_MUTE / RENICE / FADE / etc: no-op */
	}

	return 0;
}

static void nullsnd_queue_event(unsigned int handle, unsigned int signal, long value) { }
static sound_event_t *nullsnd_get_command(GTimeVal *wait_tvp) { return NULL; }
static void nullsnd_queue_command(unsigned int handle, unsigned int signal, long value) { }
static int nullsnd_get_data(byte **data_ptr, int *size) { return 0; }
static int nullsnd_send_data(byte *data_ptr, int maxsend) { return maxsend; }
static int nullsnd_save(struct _state *s, char *name) { return 0; }
static int nullsnd_restore(struct _state *s, char *name) { return 0; }
static void nullsnd_suspend(struct _state *s) { }
static void nullsnd_resume(struct _state *s) { }
static void nullsnd_poll(void) { }

sound_server_t sound_server_openvms_null = {
	"openvms-null",
	"0.1",
	0,                    /* flags        */
	nullsnd_init,
	nullsnd_configure,
	nullsnd_exit,
	nullsnd_get_event,
	nullsnd_queue_event,
	nullsnd_get_command,
	nullsnd_queue_command,
	nullsnd_get_data,
	nullsnd_send_data,
	nullsnd_save,
	nullsnd_restore,
	nullsnd_command,
	nullsnd_suspend,
	nullsnd_resume,
	nullsnd_poll
};

#endif /* OPENVMS */
