Transposer/TransposerScene.cs
namespace Sandbox.Transposer;

/// <summary>
/// Available scene types in the game.
/// </summary>
public enum SceneType { MainMenu, Game }

/// <summary>
/// Animation overlay data for a recently swapped grid region.
/// Shows a red flash that fades over 1 second.
/// </summary>
public class SwapAnimationInfo
{
	public int A;
	public int B;
	public int Size;
	public float Time;

	public SwapAnimationInfo( int a, int b, int size, float time )
	{
		A = a;
		B = b;
		Size = size;
		Time = time;
	}
}

/// <summary>
/// Callback invoked when a fade-to-black completes.
/// </summary>
public delegate void FadeToBlackDelegate();

/// <summary>
/// Base scene class for Transposer. Manages entity lifecycle, draw ordering,
/// the grid-transposing pixel rearrangement effect, and screen fading.
///
/// Direct port of the original Unity Scene class. Not an engine Component —
/// scenes are plain C# objects managed by <see cref="TransposerSceneManager"/>.
/// </summary>
public class TransposerScene
{
	// ── Entity management ────────────────────────────────────────────────
	protected List<Entity> _entities = new();
	protected List<Entity> _entitiesToAdd = new();
	protected List<Entity> _entitiesToRemove = new();

	// ── Scene switching ──────────────────────────────────────────────────
	protected bool _switchingScene;
	private SceneType _switchingSceneType;
	/// <summary>Set to true the frame the scene switch executes; never reset — the scene is dead after this.</summary>
	protected bool _sceneSwitched;

	// ── Frame skips ──────────────────────────────────────────────────────
	protected int _frameSkips;
	public int FrameSkips { get => _frameSkips; set => _frameSkips = value; }

	// ── Fading ───────────────────────────────────────────────────────────
	protected bool _fadingToBlack;
	private float _fadeToBlackTimer;
	private float _fadeToBlackTime;
	private FadeToBlackDelegate _fadeToBlackCallback;

	protected bool _fadingIn;
	private float _fadeInTimer;
	private float _fadeInTime;

	public void FadeToBlack( float time )
	{
		_fadingToBlack = true;
		_fadeToBlackTimer = time;
		_fadeToBlackTime = time;
	}

	public void SetFadeToBlackCallback( FadeToBlackDelegate callback ) =>
		_fadeToBlackCallback = callback;

	public void FadeIn( float time )
	{
		_fadingIn = true;
		_fadeInTimer = time;
		_fadeInTime = time;
	}

	// ── Grid transposing ─────────────────────────────────────────────────
	// The signature mechanic: the screen is divided into grid cells that
	// get shuffled each time a coin is collected. Starts at 64px chunks
	// and progressively halves (64→32→16→8).
	private Dictionary<int, int[]> _gridDrawOrders = new();
	private Dictionary<int, List<int>> _gridSwapOrders = new();
	private List<SwapAnimationInfo> _swapAnimations = new();
	protected int _currentGridSize;

	// Tracks which grid sizes have had at least one swap. Sizes with no swaps
	// are still in identity order and can be skipped in ApplyGridTransposing.
	private HashSet<int> _swappedSizes = new();

	// Reusable scratch buffer for ApplyGridTransposing — avoids a per-frame allocation.
	private Color32[] _transposeBuffer;

	// ── Scene manager reference ──────────────────────────────────────────
	public TransposerSceneManager SceneManager { get; set; }

	// ── Screen reference ─────────────────────────────────────────────────
	protected PixelScreen Screen => PixelScreen.Instance;

	// ── Lifecycle ────────────────────────────────────────────────────────

	public virtual void Activate()
	{
		int size = 64;
		while ( size >= 8 )
		{
			int gridW = Screen.PixelWidth / size;
			int gridH = Screen.PixelHeight / size;

			int[] drawOrder = new int[gridW * gridH];
			List<int> swapOrder = new();

			for ( int i = 0; i < drawOrder.Length; i++ )
			{
				drawOrder[i] = i;
				swapOrder.Add( i );
			}

			_gridDrawOrders[size] = drawOrder;

			PixelUtils.Shuffle( swapOrder );
			_gridSwapOrders[size] = swapOrder;

			size /= 2;
		}

		_currentGridSize = 64;
		_switchingScene = false;
		_sceneSwitched = false;
	}

	public virtual void Deactivate()
	{
		_entities.Clear();
		_entitiesToAdd.Clear();
		_entitiesToRemove.Clear();
		_frameSkips = 0;

		_gridDrawOrders.Clear();
		_gridSwapOrders.Clear();
		_swapAnimations.Clear();
		_swappedSizes.Clear();

		_fadingToBlack = false;
		_fadingIn = false;
		_fadeToBlackCallback = null;
		_switchingScene = false;
		_sceneSwitched = false;
	}

	// ── Update ───────────────────────────────────────────────────────────

	/// <summary>
	/// Process deferred entity adds/removes, sort, and draw all base entities
	/// without running any UpdateEntity logic. Used when the game is paused.
	/// </summary>
	public void DrawEntitiesOnly()
	{
		foreach ( Entity e in _entitiesToAdd )
			_entities.Add( e );
		_entitiesToAdd.Clear();

		foreach ( Entity e in _entitiesToRemove )
			_entities.Remove( e );
		_entitiesToRemove.Clear();

		_entities.Sort( ( a, b ) =>
		{
			if ( a.Layer != b.Layer ) return b.Layer.CompareTo( a.Layer );
			return b.Depth.CompareTo( a.Depth );
		} );

		foreach ( Entity e in _entities )
			e.Draw();

		ApplyGridTransposing();
	}

	public virtual void UpdateScene( float deltaTime )
	{
		// Process deferred entity adds/removes.
		foreach ( Entity e in _entitiesToAdd )
			_entities.Add( e );
		_entitiesToAdd.Clear();

		foreach ( Entity e in _entitiesToRemove )
			_entities.Remove( e );
		_entitiesToRemove.Clear();

		// Sort entities by layer (descending) then depth (descending).
		// Higher layer = drawn first (behind), lower layer = on top.
		_entities.Sort( ( a, b ) =>
		{
			if ( a.Layer != b.Layer ) return b.Layer.CompareTo( a.Layer );
			return b.Depth.CompareTo( a.Depth );
		} );

		if ( _frameSkips > 0 )
		{
			_frameSkips--;
		}
		else
		{
			for ( int i = _entities.Count - 1; i >= 0; i-- )
				_entities[i].UpdateEntity( deltaTime );
		}

		if ( _switchingScene )
		{
			SceneManager?.SetScene( _switchingSceneType );
			_switchingScene = false;
			_sceneSwitched = true;
		}
		else
		{
			foreach ( Entity e in _entities )
				e.Draw();
		}

		// Apply grid transposing to the pixel buffer.
		ApplyGridTransposing();
	}

	// ── Grid transposing ─────────────────────────────────────────────────

	/// <summary>
	/// Rearrange the pixel buffer according to the shuffled grid draw orders.
	/// Called after all entities have drawn their pixels.
	/// </summary>
	private void ApplyGridTransposing()
	{
		if ( _swappedSizes.Count == 0 )
			return;

		Color32[] pixels = Screen.GetPixelsWritable();
		int screenW = Screen.PixelWidth;

		// Ensure scratch buffer is allocated (persists across frames).
		if ( _transposeBuffer is null || _transposeBuffer.Length != pixels.Length )
			_transposeBuffer = new Color32[pixels.Length];

		int size = 64;
		while ( size >= 8 )
		{
			// Skip sizes with no swaps — their draw order is still identity.
			if ( !_swappedSizes.Contains( size ) )
			{
				size /= 2;
				continue;
			}

			int gridW = screenW / size;

			// Snapshot into reusable buffer so remapping reads original positions.
			Array.Copy( pixels, _transposeBuffer, pixels.Length );

			int[] drawOrder = _gridDrawOrders[size];

			for ( int i = 0; i < drawOrder.Length; i++ )
			{
				int newOrder = drawOrder[i];
				if ( newOrder == i )
					continue; // Identity cell — no movement needed.

				int gridX = i % gridW;
				int gridY = i / gridW;
				int origPxX = gridX * size;
				int origPxY = gridY * size;

				int newGridX = newOrder % gridW;
				int newGridY = newOrder / gridW;
				int newPxX = newGridX * size;
				int newPxY = newGridY * size;

				for ( int x = 0; x < size; x++ )
				{
					for ( int y = 0; y < size; y++ )
					{
						int origIdx = (origPxY + y) * screenW + (origPxX + x);
						int newIdx = (newPxY + y) * screenW + (newPxX + x);

						if ( origIdx < pixels.Length && newIdx < _transposeBuffer.Length )
							pixels[origIdx] = _transposeBuffer[newIdx];
					}
				}
			}

			size /= 2;
		}

		Screen.MarkDirty();
	}

	/// <summary>
	/// Swap two grid cells in the draw order and trigger the red flash.
	/// Called when the player collects a coin.
	/// </summary>
	public void SwapGridSquares()
	{
		int size = 64;

		while ( true )
		{
			if ( size < 8 )
				return;

			if ( !_gridSwapOrders.ContainsKey( size ) )
			{
				size /= 2;
				continue;
			}

			List<int> swapOrder = _gridSwapOrders[size];
			if ( swapOrder.Count < 2 )
			{
				size /= 2;
				_currentGridSize = size;
				continue;
			}

			break;
		}

		List<int> order = _gridSwapOrders[size];
		int[] drawOrder = _gridDrawOrders[size];

		// Pull the next two cell indices from the front of the pre-shuffled swap order.
		// Each call consumes one pair; when the list drops below 2 entries the game
		// shrinks to the next size level.
		int a = order[0];
		int b = order[1];
		order.RemoveAt( 0 );
		order.RemoveAt( 0 ); // Index 1 has shifted to 0 after the first RemoveAt.

		// Swap in draw order.
		(drawOrder[a], drawOrder[b]) = (drawOrder[b], drawOrder[a]);

		_swappedSizes.Add( size );
		_swapAnimations.Add( new SwapAnimationInfo( a, b, size, 1.0f ) );
	}

	// ── Fade handling ────────────────────────────────────────────────────

	protected void HandleFading( float deltaTime )
	{
		if ( _fadingIn )
		{
			float opacity = PixelUtils.Map( _fadeInTimer, _fadeInTime, 0f, 1f, 0f, EaseType.SineOut );
			Screen.AddPixels( new Color32( 0, 0, 0, (byte)(opacity * 255f) ) );

			_fadeInTimer -= deltaTime;
			if ( _fadeInTimer <= 0f )
				_fadingIn = false;
		}
		else if ( _fadingToBlack )
		{
			_fadeToBlackTimer -= deltaTime;
			float opacity = PixelUtils.Map( _fadeToBlackTimer, _fadeToBlackTime, 0f, 0f, 1f, EaseType.SineOut );
			Screen.AddPixels( new Color32( 0, 0, 0, (byte)(opacity * 255f) ) );

			if ( _fadeToBlackTimer <= 0f )
			{
				_fadingToBlack = false;
				_fadeToBlackCallback?.Invoke();
				_fadeToBlackCallback = null;
			}
		}
	}

	/// <summary>
	/// Draw the red flash overlay on recently swapped grid cells.
	/// </summary>
	// Called AFTER ApplyGridTransposing, so the flash overlay lands at the
	// post-swap visual position of each cell — not where the cell was before.
	protected void HandleTransposing( float deltaTime )
	{
		for ( int i = _swapAnimations.Count - 1; i >= 0; i-- )
		{
			SwapAnimationInfo info = _swapAnimations[i];

			int gridW = Screen.PixelWidth / info.Size;

			int gridXa = info.A % gridW;
			int gridYa = info.A / gridW;
			int pxXa = gridXa * info.Size;
			int pxYa = gridYa * info.Size;

			int gridXb = info.B % gridW;
			int gridYb = info.B / gridW;
			int pxXb = gridXb * info.Size;
			int pxYb = gridYb * info.Size;

			float opacity = PixelUtils.Map( info.Time, 1f, 0f, 1f, 0f );
			Color32 flashColor = new( 255, 0, 0, (byte)(opacity * 255f) );

			for ( int x = 0; x < info.Size; x++ )
			{
				for ( int y = 0; y < info.Size; y++ )
				{
					Screen.AddPixel( pxXa + x, Screen.PixelHeight - 1 - pxYa - y, flashColor );
					Screen.AddPixel( pxXb + x, Screen.PixelHeight - 1 - pxYb - y, flashColor );
				}
			}

			info.Time -= deltaTime;
			if ( info.Time <= 0f )
				_swapAnimations.RemoveAt( i );
		}
	}

	// ── Collision ────────────────────────────────────────────────────────

	public Entity Collide( Entity entity, string type, int x, int y )
	{
		foreach ( Entity other in _entities )
		{
			if ( other.Type != type || !entity.Collideable || !other.Collideable || other == entity )
				continue;

			if ( entity.GetOffsetHitbox( x, y ).Overlaps( other.OffsetHitbox ) )
				return other;
		}
		return null;
	}

	public List<Entity> CollideWithAll( Entity entity, string type, int x, int y )
	{
		List<Entity> results = new();
		foreach ( Entity other in _entities )
		{
			if ( other.Type != type || !entity.Collideable || !other.Collideable || other == entity )
				continue;

			if ( entity.GetOffsetHitbox( x, y ).Overlaps( other.OffsetHitbox ) )
				results.Add( other );
		}
		return results;
	}

	public bool IsInBounds( Entity entity, int x, int y, int padding = 0 )
	{
		PixelRect hitbox = entity.GetOffsetHitbox( x, y );

		return hitbox.XMax < Screen.PixelWidth - padding
			&& hitbox.XMin >= padding
			&& hitbox.YMax < Screen.PixelHeight - padding
			&& hitbox.YMin >= padding;
	}

	// ── HUD helpers ──────────────────────────────────────────────────────

	/// <summary>
	/// Draw the "mute" sprite at 0.25 opacity in the top-right corner.
	/// </summary>
	protected void DrawMuteIcon()
	{
		const int MARGIN_X = 3;
		const int MARGIN_Y = 2;

		AnimationData anim = SpriteManager.GetAnimation( "mute", "default" );
		if ( anim is null || anim.Frames.Count == 0 )
			return;

		int startX = Screen.PixelWidth  - anim.AnimSize.X - MARGIN_X;
		int startY = Screen.PixelHeight - anim.AnimSize.Y - MARGIN_Y;

		foreach ( PixelData pd in anim.Frames[0].Pixels )
		{
			Color32 c = pd.Color;
			Screen.AddPixel( startX + pd.Position.X, startY + pd.Position.Y,
				new Color32( c.r, c.g, c.b, (byte)(c.a * 0.25f) ) );
		}
	}

	// ── Entity management ────────────────────────────────────────────────

	public void AddEntity( Entity e )
	{
		e.ContainingScene = this;
		_entitiesToAdd.Add( e );
	}

	public void RemoveEntity( Entity e )
	{
		_entitiesToRemove.Add( e );
	}

	public void SwitchScene( SceneType sceneType )
	{
		_switchingScene = true;
		_switchingSceneType = sceneType;
	}
}