Transposer/AudioManager.cs
namespace Sandbox.Transposer;

/// <summary>
/// Sound effect identifiers for <see cref="AudioManager"/>.
/// </summary>
public enum Sfx
{
	BloodSplat,
	Footstep,
	CollectCoin,
	CoinTwinkle,
	EnemyTouchPlayer,
}

/// <summary>
/// Manages all audio playback for Transposer: SFX with per-clip pitch/volume
/// randomisation and looping music with crossfade.
///
/// Audio files are expected in <c>sounds/transposer/</c>:
///   SFX:   drip.sound, step.sound, cha-ching.sound, twinkle.sound, laugh.sound
///   Music: music/song1.sound, music/song2.sound, music/song3.sound
///
/// Each <c>.sound</c> file is a sound event asset that references the actual
/// audio file (.wav/.ogg). Create them via the editor's sound event creator.
///
/// Music pitch increases as coins are collected — the <see cref="MusicPitch"/>
/// property is applied to the active music handle every frame via <see cref="Tick"/>.
/// </summary>
public static class AudioManager
{
	private static bool _isMuted;
	public static bool IsMuted => _isMuted;

	private const float MAX_MUSIC_VOLUME = 0.15f;

	private static bool _gamePaused;
	public static float PausePitchMin { get; set; } = 0.75f;
	public static float PausePitchMax { get; set; } = 0.95f;
	private static float _pausePitchMult = 0.8f;
	public static float PausePitchMult { get => _pausePitchMult; set => _pausePitchMult = value; }
	private const float PAUSE_VOLUME_MULT  = 0.5f;

	private static float _currentMusicVolume; // base volume ignoring pause

	// ── SFX path mappings ────────────────────────────────────────────────
	// Sound event names matching the .sound files under Assets/sounds/transposer/.
	private const string SFX_DRIP = "drip";
	private const string SFX_STEP = "step";
	private const string SFX_COLLECT = "cha-ching";
	private const string SFX_TWINKLE = "twinkle";
	private const string SFX_LAUGH = "laugh";

	// ── Music tracks ───────────────────────────────────────────────────
	private static readonly string[] MusicTracks = new[]
	{
		"song1",
		"song2",
		"song3",
	};

	private static int _currentTrackIndex = -1;

	// ── Music state ──────────────────────────────────────────────────────
	private static SoundHandle _musicHandle;
	private static SoundHandle _fadingOutHandle;
	private static float _fadeOutTimer;
	private static float _fadeOutDuration;
	private static float _fadeInTimer;
	private static float _fadeInDuration;
	private static bool _isFadingIn;
	private static bool _isFadingOut;

	private static float _musicPitch = 1.0f;
	public static float MusicPitch
	{
		get => _musicPitch;
		set => _musicPitch = value;
	}

	// ── Coin collect pitch wobble ────────────────────────────────────────
	// Keeps a handle to the most recent coin collect SFX and randomly
	// nudges its pitch while it's still playing, creating a warble effect.
	private static SoundHandle _collectHandle;
	private static float _collectBasePitch;
	private static float _collectWobbleTimer;
	private const float WOBBLE_CHECK_INTERVAL = 0.1f; // check every 100ms
	private const float WOBBLE_CHANCE = 0.10f;           // 10% chance each check
	private const float WOBBLE_RANGE_LOW = -0.13f;       // max downward pitch nudge
	private const float WOBBLE_RANGE_HIGH = 0.15f;       // max upward pitch nudge

	// ── SFX playback ─────────────────────────────────────────────────────

	/// <summary>
	/// Play a sound effect with the original Transposer pitch/volume settings.
	/// Each SFX uses randomised pitch within a range for variety.
	/// </summary>
	public static void PlaySfx( Sfx sfx )
	{
		switch ( sfx )
		{
			case Sfx.BloodSplat:
				PlaySound( SFX_DRIP, Game.Random.Float( 0.9f, 1.4f ), 0.8f );
				break;
			case Sfx.Footstep:
				PlaySound( SFX_STEP, Game.Random.Float( 0.9f, 1.4f ), 0.45f );
				break;
			case Sfx.CollectCoin:
				_collectBasePitch = Game.Random.Float( 0.4f, 1.1f );
				_collectHandle = PlaySound( SFX_COLLECT, _collectBasePitch, 0.5f );
				_collectWobbleTimer = 0f;
				break;
			case Sfx.CoinTwinkle:
				PlaySound( SFX_TWINKLE, 1.0f, 0.15f );
				break;
			case Sfx.EnemyTouchPlayer:
				PlaySound( SFX_LAUGH, Game.Random.Float( 0.8f, 1.0f ), 0.25f );
				break;
		}
	}

	/// <summary>
	/// Fire-and-forget a 2D (non-positional) sound with the given pitch and volume.
	/// </summary>
	private static SoundHandle PlaySound( string name, float pitch, float volume )
	{
		SoundHandle handle = Sound.Play( name );
		if ( !handle.IsValid() ) return handle;
		handle.Pitch = pitch;
		handle.Volume = volume;
		return handle;
	}

	// ── Music playback ───────────────────────────────────────────────────

	/// <summary>
	/// Start music from silence, fading in over <paramref name="time"/> seconds.
	/// Picks a random track if none is currently selected.
	/// </summary>
	public static void FadeInMusic( float time )
	{
		StopMusic();

		int trackIndex = PickRandomTrack();
		_currentTrackIndex = trackIndex;

		_gamePaused = false;
		_musicHandle = Sound.Play( MusicTracks[trackIndex] );
		_currentMusicVolume = 0f;
		if ( _musicHandle.IsValid() )
		{
			_musicHandle.Volume = 0f;
			_musicHandle.Pitch = _musicPitch;
		}

		_isFadingIn = true;
		_fadeInTimer = time;
		_fadeInDuration = time;
	}

	/// <summary>
	/// Crossfade from the current track to a different random track
	/// over <paramref name="time"/> seconds.
	/// </summary>
	public static void SwitchMusic( float time )
	{
		// Move the current track to the fade-out slot.
		if ( _musicHandle.IsValid() && _musicHandle.IsPlaying )
		{
			_fadingOutHandle = _musicHandle;
			_isFadingOut = true;
			_fadeOutTimer = time;
			_fadeOutDuration = time;
		}

		// Start a new track fading in.
		int trackIndex = PickRandomTrack();
		_currentTrackIndex = trackIndex;

		_musicHandle = Sound.Play( MusicTracks[trackIndex] );
		if ( _musicHandle.IsValid() )
		{
			_musicHandle.Volume = 0f;
			_musicHandle.Pitch = _musicPitch;
		}

		_isFadingIn = true;
		_fadeInTimer = time;
		_fadeInDuration = time;
	}

	/// <summary>
	/// Stop all music immediately.
	/// </summary>
	public static void StopMusic()
	{
		if ( _musicHandle.IsValid() )
			_musicHandle.Stop();

		if ( _fadingOutHandle.IsValid() )
			_fadingOutHandle.Stop();

		_isFadingIn = false;
		_isFadingOut = false;
	}

	/// <summary>
	/// Must be called once per frame (from the game entry point) to drive
	/// music fades, pitch updates, and coin collect wobble.
	/// </summary>
	public static void Tick( float deltaTime )
	{
		// ── Coin collect pitch wobble ─────────────────────────────────
		if ( _collectHandle.IsValid() && _collectHandle.IsPlaying )
		{
			_collectWobbleTimer -= deltaTime;
			if ( _collectWobbleTimer <= 0f )
			{
				_collectWobbleTimer = WOBBLE_CHECK_INTERVAL;
				if ( Game.Random.Float( 0f, 1f ) < WOBBLE_CHANCE )
				{
					float nudge = Game.Random.Float( WOBBLE_RANGE_LOW, WOBBLE_RANGE_HIGH );
					_collectHandle.Pitch = _collectBasePitch + nudge;
				}
			}
		}

		// Apply current pitch and volume (with pause modifiers) to the active music.
		float pitchMult  = _gamePaused ? _pausePitchMult : 1f;
		float volumeMult = _isMuted ? 0f : (_gamePaused ? PAUSE_VOLUME_MULT : 1f);

		if ( _musicHandle.IsValid() && _musicHandle.IsPlaying )
			_musicHandle.Pitch = _musicPitch * pitchMult;

		// Fade in.
		if ( _isFadingIn && _fadeInDuration > 0f )
		{
			_fadeInTimer -= deltaTime;
			float t = 1f - Math.Clamp( _fadeInTimer / _fadeInDuration, 0f, 1f );
			_currentMusicVolume = t * MAX_MUSIC_VOLUME;

			if ( _musicHandle.IsValid() )
				_musicHandle.Volume = _currentMusicVolume * volumeMult;

			if ( _fadeInTimer <= 0f )
			{
				_isFadingIn = false;
				_currentMusicVolume = MAX_MUSIC_VOLUME;
			}
		}
		else if ( _musicHandle.IsValid() && _musicHandle.IsPlaying )
		{
			// Steady state — keep volume correct if pause state changed.
			_musicHandle.Volume = _currentMusicVolume * volumeMult;
		}

		// Fade out the old track.
		if ( _isFadingOut && _fadeOutDuration > 0f )
		{
			_fadeOutTimer -= deltaTime;
			float t = Math.Clamp( _fadeOutTimer / _fadeOutDuration, 0f, 1f );
			float volume = t * MAX_MUSIC_VOLUME;

			if ( _fadingOutHandle.IsValid() )
				_fadingOutHandle.Volume = volume;

			if ( _fadeOutTimer <= 0f )
			{
				if ( _fadingOutHandle.IsValid() )
					_fadingOutHandle.Stop();
				_isFadingOut = false;
			}
		}
	}

	// ── Pause ────────────────────────────────────────────────────────────

	public static void SetGamePaused( bool paused )
	{
		_gamePaused = paused;
	}

	// ── Toggle ───────────────────────────────────────────────────────────

	public static void ToggleMute()
	{
		_isMuted = !_isMuted;
		// Volume is applied every Tick — no need to touch the handle here.
	}

	// ── Helpers ──────────────────────────────────────────────────────────

	/// <summary>
	/// Pick a random track index different from the currently playing one.
	/// </summary>
	private static int PickRandomTrack()
	{
		if ( MusicTracks.Length <= 1 )
			return 0;

		int index;
		do
		{
			index = Game.Random.Next( 0, MusicTracks.Length );
		}
		while ( index == _currentTrackIndex );

		return index;
	}
}