PixelStudio.cs
namespace PixelPusher;

public static class PixelStudio
{
	public static PushTool Tool = PushTool.Push;
	public static byte Fg = 7;
	public static byte Bg;
	public static int Brush = 1;
	public static bool Circle = true;
	public static int Zoom = 16;
	public static bool Grid = true;
	public static bool FillShape;
	public static StampRole Role = StampRole.Solid;
	public static int PanX;
	public static int PanY;
	public static bool Onion = true;
	public static bool Mirror;
	public static int HoverPx;
	public static int HoverPy;
	public static bool Playing;
	public static int Fps = 8;
	public static int CursorX;
	public static int CursorY;
	public static bool PadAim;

	static readonly PushTool[] ToolOrder =
	{
		PushTool.Push, PushTool.Flood, PushTool.Sip, PushTool.Block, PushTool.Line, PushTool.Oval
	};

	static float _padAccX;
	static float _padAccY;
	static float _playT;
	static float _paintHold;

	static bool _stroke;
	static int _sx;
	static int _sy;
	static int _lx;
	static int _ly;

	public static PixelDoc Doc => Desk.Sprite;

	public static void SelectTool( PushTool tool )
	{
		Tool = tool;
		GameFlow.Bump();
	}

	public static void SelectColor( byte index )
	{
		Fg = index;
		GameFlow.Bump();
	}

	public static void SwapFgBg()
	{
		(Fg, Bg) = (Bg, Fg);
		GameFlow.Bump();
	}

	public static void SetBrush( int size )
	{
		Brush = Math.Clamp( size, 1, 8 );
		GameFlow.Bump();
	}

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

	public static void ZoomAt( int vx, int vy, int dir )
	{
		var doc = Doc;
		var old = Math.Max( 1, Zoom );
		var next = Math.Clamp( old + dir, 1, 32 );
		if ( next == old || doc is null )
		{
			SetZoom( next );
			return;
		}

		var ox = (CrtView.VW - doc.Width * old) / 2 - PanX;
		var oy = (CrtView.VH - doc.Height * old) / 2 - PanY;
		var px = (vx - ox) / (float)old;
		var py = (vy - oy) / (float)old;
		Zoom = next;
		PanX = (int)MathF.Round( (CrtView.VW - doc.Width * next) / 2f - vx + px * next );
		PanY = (int)MathF.Round( (CrtView.VH - doc.Height * next) / 2f - vy + py * next );
		GameFlow.Bump();
	}

	public static void NewCanvas( int size )
	{
		size = Math.Clamp( size, 8, 256 );
		Desk.Sprite = new PixelDoc( size, size, Palettes.Named( Desk.Sprite?.PaletteName ) )
		{
			Name = $"sprite_{size}"
		};
		PanX = 0;
		PanY = 0;
		GameFlow.ShowToast( $"{size}×{size} canvas" );
		GameFlow.Bump();
	}

	public static void SetPalette( PalettePack pack )
	{
		Doc?.ApplyPalette( pack );
		GameFlow.Bump();
	}

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

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

	public static void Clear()
	{
		Doc?.Clear();
		GameFlow.Bump();
	}

	public static void SetRole( StampRole role )
	{
		Role = role;
		GameFlow.Bump();
	}

	public static void ToggleFill()
	{
		FillShape = !FillShape;
		GameFlow.Bump();
	}

	public static void ToggleCircle()
	{
		Circle = !Circle;
		GameFlow.Bump();
	}

	public static void SendToTileset()
	{
		if ( Doc is null || Desk.Tileset is null )
			return;
		var id = Math.Max( 1, StageStudio.TileId );
		Desk.Tileset.StampFromSprite( Doc, id );
		Desk.Tileset.SetFlag( id, TilesetDoc.FlagFromRole( Role ) );
		GameFlow.ShowToast( $"stamped tile {id} · {TilesetDoc.FlagName( TilesetDoc.FlagFromRole( Role ) )}" );
		GameFlow.Bump();
	}

	public static void FlipH() { Doc?.FlipH(); Desk.Touch(); GameFlow.Bump(); }
	public static void FlipV() { Doc?.FlipV(); Desk.Touch(); GameFlow.Bump(); }
	public static void Rotate() { Doc?.Rotate90(); Desk.Touch(); GameFlow.Bump(); }
	public static void ToggleMirror()
	{
		Mirror = !Mirror;
		GameFlow.ShowToast( Mirror ? "mirror on" : "mirror off" );
		GameFlow.Bump();
	}

	public static void ReplaceUnderCursor()
	{
		var doc = Doc;
		if ( doc is null )
			return;
		var from = (byte)doc.IndexAt( HoverPx, HoverPy );
		doc.Replace( from, Fg );
		Desk.Touch();
		GameFlow.Bump();
	}

	public static void Pointer( int px, int py, bool left, bool right, bool down, bool released )
	{
		var doc = Doc;
		if ( doc is null )
			return;
		if ( DeskNet.GuestLocked )
			return;

		HoverPx = px;
		HoverPy = py;

		if ( StudioMouse.AltHeld )
		{
			if ( down || left || right )
			{
				var picked = (byte)doc.IndexAt( px, py );
				if ( right ) Bg = picked;
				else Fg = picked;
				GameFlow.Bump();
			}
			return;
		}

		var index = right ? Bg : Fg;
		if ( right && Tool != PushTool.Sip )
			index = 0;

		if ( down )
		{
			_stroke = true;
			_sx = px;
			_sy = py;
			_lx = px;
			_ly = py;
			doc.BeginStroke();

			switch ( Tool )
			{
				case PushTool.Sip:
					var picked = (byte)doc.IndexAt( px, py );
					if ( right ) Bg = picked;
					else Fg = picked;
					GameFlow.Bump();
					break;
				case PushTool.Flood:
					doc.Flood( px, py, index );
					break;
				case PushTool.Push:
					doc.StampBrush( px, py, index, Brush, Circle );
					break;
			}
			Desk.Touch();
			return;
		}

		if ( !_stroke )
			return;

		if ( left || right )
		{
			if ( Tool == PushTool.Push )
				doc.Line( _lx, _ly, px, py, index, Brush, Circle );
			_lx = px;
			_ly = py;
			Desk.Touch();
			return;
		}

		if ( released )
		{
			if ( Tool == PushTool.Block )
				doc.Rect( _sx, _sy, px, py, index, FillShape, Brush );
			else if ( Tool == PushTool.Line )
				doc.Line( _sx, _sy, px, py, index, Brush, Circle );
			else if ( Tool == PushTool.Oval )
				doc.Ellipse( _sx, _sy, px, py, index, FillShape, Brush );
			_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 = _lx;
		y1 = _ly;
		return _stroke && (Tool == PushTool.Block || Tool == PushTool.Line || Tool == PushTool.Oval);
	}

	public static void ToggleOnion()
	{
		Onion = !Onion;
		GameFlow.ShowToast( Onion ? "onion on" : "onion off" );
		GameFlow.Bump();
	}

	public static void TogglePlay()
	{
		Playing = !Playing;
		_playT = 0;
		GameFlow.Bump();
	}

	public static void AddFrame()
	{
		Doc?.AddFrame();
		Playing = false;
		GameFlow.ShowToast( $"frame {(Doc?.Frame ?? 0) + 1}/{Doc?.FrameCount ?? 1}" );
		GameFlow.Bump();
	}

	public static void DeleteFrame()
	{
		Doc?.DeleteFrame();
		Playing = false;
		GameFlow.Bump();
	}

	public static void PrevFrame()
	{
		var doc = Doc;
		if ( doc is null )
			return;
		Playing = false;
		doc.SetFrame( doc.Frame == 0 ? doc.FrameCount - 1 : doc.Frame - 1 );
		GameFlow.Bump();
	}

	public static void NextFrame()
	{
		var doc = Doc;
		if ( doc is null )
			return;
		Playing = false;
		doc.SetFrame( (doc.Frame + 1) % doc.FrameCount );
		GameFlow.Bump();
	}

	public static void CycleTool( int dir )
	{
		var i = Array.IndexOf( ToolOrder, Tool );
		if ( i < 0 ) i = 0;
		i = (i + dir + ToolOrder.Length) % ToolOrder.Length;
		SelectTool( ToolOrder[i] );
	}

	public static void Tick()
	{
		if ( GameFlow.Screen != GameScreen.Pixel || Doc is null )
			return;

		TickKeys();
		TickPad();
		TickPlay();
	}

	static void TickKeys()
	{
		try
		{
			if ( Key( "z" ) ) Undo();
			if ( Key( "y" ) ) Redo();
			if ( Key( "o" ) ) ToggleOnion();
			if ( Key( "n" ) ) AddFrame();
			if ( Key( "backspace" ) || Key( "del" ) ) DeleteFrame();
			if ( Key( "," ) ) PrevFrame();
			if ( Key( "." ) ) NextFrame();
			if ( Key( "-" ) ) SetBrush( Brush - 1 );
			if ( Key( "=" ) ) SetBrush( Brush + 1 );
			if ( Key( "g" ) ) { Grid = !Grid; GameFlow.Bump(); }
			if ( Key( "x" ) ) SwapFgBg();
			if ( Key( "h" ) ) FlipH();
			if ( Key( "v" ) ) FlipV();
			if ( Key( "r" ) ) Rotate();
			if ( Key( "m" ) ) ToggleMirror();
			if ( Key( "p" ) ) ReplaceUnderCursor();
		}
		catch
		{
			// raw keys not always available
		}

		if ( Input.Pressed( "Jump" ) )
			TogglePlay();
		if ( Input.Pressed( "SlotPrev" ) )
			CycleTool( -1 );
		if ( Input.Pressed( "SlotNext" ) )
			CycleTool( 1 );
		if ( Input.Pressed( "Reload" ) )
			Undo();
		if ( Input.Pressed( "Use" ) )
			SelectTool( PushTool.Sip );
		if ( Input.Pressed( "View" ) )
			ToggleOnion();
	}

	static bool Key( string name )
	{
		try
		{
			return Input.Keyboard.Pressed( name );
		}
		catch
		{
			return false;
		}
	}

	static void TickPad()
	{
		if ( !Input.UsingController )
		{
			PadAim = false;
			_paintHold = 0f;
			return;
		}

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

		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 )
		{
			_padAccX = 0;
			_padAccY = 0;
		}
		else
		{
			PadAim = true;
			var speed = 9f * Time.Delta * (Input.Down( "Run" ) ? 2.2f : 1f);
			_padAccX += ax * speed;
			_padAccY += ay * speed;
			var doc = Doc;
			while ( MathF.Abs( _padAccX ) >= 1f )
			{
				CursorX = Math.Clamp( CursorX + Math.Sign( _padAccX ), 0, doc.Width - 1 );
				_padAccX -= Math.Sign( _padAccX );
			}
			while ( MathF.Abs( _padAccY ) >= 1f )
			{
				CursorY = Math.Clamp( CursorY + Math.Sign( _padAccY ), 0, doc.Height - 1 );
				_padAccY -= Math.Sign( _padAccY );
			}
			GameFlow.Bump();
		}

		if ( !PadAim && !Input.UsingController )
			return;

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

	static void TickPlay()
	{
		if ( !Playing || Doc is null || Doc.FrameCount < 2 )
			return;
		_playT += Time.Delta;
		var step = 1f / Math.Clamp( Fps, 2, 24 );
		if ( _playT < step )
			return;
		_playT = 0;
		Doc.SetFrame( (Doc.Frame + 1) % Doc.FrameCount );
		GameFlow.Bump();
	}
}