Editor/Tool/ArchSubtool.cs
using System;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public abstract class ArchSubtool : EditorTool {
	ToolSidebarWidget sidebar;

	protected ArchSubtool( ArchTool owner ) {
		Owner = owner;
	}

	protected ArchTool Owner { get; }

	static readonly ArchViewAxis[] PlanOnly = { ArchViewAxis.Top };

	ArchViewAxis served = (ArchViewAxis)(-1);

	protected bool Dragging { get; private set; }
	protected Vector2 DragStart { get; private set; }
	protected Vector2 DragCurrent { get; private set; }
	protected float DragStartHeight { get; private set; }
	protected float DragHeight { get; private set; }

	protected ArchRoofPart DragDeck { get; private set; }

	protected ArchCursor Pointer { get; private set; }

	protected virtual bool UsesDrag => true;

	protected virtual bool Snapped => ArchGridService.Snapping;

	public virtual ArchSnapReach SnapReach( float authored ) => ArchWallSnap.Reach( authored );

	public virtual ArchViewAxis[] Works => PlanOnly;

	public bool Serves( ArchViewAxis axis ) => Works.Contains( axis );

	public virtual bool Placing => Manager?.IsCurrentViewFocused == true;

	public virtual ArchSurface[] Surfaces => Array.Empty<ArchSurface>();

	public string Icon => ArchIcons.Get( ArchIcons.SubtoolSlug( this ), EditorTypeLibrary.GetType( GetType() )?.Icon ?? "category" );

	bool Gesturing => Dragging || Run().Active;

	public override void OnUpdate() {
		activeDraft?.Show( Gesturing );

		if ( Manager?.IsCurrentViewFocused != true ) {
			return;
		}

		var axis = Owner.Axis == ArchViewAxis.Free ? ArchViewAxis.Top : Owner.Axis;

		if ( axis != served ) {
			served = axis;
			Refresh();
		}

		if ( Serves( axis ) && Adjusting() ) {
			return;
		}

		if ( TakesFreeClick ) {
			using ( ArchGhost.Begin() ) {
				DrawFreeHover();
			}

			if ( Gizmo.WasLeftMousePressed ) {
				OnFreeClick();
			}

			return;
		}

		if ( !Serves( axis ) || !Owner.Cursor( out var cursor ) ) {
			Dragging &= !Gizmo.WasLeftMouseReleased;

			if ( !Dragging ) {
				Owner.StepOff();
			}

			return;
		}

		var point = Snapped ? cursor.Plan : cursor.Free;

		Pointer = cursor;
		DragCurrent = point;
		DragHeight = cursor.Height;

		if ( !UsesDrag ) {
			using ( ArchGhost.Begin() ) {
				DrawHover( point );
			}

			if ( Gizmo.WasLeftMousePressed ) {
				Owner.EnsureTarget();
				OnClick( point );
			}

			return;
		}

		if ( Gizmo.WasLeftMousePressed ) {
			DragStart = point;
			DragStartHeight = cursor.Height;
			DragDeck = cursor.OnDeck;
			Dragging = true;

			// Lock the work plane to where the gesture began.
			Owner.StandOn( cursor.Height, cursor.OnDeck );

			return;
		}

		using ( ArchGhost.Begin() ) {
			if ( Dragging ) {
				DrawPreview();
			} else {
				DrawHover( point );
			}
		}

		if ( !Gizmo.WasLeftMouseReleased || !Dragging ) {
			return;
		}

		Dragging = false;
		drawn = null;

		Owner.StepOff();
		Owner.EnsureTarget();
		OnDrag( DragStart, point );
	}

	protected virtual bool Adjusts => false;

	protected virtual bool Adjusting() => Adjust();

	bool adjusting;

	object drawn;

	// Suppresses handle display on the just-placed shape until another gesture completes.
	protected void Drew( object placed ) => drawn = placed;

	internal void Forget() => drawn = null;

	bool Adjust() {
		// Reset on mouse-up (spare the release frame so the commit below fires).
		if ( !Gizmo.IsLeftMouseDown && !Gizmo.WasLeftMouseReleased ) {
			adjusting = false;
		}

		if ( !Adjusts || Owner.Picked is not { } picked || !ArchShapeHandles.ShowsHandles( picked.Item, drawn ) ) {
			return false;
		}

		using ( ArchGhost.Begin() ) {
			if ( ArchHandles.Draw( Owner, picked ) ) {
				adjusting = true;
			}

			if ( adjusting ) {
				ArchEditGhost.Draw( Owner, picked );
			}
		}

		if ( !ArchShapeHandles.CapturesPlacement( ArchShapeHandles.HandlePressed, adjusting ) ) {
			return false;
		}

		if ( Gizmo.WasLeftMouseReleased ) {
			adjusting = false;
			Owner.CommitEdited( $"Edit {picked.Describe()}", picked );
		}

		return true;
	}

	protected virtual void OnDrag( Vector2 from, Vector2 to ) { }

	protected virtual void OnClick( Vector2 point ) { }

	protected virtual bool TakesFreeClick => false;

	protected virtual void OnFreeClick() { }

	protected virtual void DrawFreeHover() { }

	protected virtual void DrawHover( Vector2 point ) {
		ArchGhost.Cursor( point, DragHeight, Owner.Kit.GridSize );
	}

	protected virtual void DrawPreview() {
		ArchGhost.Cursor( DragCurrent, DragHeight, Owner.Kit.GridSize );

		Gizmo.Draw.Line(
			new Vector3( DragStart.x, DragStart.y, DragStartHeight ),
			new Vector3( DragCurrent.x, DragCurrent.y, DragHeight ) );
	}

	protected void DrawRectPreview() {
		ArchGhost.Plate( Min( DragStart, DragCurrent ), Max( DragStart, DragCurrent ), Owner.LevelHeight );
	}

	protected static Vector2 Min( Vector2 a, Vector2 b ) => new( MathF.Min( a.x, b.x ), MathF.Min( a.y, b.y ) );

	protected static Vector2 Max( Vector2 a, Vector2 b ) => new( MathF.Max( a.x, b.x ), MathF.Max( a.y, b.y ) );

	public override Widget CreateToolSidebar() {
		lent = null;

		sidebar = new ToolSidebarWidget();
		Populate();

		return sidebar;
	}

	public override Widget CreateToolFooter() => new ArchRunBar( Run, FinishRun, CancelRun );

	protected virtual ArchRunState Run() => default;

	protected virtual void FinishRun() { }

	protected virtual void CancelRun() { }

	protected virtual string Title() => "Options";

	protected virtual string Shortcut() => null;

	protected string Scope( string key ) => ArchSidebarState.Scope( this, key );

	protected virtual ArchKind? DraftKind => null;

	ArchDraft activeDraft;

	protected ArchLayerRef? PlacementParent() {
		if ( Owner.InsertionTarget is { } target ) {
			return target;
		}

		return Owner.LayerTree.Find( Owner.ActiveBuildingId )?.Ref;
	}

	public override void OnEnabled() {
		base.OnEnabled();

		LetGo();
		BeginDraft();
	}

	// Deselect anything this tool can't author, so its handles don't capture placement drags.
	void LetGo() {
		if ( Keeps || Owner.Picked?.Item is not { } item || Hosts( item ) ) {
			return;
		}

		Owner.Picked = null;

		Forget();
	}

	protected virtual bool Keeps => false;

	protected void BeginDraft() {
		if ( DraftKind is { } kind ) {
			activeDraft ??= Owner.BeginPlacement( kind, PlacementParent(), null, null, this );
		}
	}

	public override void OnDisabled() {
		Owner.StepOff();
		CancelDraft();
		base.OnDisabled();
	}

	protected void CancelDraft() {
		if ( activeDraft is not null ) {
			Owner.CancelPlacement( activeDraft );
			activeDraft = null;
		}
	}

	protected void FinishDraft() => FinishDraft( 0 );

	protected void FinishDraft( int itemId ) {
		if ( activeDraft is not null ) {
			Owner.FinishPlacement( activeDraft, itemId );
			activeDraft = null;
		}
	}

	protected virtual string Advice() => null;

	protected virtual void BuildOptions( ToolSidebarWidget panel ) { }

	public virtual bool Turns() => false;

	public virtual bool Adopts => false;

	public virtual bool Hosts( object payload ) {
		if ( !Adopts || payload is null ) {
			return false;
		}

		return ArchKinds.Load().Claiming( payload )?.Kind is { } claimed && ReferenceEquals( Authoring( claimed ), this );
	}

	public void BuildAdopted( ToolSidebarWidget panel, ArchSelection picked, Action refresh ) {
		lent = refresh;

		Adopt( picked );
		BuildOptions( panel );
	}

	Action lent;

	protected bool Lent => lent is not null;

	protected virtual void Adopt( ArchSelection picked ) { }

	public ArchSubtool Authoring( ArchKind kind ) {
		var wanted = ArchKindsAsked.Subtool( kind );

		if ( wanted.Length == 0 ) {
			return null;
		}

		return Owner.Tools.OfType<ArchSubtool>().FirstOrDefault( tool => tool.GetType().Name == wanted );
	}

	protected void Refresh() {
		if ( lent is not null ) {
			lent();

			return;
		}

		if ( !sidebar.IsValid() ) {
			return;
		}

		sidebar.Layout.Clear( true );
		Populate();
	}

	public void RefreshSidebar() => Refresh();

	void Populate() {
		var serves = Serves( Owner.Axis == ArchViewAxis.Free ? ArchViewAxis.Top : Owner.Axis );

		ArchSidebarLayout.Header( sidebar, Title(), Icon, Shortcut(), serves ? Advice() : Refusal(), Guide() );

		if ( !serves ) {
			sidebar.Layout.AddStretchCell();
			return;
		}

		ArchSidebarLayout.Editing( sidebar, EditedLayer(), Edits() ? "New" : "Clear", StopEditing );

		BuildOptions( sidebar );

		sidebar.Layout.AddStretchCell();
	}

	bool Edits() => Owner.Picked?.Item is { } item && Hosts( item );

	string EditedLayer() {
		if ( Lent || Owner.Picked?.Item is not { } item ) {
			return null;
		}

		var name = item is IArchNamed { Name.Length: > 0 } named
			? named.Name
			: ArchKinds.Load().Claiming( item )?.Label;

		if ( string.IsNullOrWhiteSpace( name ) ) {
			return null;
		}

		return Edits() ? $"Editing {name}" : $"Selected {name}";
	}

	void StopEditing() {
		Owner.Picked = null;

		Forget();
		Refresh();
	}

	Action Guide() {
		if ( ArchGuides.For( this ) is not { } written ) {
			return null;
		}

		return () => ArchGuideWindow.Open( written );
	}

	string Refusal() {
		var views = string.Join( ", ", Works.Select( axis => axis == ArchViewAxis.Top ? "the plan" : $"the {axis} elevation" ) );

		return $"Nothing to place from here — this tool works in {views}.";
	}

	protected static Widget Wrapped( string text ) => ArchPartUi.Wrapped( text );

}

public static class ArchPaletteUi {
	public static Widget Row( Widget parent, ArchPalette palette, ArchSurface surface, Action changed ) {
		var holder = new Widget( parent );
		holder.Layout = Layout.Row();
		holder.Layout.Spacing = 4;

		var label = new Label( surface.ToString() );
		label.MinimumWidth = 92;
		holder.Layout.Add( label );

		palette.TryGet( surface, out var path );

		var value = new Label( string.IsNullOrWhiteSpace( path ) ? "(inherited)" : System.IO.Path.GetFileNameWithoutExtension( path ) );
		value.MinimumWidth = 110;
		holder.Layout.Add( value );

		holder.Layout.Add( new Button( "", "image" ) {
			Clicked = () => {
				var picker = AssetPicker.Create( parent, AssetType.Material );
				picker.Window.Title = $"Material for {surface}";
				picker.OnAssetPicked = assets => {
					var asset = assets.FirstOrDefault();
					if ( asset is null ) return;

					palette.Set( surface, asset.Path );
					ArchStyle.InvalidateCache();
					changed?.Invoke();
				};
				picker.Show();
			}
		} );

		var scale = palette.ScaleFor( surface );
		var density = new LineEdit( scale > 0f ? scale.ToString( "0.####" ) : "" ) {
			PlaceholderText = ArchMesh.TexelScale.ToString( "0.####" ),
			MaximumWidth = 64,
			ToolTip = "Units per texel for this role. Blank inherits."
		};

		density.TextEdited += text => {
			palette.SetScale( surface, float.TryParse( text, out var parsed ) ? parsed : 0f );
			ArchStyle.InvalidateCache();
			changed?.Invoke();
		};

		holder.Layout.Add( density );

		holder.Layout.Add( new Button( "", "close" ) {
			Clicked = () => {
				palette.Set( surface, null );
				density.Text = "";
				ArchStyle.InvalidateCache();
				changed?.Invoke();
			}
		} );

		holder.Layout.AddStretchCell();

		return holder;
	}
}