StageDoc.cs
namespace PixelPusher;

public sealed class StageEntity
{
	public string Kind { get; set; }
	public int Tx { get; set; }
	public int Ty { get; set; }
	public string Data { get; set; }
}

public sealed class StageDoc
{
	public string Name { get; set; } = "untitled stage";
	public int Width { get; private set; }
	public int Height { get; private set; }
	public int TileSize { get; set; } = 16;
	public int[] Tiles { get; private set; }
	public List<StageEntity> Entities { get; private set; } = new();
	public int Version { get; private set; }
	public float Gravity { get; set; } = 1800f;
	public float MaxRun { get; set; } = 140f;
	public float JumpVel { get; set; } = 420f;
	public string Goal { get; set; } = "exit";

	const int MaxUndo = 100;
	readonly List<StageSnap> _undo = new();
	readonly List<StageSnap> _redo = new();

	public StageDoc( int width, int height, int tileSize = 16 )
	{
		Width = Math.Clamp( width, 8, 256 );
		Height = Math.Clamp( height, 8, 128 );
		TileSize = tileSize;
		Tiles = new int[Width * Height];
	}

	public int TileAt( int tx, int ty )
	{
		if ( (uint)tx >= (uint)Width || (uint)ty >= (uint)Height )
			return 0;
		return Tiles[ty * Width + tx];
	}

	public void SetTile( int tx, int ty, int id )
	{
		if ( (uint)tx >= (uint)Width || (uint)ty >= (uint)Height )
			return;
		var i = ty * Width + tx;
		if ( Tiles[i] == id )
			return;
		Tiles[i] = id;
		Version++;
	}

	public void FillRect( int x0, int y0, int x1, int y1, int id )
	{
		if ( x0 > x1 ) (x0, x1) = (x1, x0);
		if ( y0 > y1 ) (y0, y1) = (y1, y0);
		x0 = Math.Clamp( x0, 0, Width - 1 );
		x1 = Math.Clamp( x1, 0, Width - 1 );
		y0 = Math.Clamp( y0, 0, Height - 1 );
		y1 = Math.Clamp( y1, 0, Height - 1 );
		for ( var y = y0; y <= y1; y++ )
		for ( var x = x0; x <= x1; x++ )
			Tiles[y * Width + x] = id;
		Version++;
	}

	public void BeginStroke()
	{
		_undo.Add( Capture() );
		if ( _undo.Count > MaxUndo )
			_undo.RemoveAt( 0 );
		_redo.Clear();
	}

	public bool CanUndo => _undo.Count > 0;
	public bool CanRedo => _redo.Count > 0;

	public void Undo()
	{
		if ( _undo.Count == 0 )
			return;
		_redo.Add( Capture() );
		Restore( _undo[^1] );
		_undo.RemoveAt( _undo.Count - 1 );
		Version++;
	}

	public void Redo()
	{
		if ( _redo.Count == 0 )
			return;
		_undo.Add( Capture() );
		Restore( _redo[^1] );
		_redo.RemoveAt( _redo.Count - 1 );
		Version++;
	}

	public void Flood( int tx, int ty, int id )
	{
		if ( (uint)tx >= (uint)Width || (uint)ty >= (uint)Height )
			return;
		var target = Tiles[ty * Width + tx];
		if ( target == id )
			return;

		var stack = new Stack<(int x, int y)>();
		stack.Push( (tx, ty) );
		var seen = new bool[Width * Height];
		while ( stack.Count > 0 )
		{
			var (cx, cy) = stack.Pop();
			if ( (uint)cx >= (uint)Width || (uint)cy >= (uint)Height )
				continue;
			var i = cy * Width + cx;
			if ( seen[i] || Tiles[i] != target )
				continue;
			seen[i] = true;
			Tiles[i] = id;
			stack.Push( (cx + 1, cy) );
			stack.Push( (cx - 1, cy) );
			stack.Push( (cx, cy + 1) );
			stack.Push( (cx, cy - 1) );
		}
		Version++;
	}

	public void StrokeLine( int x0, int y0, int x1, int y1, int id )
	{
		var dx = Math.Abs( x1 - x0 );
		var dy = Math.Abs( y1 - y0 );
		var sx = x0 < x1 ? 1 : -1;
		var sy = y0 < y1 ? 1 : -1;
		var err = dx - dy;
		while ( true )
		{
			SetTile( x0, y0, id );
			if ( x0 == x1 && y0 == y1 )
				break;
			var e2 = 2 * err;
			if ( e2 > -dy ) { err -= dy; x0 += sx; }
			if ( e2 < dx ) { err += dx; y0 += sy; }
		}
	}

	public void PlaceEntity( string kind, int tx, int ty, string data = null )
	{
		if ( (uint)tx >= (uint)Width || (uint)ty >= (uint)Height )
			return;

		if ( kind == "start" )
			Entities.RemoveAll( e => e.Kind == "start" );

		var existing = Entities.Find( e => e.Tx == tx && e.Ty == ty && e.Kind == kind );
		if ( existing is not null )
		{
			if ( kind == "sign" && data is not null )
			{
				existing.Data = data;
				Version++;
				return;
			}

			Entities.Remove( existing );
			Version++;
			return;
		}

		Entities.RemoveAll( e => e.Tx == tx && e.Ty == ty );
		Entities.Add( new StageEntity { Kind = kind, Tx = tx, Ty = ty, Data = data } );
		Version++;
	}

	public void RemoveAt( int tx, int ty )
	{
		var n = Entities.RemoveAll( e => e.Tx == tx && e.Ty == ty );
		if ( TileAt( tx, ty ) != 0 )
		{
			SetTile( tx, ty, 0 );
			return;
		}
		if ( n > 0 )
			Version++;
	}

	public StageEntity Find( string kind ) => Entities.Find( e => e.Kind == kind );

	public StageSave ToSave()
	{
		return new StageSave
		{
			Name = Name,
			Width = Width,
			Height = Height,
			TileSize = TileSize,
			Tiles = CopyInts( Tiles ),
			Entities = Entities.Select( CloneEntity ).ToList(),
			Gravity = Gravity,
			MaxRun = MaxRun,
			JumpVel = JumpVel,
			Goal = Goal
		};
	}

	StageSnap Capture() => new()
	{
		Tiles = CopyInts( Tiles ),
		Entities = Entities.Select( CloneEntity ).ToList(),
		Goal = Goal
	};

	void Restore( StageSnap snap )
	{
		if ( snap.Tiles is { Length: > 0 } )
		{
			var n = Math.Min( Tiles.Length, snap.Tiles.Length );
			Array.Copy( snap.Tiles, Tiles, n );
		}
		Entities = snap.Entities ?? new List<StageEntity>();
		if ( !string.IsNullOrWhiteSpace( snap.Goal ) )
			Goal = snap.Goal;
	}

	static StageEntity CloneEntity( StageEntity e ) => new()
	{
		Kind = e.Kind,
		Tx = e.Tx,
		Ty = e.Ty,
		Data = e.Data
	};

	static int[] CopyInts( int[] src )
	{
		if ( src is null )
			return new int[0];
		var dst = new int[src.Length];
		Array.Copy( src, dst, src.Length );
		return dst;
	}

	public static StageDoc FromSave( StageSave save )
	{
		if ( save is null )
			return StarterPack.TutorialStage();
		var doc = new StageDoc( save.Width, save.Height, save.TileSize > 0 ? save.TileSize : 16 )
		{
			Name = save.Name ?? "untitled stage",
			Gravity = save.Gravity > 0 ? save.Gravity : 1800f,
			MaxRun = save.MaxRun > 0 ? save.MaxRun : 140f,
			JumpVel = save.JumpVel > 0 ? save.JumpVel : 420f,
			Goal = string.IsNullOrWhiteSpace( save.Goal ) ? "exit" : save.Goal
		};
		if ( save.Tiles is { Length: > 0 } )
		{
			var n = Math.Min( doc.Tiles.Length, save.Tiles.Length );
			Array.Copy( save.Tiles, doc.Tiles, n );
		}
		if ( save.Entities is not null )
			doc.Entities = save.Entities;
		return doc;
	}
}

public sealed class StageSave
{
	public string Name { get; set; }
	public int Width { get; set; }
	public int Height { get; set; }
	public int TileSize { get; set; }
	public int[] Tiles { get; set; }
	public List<StageEntity> Entities { get; set; }
	public float Gravity { get; set; }
	public float MaxRun { get; set; }
	public float JumpVel { get; set; }
	public string Goal { get; set; }
}

sealed class StageSnap
{
	public int[] Tiles { get; set; }
	public List<StageEntity> Entities { get; set; }
	public string Goal { get; set; }
}