Flash/Sbox/FlashEditor.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Flash;
using Sandbox;
using Sandbox.UI;
using FMatrix = Flash.Matrix;
using FJson = Flash.Json;

/// <summary>
/// One layout tweak, editable in the s&amp;box inspector. Put these on child GameObjects of the Flash Stage.
/// Path is what F2 mode shows for the selected thing (e.g. "beforefight_panel" or "#1502@26").
/// </summary>
[Title( "Flash Layout Item" ), Category( "Shoes and Daggers" ), Icon( "open_with" )]
public sealed class FlashLayoutItem : Component
{
	/// <summary>Which object to move (copy it from F2 mode).</summary>
	[Property] public string Path { get; set; } = "";
	/// <summary>Offset in pixels of the original 640x420 game.</summary>
	[Property] public float X { get; set; }
	[Property] public float Y { get; set; }
	[Property] public float ScaleX { get; set; } = 1;
	[Property] public float ScaleY { get; set; } = 1;
	[Property] public float Rotation { get; set; }
	[Property] public bool Hidden { get; set; }
	public LayoutAdjust ToAdjust() => new() { X = X, Y = Y, ScaleX = ScaleX, ScaleY = ScaleY, Rotation = Rotation, Hidden = Hidden };

	protected override void OnValidate() => FlashStage.Instance?.Editor?.ReloadFromScene();
}

/// <summary>
/// F2 in game: click something to select it, drag to move it, arrows nudge (Shift ×10), [ ] scale, , . rotate,
/// H hide, R reset, Backspace selects the parent, P pauses the game, Ctrl+S saves (FileSystem.Data/ss2/layout.json).
/// </summary>
public sealed class FlashEditor
{
	readonly FlashStage stage;
	readonly Panel root;
	Panel overlay, box;
	Label info;
	public bool Active { get; private set; }
	public bool Paused { get; private set; }
	DisplayObject selected;
	bool dragging;
	Vector2 dragStart;
	double startX, startY;
	string status = "";

	public FlashEditor( FlashStage stage, Panel root )
	{
		this.stage = stage;
		this.root = root;
	}

	Player Player => stage.Player;

	// ------------------------------------------------------------------ persistence

	const string FilePath = "ss2/layout.json";

	public void LoadLayout()
	{
		Player.Layout.Clear();
		try
		{
			if ( FileSystem.Data.FileExists( FilePath ) )
			{
				var j = FJson.Parse( FileSystem.Data.ReadAllText( FilePath ) ) as Dictionary<string, object>;
				if ( j != null )
					foreach ( var kv in j )
					{
						var a = FJson.Doubles( kv.Value );
						if ( a == null || a.Length < 6 ) continue;
						Player.Layout[kv.Key] = new LayoutAdjust { X = a[0], Y = a[1], ScaleX = a[2], ScaleY = a[3], Rotation = a[4], Hidden = a[5] != 0 };
					}
			}
		}
		catch ( Exception e ) { Log.Warning( $"FlashEditor: layout.json unreadable: {e.Message}" ); }
		// items in the scene win over the saved file
		foreach ( var item in stage.Components.GetAll<FlashLayoutItem>( FindMode.EverythingInSelfAndDescendants ) )
			if ( !string.IsNullOrWhiteSpace( item.Path ) ) Player.Layout[item.Path.Trim()] = item.ToAdjust();
		Player.ReapplyLayout();
	}

	public void ReloadFromScene()
	{
		if ( Player == null ) return;
		LoadLayout();
	}

	public void Save()
	{
		var sb = new StringBuilder( "{\n" );
		bool first = true;
		foreach ( var kv in Player.Layout )
		{
			if ( kv.Value.IsIdentity ) continue;
			var a = kv.Value;
			if ( !first ) sb.Append( ",\n" );
			first = false;
			sb.Append( "  \"" ).Append( kv.Key.Replace( "\"", "\\\"" ) ).Append( "\": [" )
				.Append( string.Join( ", ", new[] { a.X, a.Y, a.ScaleX, a.ScaleY, a.Rotation, a.Hidden ? 1 : 0 }.Select( v => v.ToString( "0.###", CultureInfo.InvariantCulture ) ) ) ).Append( ']' );
		}
		sb.Append( "\n}\n" );
		FileSystem.Data.CreateDirectory( "ss2" );
		FileSystem.Data.WriteAllText( FilePath, sb.ToString() );
		status = $"saved {Player.Layout.Count} tweaks to data/{FilePath}";
	}

	// ------------------------------------------------------------------ input

	static bool Key( string name ) => Input.Keyboard.Pressed( name ) || Input.Keyboard.Pressed( name.ToUpperInvariant() );
	static bool Held( string name ) => Input.Keyboard.Down( name ) || Input.Keyboard.Down( name.ToUpperInvariant() );

	public void Update()
	{
		if ( Key( "f2" ) ) Toggle();
		if ( !Active ) return;
		if ( Key( "p" ) ) Paused = !Paused;
		if ( Held( "ctrl" ) && Key( "s" ) ) Save();
		if ( selected == null || selected.Removed ) { selected = null; return; }

		double step = Held( "shift" ) ? 10 : 1;
		if ( Key( "left" ) ) Nudge( -step, 0 );
		if ( Key( "right" ) ) Nudge( step, 0 );
		if ( Key( "up" ) ) Nudge( 0, -step );
		if ( Key( "down" ) ) Nudge( 0, step );
		if ( Key( "[" ) || Key( "leftbracket" ) ) Scale( 1 / 1.05 );
		if ( Key( "]" ) || Key( "rightbracket" ) ) Scale( 1.05 );
		if ( Key( "," ) || Key( "comma" ) ) Rotate( -(Held( "shift" ) ? 15 : 1) );
		if ( Key( "." ) || Key( "period" ) ) Rotate( Held( "shift" ) ? 15 : 1 );
		if ( Key( "h" ) ) { var a = Edit(); a.Hidden = !a.Hidden; }

		if ( Key( "r" ) ) { Player.Layout.Remove( selected.StablePath ); selected.Adjust = null; Player.ReapplyLayout(); }
		if ( Key( "backspace" ) && selected.RenderParent != null && selected.RenderParent != Player.Root ) selected = selected.RenderParent;
	}

	void Toggle()
	{
		Active = !Active;
		if ( !Active ) { Paused = false; overlay?.Delete( true ); overlay = null; selected = null; return; }
		overlay = new Panel { Parent = root };
		overlay.Style.Position = PositionMode.Absolute;
		overlay.Style.Left = 0; overlay.Style.Top = 0;
		overlay.Style.Width = Length.Percent( 100 ); overlay.Style.Height = Length.Percent( 100 );
		overlay.Style.PointerEvents = PointerEvents.None;
		overlay.Style.ZIndex = 100000;
		info = new Label { Parent = overlay };
		info.Style.Position = PositionMode.Absolute;
		info.Style.Left = 4; info.Style.Top = 2;
		info.Style.FontSize = 8;
		info.Style.FontColor = Color.White;
		info.Style.BackgroundColor = new Color( 0, 0, 0, 0.75f );
		info.Style.WhiteSpace = WhiteSpace.PreLine;
		box = new Panel { Parent = overlay };
		box.Style.Position = PositionMode.Absolute;
		box.Style.BorderWidth = 1;
		box.Style.BorderColor = new Color( 1, 0.6f, 0, 1 );
	}

	LayoutAdjust Edit()
	{
		var key = selected.StablePath;
		if ( !Player.Layout.TryGetValue( key, out var a ) ) { a = new LayoutAdjust(); Player.Layout[key] = a; }
		selected.Adjust = a;
		return a;
	}

	void Nudge( double dx, double dy ) { var a = Edit(); a.X += dx; a.Y += dy; }
	void Scale( double f ) { var a = Edit(); a.ScaleX *= f; a.ScaleY *= f; }
	void Rotate( double deg ) { var a = Edit(); a.Rotation += deg; }

	public void MouseDown( Vector2 s )
	{
		var hit = Pick( s.x, s.y );
		if ( hit != null && hit != selected ) selected = hit;
		if ( selected == null ) return;
		dragging = true;
		dragStart = s;
		var a = Edit();
		startX = a.X; startY = a.Y;
	}

	public void MouseMove( Vector2 s )
	{
		if ( !dragging || selected == null ) return;
		// stage delta → parent space
		var pm = selected.RenderParent?.WorldMatrix ?? FMatrix.Identity;
		var inv = pm.Inverse();
		double dx = s.x - dragStart.x, dy = s.y - dragStart.y;
		double lx = inv.A * dx + inv.C * dy, ly = inv.B * dx + inv.D * dy;
		var a = Edit();
		a.X = Math.Round( startX + lx, 1 );
		a.Y = Math.Round( startY + ly, 1 );
	}

	public void MouseUp() { dragging = false; }

	/// <summary>The thing under the mouse: the leaf's container (button / clip), never the whole screen.</summary>
	DisplayObject Pick( double x, double y )
	{
		var list = new List<RenderItem>();
		Player.CollectRender( list );
		for ( int i = list.Count - 1; i >= 0; i-- )
		{
			var o = list[i].Obj;
			if ( !o.HitTestPoint( x, y, true ) ) continue;
			var parent = o.RenderParent;
			if ( parent == null || parent == Player.Root ) return o;
			return parent;
		}
		return null;
	}

	// ------------------------------------------------------------------ drawing

	public void Draw( List<RenderItem> items )
	{
		if ( !Active || info == null ) return;
		var sb = new StringBuilder();
		sb.Append( "EDIT MODE (F2)  click: select  drag: move  arrows: nudge (shift x10)  [ ]: scale  , .: rotate  H: hide  R: reset  Backspace: parent  P: pause  Ctrl+S: save\n" );
		if ( selected != null && !selected.Removed )
		{
			var a = selected.Adjust;
			sb.Append( $"Path: {selected.StablePath}   " );
			if ( a != null ) sb.Append( $"X {a.X:0.#}  Y {a.Y:0.#}  scale {a.ScaleX:0.##},{a.ScaleY:0.##}  rot {a.Rotation:0.#}{(a.Hidden ? "  HIDDEN" : "")}" );
			var b = selected.BoundsIn( selected.WorldMatrix );
			if ( b != null )
			{
				var p0 = stage.StageToScreen( new Vector2( (float)b.Value.x0, (float)b.Value.y0 ) ) / stage.StageScale;
				var p1 = stage.StageToScreen( new Vector2( (float)b.Value.x1, (float)b.Value.y1 ) ) / stage.StageScale;
				box.Style.Left = p0.x; box.Style.Top = p0.y;
				box.Style.Width = p1.x - p0.x; box.Style.Height = p1.y - p0.y;
				box.Style.Display = DisplayMode.Flex;
			}
		}
		else
		{
			sb.Append( "Nothing selected" );
			box.Style.Display = DisplayMode.None;
		}
		if ( Paused ) sb.Append( "   [PAUSED]" );
		if ( status.Length > 0 ) sb.Append( "\n" ).Append( status );
		info.Text = sb.ToString();
	}
}