StageStudio.cs
namespace PixelPusher;

public static class StageStudio
{
	public static int TileId = 2;
	public static string Stamp = "paint";
	public static int Zoom = 2;
	public static float CamX;
	public static float CamY;
	public static bool Grid = true;
	public static bool Autotile;
	public static int HoverTx;
	public static int HoverTy;
	public static int DropX;
	public static int DropY;
	public static int CursorTx;
	public static int CursorTy;
	public static bool PadAim;

	static bool _stroke;
	static float _padAccX;
	static float _padAccY;
	static float _paintHold;
	static int _sx;
	static int _sy;

	public static StageDoc Stage => Desk.Stage;
	public static TilesetDoc Tiles => Desk.Tileset;

	public static readonly string[] Stamps =
	{
		"paint", "flood", "rect", "line", "sip", "start", "coin", "spring", "exit", "check", "sign", "erase"
	};

	public static readonly string[] SignLines =
	{
		"STOCK THE COOLER",
		"JUMP",
		"SPRING AHEAD",
		"EXIT THIS WAY",
		"MIND THE PINK"
	};

	public static string NextSign( string current )
	{
		var i = Array.IndexOf( SignLines, current );
		return SignLines[(i + 1) % SignLines.Length];
	}

	public static void SelectTile( int id )
	{
		TileId = Math.Max( 0, id );
		Stamp = "paint";
		GameFlow.Bump();
	}

	public static void SelectStamp( string stamp )
	{
		Stamp = stamp ?? "paint";
		GameFlow.Bump();
	}

	public static void SetZoom( int zoom )
	{
		Zoom = Math.Clamp( zoom, 1, 8 );
		GameFlow.Bump();
	}

	public static void ZoomAt( int vx, int vy, int dir )
	{
		var old = Math.Max( 1, Zoom );
		var next = Math.Clamp( old + dir, 1, 8 );
		if ( next == old )
			return;
		var wx = CamX + vx / (float)old;
		var wy = CamY + vy / (float)old;
		Zoom = next;
		CamX = wx - vx / (float)next;
		CamY = wy - vy / (float)next;
		ClampCam();
		GameFlow.Bump();
	}

	public static void FocusPlayer( float px, float py )
	{
		CamX = px - 160f;
		CamY = py - 90f;
		ClampCam();
	}

	public static void Pointer( int tx, int ty, bool left, bool right, bool down, bool released )
	{
		var stage = Stage;
		if ( stage is null )
			return;
		if ( DeskNet.GuestLocked )
			return;

		HoverTx = tx;
		HoverTy = ty;
		DropX = tx;
		DropY = ty;

		if ( StudioMouse.AltHeld || Stamp == "sip" )
		{
			if ( down || left )
			{
				var id = stage.TileAt( tx, ty );
				if ( id > 0 )
					SelectTile( id );
			}
			return;
		}

		if ( down )
		{
			_stroke = true;
			_sx = tx;
			_sy = ty;
			Stage.BeginStroke();
			if ( Stamp is "rect" or "line" )
			{
				Desk.Touch();
				return;
			}
			Apply( tx, ty, right );
			Desk.Touch();
			return;
		}

		if ( _stroke && (left || right) && (Stamp == "paint" || Stamp == "erase" || right) )
		{
			Apply( tx, ty, right );
			Desk.Touch();
		}

		if ( released )
		{
			if ( Stamp == "rect" )
				stage.FillRect( _sx, _sy, tx, ty, right ? 0 : TileId );
			else if ( Stamp == "line" )
				stage.StrokeLine( _sx, _sy, tx, ty, right ? 0 : TileId );
			_stroke = false;
			Desk.Touch();
			GameFlow.Bump();
		}
	}

	public static bool Preview( out int x0, out int y0, out int x1, out int y1 )
	{
		x0 = _sx;
		y0 = _sy;
		x1 = HoverTx;
		y1 = HoverTy;
		return _stroke && Stamp is "rect" or "line";
	}

	public static void Pan( float dx, float dy )
	{
		CamX += dx;
		CamY += dy;
		ClampCam();
	}

	public static void TickCamera()
	{
		if ( GameFlow.Screen != GameScreen.Stage )
			return;

		if ( Input.UsingController )
		{
			TickPad();
			return;
		}

		var speed = 280f * Time.Delta;
		if ( Input.Down( "Run" ) )
			speed *= 2.2f;
		if ( Input.Down( "Left" ) ) CamX -= speed;
		if ( Input.Down( "Right" ) ) CamX += speed;
		if ( Input.Down( "Forward" ) ) CamY -= speed;
		if ( Input.Down( "Backward" ) ) CamY += speed;
		ClampCam();
	}

	static void TickPad()
	{
		var stage = Stage;
		if ( stage is null )
			return;

		var look = Input.AnalogLook;
		CamX += look.yaw * 8f;
		CamY += look.pitch * 8f;
		ClampCam();

		var move = Input.AnalogMove;
		var ax = move.y;
		var ay = -move.x;
		if ( MathF.Abs( ax ) < 0.35f ) ax = 0;
		if ( MathF.Abs( ay ) < 0.35f ) ay = 0;
		if ( ax != 0 || ay != 0 )
		{
			PadAim = true;
			var speed = 8f * Time.Delta * (Input.Down( "Run" ) ? 2f : 1f);
			_padAccX += ax * speed;
			_padAccY += ay * speed;
			while ( MathF.Abs( _padAccX ) >= 1f )
			{
				CursorTx = Math.Clamp( CursorTx + Math.Sign( _padAccX ), 0, stage.Width - 1 );
				_padAccX -= Math.Sign( _padAccX );
			}
			while ( MathF.Abs( _padAccY ) >= 1f )
			{
				CursorTy = Math.Clamp( CursorTy + Math.Sign( _padAccY ), 0, stage.Height - 1 );
				_padAccY -= Math.Sign( _padAccY );
			}
			HoverTx = CursorTx;
			HoverTy = CursorTy;
			DropX = CursorTx;
			DropY = CursorTy;
			GameFlow.Bump();
		}

		if ( Input.Pressed( "SlotPrev" ) )
			CycleStamp( -1 );
		if ( Input.Pressed( "SlotNext" ) )
			CycleStamp( 1 );
		if ( Input.Pressed( "Jump" ) )
			GameFlow.StartPlaytest();

		if ( CrtView.PointerOn )
		{
			_paintHold = 0f;
			return;
		}

		var paint = Input.Down( "Attack1" );
		var erase = Input.Down( "Attack2" );
		if ( paint || erase )
		{
			var down = _paintHold <= 0f;
			Pointer( CursorTx, CursorTy, paint, erase, down, false );
			_paintHold += Time.Delta;
			Coach.OnStagePaint();
		}
		else if ( _paintHold > 0f )
		{
			Pointer( CursorTx, CursorTy, false, false, false, true );
			_paintHold = 0;
		}
	}

	public static void CycleStamp( int dir )
	{
		var i = Array.IndexOf( Stamps, Stamp );
		if ( i < 0 ) i = 0;
		i = (i + dir + Stamps.Length) % Stamps.Length;
		SelectStamp( Stamps[i] );
	}

	public static void Undo()
	{
		Stage?.Undo();
		GameFlow.Bump();
	}

	public static void Redo()
	{
		Stage?.Redo();
		GameFlow.Bump();
	}

	public static void SetGoal( string goal )
	{
		if ( Stage is null )
			return;
		Stage.BeginStroke();
		Stage.Goal = goal == "grab" ? "grab" : "exit";
		Desk.Touch();
		GameFlow.ShowToast( Stage.Goal == "grab" ? "goal · grab every coin" : "goal · reach the exit" );
		GameFlow.Bump();
	}

	public static void CycleTileFlag()
	{
		var tiles = Tiles;
		if ( tiles is null || TileId <= 0 )
			return;
		var next = TilesetDoc.CycleFlag( tiles.Flag( TileId ) );
		tiles.SetFlag( TileId, next );
		Desk.Touch();
		GameFlow.ShowToast( $"tile {TileId} · {TilesetDoc.FlagName( next )}" );
		GameFlow.Bump();
	}

	static void Apply( int tx, int ty, bool erase )
	{
		var stage = Stage;
		if ( stage is null )
			return;

		if ( erase || Stamp == "erase" )
		{
			stage.RemoveAt( tx, ty );
			return;
		}

		if ( Stamp == "flood" )
		{
			stage.Flood( tx, ty, TileId );
			return;
		}

		if ( Stamp == "paint" )
		{
			PaintTile( tx, ty );
			return;
		}

		if ( Stamp == "sign" )
		{
			var existing = stage.Entities.Find( e => e.Tx == tx && e.Ty == ty && e.Kind == "sign" );
			var data = existing is null ? SignLines[0] : NextSign( existing.Data );
			stage.PlaceEntity( "sign", tx, ty, data );
			GameFlow.ShowToast( data );
			return;
		}

		stage.PlaceEntity( Stamp, tx, ty );
	}

	static void PaintTile( int tx, int ty )
	{
		var stage = Stage;
		var id = TileId;
		if ( Autotile && id > 0 )
			id = AutotileId( tx, ty, id );
		stage.SetTile( tx, ty, id );
		if ( !Autotile )
			return;
		RefreshNeighbor( tx - 1, ty );
		RefreshNeighbor( tx + 1, ty );
		RefreshNeighbor( tx, ty - 1 );
		RefreshNeighbor( tx, ty + 1 );
	}

	static void RefreshNeighbor( int tx, int ty )
	{
		var stage = Stage;
		var tiles = Tiles;
		if ( stage is null || tiles is null )
			return;
		var id = stage.TileAt( tx, ty );
		if ( id <= 0 || !tiles.IsSolid( id ) )
			return;
		stage.SetTile( tx, ty, AutotileId( tx, ty, TileId ) );
	}

	static int AutotileId( int tx, int ty, int baseId )
	{
		var tiles = Tiles;
		if ( tiles is null )
			return baseId;
		var n = tiles.IsSolid( Stage.TileAt( tx, ty - 1 ) ) ? 1 : 0;
		var e = tiles.IsSolid( Stage.TileAt( tx + 1, ty ) ) ? 2 : 0;
		var s = tiles.IsSolid( Stage.TileAt( tx, ty + 1 ) ) ? 4 : 0;
		var w = tiles.IsSolid( Stage.TileAt( tx - 1, ty ) ) ? 8 : 0;
		var variant = n | e | s | w;
		var id = baseId + (variant % 4);
		if ( id >= tiles.TileCount )
			id = baseId;
		return Math.Max( 1, id );
	}

	static void ClampCam()
	{
		var stage = Stage;
		if ( stage is null )
			return;
		var worldW = stage.Width * stage.TileSize;
		var worldH = stage.Height * stage.TileSize;
		CamX = Math.Clamp( CamX, 0, Math.Max( 0, worldW - 320 ) );
		CamY = Math.Clamp( CamY, 0, Math.Max( 0, worldH - 180 ) );
	}
}