Editor/Tool/ArchLayersDockPanel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
using static Editor.BaseItemWidget;

namespace Sunless.Architecture;

[Dock( "Editor", "Plan Layers", "layers" )]
public sealed class ArchLayersDockPanel : Widget {
	public const string DockName = "Plan Layers";

	readonly ArchLayerStack stack;
	readonly Layout header;
	readonly Label breadcrumb;
	readonly List<(IArchDockToggle Toggle, IconButton Button)> contributed = new();

	ArchLayerTree projected = new();
	HashSet<int> built = new();

	bool pickedInStack;

	bool trackedActive;
	bool trackedGeometry;
	ArchPlan trackedPlan;
	int trackedRevision;
	int trackedDrafts;
	int trackedAssemblies;
	object trackedPicked;

	public ArchLayersDockPanel( Widget parent ) : base( parent ) {
		Layout = Layout.Column();
		Layout.Margin = 4;
		Layout.Spacing = 4;

		header = Layout.AddRow();
		header.Spacing = 2;

		breadcrumb = new Label( "Nothing picked" );
		breadcrumb.WordWrap = true;
		breadcrumb.MinimumHeight = 16;
		Layout.Add( breadcrumb );

		stack = new ArchLayerStack( this );
		stack.ItemClicked += OnItemClicked;
		Layout.Add( stack, 1 );

		BuildHeader();
	}

	public static void Open() {
		EditorWindow.DockManager.SetDockState( DockName, true );
		EditorWindow.DockManager.RaiseDock( DockName );
	}

	public ArchLayerTree Projected => projected;

	[EditorEvent.Frame]
	void OnFrame() {
		var tool = ArchTool.Active;
		var plan = tool?.Plan;
		var picked = tool?.Picked?.Item;
		var drafts = tool?.Drafts.Count( draft => draft.Standing ) ?? 0;
		var assemblies = plan?.Assemblies.Count ?? 0;

		var planChanged = (tool is not null) != trackedActive
			|| ArchTool.ShowGeometry != trackedGeometry
			|| !ReferenceEquals( plan, trackedPlan )
			|| (tool?.Revision ?? 0) != trackedRevision
			|| drafts != trackedDrafts
			|| assemblies != trackedAssemblies;

		trackedActive = tool is not null;
		trackedGeometry = ArchTool.ShowGeometry;
		trackedPlan = plan;
		trackedRevision = tool?.Revision ?? 0;
		trackedDrafts = drafts;
		trackedAssemblies = assemblies;

		if ( planChanged ) {
			Rebuild();
		}

		SyncContributed();

		if ( tool?.Flashing == true ) {
			stack.Update();
		}

		var inStack = pickedInStack;

		pickedInStack = false;

		SyncFaces( tool, inStack );

		if ( ReferenceEquals( picked, trackedPicked ) ) {
			return;
		}

		trackedPicked = picked;

		if ( inStack ) {
			UpdateBreadcrumb( projected.Find( picked ) );

			return;
		}

		SyncSelection();
	}

	// The tool owns the pick — the stack writes it only on a stack click, never reads it back.
	void SyncFaces( ArchTool tool, bool inStack ) {
		if ( tool is null ) {
			return;
		}

		if ( inStack ) {
			tool.SelectFaces( Wanted() );

			return;
		}

		if ( Same( tool.DebugFaces, Wanted() ) ) {
			return;
		}

		Reveal( tool );
	}

	List<ArchFaceDetail> Wanted() {
		var picked = stack.SelectedItems.ToList();

		return picked
			.OfType<ArchFacePiece>()
			.SelectMany( piece => piece.Faces )
			.Concat( picked.OfType<ArchFaceGroup>().SelectMany( group => group.Every ) )
			.Concat( picked.OfType<ArchFaceDetail>() )
			.Distinct()
			.ToList();
	}

	static bool Same( IReadOnlyList<ArchFaceDetail> left, IReadOnlyList<ArchFaceDetail> right ) {
		return left.Count == right.Count && left.All( right.Contains );
	}

	bool Reveal( ArchTool tool ) {
		if ( !ArchTool.ShowGeometry ) {
			return false;
		}

		if ( tool.DebugFaces.Count == 0 ) {
			stack.UnselectAll();

			return false;
		}

		foreach ( var face in tool.DebugFaces ) {
			if ( projected.Find( face.LayerId ) is not { } node ) {
				continue;
			}

			for ( var ancestor = node; ancestor is not null; ancestor = ancestor.Parent ) {
				stack.Open( projected.StableKey( ancestor ) );
			}

			stack.Open( (face.LayerId, "geometry") );

			foreach ( var piece in tool.BuiltFaces( face.LayerId ).Where( piece => piece.Faces.Contains( face ) ) ) {
				stack.Open( piece );

				foreach ( var group in Holding( piece.Groups, face ) ) {
					stack.Open( group );
				}
			}
		}

		stack.SelectItems( tool.DebugFaces, false );
		stack.ScrollTo( tool.DebugFaces[^1] );

		return true;
	}

	static IEnumerable<ArchFaceGroup> Holding( IEnumerable<ArchFaceGroup> groups, ArchFaceDetail face ) {
		foreach ( var group in groups.Where( group => group.Holds( face ) ) ) {
			yield return group;

			foreach ( var child in Holding( group.Children, face ) ) {
				yield return child;
			}
		}
	}

	void BuildHeader() {
		header.Clear( true );

		header.Add( Tool( "add", "Add a layer inside the insertion target", AddMenu ) );
		header.Add( Tool( "create_new_folder", "Group the selected layer - a group is the scope an operation inside it may reach", GroupPicked ) );
		header.Add( Tool( "folder_off", "Dissolve the selected group; its members keep their own homes", UngroupPicked ) );
		header.Add( Tool( "visibility", "Show every layer again", () => ArchTool.Active?.ShowEveryLayer() ) );

		var isolate = new IconButton( "filter_center_focus" ) {
			ToolTip = "Fold away the groups the current edit is not happening inside",
			IconSize = 15,
			FixedSize = 22,
			IsToggle = true,
			IsActive = ArchTool.IsolateGroups
		};

		isolate.OnToggled = value => {
			ArchTool.IsolateGroups = value;
			stack.Update();
		};

		header.Add( isolate );

		var geometry = new IconButton( "widgets" ) {
			ToolTip = "Show what each layer BUILT - its pieces and their faces, for picking one and copying its debug info",
			IconSize = 15,
			FixedSize = 22,
			IsToggle = true,
			IsActive = ArchTool.ShowGeometry
		};

		geometry.OnToggled = value => {
			ArchTool.ShowGeometry = value;

			if ( !value ) {
				ArchTool.Active?.SelectFaces( null );
			}

			Rebuild();
		};

		header.Add( geometry );

		contributed.Clear();

		foreach ( var toggle in ArchDockToggles.Contributed() ) {
			var button = Contributed( toggle );

			contributed.Add( (toggle, button) );
			header.Add( button );
		}

		header.AddStretchCell();
	}

	static IconButton Tool( string icon, string tooltip, Action clicked ) {
		var button = new IconButton( icon ) { ToolTip = tooltip, IconSize = 15, FixedSize = 22 };

		button.OnClick = () => clicked();

		return button;
	}

	void SyncContributed() {
		var standing = ArchDockToggles.Contributed();

		if ( standing.Length != contributed.Count ) {
			BuildHeader();

			return;
		}

		foreach ( var (toggle, button) in contributed ) {
			var held = ArchDockToggles.Held( toggle );

			if ( button.IsActive == held ) {
				continue;
			}

			button.IsActive = held;
			button.Update();
		}
	}

	static IconButton Contributed( IArchDockToggle toggle ) {
		var button = new IconButton( toggle.Icon ) {
			ToolTip = toggle.ToolTip,
			IconSize = 15,
			FixedSize = 22,
			IsToggle = true,
			IsActive = ArchDockToggles.Held( toggle )
		};

		button.OnToggled = value => ArchDockToggles.Press( toggle, value );

		return button;
	}

	void Rebuild() {
		projected = ArchTool.Active?.LayerTree ?? new ArchLayerTree();
		built = ArchTool.Active is { } tool ? tool.BuiltMeshes.Keys.ToHashSet() : new HashSet<int>();

		stack.UnselectAll();
		stack.Clear();

		foreach ( var group in projected.Domains ) {
			var domain = new ArchDomainRow( group.Name, group.Domain );
			stack.AddItem( domain );

			foreach ( var child in group.Children ) {
				domain.AddItem( RowFor( child ) );
			}

			foreach ( var draft in RootDrafts( group.Domain ) ) {
				domain.AddItem( new ArchDraftRow( draft ) );
			}
		}

		trackedPicked = null;
	}

	ArchLayerRow RowFor( ArchLayerNode node ) {
		var row = new ArchLayerRow( node, projected.StableKey( node ) );

		foreach ( var child in node.Children ) {
			row.AddItem( RowFor( child ) );
		}

		foreach ( var draft in DraftsUnder( node.Ref?.ItemId ?? 0 ) ) {
			row.AddItem( new ArchDraftRow( draft ) );
		}

		foreach ( var piece in ArchTool.Active?.PiecesOf( node.Ref?.ItemId ?? 0 ) ?? Array.Empty<ArchBuiltPiece>() ) {
			row.AddItem( new ArchPieceRow( piece ) );
		}

		if ( ArchTool.ShowGeometry && node.Ref is { } layer && built.Contains( layer.ItemId ) ) {
			row.AddItem( new ArchGeometryHeadRow( layer.ItemId ) );
		}

		return row;
	}

	static IEnumerable<ArchDraft> DraftsUnder( int parentId ) {
		return ArchTool.Active?.Drafts
			.Where( draft => draft.Active && draft.Standing && (draft.Parent?.ItemId ?? 0) == parentId )
			?? Enumerable.Empty<ArchDraft>();
	}

	static IEnumerable<ArchDraft> RootDrafts( ArchLayerDomain domain ) {
		var kinds = ArchKinds.Load();

		return ArchTool.Active?.Drafts
			.Where( draft => draft.Active && draft.Standing
				&& (draft.Parent is null || draft.Parent.Value.ItemId == 0)
				&& kinds.Domain( draft.Kind ) == domain )
			?? Enumerable.Empty<ArchDraft>();
	}

	void OnItemClicked( object obj ) {
		pickedInStack = true;

		var modifiers = Editor.Application.KeyboardModifiers;
		var extending = modifiers.HasFlag( KeyboardModifiers.Shift ) || modifiers.HasFlag( KeyboardModifiers.Ctrl );

		if ( !extending && obj is not (ArchGeometryFaceRow or ArchGeometryGroupRow or ArchGeometryPieceRow) ) {
			stack.UnselectAll();
			ArchTool.Active?.SelectFaces( null );
		}

		if ( obj is ArchLayerRow row ) {
			ApplySelection( row.Node );
		}
	}

	void ApplySelection( ArchLayerNode node ) {
		if ( ArchTool.Active is not { } tool ) {
			return;
		}

		if ( node.Payload is ArchLayerReference reference ) {
			if ( projected.Find( reference.TargetId ) is { } target ) {
				tool.SelectLayer( target );
			}

			return;
		}

		if ( node.Payload is null ) {
			if ( node.Kind == ArchKind.Story && node.Building is { } storyBuilding ) {
				tool.Level = node.Floor;
				tool.ActiveBuildingId = storyBuilding.Id;
			}

			return;
		}

		tool.SelectLayer( node );
	}

	void SyncSelection() {
		var node = projected.Find( ArchTool.Active?.Picked?.Item );

		UpdateBreadcrumb( node );

		if ( ArchTool.Active?.DebugFaces.Count > 0 ) {
			return;
		}

		stack.UnselectAll();

		if ( node is not null ) {
			stack.SelectItem( projected.StableKey( node ) );
		}
	}

	void UpdateBreadcrumb( ArchLayerNode selected ) {
		var tool = ArchTool.Active;

		if ( tool is null ) {
			breadcrumb.Text = "No Architecture tool active";
			return;
		}

		var target = tool.InsertionTarget is { } insertion && projected.Find( insertion.ItemId ) is { } targetNode
			? targetNode.DisplayName
			: "—";

		breadcrumb.Text = selected is null
			? $"Nothing picked · into {target}"
			: $"{projected.Breadcrumb( selected )} · into {target}";
	}

	void AddMenu() {
		if ( ArchTool.Active is not { } tool ) {
			return;
		}

		var kinds = ArchKinds.Load();
		var parent = tool.InsertionTarget ?? tool.SelectedLayer;
		var menu = new ContextMenu( this );

		foreach ( var kind in Offered( kinds, parent ) ) {
			var captured = kind;

			menu.AddOption( kinds.Label( captured ), kinds.Glyph( captured ),
				() => tool.BeginChild( parent ?? default, captured ) );
		}

		menu.OpenAtCursor();
	}

	static IReadOnlyList<ArchKind> Offered( ArchKinds kinds, ArchLayerRef? parent ) {
		var offered = parent is { } layer
			? ArchLayerRules.ValidChildren( layer.Kind )
			: kinds.RootKinds();

		return offered.Where( kind => kinds.Subtool( kind ).Length > 0 ).ToList();
	}

	void GroupPicked() {
		if ( ArchTool.Active is not { } tool ) {
			return;
		}

		var members = stack.SelectedItems
			.Select( item => projected.Find( item ) )
			.Where( node => node?.Ref is not null )
			.Select( node => node.Ref.Value.ItemId )
			.ToList();

		if ( members.Count == 0 && tool.SelectedLayer is { } layer ) {
			members.Add( layer.ItemId );
		}

		tool.GroupLayers( members );
	}

	void UngroupPicked() {
		if ( ArchTool.Active is not { } tool ) {
			return;
		}

		var group = tool.Picked?.Item as ArchSiteAssembly
			?? ArchLayerGroups.Holding( tool.Plan, tool.SelectedLayer?.ItemId ?? 0 );

		tool.UngroupLayer( group );
	}
}

// The stack itself. Rows carry their own controls, so a press has to say whether it landed on the
// eye, the lock or the row before the tree decides it was a selection.
sealed class ArchLayerStack : TreeView {
	public const float ControlWidth = 22f;

	public ArchLayerStack( Widget parent ) : base( parent ) {
		MultiSelect = true;
	}

	// A badge press on a shift-picked run of rows applies to all of them.
	public List<ArchLayerRow> Picked( ArchLayerRow clicked ) {
		var rows = Selection
			.Select( entry => ResolveNode( entry, false ) )
			.OfType<ArchLayerRow>()
			.Where( row => row.Node.Ref is not null )
			.ToList();

		return rows.Count > 1 && rows.Contains( clicked ) ? rows : new List<ArchLayerRow> { clicked };
	}

	protected override bool OnItemPressed( VirtualWidget item, MouseEvent e ) {
		if ( item.Object is ArchPieceRow piece && ArchTool.Active is { } owner ) {
			return Pressed( piece, owner, item, e );
		}

		if ( item.Object is not ArchLayerRow row || row.Node.Ref is not { } layer || ArchTool.Active is not { } tool ) {
			return base.OnItemPressed( item, e );
		}

		var local = e.LocalPosition.x - item.Rect.Left;
		var fromRight = item.Rect.Width - local;

		if ( fromRight <= ControlWidth ) {
			ArchLayerBulk.Lock( tool, Picked( row ), !row.Node.Locked, row.Node.Name );

			return false;
		}

		if ( fromRight <= ControlWidth * 2f ) {
			ArchLayerBulk.Hide( tool, Picked( row ), !tool.LayerHiddenSelf( layer.ItemId ) );
			Update();

			return false;
		}

		if ( fromRight <= ControlWidth * 3f && row.Node.Payload is IArchCollides ) {
			var menu = new ContextMenu( this );

			row.Collisions( menu, tool );
			menu.OpenAtCursor();

			return false;
		}

		if ( fromRight <= ControlWidth * 4f && row.Node.Ref is not null ) {
			var menu = new ContextMenu( this );

			row.Shadows( menu, tool );
			menu.OpenAtCursor();

			return false;
		}

		return base.OnItemPressed( item, e );
	}

	bool Pressed( ArchPieceRow row, ArchTool tool, VirtualWidget item, MouseEvent e ) {
		var fromRight = item.Rect.Width - (e.LocalPosition.x - item.Rect.Left);

		if ( fromRight <= ControlWidth ) {
			return false;
		}

		if ( fromRight <= ControlWidth * 2f ) {
			tool.TogglePieceHidden( row.Built.Owner, row.Built.Piece );
			Update();

			return false;
		}

		if ( fromRight <= ControlWidth * 3f ) {
			var menu = new ContextMenu( this );

			row.Collisions( menu, tool );
			menu.OpenAtCursor();

			return false;
		}

		if ( fromRight <= ControlWidth * 4f ) {
			var menu = new ContextMenu( this );

			row.Shadows( menu, tool );
			menu.OpenAtCursor();

			return false;
		}

		return base.OnItemPressed( item, e );
	}
}

// EVERY BULK EDIT PAYS FOR ONE COMMIT. A commit snapshots the plan, writes it to disk and rebuilds the scene, and
// the tool puts that rebuild two orders of magnitude above the edit itself - so sixteen rows done one at a time is
// sixteen freezes rather than one edit. Each row still gets Affect'd, through the list form: the single-id one
// clears what it holds, so calling it per row would flash the last row alone.
static class ArchLayerBulk {
	public static string Across( int rows ) => rows > 1 ? $" · {rows} layers" : "";

	static List<int> Ids( IEnumerable<ArchLayerRow> rows ) => rows.Select( row => row.ItemId ).ToList();

	static string Named( string verb, IReadOnlyList<ArchLayerRow> rows, string name ) {
		return rows.Count > 1 ? $"{verb} {rows.Count} layers" : $"{verb} {name}";
	}

	public static void Lock( ArchTool tool, IReadOnlyList<ArchLayerRow> rows, bool locked, string name ) {
		foreach ( var row in rows ) {
			tool.SetLayerLocked( row.Node.Ref.Value, locked );
		}

		tool.Affect( Ids( rows ) );
		tool.Bank( Named( locked ? "Lock" : "Unlock", rows, name ) );
	}

	public static void Hide( ArchTool tool, IReadOnlyList<ArchLayerRow> rows, bool hidden ) {
		foreach ( var row in rows ) {
			tool.SetLayerHidden( row.ItemId, hidden );
		}

		tool.ApplyVisibility();
	}

	public static void Enable( ArchTool tool, IReadOnlyList<ArchLayerRow> rows, bool enabled, string name ) {
		foreach ( var row in rows ) {
			tool.SetLayerEnabled( row.Node.Ref.Value, enabled );
		}

		tool.Affect( Ids( rows ) );
		tool.Touch( Named( enabled ? "Enable" : "Disable", rows, name ) );
	}

	public static void Leave( ArchTool tool, IReadOnlyList<ArchLayerRow> rows, string name ) {
		foreach ( var row in rows ) {
			ArchLayerGroups.Leave( tool.Plan, row.ItemId );
		}

		tool.Affect( Ids( rows ) );
		tool.Touch( rows.Count > 1 ? $"Take {rows.Count} layers out of their groups" : $"Take {name} out of its group" );
	}

	public static void Affect( ArchTool tool, IReadOnlyList<ArchLayerRow> rows ) {
		tool.Affect( Ids( rows ) );
	}
}

// One row per authored layer. Value is the stable identity - the payload itself for authored rows,
// a synthetic story key for virtual ones - so the tree keeps open and selected state across rebuilds.
sealed class ArchLayerRow : TreeNode {
	public ArchLayerNode Node { get; }

	public ArchLayerRow( ArchLayerNode node, object key ) : base( key ) {
		Node = node;
		Height = 22;

		// Must use the raw name, not DisplayName — SeparateIdentifier would destroy the authored casing.
		Name = node.Payload is IArchNamed named ? named.Name : node.DisplayName;
	}

	// Locked layers refuse rename — the name is part of every generated path the lock protects.
	public override bool CanEdit => !Node.Locked && Node.Payload is IArchNamed;

	public override void OnRename( VirtualWidget item, string text, List<TreeNode> selection = null ) {
		if ( ArchTool.Active is not { } tool || string.IsNullOrWhiteSpace( text ) ) {
			return;
		}

		if ( !ArchKinds.Load().TryRename( Node.Payload, text ) ) {
			return;
		}

		tool.Affect( ItemId );

		tool.Touch( $"Rename {text}" );
	}

	bool IsGroup => Node.Payload is ArchSiteAssembly;

	public int ItemId => Node.Ref?.ItemId ?? 0;

	public override void OnPaint( VirtualWidget item ) {
		var tool = ArchTool.Active;
		var kinds = ArchKinds.Load();
		var rect = item.Rect;

		PaintSelection( item );

		if ( IsGroup ) {
			Paint.ClearPen();
			Paint.SetBrush( Theme.Blue.WithAlpha( 0.12f ) );
			Paint.DrawRect( rect, 2f );
		}

		var flash = tool?.Flash( ItemId ) ?? 0f;

		if ( flash > 0f ) {
			Paint.ClearPen();
			Paint.SetBrush( Theme.Green.WithAlpha( 0.45f * flash ) );
			Paint.DrawRect( rect, 2f );
		}

		if ( tool?.InsertionTarget is { } target && target.ItemId == ItemId ) {
			Paint.ClearPen();
			Paint.SetBrush( Theme.Primary );
			Paint.DrawRect( new Rect( rect.Left, rect.Top, 3f, rect.Height ), 0 );
		}

		var muted = tool?.Muted( Node ) == true;
		var hidden = tool?.LayerHidden( ItemId ) == true;
		var alpha = muted || hidden ? 0.28f : Node.Enabled ? 1f : 0.45f;

		Paint.SetPen( Theme.Text.WithAlpha( alpha ) );
		Paint.DrawIcon( rect.Shrink( 4, 0, 0, 0 ), kinds.Glyph( Node.Kind ), 15, TextFlag.LeftCenter );

		var controls = ArchLayerStack.ControlWidth * 5f;
		var body = rect.Shrink( 24, 0, controls + 4f, 0 );

		Paint.DrawText( body, Node.DisplayName, TextFlag.LeftCenter );

		Paint.SetPen( Theme.Text.WithAlpha( 0.3f * alpha ) );
		Paint.DrawText( body, Node.Stage.ToString().ToLowerInvariant(), TextFlag.RightCenter );

		Collision( rect );
		Shadow( rect );
		Palette( rect, alpha );

		var own = tool?.LayerHiddenSelf( ItemId ) == true;

		Paint.SetPen( Theme.Text.WithAlpha( own ? 0.75f : 0.3f ) );
		Paint.DrawIcon( Control( rect, 1 ), hidden ? "visibility_off" : "visibility", 14, TextFlag.Center );

		Paint.SetPen( Node.Locked ? Theme.Yellow : Theme.Text.WithAlpha( 0.3f ) );
		Paint.DrawIcon( Control( rect, 0 ), Node.Locked ? "lock" : "lock_open", 14, TextFlag.Center );
	}

	void Palette( Rect rect, float alpha ) {
		if ( !ArchLayerPalettePicker.HasOverride( Node ) ) {
			return;
		}

		Paint.SetPen( Theme.Primary.WithAlpha( alpha ) );
		Paint.DrawIcon( Control( rect, 4 ), "palette", 14, TextFlag.Center );
	}

	void Shadow( Rect rect ) {
		if ( Node.Ref is null || ArchTool.Active is not { } tool ) {
			return;
		}

		var casts = ArchShadows.Showing( tool.Plan, tool.Kit, Node, out var own );

		Paint.SetPen( !own
			? Theme.Text.WithAlpha( 0.3f )
			: casts ? Theme.Blue : Theme.Yellow );

		Paint.DrawIcon( Control( rect, 3 ), ArchShadows.Glyph( casts ), 14, TextFlag.Center );
	}

	void Collision( Rect rect ) {
		if ( Node.Payload is not IArchCollides ) {
			return;
		}

		var mode = ArchCollision.Showing( Node, ArchTool.Active?.Kit, out var own );

		Paint.SetPen( !own
			? Theme.Text.WithAlpha( 0.3f )
			: mode == ArchCollisionMode.Complex ? Theme.Yellow : Theme.Blue );

		Paint.DrawIcon( Control( rect, 2 ), ArchCollision.Glyph( mode ), 14, TextFlag.Center );
	}

	static Rect Control( Rect row, int fromRight ) {
		var width = ArchLayerStack.ControlWidth;

		return new Rect( row.Right - width * (fromRight + 1), row.Top, width, row.Height );
	}

	public override string GetTooltip() {
		var problems = ArchTool.Active?.LayerTree.Problems( Node );

		return problems is { Count: > 0 }
			? problems[0]
			: $"{ArchKindsAsked.Label( Node.Kind )} · {Node.Stage} · id {ItemId}"
				+ (ArchLayerPalettePicker.HasOverride( Node ) ? " · palette override" : "");
	}

	public override void OnActivated() {
		if ( ArchTool.Active is { } tool && Node.Payload is not null ) {
			tool.SelectLayer( Node );
			tool.Frame();
		}
	}

	public override bool OnDragStart() {
		if ( Node.Payload is null ) {
			return false;
		}

		var drag = new Drag( TreeView );

		drag.Data.Object = Node;
		drag.Execute();

		return true;
	}

	public override DropAction OnDragDrop( ItemDragEvent e ) {
		if ( e.Data.Object is not ArchLayerNode moving || moving.Ref is not { } layer || ArchTool.Active is not { } tool ) {
			return DropAction.Ignore;
		}

		if ( ReferenceEquals( moving, Node ) || Descends( Node, moving ) ) {
			return DropAction.Ignore;
		}

		var beside = e.DropEdge.HasFlag( ItemEdge.Top ) || e.DropEdge.HasFlag( ItemEdge.Bottom );

		if ( beside ) {
			if ( !e.IsDrop ) {
				return DropAction.Move;
			}

			if ( tool.LayerTree.Reorder( tool.Plan, layer, ItemId, e.DropEdge.HasFlag( ItemEdge.Bottom ) ) ) {
				tool.Affect( layer.ItemId );
				tool.Commit( $"Reorder {moving.Name}" );
			}

			return DropAction.Move;
		}

		if ( !IsGroup && !ArchLayerRules.CanParent( Node.Kind, layer.Kind ).Allowed ) {
			return DropAction.Ignore;
		}

		if ( e.IsDrop && tool.LayerTree.Reparent( tool.Plan, layer, ItemId ) ) {
			tool.Affect( layer.ItemId );
			tool.Commit( $"Move {moving.Name} into {Node.Name}" );
		}

		return DropAction.Move;
	}

	static bool Descends( ArchLayerNode node, ArchLayerNode ancestor ) {
		for ( var current = node; current is not null; current = current.Parent ) {
			if ( ReferenceEquals( current, ancestor ) ) {
				return true;
			}
		}

		return false;
	}

	List<ArchLayerRow> Picked() => (TreeView as ArchLayerStack)?.Picked( this ) ?? new List<ArchLayerRow> { this };

	static string Across( int rows ) => ArchLayerBulk.Across( rows );

	public void Collisions( ContextMenu menu, ArchTool tool ) {
		var picked = Picked().Where( row => row.Node.Payload is IArchCollides ).ToList();

		if ( picked.Count == 0 ) {
			return;
		}

		var showing = ArchCollision.Showing( Node, tool.Kit, out _ );
		var collisions = menu.AddMenu( $"Collisions{Across( picked.Count )}", ArchCollision.Glyph( showing ) );

		var following = ArchCollision.Above( Node ) is { } parent
			? $"Follow the layer above — {ArchCollision.Describe( parent )}"
			: $"Follow the kit — {ArchCollision.Describe( tool.Kit?.Physics ?? ArchCollisionMode.Solids )}";

		collisions.AddOption( following, "settings", () => Collide( tool, picked, null ) );

		collisions.AddSeparator();

		foreach ( var mode in new[] { ArchCollisionMode.Solids, ArchCollisionMode.Convex, ArchCollisionMode.Complex, ArchCollisionMode.None } ) {
			var captured = mode;

			collisions.AddOption( ArchCollision.Describe( captured ), ArchCollision.Glyph( captured ),
				() => Collide( tool, picked, captured ) );
		}
	}

	void Collide( ArchTool tool, List<ArchLayerRow> rows, ArchCollisionMode? mode ) {
		foreach ( var row in rows ) {
			if ( row.Node.Payload is not IArchCollides host ) {
				continue;
			}

			host.Collision = mode;
		}

		ArchLayerBulk.Affect( tool, rows );
		tool.Touch( rows.Count > 1 ? $"Collisions on {rows.Count} layers" : $"Collisions {Node.Name}" );
	}

	public void Shadows( ContextMenu menu, ArchTool tool ) {
		if ( Node.Ref is null ) {
			return;
		}

		var picked = Picked();
		var showing = ArchShadows.Showing( tool.Plan, tool.Kit, Node, out _ );
		var shadows = menu.AddMenu( $"Shadows{Across( picked.Count )}", ArchShadows.Glyph( showing ) );

		shadows.AddOption( ArchShadows.Following( tool.Plan, tool.Kit, Node ), "settings", () => Cast( tool, picked, null ) );

		shadows.AddSeparator();

		foreach ( var casts in new[] { true, false } ) {
			var captured = casts;

			shadows.AddOption( ArchShadows.Describe( captured ), ArchShadows.Glyph( captured ),
				() => Cast( tool, picked, captured ) );
		}
	}

	void Cast( ArchTool tool, List<ArchLayerRow> rows, bool? casts ) {
		foreach ( var row in rows ) {
			ArchParts.Record( tool.Plan, row.ItemId, "" ).Shadows = casts;
		}

		tool.Plan.Parts.RemoveAll( record => record is { Collision: null, Shadows: null } );

		ArchLayerBulk.Affect( tool, rows );
		tool.Touch( rows.Count > 1 ? $"Shadows on {rows.Count} layers" : $"Shadows {Node.Name}" );
	}

	void Hollow( ArchTool tool, ArchLayerRef layer, IEnumerable<ArchRoom> rooms ) {
		var stripped = 0;

		foreach ( var room in rooms.ToList() ) {
			stripped += ArchFacade.Empty( room );
		}

		tool.Affect( layer.ItemId );
		tool.Commit( $"Convert {Node.Name} to Facade" );

		Log.Info( $"Architecture: {Node.Name} is a facade - {stripped} interior setting{(stripped == 1 ? "" : "s")} stripped." );
	}

	void Keep( ArchTool tool, ArchBuilding building ) {
		ArchNameDialog.Ask( TreeView, "Save Building",
			$"Saves {building.Name} and every layer standing in it, ready to stamp again from the Building tool's Saved mode.",
			building.Name, name => {
				if ( ArchLayerAssets.SaveBuilding( tool.Plan, building, name ) is not { } path ) {
					Log.Warning( $"Architecture: could not save '{name}'." );

					return;
				}

				Log.Info( $"Architecture: saved {name} to Assets/{path}." );
			} );
	}

	void Transform( ContextMenu menu, ArchTool tool ) {
		var transform = menu.AddMenu( "Transform", "open_with" );

		if ( Node.Payload is ArchBuilding building ) {
			transform.AddOption( "Move", "open_with", () => { tool.SelectLayer( Node ); tool.BeginCarry( building ); } );
		} else {
			Handle( transform, tool, "Move", "open_with", ArchHandleMode.Move );
		}

		Handle( transform, tool, "Rotate", "rotate_right", ArchHandleMode.Turn );
		Handle( transform, tool, "Resize", "aspect_ratio", ArchHandleMode.Scale );
	}

	void Handle( Menu menu, ArchTool tool, string label, string glyph, ArchHandleMode mode ) {
		menu.AddOption( label, glyph, () => {
			tool.SelectLayer( Node );
			ArchShapeHandles.Mode = mode;
		} );
	}

	public override bool OnContextMenu() {
		if ( ArchTool.Active is not { } tool || Node.Payload is null || Node.Ref is not { } layer ) {
			return false;
		}

		var menu = new ContextMenu( TreeView );
		var picked = Picked();

		var one = picked.Count == 1;

		if ( one ) {
			foreach ( var extension in ArchAddons.Load().Contributed<IArchLayerMenu>() ) {
				extension.Populate( menu, tool, Node );
			}
		}

		if ( one ) {
			menu.AddOption( "Set as insertion target", "playlist_add", () => tool.InsertionTarget = layer );
		}

		var enabling = !Node.Enabled;
		var locking = !Node.Locked;

		menu.AddOption( (enabling ? "Enable generation" : "Disable generation") + Across( picked.Count ),
			enabling ? "toggle_on" : "toggle_off",
			() => ArchLayerBulk.Enable( tool, picked, enabling, Node.Name ) );

		menu.AddOption( (locking ? "Lock" : "Unlock") + Across( picked.Count ),
			locking ? "lock" : "lock_open",
			() => ArchLayerBulk.Lock( tool, picked, locking, Node.Name ) );

		if ( one && ArchKinds.Load().Transforms( Node.Kind ) ) {
			Transform( menu, tool );
		}

		Collisions( menu, tool );
		Shadows( menu, tool );

		if ( one && ArchLayerPalettePicker.CanOpen( tool, Node ) ) {
			menu.AddOption( "Apply Palette", "palette", () => ArchLayerPalettePicker.Show( TreeView, tool, Node ) );
		}

		if ( one && Node.Payload is ArchBuilding building ) {
			menu.AddOption( "Toggle Building Layout", "architecture", () => ArchBuildingWindow.Toggle( tool, building ) );
			menu.AddOption( "Save", "inventory_2", () => Keep( tool, building ) );
			menu.AddOption( "Convert to Facade", "filter_1", () => Hollow( tool, layer, building.Rooms ) );
		}

		if ( one && Node.Payload is ArchRoom room ) {
			menu.AddOption( "Convert to Facade", "filter_1", () => Hollow( tool, layer, new[] { room } ) );
		}

		if ( one && Node.Payload is ArchCutPart { ResolvedDamage: ArchDamageKind.Masonry } cut ) {
			menu.AddOption( "Bake to Target", "fit_screen", () => {
				if ( !ArchDamage.BakeToTarget( tool.Plan, tool.EnsureKit(), cut ) ) {
					Log.Warning( "Architecture: damage zone does not overlap a wall corner or pillar." );
					return;
				}

				tool.Affect( layer.ItemId );
				tool.Commit( "Bake Damage to Target" );
			} );
		}

		menu.AddSeparator();

		if ( one && CanEdit ) {
			menu.AddOption( "Rename", "edit", () => {
				TreeView.SelectItem( Node.Payload );
				TreeView.BeginRename();
			} );
		}

		if ( one && IsGroup ) {
			menu.AddOption( "Ungroup", "folder_off", () => tool.UngroupLayer( (ArchSiteAssembly)Node.Payload ) );
		} else if ( !IsGroup ) {
			menu.AddOption( $"Group{Across( picked.Count )}", "create_new_folder",
				() => tool.GroupLayers( picked.Select( row => row.ItemId ).ToList() ) );

			var held = picked.Where( row => ArchLayerGroups.Holding( tool.Plan, row.ItemId ) is not null ).ToList();

			if ( held.Count > 0 ) {
				menu.AddOption( $"Take out of its group{Across( held.Count )}", "output",
					() => ArchLayerBulk.Leave( tool, held, Node.Name ) );
			}
		}

		menu.AddSeparator();

		if ( one ) {
			menu.AddOption( "Frame", "filter_center_focus", () => { tool.SelectLayer( Node ); tool.Frame(); } );
		}

		menu.AddOption( $"Delete{Across( picked.Count )}", "delete",
			() => tool.DeleteLayers( picked.Select( row => row.Node ).ToList() ) );

		menu.OpenAtCursor();

		return true;
	}
}

sealed class ArchDraftRow : TreeNode {
	public ArchDraft Draft { get; }

	public ArchDraftRow( ArchDraft draft ) : base( draft ) {
		Draft = draft;
		Height = 22;
	}

	public override void OnPaint( VirtualWidget item ) {
		var kinds = ArchKinds.Load();

		Paint.SetPen( Color.Cyan );
		Paint.DrawIcon( item.Rect.Shrink( 4, 0, 0, 0 ), kinds.Glyph( Draft.Kind ), 15, TextFlag.LeftCenter );
		Paint.DrawText( item.Rect.Shrink( 24, 0, 0, 0 ), $"{Draft.Name} — placing", TextFlag.LeftCenter );
	}
}

sealed class ArchDomainRow : TreeNode {
	readonly string label;
	readonly string glyph;

	public ArchDomainRow( string label, ArchLayerDomain domain ) : base( domain ) {
		this.label = label;
		glyph = domain switch {
			ArchLayerDomain.Buildings => "domain",
			ArchLayerDomain.Connections => "link",
			_ => "route"
		};
		Height = 24;
	}

	public override void OnPaint( VirtualWidget item ) {
		Paint.SetPen( Theme.Text.WithAlpha( 0.85f ) );
		Paint.DrawIcon( item.Rect.Shrink( 4, 0, 0, 0 ), glyph, 15, TextFlag.LeftCenter );
		Paint.DrawText( item.Rect.Shrink( 24, 0, 0, 0 ), label.ToUpperInvariant(), TextFlag.LeftCenter );
	}

	public override DropAction OnDragDrop( ItemDragEvent e ) {
		if ( e.Data.Object is not ArchLayerNode moving || moving.Ref is not { } layer || ArchTool.Active is not { } tool ) {
			return DropAction.Ignore;
		}

		if ( ArchLayerGroups.Holding( tool.Plan, layer.ItemId ) is null ) {
			return DropAction.Ignore;
		}

		if ( e.IsDrop ) {
			ArchLayerGroups.Leave( tool.Plan, layer.ItemId );
			tool.Affect( layer.ItemId );
			tool.Commit( $"Take {moving.Name} out of its group" );
		}

		return DropAction.Move;
	}
}