// Renders the Rust sound stage off the main thread. The page knows where every
// duck is in the listener's frame; it sends those places and the sounds the
// flock has already decided on. This worker keeps a few blocks ahead of the
// worklet, exactly as duck-pond's own player does.
//
// The stage is fed live: a call that arrives while the queue is full is picked
// up by the next block rendered, so it is heard a queue-length after the
// visual gesture. Eight blocks is about 85 ms at 48 kHz.

let pond = null;
let memory = null;
let block = 512;
let feed = null;
let queue = 8;
let bootGrace = 2.5;
let queued = 0;
let started = false;
const pending = [];

onmessage = async ({ data }) => {
	switch (data.type) {
		case 'start':
			await start(data);
			break;
		case 'sync':
			if (pond) sync(data.positions);
			else remember(data);
			break;
		case 'call':
		case 'dabble':
		case 'peck':
		case 'fixed':
			if (pond) apply(data);
			else remember(data);
			break;
		case 'headphones':
			if (pond) pond.quack_audio_set_headphones(data.on ? 1 : 0);
			break;
		default:
			break;
	}
};

async function start(data) {
	if (started) return;
	started = true;
	try {
		const url = new URL('quack_audio.wasm', self.location.href);
		const response = await fetch(url);
		if (!response.ok) throw new Error(`couldn't load quack_audio.wasm (${response.status})`);
		const { instance } = await WebAssembly.instantiate(await response.arrayBuffer(), {});
		pond = instance.exports;
		memory = pond.memory;
		block = pond.quack_audio_block();
		queue = Math.max(2, data.queue || queue);
		bootGrace = data.bootGrace || bootGrace;

		pond.quack_audio_new(data.sampleRate, data.seed[0], data.seed[1], data.headphones ? 1 : 0);
		const slots = Math.min(pond.quack_audio_ducks(), data.roster?.length ?? 0);
		for (let i = 0; i < slots; i++) {
			const voice = data.roster[i];
			pond.quack_audio_set_voice(i, voice[0], voice[1]);
		}
		sync(data.positions);

		feed = data.feed;
		feed.onmessage = () => {
			queued--;
			pump();
		};
		postMessage({ type: 'ready', block, queue });

		// sounds made while the module was loading are still worth hearing,
		// unless they have already gone stale
		const now = performance.now();
		for (const event of pending) {
			if (now - event.at <= bootGrace * 1000) apply(event);
		}
		pending.length = 0;
		pump();
	} catch (error) {
		postMessage({ type: 'error', message: String(error?.message ?? error) });
	}
}

function remember(data) {
	pending.push({ ...data, at: performance.now() });
}

function apply(data) {
	switch (data.type) {
		case 'sync':
			sync(data.positions);
			break;
		case 'call':
			pond.quack_audio_set_pos(data.duck, data.x, data.y);
			pond.quack_audio_call(data.duck, data.kind, data.notes, data.duration);
			break;
		case 'dabble':
			pond.quack_audio_set_pos(data.duck, data.x, data.y);
			pond.quack_audio_dabble(data.duck, data.seconds);
			break;
		case 'peck':
			pond.quack_audio_set_pos(data.duck, data.x, data.y);
			pond.quack_audio_peck(data.duck, data.onWater ? 1 : 0, data.duckling ? 1 : 0);
			break;
		case 'fixed':
			pond.quack_audio_fixed(data.kind, data.x, data.y, data.gain);
			break;
		default:
			break;
	}
}

function sync(positions) {
	if (!pond || !positions) return;
	const slots = Math.min(pond.quack_audio_ducks(), positions.length);
	for (let i = 0; i < slots; i++) {
		pond.quack_audio_set_pos(i, positions[i][0], positions[i][1]);
	}
}

function pump() {
	while (pond && feed && queued < queue) {
		// rendering may grow the module's memory, replacing its buffer, so
		// look for the memory only afterwards and copy the block out
		const ptr = pond.quack_audio_render();
		const audio = new Float32Array(memory.buffer, ptr, 2 * block).slice();
		feed.postMessage({ audio }, [audio.buffer]);
		queued++;
	}
}