// Ambient sound design for Overheard, built entirely with the Web Audio API.
// No audio files are loaded - every sound here is a sine wave generated on
// the fly by an OscillatorNode and shaped by a GainNode "envelope".
import { browser } from '$app/environment';

// ---------------------------------------------------------------------
// AudioContext setup + the "unlock on first interaction" workaround
// ---------------------------------------------------------------------
// Browsers (Chrome, Safari, Firefox) block audio from playing until the
// page has received at least one user gesture (click/tap/keypress). An
// AudioContext created before that gesture starts in a "suspended" state,
// and any sound scheduled on it is silently dropped.
//
// To handle this without requiring a manual "enable sound" button, we:
//  1. Lazily create a single shared AudioContext the first time it's needed
//     (getAudioContext below), instead of one at module load time.
//  2. Attach one-time listeners for pointerdown/keydown/touchstart to the
//     window. The very first time the user interacts with the page at all,
//     we create/resume the shared AudioContext so it's "unlocked" before
//     any chime needs to play.
//  3. Every play function also calls ctx.resume() defensively - resuming an
//     already-running context is a harmless no-op, but if the context is
//     still suspended (e.g. a chime fires before any interaction happened),
//     this gives it another chance to start once a gesture has occurred.
//
// Net effect: sound is "on" by default with no toggle, and simply stays
// silent until the user has interacted with the page once - which matches
// what every browser requires anyway.

let audioCtx = null;

function getAudioContext() {
	if (!browser) return null; // never run on the server during SSR

	if (!audioCtx) {
		// webkitAudioContext fallback covers older Safari
		const AudioContextClass = window.AudioContext || window.webkitAudioContext;
		audioCtx = new AudioContextClass();
	}

	// no-op if already running; otherwise attempts to start playback -
	// this only actually succeeds once a user gesture has occurred
	if (audioCtx.state === 'suspended') {
		audioCtx.resume();
	}

	return audioCtx;
}

if (browser) {
	// One-time "unlock" listeners: the first tap/click/keypress anywhere on
	// the page creates (or resumes) the shared AudioContext so it's ready
	// before the user does anything that should make a sound (e.g. echoing
	// a message). { once: true } removes each listener after it fires.
	const unlockAudio = () => getAudioContext();
	window.addEventListener('pointerdown', unlockAudio, { once: true });
	window.addEventListener('keydown', unlockAudio, { once: true });
	window.addEventListener('touchstart', unlockAudio, { once: true });
}

// ---------------------------------------------------------------------
// Shared helper: play a single sine-wave tone with an attack/decay envelope
// ---------------------------------------------------------------------
// A "click"-free tone needs its volume to ramp up and back down smoothly
// rather than switching on/off instantly (an instant on/off creates an
// audible pop). We do this with a GainNode whose gain value we schedule
// over time - this is the "envelope".
//
//   ctx        - the shared AudioContext
//   frequency  - pitch of the tone, in Hz
//   startTime  - when (in ctx.currentTime seconds) the tone should begin
//   duration   - total length of the tone, in seconds
//   peakGain   - maximum volume (0-1) reached during the attack
function playTone(ctx, frequency, startTime, duration, peakGain) {
	const oscillator = ctx.createOscillator();
	const gainNode = ctx.createGain();

	// 'sine' is the smoothest, purest waveform Web Audio offers - no harsh
	// overtones, which keeps the chime feeling soft/ambient rather than
	// buzzy (a square or sawtooth wave would sound much more aggressive).
	oscillator.type = 'sine';
	oscillator.frequency.value = frequency;

	// Envelope: start silent, quickly fade in (attack), then fade back out
	// to silence (decay) before the oscillator stops. The very short attack
	// (10ms) avoids a click at the start, and the longer exponential decay
	// gives the "ding" its natural-sounding tail.
	const attackTime = 0.01; // 10ms fade-in - fast enough to feel instant, slow enough to avoid a click
	gainNode.gain.setValueAtTime(0.0001, startTime); // start essentially silent
	gainNode.gain.exponentialRampToValueAtTime(peakGain, startTime + attackTime); // quick fade in to peak volume
	// exponential ramps can't target exactly 0, so we ramp down to a
	// near-silent value (0.0001) by the end of the tone's duration
	gainNode.gain.exponentialRampToValueAtTime(0.0001, startTime + duration);

	// oscillator -> gain -> speakers
	oscillator.connect(gainNode);
	gainNode.connect(ctx.destination);

	oscillator.start(startTime);
	oscillator.stop(startTime + duration);
}

// ---------------------------------------------------------------------
// Feature 1: new pin chime
// ---------------------------------------------------------------------
// A single soft "ding" played whenever a new message pin appears nearby,
// whether posted by someone else or revealed by this device's own refresh.
export function playNewPinChime() {
	const ctx = getAudioContext();
	if (!ctx) return; // SSR or AudioContext unavailable - do nothing

	const now = ctx.currentTime;

	// A5 (880Hz) is high enough to feel light/bell-like without being shrill,
	// and a single note keeps the chime unobtrusive - just a soft notice
	// rather than a full melody.
	const frequency = 880;

	// 0.4s total: comfortably inside the requested 0.3-0.5s range. Most of
	// that time is the decay tail, so it reads as one quick "ding" rather
	// than a sustained tone.
	const duration = 0.4;

	// Peak volume of 0.06 (out of a possible 0-1) keeps this firmly in
	// "ambient/background" territory - audible but never jarring, even if
	// several pins load in quick succession.
	const peakGain = 0.06;

	playTone(ctx, frequency, now, duration, peakGain);
}

// ---------------------------------------------------------------------
// Feature 2: echo confirmation tone
// ---------------------------------------------------------------------
// A gentle three-note ascending arpeggio played when the user successfully
// echoes a message - a small musical "thank you".
export function playEchoTone() {
	const ctx = getAudioContext();
	if (!ctx) return; // SSR or AudioContext unavailable - do nothing

	const now = ctx.currentTime;

	// C5 -> E5 -> G5: a major triad arpeggio. Ascending pitches read as
	// positive/affirming (vs. a descending run, which tends to sound like
	// an error or "closing" cue), and a major chord feels warm rather than
	// tense.
	const notes = [523.25, 659.25, 783.99]; // C5, E5, G5 in Hz

	// Each note is short and they overlap slightly (0.14s apart vs. each
	// lasting 0.25s), which makes the arpeggio feel like one fluid gesture
	// rather than three separate beeps. 2 gaps * 0.14s + final note's
	// 0.25s duration = 0.53s total - comfortably under the 1-second limit.
	const noteSpacing = 0.14; // seconds between each note's start time
	const noteDuration = 0.25; // seconds each individual note lasts

	// Slightly lower peak gain than the pin chime since three overlapping
	// notes add up to more total energy than one - keeps the overall
	// loudness in the same soft "ambient" range.
	const peakGain = 0.05;

	notes.forEach((frequency, index) => {
		const startTime = now + index * noteSpacing;
		playTone(ctx, frequency, startTime, noteDuration, peakGain);
	});
}

// ---------------------------------------------------------------------
// Feature 3: live "pin pop" sound
// ---------------------------------------------------------------------
// A short, bright, whimsical "boop-beep" played when a pin appears in real
// time WHILE the user is actively looking at the map (see
// subscribeToNearbyMessages in messages.js + livePinIdsStore in
// messagesStore.js) - distinct from playNewPinChime()'s single soft ambient
// "ding" (page loads/refreshes/moving) and playEchoTone()'s slower 3-note
// rising arpeggio (echoing a message).
export function playPinPopSound() {
	const ctx = getAudioContext();
	if (!ctx) return; // SSR or AudioContext unavailable - do nothing

	const now = ctx.currentTime;

	// Two quick ascending notes (E6 -> G6, a rising minor third) read as a
	// playful little "boop-beep" chime rather than a flat pop - the rising
	// pitch is what gives it a cute/tactile "ta-da!" character.
	const notes = [1318.51, 1567.98]; // E6, G6 in Hz

	// The second note starts before the first has fully decayed (50ms apart,
	// each lasting 100ms) - that overlap is what makes the pair feel like one
	// light, bouncy gesture instead of two separate beeps. Total ~0.15s, well
	// under playEchoTone's 0.53s arpeggio.
	const noteSpacing = 0.05;
	const noteDuration = 0.1;

	// Near-instant attack (5ms) keeps each note feeling crisp/percussive
	// rather than ringing, like a tiny bell tap.
	const attackTime = 0.005;

	// Slightly louder than the ambient chime's peak (0.06) so this registers
	// as a distinct little "reward", but still short enough to never feel
	// jarring even with two overlapping notes.
	const peakGain = 0.07;

	// 'triangle' has richer harmonics than playTone's 'sine', giving this a
	// brighter, more sparkly/bell-like timbre than the pure ambient chime.
	notes.forEach((frequency, index) => {
		const startTime = now + index * noteSpacing;

		const oscillator = ctx.createOscillator();
		const gainNode = ctx.createGain();

		oscillator.type = 'triangle';
		oscillator.frequency.value = frequency;

		gainNode.gain.setValueAtTime(0.0001, startTime);
		gainNode.gain.exponentialRampToValueAtTime(peakGain, startTime + attackTime);
		gainNode.gain.exponentialRampToValueAtTime(0.0001, startTime + noteDuration);

		oscillator.connect(gainNode);
		gainNode.connect(ctx.destination);

		oscillator.start(startTime);
		oscillator.stop(startTime + noteDuration);
	});
}
