Editor/UI/ArchSidebarChrome.cs
using System;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// The stable landmarks every creation sidebar opens with: what tool this is, what gesture it wants, and
// the sections that come and go beneath them.
public static class ArchSidebarLayout {
	public static void Header( ToolSidebarWidget panel, string title, string icon, string shortcut, string advice, Action guide ) {
		panel.Layout.Add( new ArchSidebarHeader( title, icon, shortcut, guide ) );

		if ( string.IsNullOrWhiteSpace( advice ) ) {
			return;
		}

		panel.Layout.Add( Advice( advice ) );
	}

	// WHAT IS PICKED, and the one press that lets go of it. A placement tool edits the layer it just placed, so
	// without this line an author tuning the seed for the next drag is silently re-cutting the last one - and a pick
	// this tool does not author holds the viewport just as hard, because its widgets take the drag before any
	// placement can. Either way the only way out was to click empty space and hope.
	public static void Editing( ToolSidebarWidget panel, string label, string action, Action stop ) {
		if ( string.IsNullOrWhiteSpace( label ) ) {
			return;
		}

		panel.Layout.Add( new ArchEditingBar( label, action, stop ) );
	}

	// The shared *Ui forms lay themselves out through AddGroup, which needs a sidebar rather than a layout, so a
	// disclosure holds one by nesting it. Here once, or every caller folding a form away repeats the dance.
	public static void Form( Layout group, Action<ToolSidebarWidget> build ) {
		var form = new ToolSidebarWidget();

		build( form );

		group.Add( form );
	}

	// Qt puts a wrappable label's minimum at one line however much text it holds, so the line count is estimated.
	public static Widget Advice( string text ) {
		var label = new Label( text );

		label.WordWrap = true;
		label.FixedWidth = ArchPartUi.PanelWidth;
		label.MinimumHeight = MathF.Ceiling( text.Length / 36f ) * 14f + 4f;
		label.SetStyles( "font-size: 10px; color: rgba(255,255,255,0.48);" );

		return label;
	}
}

// The line that says the form is aimed at a layer rather than at the next placement, and the one press back to
// seeding. Nothing is deleted or moved by it - only the form stops writing to what is standing.
public sealed class ArchEditingBar : Widget {
	public ArchEditingBar( string label, string action, Action stop ) {
		FixedHeight = 24f;
		Layout = Layout.Row();
		Layout.Spacing = 4;

		var caption = new Label( label ) {
			ToolTip = "A picked layer holds its widgets over the viewport, and every control below writes to it."
		};

		caption.SetStyles( "font-size: 10px; color: rgba(255,140,30,0.95);" );

		Layout.Add( caption, 1 );
		Layout.Add( new Button( action, "close" ) {
			FixedHeight = 20f,
			ToolTip = "Let go of this layer and draw a new one",
			Clicked = () => stop?.Invoke()
		} );
	}
}

// The HOW TO chip stands only where a guide is written for this tool, so its presence is itself the promise
// that there is more to read than the one line under it.
public sealed class ArchSidebarHeader : Widget {
	const float ChipWidth = 56f;

	readonly string title;
	readonly string icon;
	readonly string shortcut;
	readonly Action guide;

	bool hovered;

	public ArchSidebarHeader( string title, string icon, string shortcut, Action guide ) {
		this.title = title;
		this.icon = icon;
		this.shortcut = shortcut;
		this.guide = guide;

		FixedHeight = 36f;
		MouseTracking = guide is not null;
	}

	Rect Chip => new( Width - ChipWidth, 9f, ChipWidth, 18f );

	protected override void OnMouseMove( MouseEvent e ) {
		var was = hovered;

		hovered = guide is not null && Chip.IsInside( e.LocalPosition );

		if ( was != hovered ) {
			Cursor = hovered ? CursorShape.Finger : CursorShape.Arrow;
			Update();
		}
	}

	protected override void OnMouseLeave() {
		if ( !hovered ) {
			return;
		}

		hovered = false;
		Cursor = CursorShape.Arrow;
		Update();
	}

	protected override void OnMouseClick( MouseEvent e ) {
		if ( guide is null || !Chip.IsInside( e.LocalPosition ) ) {
			return;
		}

		guide();
		e.Accepted = true;
	}

	protected override void OnPaint() {
		Paint.Antialiasing = true;
		Paint.TextAntialiasing = true;

		var body = LocalRect;
		var reserved = guide is null ? 0f : ChipWidth + 6f;

		Paint.SetPen( Theme.Blue );
		Paint.DrawIcon( new Rect( body.Left, body.Top + 9f, 18f, 18f ), icon, 18f );

		Paint.SetDefaultFont( 10f, 600 );
		Paint.SetPen( Theme.Text );
		Paint.DrawText( body.Shrink( 25f, 0f, reserved, 0f ), title, TextFlag.LeftCenter );

		if ( !string.IsNullOrWhiteSpace( shortcut ) ) {
			Paint.SetDefaultFont( 7f, 500 );
			Paint.SetPen( Theme.TextControl.WithAlpha( 0.4f ) );
			Paint.DrawText( body.Shrink( 0f, 0f, reserved, 0f ), shortcut, TextFlag.RightCenter );
		}

		if ( guide is not null ) {
			PaintChip();
		}

		Paint.ClearBrush();
		Paint.SetPen( Theme.Text.WithAlpha( 0.09f ) );
		Paint.DrawLine( new Vector2( body.Left, body.Bottom - 1f ), new Vector2( body.Right, body.Bottom - 1f ) );
	}

	void PaintChip() {
		var chip = Chip;

		Paint.SetBrushAndPen( Theme.Blue.WithAlpha( hovered ? 0.28f : 0.12f ), Theme.Blue.WithAlpha( hovered ? 0.8f : 0.35f ) );
		Paint.DrawRect( chip, 9 );

		Paint.ClearBrush();
		Paint.SetPen( Theme.Blue.WithAlpha( hovered ? 1f : 0.75f ) );
		Paint.DrawIcon( new Rect( chip.Left + 5f, chip.Top + 3f, 12f, 12f ), "help_outline", 12f );

		Paint.SetDefaultFont( 6.5f, 700 );
		Paint.DrawText( new Rect( chip.Left + 18f, chip.Top, chip.Width - 21f, chip.Height ), "HOW TO", TextFlag.LeftCenter );
	}
}

// A disclosure that keeps its own open state per subtool, and that is simply absent when the choice above
// it cannot use it - a greyed control still asks the reader to work out why it cannot be used.
public sealed class ArchSidebarSection : Widget {
	public static void Show( ToolSidebarWidget panel, string key, string title, Action<Layout> build ) {
		Show( panel, key, title, true, true, false, build );
	}

	public static void Show( ToolSidebarWidget panel, string key, string title, bool visible, Action<Layout> build ) {
		Show( panel, key, title, visible, true, false, build );
	}

	public static void Show( ToolSidebarWidget panel, string key, string title, bool visible, bool expanded, bool collapsible, Action<Layout> build ) {
		if ( !visible ) {
			return;
		}

		var section = new ArchSidebarSection( key, title, expanded, collapsible );

		build( section.content.Layout );

		panel.Layout.Add( section );
	}

	public static void Disclosure( ToolSidebarWidget panel, string key, string title, bool visible, Action<Layout> build ) {
		Show( panel, key, title, visible, false, true, build );
	}

	readonly string key;
	readonly string title;
	readonly bool collapsible;
	readonly Widget content;

	bool open;
	bool hovered;

	ArchSidebarSection( string key, string title, bool expanded, bool collapsible ) {
		this.key = key;
		this.title = title;
		this.collapsible = collapsible;

		open = !collapsible || ArchSidebarState.Expanded( key, expanded );

		Layout = Layout.Column();
		Layout.Spacing = 2;

		content = new Widget( this ) { Layout = Layout.Column() };
		content.Layout.Spacing = 3;
		content.Visible = open;

		Layout.Add( content );

		Margins();

		HorizontalSizeMode = SizeMode.Expand | SizeMode.CanGrow;
		VerticalSizeMode = SizeMode.CanShrink;
		MouseTracking = true;
	}

	// A zero hint takes the height from the content's minimum, which is what keeps a collapsed section thin.
	protected override Vector2 SizeHint() => 0;

	void Margins() {
		Layout.Margin = new Sandbox.UI.Margin( 6, 15, 6, open ? 6 : 0 );
	}

	void Toggle() {
		open = !open;
		content.Visible = open;

		Margins();
		ArchSidebarState.Expand( key, open );
		Update();
	}

	protected override void OnMouseMove( MouseEvent e ) {
		var was = hovered;
		hovered = collapsible && e.LocalPosition.y < 14f;

		if ( was != hovered ) {
			Update();
		}
	}

	protected override void OnMouseLeave() {
		if ( !hovered ) {
			return;
		}

		hovered = false;
		Update();
	}

	protected override void OnMouseClick( MouseEvent e ) {
		if ( !collapsible || e.LocalPosition.y > 14f ) {
			return;
		}

		Toggle();
		e.Accepted = true;
	}

	protected override void OnPaint() {
		Paint.Antialiasing = true;
		Paint.TextAntialiasing = true;

		if ( open ) {
			var frame = Paint.LocalRect;
			frame.Top += 6f;
			frame = frame.Shrink( 0, 0, 1, 1 );

			Paint.SetBrushAndPen( Theme.Text.WithAlpha( 0.012f ), Theme.Text.WithAlpha( 0.09f ) );
			Paint.DrawRect( frame, 4 );
		}

		Paint.SetPen( Theme.TextControl.WithAlpha( hovered ? 1f : 0.55f ) );
		Paint.SetDefaultFont( 7f, 600 );

		if ( collapsible ) {
			Paint.DrawIcon( new Rect( 1, -2, 12, 14 ), open ? "expand_more" : "chevron_right", 10 );
			Paint.DrawText( new Vector2( 15, 0 ), title.ToUpperInvariant() );
			Cursor = CursorShape.Finger;
		} else {
			Paint.DrawText( new Vector2( 10, 0 ), title.ToUpperInvariant() );
		}
	}
}

public readonly record struct ArchRunState( bool Active, string Progress );

// Finish and Cancel exist only while a chained operation is live. It rides the tool FOOTER, outside the
// sidebar's scroll, so a long form cannot push the way out of a run off the bottom.
public sealed class ArchRunBar : Widget {
	readonly Func<ArchRunState> read;
	readonly Label progress;

	public ArchRunBar( Func<ArchRunState> read, Action finish, Action cancel ) {
		this.read = read;

		Layout = Layout.Row();
		Layout.Margin = new Sandbox.UI.Margin( 8, 6, 8, 6 );
		Layout.Spacing = 6;

		progress = new Label( "" );
		progress.SetStyles( "font-size: 10px; color: rgba(255,255,255,0.7);" );

		Layout.Add( progress );
		Layout.AddStretchCell();
		Layout.Add( new Button( "Cancel", "close" ) { Clicked = () => cancel?.Invoke() } );
		Layout.Add( new Button.Primary( "Finish", "check" ) { Clicked = () => finish?.Invoke() } );

		Visible = false;
	}

	[EditorEvent.Frame]
	public void Poll() {
		var state = read();

		if ( Visible != state.Active ) {
			Visible = state.Active;
		}

		if ( !state.Active ) {
			return;
		}

		var text = state.Progress ?? "";

		if ( progress.Text != text ) {
			progress.Text = text;
		}
	}

	protected override void OnPaint() {
		Paint.ClearPen();
		Paint.SetBrush( Theme.TabBackground );
		Paint.DrawRect( LocalRect );

		Paint.SetPen( Theme.Blue.WithAlpha( 0.5f ) );
		Paint.DrawLine( new Vector2( 0, 0 ), new Vector2( Width, 0 ) );
	}
}