Editor/Designers/ArchDesignerDialog.cs
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// A designer decides three things: its form, what the stage shows, what Apply hands back.
public abstract class ArchDesignerDialog : Dialog {
	protected Widget Form { get; private set; }
	protected ArchDesignerStage Stage { get; private set; }

	// A designer whose subject is sometimes ART rather than geometry stands a second viewport in the same slot and
	// swaps to it, so aiming a prefab is a step of one design rather than a second window over the top of it.
	Widget aiming;
	Label caption;
	Label advice;
	Widget views;

	protected ArchDesignerDialog( Widget parent, string title, string icon ) : base( parent ) {
		Window.Title = title;
		Window.SetWindowIcon( icon );
		Window.SetModal( true, true );
		Window.Size = new Vector2( 1080, 720 );
		Window.MinimumWidth = 900;
		Window.MinimumHeight = 620;
	}

	protected abstract void Populate();

	protected abstract ArchStaged Staged();

	protected abstract void Apply();

	protected abstract void SaveTemplate();

	protected abstract string SaveLabel { get; }

	// Called by the subclass once its draft exists: the base constructor would read unassigned fields.
	protected void Assemble( ArchKit kit, string caption, string advice, string footnote ) {
		Layout = Layout.Column();
		Layout.Margin = 0;
		Layout.Spacing = 0;

		var split = Layout.AddRow( 1 );

		split.Add( Sidebar() );
		split.AddSeparator();
		split.Add( Viewport( kit, caption, advice ), 1 );

		Layout.AddSeparator();

		var footer = Layout.AddRow();
		footer.Margin = 10;
		footer.Spacing = 6;
		footer.Add( new Label( footnote ) );
		footer.Add( new Button( "Copy Debug", "content_copy" ) { Clicked = CopyDebug } );
		footer.AddStretchCell();
		footer.Add( new Button( "Cancel", "close" ) { Clicked = Close } );
		footer.Add( new Button( SaveLabel, "save" ) { Clicked = SaveTemplate } );
		footer.Add( new Button.Primary( "Apply", "check" ) { Clicked = Apply } );

		Repopulate();
	}

	// Resolved vs generated vs plan - one paste to settle data-or-geometry.
	protected virtual string Diagnose() => null;

	void CopyDebug() {
		var report = string.Join( "\n",
			$"# {Window.Title}",
			"",
			"## resolved",
			Diagnose() ?? "nothing reported",
			"",
			"## generated",
			Stage.Describe(),
			"",
			"## plan",
			ArchStorage.Snapshot( Staged().Plan ) );

		EditorUtility.Clipboard.Copy( report );
		Log.Info( $"Architecture: {Window.Title} debug copied to the clipboard ({report.Length} characters)." );
	}

	// Relaid out, not greyed: options that don't apply are not there at all.
	protected void Repopulate() {
		Form.Layout.Clear( true );

		Populate();

		Form.Layout.AddStretchCell();
		Stage.Restage();
	}

	protected static Widget Note( string text ) {
		var label = new Label( text );

		label.WordWrap = true;
		label.FixedWidth = 200f;
		label.MinimumHeight = MathF.Ceiling( text.Length / 34f ) * 15f + 4f;
		label.SetStyles( "font-size: 10px; color: rgba(255,255,255,0.55);" );

		return label;
	}

	protected Widget Heading( string text ) {
		var label = new Label( text );
		label.SetStyles( "font-size: 13px; font-weight: 700; margin-top: 8px;" );

		return label;
	}

	protected Widget Row( string label, Widget control ) {
		var row = new Widget( this ) { Layout = Layout.Row() };
		row.Layout.Spacing = 6;
		row.Layout.Add( new Label( label ) { MinimumWidth = 112 } );
		row.Layout.Add( control, 1 );

		return row;
	}

	protected Widget Text( string label, string value, Action<string> assign, bool restages = false ) {
		var edit = new LineEdit( value ?? "" );
		edit.TextEdited += text => {
			assign( text ?? "" );

			if ( restages ) {
				Stage.Restage();
			}
		};

		return Row( label, edit );
	}

	protected Widget Number( string label, float value, Action<float> assign ) {
		void Take( string text ) {
			if ( !float.TryParse( text, out var parsed ) ) {
				return;
			}

			assign( ArchGridService.Fine( parsed ) );
			Stage.Restage();
		}

		var edit = new ArchNumberEdit( value.ToString( "0.##" ), ArchGridService.FinestSize, 1f, "0.##",
			pulled => Take( pulled.ToString( "0.##" ) ) );

		edit.TextEdited += Take;

		return Row( label, edit );
	}

	protected Widget Integer( string label, int value, Action<int> assign ) {
		void Take( string text ) {
			if ( !int.TryParse( text, out var parsed ) ) {
				return;
			}

			assign( Math.Max( 1, parsed ) );
			Stage.Restage();
		}

		var edit = new ArchNumberEdit( value.ToString(), 1f, 0.25f, "0", pulled => Take( pulled.ToString( "0" ) ) );

		edit.TextEdited += Take;

		return Row( label, edit );
	}

	protected Widget Check( string label, bool value, Action<bool> assign ) {
		var check = new Checkbox( label ) { Value = value };
		check.Toggled += () => {
			assign( check.Value );
			Stage.Restage();
		};

		return check;
	}

	// A toggle that decides whether other rows exist, so it repopulates.
	protected Widget Gate( string label, bool value, Action<bool> assign ) {
		var check = new Checkbox( label ) { Value = value };
		check.Toggled += () => {
			assign( check.Value );
			Repopulate();
		};

		return check;
	}

	Widget Sidebar() {
		var sidebar = new Widget( this ) { Layout = Layout.Column() };
		sidebar.MinimumWidth = 330;
		sidebar.MaximumWidth = 330;
		sidebar.Layout.Margin = 8;

		var scroll = new ScrollArea( sidebar );
		scroll.HorizontalScrollbarMode = ScrollbarMode.Off;

		Form = new Widget( scroll ) { Layout = Layout.Column() };
		Form.Layout.Margin = 4;
		Form.Layout.Spacing = 6;
		scroll.Canvas = Form;
		sidebar.Layout.Add( scroll, 1 );

		return sidebar;
	}

	// A designer that never aims art leaves this null and keeps the one generated stage it always had.
	protected virtual Widget Aiming( Widget parent ) => null;

	// Which viewport the right-hand side is showing. The generated stage is the resting state, so a step that
	// stops aiming does not have to remember to say so.
	protected void Aim( bool art, string title, string note ) {
		if ( aiming is not null ) {
			aiming.Visible = art;
		}

		Stage.Visible = !art || aiming is null;
		views.Visible = Stage.Visible;

		caption.Text = title;
		advice.Text = note;
	}

	Widget Viewport( ArchKit kit, string title, string note ) {
		var column = new Widget( this ) { Layout = Layout.Column() };
		column.Layout.Margin = 0;
		column.Layout.Spacing = 0;

		var heading = column.Layout.AddColumn();
		heading.Margin = 12;
		heading.Spacing = 2;

		caption = new Label( title );
		caption.SetStyles( "font-size: 15px; font-weight: 700;" );
		heading.Add( caption );

		advice = new Label( note ) { WordWrap = true };
		heading.Add( advice );

		Stage = new ArchDesignerStage( column, kit, Staged );
		column.Layout.Add( Stage, 1 );

		aiming = Aiming( column );

		if ( aiming is not null ) {
			aiming.Visible = false;
			column.Layout.Add( aiming, 1 );
		}

		views = new Widget( column ) { Layout = Layout.Row() };
		views.Layout.Margin = 8;
		views.Layout.Spacing = 6;
		views.Layout.Add( new Button( "Front", "crop_landscape" ) { Clicked = Stage.Front } );
		views.Layout.Add( new Button( "Inside", "flip" ) { Clicked = Stage.Inside } );
		views.Layout.Add( new Button( "Perspective", "view_in_ar" ) { Clicked = Stage.Perspective } );
		views.Layout.AddStretchCell();

		column.Layout.Add( views );

		return column;
	}
}