Transposer/Scenes/GameScene.cs
namespace Sandbox.Transposer;

/// <summary>
/// The main gameplay scene. Contains the player, enemies, coins, background,
/// and all particle/text effects. Handles the core loop:
/// collect coins → swap grid → spawn enemy → score → die → return to menu.
/// </summary>
public class GameScene : TransposerScene
{
	private Player _player;
	private List<Enemy> _enemies = new();
	private List<Coin> _coins = new();
	private List<Trail> _trails = new();
	private List<BloodParticle> _bloodParticles = new();
	private List<Shine> _shines = new();
	private List<Shine> _shinesToRemove = new();
	private List<BonusText> _bonusTexts = new();

	private const int MAX_BLOOD_PARTICLES = 120;

	private int _numSwaps;
	private int _score;
	private bool _isPaused;
	private bool _wasPaused;
	private ScoreText _pauseText;

	private ScoreText _deathCoinsText;
	private ScoreText _deathScoreText;
	private float _deathStatsTimer = -1f;
	private bool _showDeathStats;

	private Game.Overlay _overlay;


	public override void Activate()
	{
		base.Activate();

		PixelPoint center = new( Screen.PixelWidth / 2, Screen.PixelHeight / 2 );

		_player = new Player( center.X - 6, center.Y - 6, this );
		AddEntity( _player );

		// Spawn initial coin at least 60px from center.
		const int MIN_DISTANCE = 60;
		int newX, newY;
		do
		{
			newX = Game.Random.Next( 2, Screen.PixelWidth - 12 );
			newY = Game.Random.Next( 2, Screen.PixelHeight - 12 );
		}
		while ( Math.Abs( newX - center.X ) < MIN_DISTANCE && Math.Abs( newY - center.Y ) < MIN_DISTANCE );

		SpawnCoin( newX, newY );

		AddEntity( new Background( 0, 0, this ) );

		_numSwaps = 0;
		_score = 0;
		_isPaused = false;
		_deathStatsTimer = -1f;
		_showDeathStats  = false;
		_deathCoinsText  = null;
		_deathScoreText  = null;

		int pauseTextWidth = "paused".Length * 14; // 6 chars × (6+1)×2 px at scale 2
		_pauseText = new ScoreText( "paused", this,
			Screen.PixelWidth / 2 - pauseTextWidth / 2,
			Screen.PixelHeight / 2 - 9, "font", 2 );

		FadeIn( 0.25f );
		AudioManager.MusicPitch = 1f;
		AudioManager.FadeInMusic( 1f );

		_overlay = new Game.Overlay();
	}

	public override void Deactivate()
	{
		base.Deactivate();
		AudioManager.StopMusic();
		AudioManager.SetGamePaused( false );

		_enemies.Clear();
		_coins.Clear();
		_trails.Clear();
		_bloodParticles.Clear();
		_shines.Clear();
		_shinesToRemove.Clear();
		_bonusTexts.Clear();
		_player = null;
	}

	public override void UpdateScene( float deltaTime )
	{
		Screen.Clear( new Color32( 0, 0, 0, 255 ) );

		// Toggle mute with M.
		if ( Input.Pressed( "Mute" ) )
			AudioManager.ToggleMute();

		_isPaused = _overlay.IsPauseMenuOpen;

		if( _isPaused != _wasPaused )
		{
			if( _isPaused )
				AudioManager.PausePitchMult = Game.Random.Float( AudioManager.PausePitchMin, AudioManager.PausePitchMax );

			AudioManager.SetGamePaused( _isPaused );
		}

		_wasPaused = _isPaused;

		if ( _isPaused )
		{
			// Draw everything frozen — no UpdateEntity calls so no input, logic,
			// random rolls, or animation advances happen while paused.
			DrawEntitiesOnly();
			foreach ( Enemy e in _enemies ) e.Draw();
			foreach ( Coin c in _coins ) c.Draw();
			foreach ( Shine s in _shines ) s.Draw();
			HandleTransposing( 0f );
			HandleFading( 0f );
		}
		else
		{
			base.UpdateScene( deltaTime );
			if ( _sceneSwitched ) return;

			// Enemies, coins, and shines are managed in their own lists rather than
			// the base entity list. This preserves the original game's draw ordering:
			// entity list (background, player, particles, trails) draws first, then
			// enemies, then coins, then shines — ensuring they always appear on top.
			foreach ( Enemy e in _enemies )
				e.UpdateEntity( deltaTime );
			foreach ( Enemy e in _enemies )
				e.Draw();

			// Update and draw coins.
			foreach ( Coin c in _coins )
				c.UpdateEntity( deltaTime );
			foreach ( Coin c in _coins )
				c.Draw();

			// Shines remove themselves via RemoveShine() when their animation finishes.
			// We defer the actual list removal to avoid modifying _shines mid-iteration.
			foreach ( Shine s in _shinesToRemove )
				_shines.Remove( s );
			_shinesToRemove.Clear();

			foreach ( Shine s in _shines )
				s.UpdateEntity( deltaTime );
			foreach ( Shine s in _shines )
				s.Draw();

			HandleTransposing( deltaTime );

			// Death stats — drawn after grid warp so they're never scrambled,
			// but before HandleFading so FadeToBlack fades them out naturally.
			if ( _deathStatsTimer >= 0f )
			{
				_deathStatsTimer -= deltaTime;
				if ( _deathStatsTimer <= 0f )
					_showDeathStats = true;
			}
			if ( _showDeathStats )
			{
				_deathCoinsText.Draw();
				_deathScoreText.Draw();
			}

			HandleFading( deltaTime );
		}

		// Pause overlay and text — drawn on top of everything else.
		if ( _isPaused )
		{
			Screen.AddPixels( new Color32( 0, 0, 0, 140 ) );
			_pauseText.Draw();
		}

		// Mute icon — always drawn last, top-right corner.
		if ( AudioManager.IsMuted )
			DrawMuteIcon();
	}


	// ── Factory methods ──────────────────────────────────────────────────

	private void SpawnCoin( int x, int y )
	{
		Coin coin = new( x, y, this );
		_coins.Add( coin );
	}

	private void SpawnEnemy()
	{
		Enemy enemy = new(
			Game.Random.Next( 0, Screen.PixelWidth - 10 ),
			Game.Random.Next( 0, Screen.PixelHeight - 10 ),
			this );
		_enemies.Add( enemy );
	}

	public void RemoveBloodParticle( BloodParticle bp )
	{
		_bloodParticles.Remove( bp );
	}

	public void CreateTrail( int x, int y, float opacity )
	{
		Trail trail = new( x, y, opacity, this );
		_trails.Add( trail );
		AddEntity( trail );
	}

	public void CreateBloodParticle( int x, int y, Vector2 velocity,
		float distanceToFall, float gravity, bool playNoise = false, bool force = false )
	{
		if ( !force && _bloodParticles.Count >= MAX_BLOOD_PARTICLES )
			return;

		BloodParticle bp = new( x, y, velocity, distanceToFall, gravity, this, playNoise );
		_bloodParticles.Add( bp );
		AddEntity( bp );
	}

	public void CreateShine( int x, int y )
	{
		Shine shine = new( x, y, this );
		_shines.Add( shine );
	}

	public void RemoveShine( Shine shine )
	{
		_shinesToRemove.Add( shine );
	}

	// ── Game events ──────────────────────────────────────────────────────

	public void PlayerTouchedCoin( Coin coin )
	{
		if ( _player.IsDead )
			return;

		SwapGridSquares();
		coin.Relocate();
		SpawnEnemy();
		_player.BecomeInvincible();

		AudioManager.PlaySfx( Sfx.CollectCoin );

		// White particle explosion.
		int numParticles = Game.Random.Next( 25, 40 );
		for ( int i = 0; i < numParticles; i++ )
		{
			float angle = Game.Random.Float( 0f, 360f );
			float rad = angle * (MathF.PI / 180f);
			Vector2 dir = new( MathF.Sin( -rad ), MathF.Cos( rad ) );
			PixelParticle p = new( Color.White, _player.CenterPixelPos.X, _player.CenterPixelPos.Y,
				this, dir, Game.Random.Float( 25f, 80f ), Game.Random.Float( 1.2f, 2.7f ) );
			AddEntity( p );
		}

		// Bonus text.
		//int bonus = _currentGridSize;
		int bonus = _numSwaps + 1; // Bonus is one more than the number of swaps already performed, so first coin is +1, second is +2, etc.
		string bonusStr = "+" + bonus;
		int width = bonusStr.Length * 6;
		BonusText bt = new( bonusStr, this,
			_player.CenterPixelPos.X - (int)MathF.Round( width * 0.5f ),
			_player.CenterPixelPos.Y, "font" );
		AddEntity( bt );

		_numSwaps++;
		_score += bonus;

		if ( _numSwaps == 16 ) Sandbox.Services.Achievements.Unlock( "16_coins" );
		else if ( _numSwaps == 24 ) Sandbox.Services.Achievements.Unlock( "24_coins" );
		else if ( _numSwaps == 32 ) Sandbox.Services.Achievements.Unlock( "32_coins" );

		AudioManager.MusicPitch += 0.0075f;
	}

	public void PlayerTouchedEnemy( Enemy enemy )
	{
		Globals.HasPlayedOnce = true;
		Globals.LastNumSwaps = _numSwaps;
		Globals.LastScore = _score;

		if ( _score > Globals.HighScore )
			Globals.HighScore = _score;

		//Log.Info( $"[Transposer] PlayerDied score={_score} stat='{Globals.LEADERBOARD_STAT}'" );
		//if ( !Game.IsEditor )
		Sandbox.Services.Stats.SetValue( Globals.LEADERBOARD_STAT, _score );

		// Build death-stats texts. Drawn directly (not in entity list) so grid warp never touches them.
		// coins: upper third of screen; score: lower third — evenly spaced.
		string coinsStr = "coins: " + _numSwaps;
		string scoreStr = "score: " + _score;
		int coinsX = Screen.PixelWidth  / 2 - (coinsStr.Length * 7 - 1) / 2;
		int scoreX = Screen.PixelWidth  / 2 - (scoreStr.Length * 7 - 1) / 2;
		int coinsY = Screen.PixelHeight * 2 / 3;
		int scoreY = Screen.PixelHeight / 6;
		_deathCoinsText = new ScoreText( coinsStr, this, coinsX, coinsY, "font" );
		_deathScoreText = new ScoreText( scoreStr, this, scoreX, scoreY, "font" );
		_deathStatsTimer = 1f;
		_showDeathStats  = false;

		_player.Die();
		AudioManager.SwitchMusic( 2f );
	}

}