Editor/Tool/Subtools/ArchBoolSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

[Title( "Boolean Modifiers" ), Icon( "join_inner" ), Group( "11" )]
public sealed class ArchBoolSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	public override ArchSurface[] Surfaces => new[]
	{
		ArchSurface.Deck, ArchSurface.Foundation, ArchSurface.WallCap, ArchSurface.Pillar, ArchSurface.WallBase, ArchSurface.Trim,
		ArchSurface.Sign, ArchSurface.Railing, ArchSurface.Soffit
	};

	readonly ArchPlatformPart standing = new();
	readonly ArchCutPart sinking = new() {
		Damage = ArchDamageKind.Masonry,
		BreakEdges = true,
		DamageAmount = 1f,
		DamageCellWidth = 16f,
		DamageCellLength = 8f,
		DamageDepth = 6f,
		MasonryPattern = ArchMasonryPattern.ExposedEdge,
		MasonryCarves = false,
		Extrude = ArchExtrudeKind.Quoins,
		ExtrudeDepth = 4f,
		ExtrudeCourse = 16f,
		Affects = ArchDamage.Targets( ArchDamageKind.Masonry )
	};
	readonly ArchWallModPart detail = new() { Side = ArchWallSide.OnSurface };
	readonly ArchTrimPart edging = new() { Profile = "gutter", Surface = ArchSurface.Trim };
	readonly ArchBeamPart timber = new();

	ArchPillarPart column = new();
	ArchFixtureSubtool fixtures;

	ArchFixtureSubtool Fixtures => fixtures ??= new ArchFixtureSubtool( Owner, Refresh );

	ArchBoolOp op = ArchBoolOp.Add;

	ArchBoolOp zone = ArchBoolOp.Damage;

	ArchBoolAdd kind = ArchBoolAdd.Slab;
	ArchBoolReach aim = ArchBoolReach.Dragged;
	ArchBoolForm form = ArchBoolForm.Box;
	ArchCursorFace face = ArchCursorFace.Floor;

	int segments = ArchFootprint.LeastSegments;
	float rise = 24f;
	float recess = 4f;
	float opening = 96f;
	bool wallFace;
	bool edged;

	protected override ArchKind? DraftKind => op switch {
		ArchBoolOp.Add => kind switch {
			ArchBoolAdd.Slab or ArchBoolAdd.Ramp => ArchKind.Platform,
			ArchBoolAdd.Pillar => ArchKind.Pillar,
			_ => ArchKind.Beam
		},
		ArchBoolOp.Subtract or ArchBoolOp.Damage or ArchBoolOp.Extrude => ArchKind.Cut,
		ArchBoolOp.Fixture => Fixtures.MergedDraftKind,
		_ => ArchKind.WallMod
	};

	public override bool Adopts => true;

	public override bool Hosts( object payload ) {
		return payload is ArchPillarPart or ArchWallModPart || base.Hosts( payload );
	}

	protected override void Adopt( ArchSelection picked ) {
		if ( picked?.Item is ArchLadderPart or ArchBalconyPart or ArchExteriorStairPart ) {
			op = ArchBoolOp.Fixture;
			Fixtures.MergedAdopt( picked );
		}
	}

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

		ArchWorkflow.Restart( Scope( "flow" ) );
		Fixtures.MergedEnable();
	}

	ArchPillarType Pillar => Owner.FindPillarType( column.Type ) ?? Owner.DefaultPillar();

	bool Hangs => op == ArchBoolOp.Add && kind is ArchBoolAdd.Beam or ArchBoolAdd.Slats;

	// Slabs/ramps generate at ground level; everything else rides the building's lift.
	bool Lifted => op != ArchBoolOp.Add || kind is not (ArchBoolAdd.Slab or ArchBoolAdd.Ramp);

	protected override bool Adjusts => true;

	protected override string Title() => "Boolean Modifiers";

	protected override string Advice() => op switch {
		ArchBoolOp.Add => kind switch {
			ArchBoolAdd.Slab => "Drag a box or a circle on open ground — it stands as a solid.",
			ArchBoolAdd.Ramp => "Drag from the foot to the head — the deck climbs the way you dragged, from the ground up to the rise.",
			ArchBoolAdd.Pillar => "Drag the column's footprint — it stands from this storey's floor, dressed by the pillar type.",
			ArchBoolAdd.Beam => "Drag the member — it hangs under the ceiling of the room you drew it in.",
			_ => "Drag the zone — it fills with members under the ceiling of the room you drew it in."
		},
		ArchBoolOp.Subtract when sinking.Ramp
			=> "Drag from the foot to the head — the floor it leaves behind climbs the way you dragged, so what is left under it is a ramp.",

		ArchBoolOp.Subtract => Reach() switch {
			ArchBoolReach.IntoWall => "Drag across a wall to open only that wall and its earlier fittings between the sill and head.",
			ArchBoolReach.IntoCeiling => "Drag a box or a circle — it is taken up into the ceiling body only, so a shallow one leaves a coffer.",
			ArchBoolReach.IntoFloor => "Drag a box or a circle — it is sunk into the floor slab only, so a shallow one leaves a tray.",
			_ => "Drag a box or a circle — it opens this storey's floor and everything standing over it, out through the roof. Pull the top face down for a pit."
		},
		ArchBoolOp.Damage => $"Drag a damage zone, then choose which supported parts receive its {sinking.Damage.ToString().ToLowerInvariant()} response.",
		ArchBoolOp.Extrude => "Drag over a corner, the middle of an elevation or its head — every wall face the zone covers stands proud, parting round the openings.",
		ArchBoolOp.Fixture => Fixtures.MergedAdvice,
		_ => detail.Kind.Plated()
			? "Drag through a wall — every wall the box covers takes a plate at its head on the face you came from."
			: "Drag through a wall — every wall the box covers takes a block on the face you came from."
	};

	ArchBoolReach Reach() => ArchBool.Aimed( aim, face, wallFace );

	void Aiming() {
		if ( Owner.Cursor( out var cursor ) ) {
			face = cursor.Face;
			wallFace = cursor.SnappedTo is not null;

			if ( Dragging && Owner.NearestWall( DragStart, out _, out _, out var distance ) is not null && distance < 0.5f ) {
				wallFace = true;
			}
		}
	}

	ArchBoolDrag Sketch( Vector2 from, Vector2 to ) => Sketch( from, to, Host( from, to ) );

	protected override void DrawHover( Vector2 point ) {
		if ( op == ArchBoolOp.Fixture ) {
			Fixtures.MergedHover( point );
			return;
		}

		base.DrawHover( point );
	}

	ArchBoolDrag Sketch( Vector2 from, Vector2 to, ArchBoolHost host ) {
		var floor = Owner.LevelHeight;
		var min = Min( from, to );
		var max = Max( from, to );
		var plane = Plane( host, min, max, floor );

		return ArchBool.Sketch( Owner.Grid, Owner.Kit, op, from, to, plane, Climb( min, max, plane ),
			form, segments, Reach(), Depth(), Surface( min, max, floor ) );
	}

	float Depth() => Reach() == ArchBoolReach.IntoWall ? opening : recess;

	ArchBoolHost Host( Vector2 from, Vector2 to ) {
		return ArchBool.Host( Owner.Plan, Owner.ActiveBuilding(), (Min( from, to ) + Max( from, to )) * 0.5f );
	}

	float Plane( ArchBoolHost host, Vector2 min, Vector2 max, float floor ) {
		if ( op != ArchBoolOp.Add ) {
			return host.Road is null ? floor : ArchPlatform.Grade( Owner.Scene, min, max, floor );
		}

		return kind switch {
			ArchBoolAdd.Slab or ArchBoolAdd.Ramp => ArchPlatform.Grade( Owner.Scene, min, max, floor ),
			ArchBoolAdd.Pillar => floor,
			_ => Soffit( min, max, floor ) - MathF.Max( 1f, timber.Drop )
		};
	}

	float Climb( Vector2 min, Vector2 max, float plane ) {
		if ( op == ArchBoolOp.Add ) {
			return Hangs ? MathF.Max( 1f, timber.Drop ) : rise;
		}

		return ArchBool.Through( Owner.Plan, Owner.Kit, Owner.Level, min, max, plane );
	}

	float Surface( Vector2 min, Vector2 max, float floor ) {
		return Reach() == ArchBoolReach.IntoCeiling ? Soffit( min, max, floor ) : floor;
	}

	float Soffit( Vector2 min, Vector2 max, float floor ) {
		var centre = (min + max) * 0.5f;

		if ( Owner.RoomOnLevel( centre ) is { } room && Owner.Plan.OwnerOf( room ) is { } building ) {
			return ArchFloorGen.Overhead( Owner.Plan, building, room, Owner.Kit, centre );
		}

		return floor + Owner.Kit.WallHeight - ArchFloorGen.CeilingDepth( Owner.Kit );
	}

	protected override void DrawPreview() {
		if ( op == ArchBoolOp.Fixture ) {
			Fixtures.MergedPreview( DragStart, DragCurrent );
			return;
		}

		Aiming();

		if ( op == ArchBoolOp.Detail ) {
			DrawRectPreview();
			DrawCovered( Covered( DragStart, DragCurrent ).ToList() );

			return;
		}

		var host = Host( DragStart, DragCurrent );
		var drag = Sketch( DragStart, DragCurrent, host );
		var loop = drag.Loop();
		var lift = host.Building is null || !Lifted ? 0f : ArchAsks.Lift( Owner.Plan, host.Building, Owner.Kit );

		ArchGhost.Solid( loop, Floor( drag, lift ), Deck( drag, lift ) );
		ArchGhost.Footprint( loop, Plate( drag ) + lift, ArchGhost.Nth( (int)op ) );
		ArchGhost.Note( new Vector3( drag.Max.x, drag.Max.y, drag.Top + lift ),
			$"{Named()} — {drag.Max.x - drag.Min.x:0} x {drag.Max.y - drag.Min.y:0}, {drag.Rise:0} {(op == ArchBoolOp.Add ? "high" : "deep")}{Sides()}{Members( drag )}{Gradient( drag )}" );
	}

	bool Ramps => op == ArchBoolOp.Add ? kind == ArchBoolAdd.Ramp : op == ArchBoolOp.Subtract && sinking.Ramp;

	float Plate( ArchBoolDrag drag ) => op == ArchBoolOp.Add && !Ramps ? drag.Top : drag.Bottom;

	ArchCarvePlane Deck( ArchBoolDrag drag, float lift ) {
		return Ramps && op == ArchBoolOp.Add
			? Raked( drag, drag.Top + lift )
			: ArchCarvePlane.Level( drag.Top + lift );
	}

	ArchCarvePlane Floor( ArchBoolDrag drag, float lift ) {
		return Ramps && op == ArchBoolOp.Subtract
			? Raked( drag, drag.Top + lift )
			: ArchCarvePlane.Level( drag.Bottom + lift );
	}

	ArchCarvePlane Raked( ArchBoolDrag drag, float head ) {
		return ArchRamp.Deck( drag.Loop(), head, Fall( drag ), drag.Climb );
	}

	float Fall( ArchBoolDrag drag ) => op == ArchBoolOp.Subtract ? ArchRamp.Fall( sinking, drag.Rise ) : drag.Rise;

	string Gradient( ArchBoolDrag drag ) {
		if ( !Ramps ) {
			return "";
		}

		var run = ArchRamp.Run( drag.Loop(), ArchRamp.Climb( drag.Climb ) );
		var fall = Fall( drag );

		return run < 1f || fall < 1f ? "" : $" · 1:{run / fall:0.#}";
	}

	string Named() => op switch {
		ArchBoolOp.Add => kind.ToString().ToLowerInvariant(),
		ArchBoolOp.Subtract when sinking.Ramp => "ramp cut",
		ArchBoolOp.Subtract => Reach() switch {
			ArchBoolReach.Through => "cut",
			ArchBoolReach.IntoWall => "wall cut",
			_ => "recess"
		},
		ArchBoolOp.Damage => "damage",
		_ => "block"
	};

	string Members( ArchBoolDrag drag ) {
		if ( kind != ArchBoolAdd.Slats || op != ArchBoolOp.Add ) {
			return "";
		}

		return $" · {ArchBeamGen.Lanes( ArchBool.Hung( drag, Placement( null ) ) )} members";
	}

	string Sides() => form == ArchBoolForm.Circle ? $" · {segments} sides" : "";

	protected override void OnDrag( Vector2 from, Vector2 to ) {
		if ( op == ArchBoolOp.Fixture ) {
			if ( Fixtures.MergedPlace( from, to ) is { } fixture ) {
				var fixtureId = fixture switch {
					ArchLadderPart ladder => ladder.Id,
					ArchBalconyPart balcony => balcony.Id,
					ArchExteriorStairPart flight => flight.Id,
					_ => 0
				};

				Drew( fixture );
				FinishDraft( fixtureId );
			}

			return;
		}

		if ( op == ArchBoolOp.Detail ) {
			PlaceDetail( from, to );

			return;
		}

		var host = Host( from, to );
		var drag = Sketch( from, to, host );
		var room = Owner.RoomOnLevel( (drag.Min + drag.Max) * 0.5f );

		if ( ArchBool.Place( Owner.Plan, host, Owner.Level, drag, Placement( room ) ) is not { } placed ) {
			Log.Info( Refused( room ) );

			return;
		}

		var id = placed switch {
			ArchPlatformPart platform => platform.Id,
			ArchCutPart cut => cut.Id,
			ArchPillarPart pillar => pillar.Id,
			ArchBeamPart beam => beam.Id,
			_ => 0
		};

		if ( edged ) {
			Edge( room, id );
		}

		FinishDraft( id );

		if ( host.Building is not null ) {
			Owner.ActiveBuildingId = host.Building.Id;
		}

		Owner.Picked = new ArchSelection {
			Item = placed,
			Room = room,
			Building = placed is ArchPlatformPart placedPlatform ? Owner.Plan.OwnerOf( placedPlatform ) : host.Building
		};

		Drew( placed );

		Owner.Commit( $"Boolean {op}" );

		Refresh();
	}

	ArchBoolPlacement Placement( ArchRoom room ) {
		return new ArchBoolPlacement {
			Op = op,
			Kind = kind,
			Reach = Reach(),
			Room = room,
			Platform = standing,
			Cut = sinking,
			Pillar = column,
			Beam = timber
		};
	}

	string Refused( ArchRoom room ) {
		if ( op == ArchBoolOp.Add && Host( DragStart, DragCurrent ).Building is null ) {
			return $"Architecture: a {kind.ToString().ToLowerInvariant()} stands on a storey and a road has none — out there only Subtract applies.";
		}

		if ( room is null && op == ArchBoolOp.Add && kind is not (ArchBoolAdd.Slab or ArchBoolAdd.Ramp) ) {
			return $"Architecture: a {kind.ToString().ToLowerInvariant()} is filed under a room, and there is none on this storey under that drag.";
		}

		return $"Architecture: that drag is smaller than {ArchBool.Least( op )} inches on a side, so there is nothing in it.";
	}

	void Edge( ArchRoom under, int hostId ) {
		var room = under ?? Owner.ActiveRoom();

		if ( room is null || hostId == 0 ) {
			return;
		}

		var trim = new ArchTrimPart {
			Id = Owner.Plan.AllocateId(),
			Name = "Edge Trim",
			Profile = edging.Profile,
			Surface = edging.Surface,
			FollowsId = hostId,
			Closed = true
		};

		if ( ArchTrimAffector.Refollow( Owner.Plan, trim ) ) {
			room.Trims.Add( trim );
		}
	}

	void PlaceDetail( Vector2 from, Vector2 to ) {
		var placing = Covered( from, to ).ToList();

		if ( placing.Count == 0 ) {
			Log.Info( "Architecture: no wall under that box — drag one that covers a wall, or start it in the room and pull it through." );

			return;
		}

		foreach ( var pending in placing ) {
			pending.Part.Id = Owner.Plan.AllocateId();
			pending.Part.Name = $"{pending.Part.Kind} {pending.Wall.Modifiers.Count + 1}";

			pending.Wall.Modifiers.Add( pending.Part );
		}

		FinishDraft( placing[0].Part.Id );
		Owner.Commit( $"Place {placing.Count} {detail.Kind}" );
	}

	void DrawCovered( IReadOnlyList<Pending> pending ) {
		foreach ( var block in pending ) {
			ArchWallModBlocks.Preview( block.Part, block.Wall, Owner.Kit, block.Height, block.Floor );
		}
	}

	IEnumerable<Pending> Covered( Vector2 from, Vector2 to ) {
		var min = Min( from, to );
		var max = Max( from, to );

		if ( MathF.Max( max.x - min.x, max.y - min.y ) < Owner.Grid.SubgridSize() ) {
			var nearest = Owner.NearestWall( from, out var room, out var along, out var distance );

			if ( nearest is not null && room is not null && distance <= Reach( nearest ) ) {
				yield return Placed( nearest, room, along, MathF.Max( ArchGridService.FinestSize, detail.Width ), from );
			}

			yield break;
		}

		foreach ( var room in Owner.Plan.AllRooms().Where( entry => entry.Floor == Owner.Level ) ) {
			foreach ( var wall in room.Walls ) {
				if ( Crosses( wall, min, max, out var along, out var width ) ) {
					yield return Placed( wall, room, along, width, (min + max) * 0.5f );
				}
			}
		}
	}

	bool Crosses( ArchWall wall, Vector2 min, Vector2 max, out float along, out float width ) {
		along = 0f;
		width = 0f;

		if ( wall.Length < 1f ) {
			return false;
		}

		var corners = new[] { min, new Vector2( max.x, min.y ), max, new Vector2( min.x, max.y ) };
		var runs = corners.Select( corner => Vector2.Dot( corner - wall.Start, wall.Direction ) ).ToList();
		var sides = corners.Select( corner => Vector2.Dot( corner - wall.Start, wall.Normal ) ).ToList();

		if ( sides.Min() > Reach( wall ) || sides.Max() < -Reach( wall ) ) {
			return false;
		}

		var start = MathF.Max( runs.Min(), 0f );
		var finish = MathF.Min( runs.Max(), wall.Length );

		if ( finish - start < ArchGridService.FinestSize ) {
			return false;
		}

		along = (start + finish) * 0.5f;
		width = finish - start;

		return true;
	}

	float Reach( ArchWall wall ) => ArchWallSection.Thickness( wall, Owner.Kit ) * 0.5f + Owner.Kit.GridSize;

	Pending Placed( ArchWall wall, ArchRoom room, float along, float width, Vector2 at ) {
		var building = Owner.Plan.OwnerOf( room );
		var lift = building is null ? 0f : ArchAsks.Lift( Owner.Plan, building, Owner.Kit );
		var standing = ArchWallSection.Height( wall, room, Owner.Kit );

		return new Pending {
			Wall = wall,
			Room = room,
			Height = standing,
			Floor = room.BaseHeight + lift,
			Part = new ArchWallModPart {
				Kind = detail.Kind,
				Side = Facing( wall, at ),
				Along = ArchGridService.Fine( along ),
				Width = ArchGridService.Fine( width ),
				Base = detail.Kind.Seat( detail.Base, standing ),
				Height = detail.Kind.Rise( 0f ),
				Depth = detail.Depth,
				Field = detail.Field
			}
		};
	}

	ArchWallSide Facing( ArchWall wall, Vector2 at ) {
		if ( detail.Side != ArchWallSide.OnSurface ) {
			return detail.Side;
		}

		return Vector2.Dot( at - wall.Start, wall.Normal ) < 0f ? ArchWallSide.Outside : ArchWallSide.Inside;
	}

	static List<Vector2> Loop( ArchWall wall, float from, float to, float near, float far ) {
		return new List<Vector2>
		{
			Point( wall, from, near ),
			Point( wall, to, near ),
			Point( wall, to, far ),
			Point( wall, from, far )
		};
	}

	static Vector2 Point( ArchWall wall, float along, float across ) => wall.PointAt( along ) + wall.Normal * across;

	protected override void BuildOptions( ToolSidebarWidget panel ) {
		if ( Lent ) {
			BuildLent( panel );

			return;
		}

		using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );

		flow.Step( "Operation", Operation, OperationGlyph, PickOperation );

		if ( AsksKind ) {
			flow.Step( "Kind", KindName, KindGlyph, PickKind );
		}

		if ( AsksForm ) {
			flow.Step( "Form", FormName, form == ArchBoolForm.Circle ? "circle" : "crop_square", PickForm );
		}

		if ( AsksReach ) {
			flow.Step( "Reach", ReachName, ReachGlyph, PickReach );
		}

		flow.Step( "Dressing", DressingName, "tune", BuildDressing );

		if ( AsksForm && op != ArchBoolOp.Extrude ) {
			flow.Step( "Edge run", edged ? edging.Profile : "None", "timeline", BuildEdge );
		}
	}

	void BuildLent( ToolSidebarWidget panel ) {
		if ( Owner.Picked is not { Item: not null } picked ) {
			return;
		}

		if ( picked.Item is ArchLadderPart or ArchBalconyPart or ArchExteriorStairPart ) {
			Fixtures.MergedOptions( panel );

			return;
		}

		ArchPartUi.Sheet( panel, Owner, picked, Refresh );
	}

	bool Zoning => op is ArchBoolOp.Damage or ArchBoolOp.Extrude;

	bool AsksKind => op is ArchBoolOp.Add or ArchBoolOp.Detail || Zoning;

	bool AsksForm => op is ArchBoolOp.Add or ArchBoolOp.Subtract || Zoning;

	bool AsksReach => op is ArchBoolOp.Subtract or ArchBoolOp.Damage;

	string Operation => op switch {
		ArchBoolOp.Add => "Add",
		ArchBoolOp.Subtract => "Subtract",
		ArchBoolOp.Fixture => "Fixture",
		ArchBoolOp.Detail => "Face fitting",
		_ => "Zone"
	};

	string OperationGlyph => op switch {
		ArchBoolOp.Add => "add_box",
		ArchBoolOp.Subtract => "disabled_by_default",
		ArchBoolOp.Fixture => "table_rows",
		ArchBoolOp.Detail => "view_column",
		_ => "select_all"
	};

	void PickOperation( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Add — the shape stands as a solid", "bool_add", "add_box",
			op == ArchBoolOp.Add, () => Operating( ArchBoolOp.Add, step ) );

		grid.Pick( "Subtract — the shape is taken out of everything it passes through", "bool_subtract", "disabled_by_default",
			op == ArchBoolOp.Subtract, () => Operating( ArchBoolOp.Subtract, step ) );

		grid.Pick( "Zone — an influence zone: what it covers breaks, stands proud or is bitten back", "bool_zone", "select_all",
			Zoning, () => Operating( zone, step ) );

		grid.Pick( "Fixtures — ladders, balconies and exterior stairs hung from a wall", "bool_fixtures", "table_rows",
			op == ArchBoolOp.Fixture, () => Operating( ArchBoolOp.Fixture, step ) );

		grid.Pick( "Face fitting — one block per wall face the box covers, on the side you came from", "bool_detail", "view_column",
			op == ArchBoolOp.Detail, () => Operating( ArchBoolOp.Detail, step ) );
	}

	void Operating( ArchBoolOp chosen, ArchWorkflowStep step ) {
		op = chosen;

		step.Chose();
	}

	string KindName => op switch {
		ArchBoolOp.Add => kind.ToString(),
		ArchBoolOp.Damage => "Damage",
		ArchBoolOp.Extrude => sinking.Extrude == ArchExtrudeKind.Indent ? "Recess" : sinking.Extrude.ToString(),
		_ => detail.Kind.ToString()
	};

	string KindGlyph => op switch {
		ArchBoolOp.Add => kind switch {
			ArchBoolAdd.Slab => "crop_landscape",
			ArchBoolAdd.Ramp => "trending_up",
			ArchBoolAdd.Pillar => "view_column",
			ArchBoolAdd.Beam => "horizontal_rule",
			_ => "view_stream"
		},
		ArchBoolOp.Damage => "broken_image",
		ArchBoolOp.Extrude => ArchExtrude.Glyph( sinking.Extrude ),
		_ => "view_column"
	};

	void PickKind( ArchWorkflowStep step ) {
		if ( op == ArchBoolOp.Add ) {
			PickSolid( step );

			return;
		}

		if ( Zoning ) {
			PickZone( step );

			return;
		}

		ArchSidebarLayout.Form( step.Layout, form => ArchWallForms.ModKinds( form, detail.Kind, chosen => {
			ArchWallModUi.Adopt( detail, chosen );
			step.Chose();
		} ) );
	}

	void PickSolid( ArchWorkflowStep step ) {
		using var kinds = ArchIconGrid.In( step.Layout );

		kinds.Pick( "Slab — a plinth poured on the ground it covers", "bool_slab", "crop_landscape",
			kind == ArchBoolAdd.Slab, () => Adopt( ArchBoolAdd.Slab, step ) );

		kinds.Pick( "Ramp — the same slab with a raked deck, climbing the way you drag", "bool_ramp", "trending_up",
			kind == ArchBoolAdd.Ramp, () => Adopt( ArchBoolAdd.Ramp, step ) );

		kinds.Pick( "Pillar — a column on the dragged footprint, dressed by a pillar type", "bool_pillar", "view_column",
			kind == ArchBoolAdd.Pillar, () => Adopt( ArchBoolAdd.Pillar, step ) );

		kinds.Pick( "Beam — one member hung under the ceiling", "bool_beam", "horizontal_rule",
			kind == ArchBoolAdd.Beam, () => Adopt( ArchBoolAdd.Beam, step ) );

		kinds.Pick( "Slats — the shape filled with members", "bool_slats", "view_stream",
			kind == ArchBoolAdd.Slats, () => Adopt( ArchBoolAdd.Slats, step ) );
	}

	void PickZone( ArchWorkflowStep step ) {
		using var zones = ArchIconGrid.In( step.Layout );

		zones.Pick( "Damage — the parts it covers break, spall or lose panels", "bool_damage", "broken_image",
			op == ArchBoolOp.Damage, () => Zoned( ArchBoolOp.Damage, ArchExtrudeKind.None, step ) );

		foreach ( var standing in Proud ) {
			var chosen = standing;

			zones.Pick( ArchExtrude.Label( chosen ), $"extrude_{chosen.ToString().ToLowerInvariant()}", ArchExtrude.Glyph( chosen ),
				op == ArchBoolOp.Extrude && sinking.Extrude == chosen, () => Zoned( ArchBoolOp.Extrude, chosen, step ) );
		}
	}

	static readonly ArchExtrudeKind[] Proud =
	{
		ArchExtrudeKind.Face, ArchExtrudeKind.Quoins, ArchExtrudeKind.Courses, ArchExtrudeKind.Indent, ArchExtrudeKind.Pier
	};

	void Zoned( ArchBoolOp chosen, ArchExtrudeKind standing, ArchWorkflowStep step ) {
		op = chosen;
		zone = chosen;

		if ( standing == ArchExtrudeKind.None ) {
			sinking.Affects = ArchDamage.Targets( sinking.ResolvedDamage );
		} else {
			sinking.Extrude = standing;
			sinking.Affects = ArchExtrude.Targets( standing );
		}

		step.Chose();
	}

	void Adopt( ArchBoolAdd chosen, ArchWorkflowStep step ) {
		if ( chosen != kind ) {
			rise = Rising( chosen );
		}

		kind = chosen;

		step.Chose();
	}

	float Rising( ArchBoolAdd chosen ) => chosen switch {
		ArchBoolAdd.Pillar or ArchBoolAdd.Ramp => Owner.Kit.WallHeight,
		ArchBoolAdd.Slab => 24f,
		_ => rise
	};

	string FormName => form == ArchBoolForm.Circle ? $"Circle · {segments} sides" : "Box";

	void PickForm( ArchWorkflowStep step ) {
		using ( var grid = ArchIconGrid.In( step.Layout ) ) {
			grid.Pick( "Box — four corners on the grid", "bool_box", "crop_square", form == ArchBoolForm.Box,
				() => { form = ArchBoolForm.Box; step.Chose(); } );

			grid.Pick( "Circle — an n-gon inscribed in the drag", "bool_circle", "circle", form == ArchBoolForm.Circle,
				() => { form = ArchBoolForm.Circle; Refresh(); } );
		}

		if ( form != ArchBoolForm.Circle ) {
			return;
		}

		step.Layout.Add( ArchPartUi.Integer( "Sides", segments, ArchFootprint.LeastSegments,
			value => segments = Math.Max( ArchFootprint.LeastSegments, value ) ) );
	}

	string ReachName => aim switch {
		ArchBoolReach.Through => "Through",
		ArchBoolReach.IntoCeiling => $"Ceiling · {recess:0}",
		ArchBoolReach.IntoFloor => $"Floor · {recess:0}",
		ArchBoolReach.IntoWall => $"Wall · {opening:0}",
		_ => "Where you drag"
	};

	string ReachGlyph => aim switch {
		ArchBoolReach.Through => "vertical_align_top",
		ArchBoolReach.IntoCeiling => "border_top",
		ArchBoolReach.IntoFloor => "border_bottom",
		ArchBoolReach.IntoWall => "door_open",
		_ => "ads_click"
	};

	void PickReach( ArchWorkflowStep step ) {
		using ( var grid = ArchIconGrid.In( step.Layout ) ) {
			grid.Pick( "Where you drag — the floor looking down, the ceiling looking up at it", "bool_dragged", "ads_click",
				aim == ArchBoolReach.Dragged, () => Aiming( ArchBoolReach.Dragged, step ) );

			grid.Pick( "Through — out past everything standing over it", "bool_through", "vertical_align_top",
				aim == ArchBoolReach.Through, () => Aiming( ArchBoolReach.Through, step ) );

			grid.Pick( "Into the ceiling — a coffer, taken up into the ceiling body only", "bool_coffer", "border_top",
				aim == ArchBoolReach.IntoCeiling, () => { aim = ArchBoolReach.IntoCeiling; Refresh(); } );

			grid.Pick( "Into the floor — a tray, sunk into the slab only", "bool_tray", "border_bottom",
				aim == ArchBoolReach.IntoFloor, () => { aim = ArchBoolReach.IntoFloor; Refresh(); } );

			grid.Pick( "Into the wall — an opening between its sill and head", "bool_wall", "door_open",
				aim == ArchBoolReach.IntoWall, () => { aim = ArchBoolReach.IntoWall; Refresh(); } );
		}

		if ( aim == ArchBoolReach.IntoWall ) {
			step.Layout.Add( ArchPartUi.Number( "Opening height", opening, 96f, value => opening = MathF.Max( 4f, value ) ) );

			return;
		}

		if ( aim is ArchBoolReach.IntoCeiling or ArchBoolReach.IntoFloor ) {
			step.Layout.Add( ArchPartUi.Number( "Recess depth", recess, 4f, value => recess = MathF.Max( 1f, value ) ) );
		}
	}

	void Aiming( ArchBoolReach chosen, ArchWorkflowStep step ) {
		aim = chosen;

		step.Chose();
	}

	string DressingName => op switch {
		ArchBoolOp.Fixture => Fixtures.MergedTitle,
		ArchBoolOp.Add => Hangs ? $"{MathF.Max( 1f, timber.Drop ):0} drop" : Ramps ? $"{rise:0} head" : $"{rise:0} rise",
		ArchBoolOp.Damage => sinking.ResolvedDamage.ToString(),
		ArchBoolOp.Extrude => $"{ArchExtrude.Reach( sinking ):0} deep",
		ArchBoolOp.Detail => detail.Side.ToString(),
		_ => sinking.Enclosure == CutEnclosure.Walls ? "Walled shaft" : "Open shaft"
	};

	void BuildDressing( ArchWorkflowStep step ) {
		if ( op == ArchBoolOp.Fixture ) {
			ArchSidebarLayout.Form( step.Layout, Fixtures.MergedOptions );

			return;
		}

		if ( op == ArchBoolOp.Detail ) {
			BuildFitting( step );

			return;
		}

		ArchSidebarLayout.Form( step.Layout, form => {
			switch ( op ) {
				case ArchBoolOp.Add:
					BuildSolid( form );
					break;

				case ArchBoolOp.Damage:
					ArchBoolUi.Damage( form, sinking, Refresh, null );
					break;

				case ArchBoolOp.Extrude:
					ArchBoolUi.Extrude( form, sinking, Refresh, null, kinds: false );
					break;

				default:
					ArchBoolUi.Cut( form, sinking, Refresh, null );
					break;
			}
		} );
	}

	void BuildSolid( ToolSidebarWidget form ) {
		if ( kind == ArchBoolAdd.Pillar ) {
			form.AddGroup( "Placement" ).Add( ArchPartUi.Number( "Height", rise, 128f, value => rise = MathF.Max( 4f, value ) ) );

			ArchAsks.PillarKinds( form, Owner.OfferedPillars(), column.Type, Dress );
			ArchAsks.BuildPillar( form, column, Pillar, Refresh, null );

			return;
		}

		if ( Hangs ) {
			if ( kind == ArchBoolAdd.Slats ) {
				ArchAsks.PillarKinds( form, Owner.OfferedPillars(), timber.Type,
					type => { ArchBeamGen.Dressed( timber, type ); Refresh(); } );
			}

			ArchBoolUi.Section( form, timber, kind == ArchBoolAdd.Slats, null );

			return;
		}

		form.AddGroup( "Placement" ).Add( ArchPartUi.Number( Ramps ? "Head" : "Rise", rise, 24f, value => rise = MathF.Max( 4f, value ) ) );

		if ( Ramps ) {
			return;
		}

		ArchBoolUi.Guard( form, standing, Refresh, null );
		ArchBoolUi.Coping( form, standing, Refresh, null );
	}

	void BuildFitting( ArchWorkflowStep step ) {
		ArchSidebarLayout.Form( step.Layout, form => {
			ArchWallForms.ModSides( form, detail.Side, true, side => { detail.Side = side; Refresh(); } );
			ArchWallForms.ModSizes( form, detail, null, false );
			ArchWallForms.ModFill( form, detail, null );
		} );

		step.Layout.Add( ArchSidebarLayout.Advice( detail.Kind.Plated()
			? "Seated at its wall's head like a fascia board — drag the plate's foot and head widgets to move it down the elevation."
			: "Placed at its wall's own head — drag the block's head widget to run it past the floor line." ) );
	}

	void Dress( ArchPillarType type ) {
		column = type.Standing( default, 0f, 0f );

		Refresh();
	}

	void BuildEdge( ArchWorkflowStep step ) {
		step.Layout.Add( ArchPartUi.Check( "Trim the edge", edged, value => { edged = value; Refresh(); } ) );

		if ( !edged ) {
			return;
		}

		step.Layout.Add( Wrapped( "A run filed under the shape, tracing its outline — round on a circle, square on a box." ) );

		ArchSidebarLayout.Form( step.Layout, form => ArchPartUi.Trim( form, edging, Owner.Kit, null ) );
	}


	sealed class Pending {
		public ArchWall Wall;
		public ArchRoom Room;
		public float Height;
		public float Floor;
		public ArchWallModPart Part;
	}
}