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

namespace Sunless.Architecture;

public enum BuildingMode {
	New,
	Extend,
	Prefab,
	Row,
	Saved
}

[Title( "Building" ), Icon( "domain_add" ), Group( "02" )]
public sealed class ArchBuildingSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	protected override ArchKind? DraftKind => ArchKind.Building;

	public override ArchSurface[] Surfaces => new[] { ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.Floor, ArchSurface.Roof, ArchSurface.Soffit, ArchSurface.WallCap };

	BuildingMode mode;
	string archetype = "house";
	bool flipX;
	bool flipY;
	bool withRoof = true;
	bool withFloor = true;
	bool withGutters = true;
	bool withFoundation = true;
	RoofStyle roofStyle = RoofStyle.Hip;
	RidgeRun ridge = RidgeRun.Auto;
	SectionRoof wingRoof = SectionRoof.Continue;
	float eaveDrop;

	float wallHeight;
	float wallThickness;
	float roofThickness;
	float roofPitch;
	bool withFrame;
	bool withFascia = true;
	bool withSoffit = true;
	bool withCeiling;

	bool seeded;

	ArchBuildingAsset saved;
	ArchAssetGhost savedGhost;
	List<ArchPresetItem<ArchBuildingAsset>> savedOffered;
	int savedStamp = -1;
	int quarters;

	float placementAngle;

	int placedRoom;
	int placedRoof;
	int placedStamp;
	bool placedMerged;

	bool Extending => mode == BuildingMode.Extend;

	bool Rowing => mode == BuildingMode.Row;

	bool Stamping => mode == BuildingMode.Saved;

	protected override bool UsesDrag => !Stamping;

	ArchArchetype Chosen => Owner.FindArchetype( archetype );

	float Facing => placementAngle + quarters * 90f;

	bool Aimable => !Extending && !Rowing;

	protected override string Title() => Stamping ? "Saved Building" : "Building";

	bool Roofed => Extending ? wingRoof != SectionRoof.None : withRoof;

	protected override string Advice() => mode switch {
		BuildingMode.Extend => "Drag a wing onto the active building. It shares the wall it abuts, and inherits its type.",
		BuildingMode.Prefab => "Drag the plot. The type's canned layout is laid out across it in one go.",
		BuildingMode.Row => "Drag the frontage and how far back it reaches. Along a street it takes that road's verge and the units step with the curve.",
		BuildingMode.Saved => "Pick a saved building and it follows the cursor. R turns it a quarter, the Bearing box aims it at anything else; click stands it there.",
		_ => "Drag the outer shell. The chosen type decides its heights, roof and trims."
	};

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

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

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

		if ( Stamping && savedOffered is not null && savedStamp != ArchLayerAssets.Revision ) {
			Refresh();
		}
	}

	public override bool Turns() {
		if ( !Stamping || !Placing || saved is null ) {
			return false;
		}

		quarters = (quarters + 1) % 4;

		return true;
	}

	protected override void DrawHover( Vector2 point ) {
		base.DrawHover( point );

		if ( !Stamping || savedGhost is not { Shapes.Count: > 0 } ghost ) {
			return;
		}

		var map = ArchBuildingStamp.Landing( ghost, point, Facing );

		foreach ( var shape in ghost.Shapes ) {
			ArchGhost.Prism( shape.Loop.Select( corner => map.Of( corner ) ).ToList(), shape.Bottom, shape.Top );
		}

		var span = map.Swapped( ghost.Max - ghost.Min );

		ArchGhost.Note( new Vector3( point.x + span.x, point.y + span.y, Owner.LevelHeight ),
			$"{saved.Title} — {span.x:0} x {span.y:0}, turned {Facing:0.##}°" );
	}

	protected override void OnClick( Vector2 point ) {
		if ( !Stamping ) {
			return;
		}

		if ( saved is null ) {
			Log.Info( "Architecture: pick a saved building before clicking - there is nothing to stand yet." );
			return;
		}

		if ( ArchBuildingStamp.Place( Owner.Plan, saved, point, Facing ) is not { } placed ) {
			Log.Info( $"Architecture: {saved.Title} could not be stamped there." );
			return;
		}

		Owner.ActiveBuildingId = placed.Id;
		Owner.ActiveRoomId = placed.Rooms[0].Id;

		FinishDraft( placed.Id );
		BeginDraft();
		Owner.CommitPlacement( $"Place {saved.Title}", placed );
	}

	protected override void DrawPreview() {
		if ( mode == BuildingMode.Prefab ) {
			DrawPrefabPreview();
			return;
		}

		if ( Rowing ) {
			DrawRowPreview();
			return;
		}

		var standard = ArchArchetypeRules.Standing( wallHeight, Owner.Kit );
		var height = Extending ? MathF.Max( 32f, standard - MathF.Max( 0f, eaveDrop ) ) : standard;
		var placement = Resolve( DragStart, DragCurrent );

		if ( !placement.IsUsable || Extending && !placement.TouchesHost ) {
			DrawRectPreview();
			return;
		}

		var aimed = Aimable && MathF.Abs( Facing ) > 0.001f;

		if ( aimed ) {
			ArchGhost.Prism( Turned( placement.Min, placement.Max ), Owner.LevelHeight, Owner.LevelHeight + height );
		} else {
			ArchGhost.Volume( placement.Min, placement.Max, Owner.LevelHeight, Owner.LevelHeight + height );
		}

		ArchGhost.Note( new Vector3( placement.Max.x, placement.Max.y, Owner.LevelHeight + height ),
			aimed
				? $"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}, turned {Facing:0.##}°"
				: $"{placement.Max.x - placement.Min.x:0} x {placement.Max.y - placement.Min.y:0}" );

		if ( !withRoof && !Extending || aimed ) {
			return;
		}

		var min = placement.Min;
		var max = placement.Max;
		var style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;
		var pitch = roofPitch > 0.5f ? roofPitch : Owner.Kit.RoofPitch;

		ArchGhost.Pitch( min, max, Owner.LevelHeight + height, pitch, style, ArchBuild.Ridged( ridge, min, max ) );
	}

	void DrawPrefabPreview() {
		var recipe = Owner.FindArchetype( archetype );

		if ( recipe is null || !recipe.HasLayout ) {
			DrawRectPreview();
			return;
		}

		var plot = ResolvedPrefabPlot( recipe, DragStart, DragCurrent, out var usable );

		if ( !usable ) {
			DrawRectPreview();
			return;
		}

		ArchGhost.Plate( plot.Min, plot.Max, Owner.LevelHeight, 8 );

		foreach ( var step in recipe.Steps ) {
			if ( step.Kind is not (ArchStepKind.Shell or ArchStepKind.Wing or ArchStepKind.Canopy) ) {
				continue;
			}

			plot.Resolve( step.Rect, out var min, out var max );
			ArchGhost.Volume( min, max, Owner.LevelHeight, Owner.LevelHeight + Standing( recipe, step ) );
		}

		ArchGhost.Note( new Vector3( plot.Max.x, plot.Max.y, Owner.LevelHeight + 264f ),
			$"{recipe.Title} — {plot.Size.x:0} x {plot.Size.y:0}" );
	}

	void DrawRowPreview() {
		var row = ResolvedRow( DragStart, DragCurrent );

		if ( !row.IsUsable ) {
			DrawRectPreview();
			return;
		}

		var standing = ArchArchetypeRules.Standing( wallHeight, Owner.Kit );
		var storey = standing + Owner.Kit.FloorThickness;
		var pitch = roofPitch > 0.5f ? roofPitch : Owner.Kit.RoofPitch;

		foreach ( var bay in row.Bays ) {
			var plate = Owner.LevelHeight + standing + storey * (bay.Storeys - 1);

			ArchGhost.Volume( bay.Min, bay.Max, Owner.LevelHeight, plate );

			if ( withRoof ) {
				ArchGhost.Pitch( bay.Min, bay.Max, plate, pitch, roofStyle, ArchBuild.Ridged( ridge, bay.Min, bay.Max ) );
			}
		}

		var last = row.Bays[^1];

		ArchGhost.Note( new Vector3( last.Max.x, last.Max.y, Owner.LevelHeight + standing ),
			row.RoadId > 0
				? $"{row.Bays.Count} units on the verge — {row.Frontage:0} x {row.Depth:0}"
				: $"{row.Bays.Count} units — {row.Frontage:0} x {row.Depth:0}" );
	}

	protected override void OnDrag( Vector2 from, Vector2 to ) {
		if ( mode == BuildingMode.Prefab ) {
			PlacePrefab( from, to );
			return;
		}

		if ( Rowing ) {
			PlaceRow( from, to );
			return;
		}

		var placement = Resolve( from, to );
		var min = placement.Min;
		var max = placement.Max;

		if ( !placement.IsUsable || Extending && !placement.TouchesHost ) {
			Log.Info( Extending
				? "Architecture: an extension must finish against an empty edge of the active building."
				: "Architecture: that drag is fully occupied. Start or finish the drag in empty grid space." );
			return;
		}

		var rules = Authored();

		if ( Extending && placement.Host is { Rooms.Count: > 0 } active ) {
			active.Archetype = Chosen?.Name ?? active.Archetype;

			Owner.ActiveBuildingId = active.Id;

			var wing = ArchBuild.Extend(
				Owner.Plan, active, Owner.Kit, Owner.Level, Owner.LevelHeight, min, max,
				wingRoof, eaveDrop, rules.Floor ?? true, rules.Gutters ?? true, ridge );

			if ( wing is null ) {
				Log.Info( "Architecture: that wing has no empty grid space beside the building." );
				return;
			}

			ArchArchetypeRules.Apply( wing, rules, Owner.Kit );

			Owner.ActiveRoomId = wing.Room.Id;
			CancelDraft();
			Owner.CommitPlacement( "Place Building Wing", active );
			Remember( wing );

			return;
		}

		var building = new ArchBuilding {
			Id = Owner.Plan.AllocateId(),
			Name = $"{Chosen?.Title ?? "Building"}{Owner.Plan.Buildings.Count + 1}",
			Archetype = Chosen?.Name ?? "",
			GuttersEnabled = rules.Gutters ?? true
		};

		var shell = ArchBuild.Shell(
			Owner.Plan, building, Owner.Kit, Owner.Level, Owner.LevelHeight, min, max, rules.Floor ?? true,
			withRoof ? roofStyle : null, rules.Gutters ?? true, ridge );

		if ( shell is null ) {
			Log.Info( "Architecture: that building footprint has no empty grid space." );
			return;
		}

		ArchArchetypeRules.Apply( shell, rules, Owner.Kit );

		Owner.Plan.Units.Add( building );

		if ( Aimable && ArchHandles.Pivot( building, out var about ) ) {
			ArchCarry.Turn( building, about, Facing );
		}

		Owner.ActiveBuildingId = building.Id;
		Owner.ActiveRoomId = shell.Room.Id;
		FinishDraft( building.Id );
		Owner.CommitPlacement( "Place Building", building );
		Remember( shell );
	}

	static List<Vector2> Turned( Vector2 min, Vector2 max, float degrees ) => ArchFootprint.Turned( ArchFootprint.Rect( min, max ), (min + max) * 0.5f, degrees );

	List<Vector2> Turned( Vector2 min, Vector2 max ) => Turned( min, max, Facing );

	ArchRectanglePlacement Resolve( Vector2 from, Vector2 to ) {
		return new ArchBoundaryPlacement( Owner.Plan, Owner.Kit )
			.Outside( Owner.Level, from, to, Extending ? Owner.ActiveBuilding() : null );
	}

	void Remember( ArchSection section ) {
		placedRoom = section.Room?.Id ?? 0;
		placedRoof = section.Roof?.Id ?? 0;
		placedStamp = Owner.Plan.NextId;
		placedMerged = section.Merged;

		Refresh();
	}

	ArchRoom Editing => placedRoom != 0 && Owner.Plan.NextId == placedStamp ? Owner.Plan.FindRoom( placedRoom ) : null;

	void Restyle() {
		if ( placedRoom == 0 || Owner.Plan.NextId != placedStamp ) {
			return;
		}

		if ( Owner.Plan.FindRoom( placedRoom ) is not { } room ) {
			placedRoom = 0;
			return;
		}

		var building = Owner.Plan.OwnerOf( room );
		var roof = building?.Roofs.FirstOrDefault( part => part.Id == placedRoof );

		if ( building is not null && Chosen is { } chosen ) {
			building.Archetype = chosen.Name;
		}

		if ( roof is not null && !placedMerged ) {
			roof.Style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;
			roof.RidgeAlongX = ArchBuild.Ridged( ridge, roof.Min, roof.Max );
		}

		ArchArchetypeRules.Apply( new ArchSection { Room = room, Roof = roof, Merged = placedMerged }, Authored(), Owner.Kit );

		Owner.Touch( "Restyle Section" );
	}

	void Set( Action change ) {
		change();
		Restyle();
	}

	void Relayout( Action change ) {
		Set( change );
		Refresh();
	}

	ArchSectionRules Authored() {
		var type = Chosen is { } chosen ? (Extending ? chosen.Wing : chosen.Shell) : new ArchSectionRules();

		return new ArchSectionRules {
			WallHeight = wallHeight,
			WallThickness = wallThickness,
			RoofThickness = roofThickness,
			Ridge = ridge,
			RoofPitch = roofPitch,
			Overhang = type.Overhang,
			FrameSpacing = type.FrameSpacing,
			Floor = withFloor,
			Foundation = withFoundation,
			Ceiling = withCeiling,
			Gutters = withGutters,
			Fascia = withFascia,
			Soffit = withSoffit,
			Frame = withFrame,
			FloorBoards = type.FloorBoards,
			FloorBoardYaw = type.FloorBoardYaw,
			Palette = type.Palette
		};
	}

	float Standing( ArchArchetype recipe, ArchArchetypeStep step ) {
		if ( step.Kind == ArchStepKind.Canopy ) {
			return step.HeadHeight;
		}

		var type = step.Kind == ArchStepKind.Wing ? recipe.Wing : recipe.Shell;
		var height = step.Rules.WallHeight > 1f ? step.Rules.WallHeight : type.WallHeight;

		return ArchArchetypeRules.Standing( height, Owner.Kit );
	}

	void PlacePrefab( Vector2 from, Vector2 to ) {
		var recipe = Owner.FindArchetype( archetype );

		if ( recipe is null || !recipe.HasLayout ) {
			Log.Info( $"Architecture: '{archetype}' has no canned layout - use New with the type chosen instead. Types live in Assets/{ArchStorage.ArchetypeDirectory}." );
			return;
		}

		var plot = ResolvedPrefabPlot( recipe, from, to, out var usable );

		if ( !usable ) {
			Log.Info( $"Architecture: {recipe.Title} has no empty grid space on that plot." );
			return;
		}

		var placed = ArchArchetypeBuild.Place( Owner.Plan, Owner.Kit, recipe, plot, Owner.Level, Owner.LevelHeight, Owner.PillarTypes );

		if ( placed is null ) {
			Log.Info( $"Architecture: {recipe.Title} laid out nothing on that plot." );
			return;
		}

		foreach ( var note in placed.Skipped ) {
			Log.Info( $"Architecture: {recipe.Title} skipped {note}." );
		}

		Owner.ActiveBuildingId = placed.Building.Id;
		Owner.ActiveRoomId = placed.Building.Rooms[0].Id;
		Owner.CommitPlacement( $"Place {recipe.Title}", placed.Building );
	}

	ArchPlot ResolvedPrefabPlot( ArchArchetype recipe, Vector2 from, Vector2 to, out bool usable ) {
		return new ArchBoundaryPlacement( Owner.Plan, Owner.Kit )
			.PlotOutside( Owner.Level, from, to, recipe.MinimumPlot, flipX, flipY, out usable );
	}

	void PlaceRow( Vector2 from, Vector2 to ) {
		if ( Chosen is not { } type ) {
			Log.Info( $"Architecture: a row takes its unit width from a building type, and none are in Assets/{ArchStorage.ArchetypeDirectory}." );
			return;
		}

		var row = ResolvedRow( from, to );

		if ( !row.IsUsable ) {
			Log.Info( $"Architecture: that drag names no frontage a {type.Title} unit fits along. Drag further along the street, or deeper back from it." );
			return;
		}

		var placed = ArchRowPlacement.Stand(
			Owner.Plan, Owner.Kit, row, type, Authored(), Owner.Level, Owner.LevelHeight,
			withRoof ? roofStyle : null, ridge );

		if ( placed is null ) {
			Log.Info( "Architecture: every bay of that row landed on occupied ground." );
			return;
		}

		if ( row.Blocked > 0 ) {
			Log.Info( $"Architecture: {row.Blocked} of the row's bays stood on occupied ground and were left out." );
		}

		Owner.ActiveBuildingId = placed.Buildings[0].Id;
		Owner.ActiveRoomId = placed.Buildings[0].Rooms[0].Id;
		CancelDraft();
		Owner.CommitPlacement( $"Place Street Row of {placed.Buildings.Count}", placed.Buildings.ToArray() );
	}

	ArchRowShape ResolvedRow( Vector2 from, Vector2 to ) {
		var road = Owner.RoadAt( from, ArchRowPlacement.Verge, out _ ) ?? Owner.RoadAt( to, ArchRowPlacement.Verge, out _ );

		return ArchRowPlacement.Resolve(
			Owner.Plan, Owner.Kit, Chosen, Owner.Level, from, to,
			road, road is null ? null : Owner.Resolved( road.Id, road.Curve ),
			withRoof && roofStyle == RoofStyle.Flat );
	}

	void Layout( ToolSidebarWidget panel ) {
		var building = Owner.ActiveBuilding();
		var button = new Button.Primary( "Toggle Building Layout", "architecture" ) {
			Clicked = () => ArchBuildingWindow.Toggle( Owner, building )
		};

		button.Enabled = building is not null;
		button.ToolTip = building is null
			? "Drag a shell first — the layout drawing works on a building that stands"
			: $"The plan of {building.Name}, storey by storey: move a corner, push a run, cut a corner off at an angle";

		panel.AddGroup( "Layout" ).AddRow().Add( button );
	}

	protected override void BuildOptions( ToolSidebarWidget panel ) {
		Layout( panel );

		Inherit();

		if ( Stamping && (savedOffered is null || savedStamp != ArchLayerAssets.Revision) ) {
			RereadSaved();
		}

		if ( Editing is { } room ) {
			panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {room.Name} live. Place anything else and these go back to seeding the next drag." ) );
		}

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

		flow.Step( "Mode", ModeName, ModeGlyph, PickMode );

		if ( Stamping ) {
			flow.Step( "Saved building", saved?.Title ?? "None", "inventory_2", step => Stamps( panel, step ) );
		} else {
			flow.Step( "Type", Chosen?.Title ?? "None", "domain", step => Types( panel, step ) );
		}

		if ( mode == BuildingMode.Prefab ) {
			flow.Step( "Mirror", MirrorName, "flip", PickMirror );
		} else if ( !Stamping ) {
			flow.Step( "Include", IncludeName, "checklist", PickInclude );

			if ( Covered ) {
				flow.Step( Extending ? "Wing roof" : "Roof", RoofName, RoofGlyph, PickRoof );
			}

			if ( Wants( ArchOptionGroup.Section ) ) {
				flow.Step( "Structure", StructureName, "straighten", BuildStructure );
			}
		}

		// Typed, not picked — can never auto-advance; must be last
		if ( Aimable ) {
			flow.Step( "Bearing", BearingName, "explore", BuildBearing );
		}
	}

	bool Covered => Extending ? Wants( ArchOptionGroup.WingRoof ) : withRoof;

	bool Wants( ArchOptionGroup group ) => Chosen?.Wants( group ) ?? true;

	string ModeName => mode switch {
		BuildingMode.Extend => "Extend",
		BuildingMode.Prefab => "Canned layout",
		BuildingMode.Row => "Street row",
		BuildingMode.Saved => "Saved building",
		_ => "New building"
	};

	string ModeGlyph => mode switch {
		BuildingMode.Extend => "add_home_work",
		BuildingMode.Prefab => "auto_awesome_motion",
		BuildingMode.Row => "view_column",
		BuildingMode.Saved => "inventory_2",
		_ => "domain_add"
	};

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

		Mode( grid, step, BuildingMode.New, "mode_new_building", "New building — drag the outer shell", "domain_add" );
		Mode( grid, step, BuildingMode.Extend, "mode_extend_building", "Extend the active building — drag a wing onto it", "add_home_work" );
		Mode( grid, step, BuildingMode.Prefab, "mode_archetype", "Place the type's canned layout — one drag lays the whole unit out", "auto_awesome_motion" );
		Mode( grid, step, BuildingMode.Row, "mode_street_row", "Street row — drag the frontage and its depth; one party-walled unit per bay, at the type's own unit width", "view_column" );
		Mode( grid, step, BuildingMode.Saved, "mode_saved_building", "Saved building — stamp one you authored earlier back down, exactly as it stands", "inventory_2" );
	}

	void Mode( ArchIconGrid grid, ArchWorkflowStep step, BuildingMode choice, string slug, string tooltip, string fallback ) {
		grid.Pick( tooltip, slug, fallback, mode == choice, () => {
			mode = choice;
			seeded = false;

			step.Chose();
		} );
	}

	string MirrorName => flipX && flipY ? "X and Y" : flipX ? "Across X" : flipY ? "Across Y" : "None";

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

		grid.Toggle( "Mirror the layout across X", "flip_x", "swap_horiz", flipX, value => flipX = value );
		grid.Toggle( "Mirror the layout across Y", "flip_y", "swap_vert", flipY, value => flipY = value );
	}

	string IncludeName {
		get {
			var carried = new List<string>();

			if ( Wants( ArchOptionGroup.Foundation ) ) {
				carried.Add( withFoundation ? "Raised" : "Nested" );
			}

			if ( withFloor ) {
				carried.Add( "floor" );
			}

			if ( Roofed && withGutters ) {
				carried.Add( "gutters" );
			}

			if ( !Extending && !withRoof ) {
				carried.Add( "no roof" );
			}

			return carried.Count > 0 ? string.Join( ", ", carried ) : "Shell only";
		}
	}

	void PickInclude( ArchWorkflowStep step ) {
		if ( Wants( ArchOptionGroup.Foundation ) ) {
			using var footing = ArchIconGrid.In( step.Layout );

			footing.Pick( "Raised foundation — the plinth stands proud of grade and the building sits up on it",
				"foundation_raised", "vertical_align_top", withFoundation, () => Answer( () => withFoundation = true, step ) );

			footing.Pick( "Nested foundation — the footing is buried and the floor sits on grade",
				"foundation_nested", "vertical_align_bottom", !withFoundation, () => Answer( () => withFoundation = false, step ) );
		}

		using var grid = ArchIconGrid.In( step.Layout );

		grid.Toggle( "Floor slab", "opt_floor_slab", "layers", withFloor, value => Set( () => withFloor = value ) );

		if ( Roofed ) {
			grid.Toggle( "Gutters and downpipes", "opt_gutters", "water_damage", withGutters, value => Set( () => withGutters = value ) );
		}

		if ( !Extending ) {
			grid.Toggle( "Roof", "opt_roof", "roofing", withRoof, value => Relayout( () => withRoof = value ) );
		}
	}

	string RoofName => Extending ? wingRoof.ToString() : roofStyle.ToString();

	string RoofGlyph => Extending ? Fallback( wingRoof ) : ArchIcons.RoofStyleGlyph( roofStyle );

	void PickRoof( ArchWorkflowStep step ) {
		if ( Extending ) {
			PickWingRoof( step );

			return;
		}

		if ( Wants( ArchOptionGroup.RoofStyle ) ) {
			using var grid = ArchIconGrid.In( step.Layout );

			foreach ( var value in Enum.GetValues<RoofStyle>() ) {
				var captured = value;

				grid.Pick( captured.ToString(), ArchIcons.RoofStyleSlug( captured ), ArchIcons.RoofStyleGlyph( captured ), roofStyle == captured,
					() => Answer( () => roofStyle = captured, step ) );
			}
		}

		Ridge( step.Layout );
	}

	void PickWingRoof( ArchWorkflowStep step ) {
		using ( var grid = ArchIconGrid.In( step.Layout ) ) {
			foreach ( var value in Enum.GetValues<SectionRoof>() ) {
				var captured = value;

				grid.Pick( Describe( captured ), $"wing_{captured}".ToLowerInvariant(), Fallback( captured ), wingRoof == captured,
					() => Answer( () => wingRoof = captured, step ) );
			}
		}

		if ( wingRoof is not (SectionRoof.Continue or SectionRoof.None) ) {
			step.Layout.Add( ArchPartUi.Number( "Eave drop", eaveDrop, 0f, value => Set( () => eaveDrop = value ) ) );
		}

		Ridge( step.Layout );
	}

	void Answer( Action change, ArchWorkflowStep step ) {
		Set( change );

		step.Chose();
	}

	string StructureName => $"{ArchArchetypeRules.Standing( wallHeight, Owner.Kit ):0} high";

	void BuildStructure( ArchWorkflowStep step ) {
		step.Layout.Add( ArchPartUi.Number( "Wall height", wallHeight, 0f, value => Set( () => wallHeight = value ) ) );
		var shownWall = wallThickness > 0f ? wallThickness : Editing?.Walls.FirstOrDefault()?.Thickness ?? Owner.Kit.WallThickness;
		step.Layout.Add( ArchPartUi.Number( "Wall thickness", shownWall > 0f ? shownWall : Owner.Kit.WallThickness, Owner.Kit.WallThickness,
			value => Set( () => wallThickness = MathF.Max( 1f, value ) ) ) );

		if ( Roofed ) {
			var style = Extending ? ArchBuild.Winged( wingRoof ) : roofStyle;
			if ( style != RoofStyle.Flat )
			{
				step.Layout.Add( ArchPartUi.Number( "Roof pitch", roofPitch, 0f, value => Set( () => roofPitch = value ) ) );
			}
			var placed = Editing is { } room ? Owner.Plan.OwnerOf( room )?.Roofs.FirstOrDefault( part => part.Id == placedRoof ) : null;
			var shownRoof = roofThickness > 0f ? roofThickness : placed?.Thickness ?? Owner.Kit.RoofThickness;
			step.Layout.Add( ArchPartUi.Number( style == RoofStyle.Flat ? "Slab thickness" : "Roof thickness", shownRoof > 0f ? shownRoof : Owner.Kit.RoofThickness, Owner.Kit.RoofThickness,
				value => Set( () => roofThickness = MathF.Max( 1f, value ) ) ) );
		}

		using var grid = ArchIconGrid.In( step.Layout );

		grid.Toggle( "Ceiling under the roof", "opt_ceiling", "square", withCeiling, value => Set( () => withCeiling = value ) );

		if ( !Roofed ) {
			return;
		}

		grid.Toggle( "Exposed rafters and purlins under the deck", "opt_roof_frame", "reorder", withFrame, value => Set( () => withFrame = value ) );
		grid.Toggle( "Fascia along the eaves", "opt_fascia", "border_bottom", withFascia, value => Set( () => withFascia = value ) );
		grid.Toggle( "Lined soffit under the overhang", "opt_soffit", "flip_to_back", withSoffit, value => Set( () => withSoffit = value ) );
	}

	string BearingName => MathF.Abs( Facing ) < 0.001f ? "Square on" : $"{Facing:0.##}°";

	void BuildBearing( ArchWorkflowStep step ) {
		step.Layout.Add( ArchPartUi.Angle( "Angle", placementAngle, value => placementAngle = value ) );
		step.Layout.Add( ArchPartUi.Increment( Refresh ) );
	}

	void Types( ToolSidebarWidget panel, ArchWorkflowStep step ) {
		var offered = Offered();
		var removed = ArchStorage.HiddenArchetypes();

		if ( offered.Count == 0 && removed.Count == 0 ) {
			step.Layout.Add( ArchSidebarLayout.Advice( mode == BuildingMode.Prefab
				? "No building type carries a canned layout. Use New with a type chosen instead."
				: $"No building types. Authored ones live in Assets/{ArchStorage.ArchetypeDirectory}." ) );

			return;
		}

		var shelf = new ArchPresetBrowser<ArchArchetype>( panel, Owner.Kit, Scope( "types" ), "Building types" ) {
			Chosen = type => string.Equals( archetype, type.Name, StringComparison.OrdinalIgnoreCase ),
			Choose = type => Adopt( type, step ),
			Design = () => new ArchBuildingDesigner( panel, Owner, Chosen, designed => ApplyDesigned( designed, step ) ).Show(),
			Reload = Reread,
			Remove = item => Remove( item.Value ),
			Restore = () => {
				ArchStorage.RestoreArchetypes();
				Reread();
			}
		};

		shelf.DesignButton.Visible = true;
		shelf.DesignButton.ToolTip = "Design a building type";
		shelf.ReloadButton.Visible = true;
		shelf.RestoreButton.Visible = removed.Count > 0;
		shelf.RestoreButton.ToolTip = $"Bring back {string.Join( ", ", removed )}";
		shelf.Set( offered );

		step.Add( shelf );
	}

	void Stamps( ToolSidebarWidget panel, ArchWorkflowStep step ) {
		if ( savedOffered.Count == 0 ) {
			step.Layout.Add( ArchSidebarLayout.Advice( "No saved buildings yet. Right-click a building in the Plan Layers stack and Save, and it turns up here." ) );

			return;
		}

		var shelf = new ArchPresetBrowser<ArchBuildingAsset>( panel, Owner.Kit, Scope( "stamps" ), "Saved buildings" ) {
			Chosen = asset => saved is not null && string.Equals( saved.Name, asset.Name, StringComparison.OrdinalIgnoreCase ),
			Choose = asset => {
				Hold( asset );

				step.Chose();
			},
			Reload = RereadSaved,
			Remove = item => Discard( item.Value )
		};

		shelf.ReloadButton.Visible = true;
		shelf.Set( savedOffered );

		step.Add( shelf );
	}

	List<ArchPresetItem<ArchBuildingAsset>> Kept() {
		return ArchLayerAssets.Buildings()
			.Select( asset => new ArchPresetItem<ArchBuildingAsset> {
				Value = asset,
				Name = asset.Title,
				Detail = ArchBuildingStamp.Describe( asset ),
				Identity = ArchBuildingStamp.Identity( asset ),
				Category = "Saved",
				Glyph = "home_work",
				Tags = asset.Name,
				View = ArchPresetPreview.Quarter,
				Focus = ArchPreviewFocus.Whole,
				Recipe = () => ArchBuildingStamp.Stage( asset )
			} )
			.ToList();
	}

	void Discard( ArchBuildingAsset asset ) {
		if ( !ArchLayerAssets.RemoveBuilding( asset.Name ) ) {
			Log.Warning( $"Architecture: could not remove the saved building '{asset.Title}'." );

			return;
		}

		if ( saved is not null && string.Equals( saved.Name, asset.Name, StringComparison.OrdinalIgnoreCase ) ) {
			Hold( null );
		}

		RereadSaved();
		Refresh();
	}

	void Hold( ArchBuildingAsset asset ) {
		saved = asset;
		savedGhost = asset is null ? null : ArchBuildingStamp.Ghost( asset );
	}

	void RereadSaved() {
		var chosen = saved?.Name;

		savedStamp = ArchLayerAssets.Revision;
		savedOffered = Kept();

		Hold( savedOffered.Select( item => item.Value ).FirstOrDefault( asset => string.Equals( asset.Name, chosen, StringComparison.OrdinalIgnoreCase ) ) );
	}

	void Reread() {
		Owner.ReloadArchetypes();
		Seed();
		Refresh();
	}

	void Remove( ArchArchetype type ) {
		if ( !ArchStorage.RemoveArchetype( type.Name ) ) {
			Log.Warning( $"Architecture: could not remove the building type '{type.Name}'." );
			return;
		}

		Owner.ReloadArchetypes();

		// A drag reads its heights off the chosen type, so removing that one has to leave another standing.
		if ( Owner.FindArchetype( archetype ) is null ) {
			archetype = Owner.Archetypes.FirstOrDefault()?.Name ?? "";
		}

		Seed();
		Refresh();
	}

	List<ArchPresetItem<ArchArchetype>> Offered() {
		return Owner.Archetypes
			.Where( type => mode != BuildingMode.Prefab || type.HasLayout )
			.Select( type => new ArchPresetItem<ArchArchetype> {
				Value = type,
				Name = type.Title,
				Detail = ArchArchetypeStage.Describe( type ),
				Identity = ArchArchetypeStage.Identity( type ),
				Category = type.HasLayout ? "Has layout" : "Shell",
				Badge = type.HasLayout ? "layout" : null,
				Glyph = type.Icon,
				Tags = $"{type.Name} {type.Description}",
				View = ArchPresetPreview.Quarter,
				Focus = ArchPreviewFocus.Whole,
				Recipe = () => ArchArchetypeStage.Of( type, Owner.Kit )
			} )
			.ToList();
	}

	void Inherit() {
		if ( !Extending || seeded ) {
			return;
		}

		if ( Owner.ActiveBuilding()?.Archetype is { Length: > 0 } existing && Owner.FindArchetype( existing ) is not null ) {
			archetype = existing;
		}

		Seed();
	}

	void Adopt( ArchArchetype type, ArchWorkflowStep step ) {
		archetype = type.Name;

		Seed();
		Restyle();

		step.Chose();
	}

	void ApplyDesigned( ArchArchetype designed, ArchWorkflowStep step ) {
		archetype = designed.Name;

		Owner.RegisterArchetype( designed );
		Seed();
		Restyle();

		step.Chose();
	}

	void Seed() {
		if ( Chosen is not { } type ) {
			return;
		}

		var rules = Extending ? type.Wing : type.Shell;

		if ( rules.Roof is { } style ) {
			roofStyle = style;
		}

		wingRoof = type.WingRoof;
		eaveDrop = type.WingEaveDrop;
		wallHeight = rules.WallHeight;
		wallThickness = rules.WallThickness > 0f ? rules.WallThickness : rules.Wall?.Thickness ?? 0f;
		roofThickness = rules.RoofThickness;
		roofPitch = rules.RoofPitch;
		withGutters = rules.Gutters ?? true;
		withFloor = rules.Floor ?? true;
		withFoundation = rules.Foundation ?? true;
		withFascia = rules.Fascia ?? true;
		withSoffit = rules.Soffit ?? true;
		withCeiling = rules.Ceiling ?? false;
		withFrame = rules.Frame ?? false;

		seeded = true;
	}

	void Ridge( Layout roof ) {
		if ( !Wants( ArchOptionGroup.Ridge ) || !Roofed
			|| !ArchRoofPlane.Ridged( Extending ? ArchBuild.Winged( wingRoof ) : roofStyle ) ) {
			return;
		}

		using var grid = ArchIconGrid.In( roof );

		foreach ( var value in Enum.GetValues<RidgeRun>() ) {
			var captured = value;

			grid.Pick( ArchIcons.RidgeAdvice( captured ), ArchIcons.RidgeSlug( captured ), ArchIcons.RidgeGlyph( captured ), ridge == captured,
				() => Set( () => ridge = captured ) );
		}
	}

	static string Fallback( SectionRoof choice ) => choice switch {
		SectionRoof.Continue => "merge_type",
		SectionRoof.Hip => "roofing",
		SectionRoof.Gable => "change_history",
		SectionRoof.Capped => "crop_din",
		_ => "block"
	};

	static string Describe( SectionRoof choice ) => choice switch {
		SectionRoof.Continue => "Continue the roof (one hip, valleys)",
		SectionRoof.Hip => "Own hip, stepped down",
		SectionRoof.Gable => "Own gable, stepped down",
		SectionRoof.Capped => "Flat with a parapet cap",
		_ => "No roof"
	};
}