Editor/Road/ArchRoadUi.cs

Editor UI builder for road, bridge and tunnel parts. It constructs sidebar sections and controls to edit ArchRoadPart, ArchBridgePart, ArchTunnelPart and crossings, reading types from catalogs and wiring change/refresh callbacks.

File Access
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchRoadUi
{
	public static void Build( ToolSidebarWidget panel, ArchRoadPart road, ArchKit kit, Action refresh, Action changed )
	{
		Carriageway( panel, road, changed );
		Pavement( panel, road, changed );
		Markings( panel, road, kit, changed );
		Barriers( panel, road, refresh, changed );
		Ground( panel, road, changed );
	}

	// The section a drag lays down. Camber and precision describe how it is built rather than how wide it is,
	// so they fold with the rest of the construction.
	static void Carriageway( ToolSidebarWidget panel, ArchRoadPart road, Action changed )
	{
		ArchSidebarSection.Show( panel, "Road.placement", "Carriageway", group =>
		{
			group.Add( ArchPartUi.Number( "Width", road.Width, 288f, value => road.Width = value, changed ) );
			group.Add( ArchPartUi.Integer( "Lanes", road.Lanes, 2, value => road.Lanes = value, changed ) );
		} );

		ArchSidebarSection.Disclosure( panel, "Road.build", "Construction", true, group =>
		{
			group.Add( ArchPartUi.Number( "Camber", road.Camber, 1.5f, value => road.Camber = value, changed ) );
			group.Add( ArchPartUi.Number( "Precision", road.Precision, 48f, value => road.Precision = value, changed ) );
		} );
	}

	static void Pavement( ToolSidebarWidget panel, ArchRoadPart road, Action changed )
	{
		ArchSidebarSection.Show( panel, "Road.pavement", "Pavement", group =>
		{
			using ( var grid = ArchIconGrid.In( group ) )
			{
				foreach ( var side in Enum.GetValues<RoadSide>() )
				{
					var captured = side;

					grid.Pick( $"Pavement: {captured}", Slug( captured ), SideGlyph( captured ), road.Pavements == captured,
						() => { road.Pavements = captured; changed?.Invoke(); } );
				}
			}

			using ( var grid = ArchIconGrid.In( group ) )
			{
				foreach ( var style in Enum.GetValues<KerbStyle>() )
				{
					var captured = style;

					grid.Pick( $"Kerb: {captured}", $"kerb_{captured}".ToLowerInvariant(), KerbGlyph( captured ),
						road.Kerb == captured, () => { road.Kerb = captured; changed?.Invoke(); } );
				}
			}

			group.Add( ArchPartUi.Number( "Width", road.PavementWidth, 96f, value => road.PavementWidth = value, changed ) );
		} );

		ArchSidebarSection.Disclosure( panel, "Road.kerb", "Kerb and paving", true, group =>
		{
			group.Add( ArchPartUi.Number( "Kerb height", road.KerbHeight, 6f, value => road.KerbHeight = value, changed ) );
			group.Add( ArchPartUi.Number( "Kerb stone", road.KerbWidth, 5f, value => road.KerbWidth = value, changed ) );
			group.Add( ArchPartUi.Integer( "Courses", road.PavementCourses, 2, value => road.PavementCourses = value, changed ) );
			group.Add( ArchPartUi.Number( "Paver size", road.PaverSize, 0f, value => road.PaverSize = value, changed ) );
		} );
	}

	static void Markings( ToolSidebarWidget panel, ArchRoadPart road, ArchKit kit, Action changed )
	{
		ArchSidebarSection.Disclosure( panel, "Road.markings", "Markings", true, group =>
		{
			group.Add( Line( "Centre", road.CentreLine, kit, value => road.CentreLine = value, changed ) );
			group.Add( Line( "Lane", road.LaneLine, kit, value => road.LaneLine = value, changed ) );
			group.Add( Line( "Edge", road.EdgeLine, kit, value => road.EdgeLine = value, changed ) );
		} );
	}

	static void Barriers( ToolSidebarWidget panel, ArchRoadPart road, Action refresh, Action changed )
	{
		ArchSidebarSection.Show( panel, "Road.barrier", "Barrier side", group =>
		{
			using ( var grid = ArchIconGrid.In( group ) )
			{
				foreach ( var side in Enum.GetValues<RoadSide>() )
				{
					var captured = side;

					grid.Pick( $"Barrier: {captured}", Slug( captured ), SideGlyph( captured ), road.Barriers == captured,
						() => { road.Barriers = captured; refresh?.Invoke(); changed?.Invoke(); } );
				}
			}

			if ( road.Barriers != RoadSide.None )
			{
				group.Add( ArchPartUi.Number( "Inset", road.BarrierInset, 10f, value => road.BarrierInset = value, changed ) );
			}
		} );

		// No barrier has no inset to set and no bays to divide, so neither is there rather than greyed.
		if ( road.Barriers != RoadSide.None )
		{
			ArchBarrierUi.Build( panel, road.Barrier, refresh, changed );
		}
	}

	static void Ground( ToolSidebarWidget panel, ArchRoadPart road, Action changed )
	{
		ArchSidebarSection.Show( panel, "Road.ground", "Ground", group =>
		{
			group.Add( ArchPartUi.Check( "Follow terrain", road.Conform, value => road.Conform = value, changed ) );
			group.Add( ArchPartUi.Check( "Stamp terrain", road.Stamp, value => road.Stamp = value, changed ) );
		} );
	}

	// Shared by Road and Select, so the kind grid sits here, not in either.
	public static void Kinds( Layout group, CrossingKind current, Action<CrossingKind> assign )
	{
		using var grid = ArchIconGrid.In( group );

		foreach ( var kind in Enum.GetValues<CrossingKind>() )
		{
			var captured = kind;

			grid.Pick( $"Crossing: {captured}", $"crossing_{captured}".ToLowerInvariant(), CrossingGlyph( captured ),
				current == captured, () => assign( captured ) );
		}
	}

	public static void Crossing( ToolSidebarWidget panel, ArchRoadCrossing crossing, Action changed )
	{
		ArchSidebarSection.Show( panel, "Crossing.placement", "Crossing", group =>
		{
			Kinds( group, crossing.Kind, value => { crossing.Kind = value; changed?.Invoke(); } );

			using ( var grid = ArchIconGrid.In( group ) )
			{
				foreach ( var side in Enum.GetValues<RoadSide>() )
				{
					var captured = side;

					grid.Pick( $"Side: {captured}", Slug( captured ), SideGlyph( captured ), crossing.Side == captured,
						() => { crossing.Side = captured; changed?.Invoke(); } );
				}
			}

			group.Add( ArchPartUi.Number( "Width", crossing.Width, 132f, value => crossing.Width = value, changed ) );
			group.Add( ArchPartUi.Number( "Along the road", crossing.Distance, 0f, value => crossing.Distance = value, changed ) );
		} );

		ArchSidebarSection.Disclosure( panel, "Crossing.splay", "Splay and surface", true, group =>
		{
			group.Add( ArchPartUi.Number( "Splay", crossing.Splay, 36f, value => crossing.Splay = value, changed ) );
			group.Add( ArchPartUi.Number( "Splay segments", crossing.SplaySegments, 0f,
				value => crossing.SplaySegments = (int)MathF.Round( value ), changed ) );

			using var apron = ArchIconGrid.In( group );

			apron.Toggle( "Carry the surface across the pavement", "opt_apron", "texture", crossing.Apron,
				value => { crossing.Apron = value; changed?.Invoke(); } );
		} );
	}

	// Picking a type replaces the whole structure - a bridge IS its structure.
	public static void Types( ToolSidebarWidget panel, ArchTool tool, string current, Action<ArchBridgeType> adopt )
	{
		var types = ArchRoadCatalogs.Bridges();

		if ( types.Count == 0 )
		{
			panel.Layout.Add( ArchSidebarLayout.Advice( $"No types in Assets/{ArchRoadCatalogs.BridgeDirectoryPath}." ) );

			return;
		}

		ArchSidebarSection.Show( panel, "Bridge.type", "Type", group =>
		{
			using ( var grid = ArchIconGrid.In( group ) )
			{
				foreach ( var type in types )
				{
					var captured = type;

					grid.Pick( $"{captured.Title} — {captured.Description}", $"bridge_{captured.Name}", captured.Icon,
						string.Equals( current, captured.Name, StringComparison.OrdinalIgnoreCase ), () => adopt( captured ) );
				}
			}

			using var reload = ArchIconGrid.In( group );

			reload.Toggle( "Reload bridge types from disk", "reload_bridges", "refresh", false, _ =>
			{
				if ( ArchRoadCatalogs.Bridge( current ) is { } again )
				{
					adopt( again );
				}
			} );
		} );

		panel.Layout.Add( ArchSidebarLayout.Advice( ArchRoadCatalogs.Bridge( current ) is { } chosen
			? $"{chosen.Title} — {chosen.Description}"
			: "Hand-tuned — no type." ) );
	}

	// Road entry REPLACED, not patched - one field list stops a setting silently going missing.
	public static ArchBridgePart Retype( ArchTool tool, ArchBridgePart bridge, ArchBridgeType type )
	{
		if ( tool.Plan.RoadCarrying( bridge ) is not { } road )
		{
			return bridge;
		}

		var dressed = type.Spanning( bridge.From, bridge.To );

		dressed.Id = bridge.Id;
		dressed.Name = bridge.Name;

		road.Bridges[road.Bridges.IndexOf( bridge )] = dressed;

		return dressed;
	}

	// A blade IS the gap - no count or width; an empty type gate shows every group.
	public static void Bridge( ToolSidebarWidget panel, ArchBridgePart bridge, ArchBridgeType type, Action refresh, Action changed )
	{
		// Piers and ends decide what the span looks like from the street; the deck's own section is what the
		// .bridgetype.json already carries, so it folds.
		if ( Wanted( type, ArchBridgeGroup.Spans ) )
		{
			ArchSidebarSection.Show( panel, "Bridge.spans", "Spans", spans =>
			{
				spans.Add( ArchPartUi.Number( "Piers", bridge.Bents, 0f, value => bridge.Bents = (int)MathF.Round( value ), changed ) );
				spans.Add( ArchPartUi.Number( "Pier spacing", bridge.BentSpacing, 600f, value => bridge.BentSpacing = value, changed ) );
			} );
		}

		if ( Wanted( type, ArchBridgeGroup.Pier ) )
		{
			ArchSidebarSection.Show( panel, "Bridge.pier", "Pier", group =>
			{
				using var grid = ArchIconGrid.In( group );

				foreach ( var style in Enum.GetValues<PierStyle>() )
				{
					var captured = style;

					grid.Pick( $"Pier: {captured}", $"pier_{captured}".ToLowerInvariant(), PierGlyph( captured ),
						bridge.Pier == captured, () => { bridge.Pier = captured; refresh?.Invoke(); changed?.Invoke(); } );
				}
			} );
		}

		if ( Wanted( type, ArchBridgeGroup.Ends ) || Wanted( type, ArchBridgeGroup.Parapet ) )
		{
			ArchSidebarSection.Show( panel, "Bridge.ends", "Ends", group =>
			{
				using var grid = ArchIconGrid.In( group );

				if ( Wanted( type, ArchBridgeGroup.Ends ) )
				{
					grid.Toggle( "Abutments and wing walls where the deck meets the bank", "opt_abutment", "foundation", bridge.Abutments,
						value => { bridge.Abutments = value; refresh?.Invoke(); changed?.Invoke(); } );
				}

				if ( Wanted( type, ArchBridgeGroup.Parapet ) )
				{
					grid.Toggle( "Parapet along both deck edges", "opt_parapet", "fence", bridge.Parapet,
						value => { bridge.Parapet = value; refresh?.Invoke(); changed?.Invoke(); } );
				}
			} );
		}

		if ( Wanted( type, ArchBridgeGroup.Deck ) )
		{
			ArchSidebarSection.Disclosure( panel, "Bridge.deck", "Deck", true, deck =>
			{
				deck.Add( ArchPartUi.Number( "Slab", bridge.Slab, 14f, value => bridge.Slab = value, changed ) );
				deck.Add( ArchPartUi.Number( "Girder", bridge.Girder, 40f, value => bridge.Girder = value, changed ) );
				deck.Add( ArchPartUi.Number( "Edge setback", bridge.EdgeBeam, 8f, value => bridge.EdgeBeam = value, changed ) );
				deck.Add( ArchPartUi.Number( "Soffit ribs", bridge.Ribs, 3f, value => bridge.Ribs = (int)MathF.Round( value ), changed ) );
				deck.Add( ArchPartUi.Number( "Upstand", bridge.Upstand, 12f, value => bridge.Upstand = value, changed ) );
				deck.Add( ArchPartUi.Number( "Upstand width", bridge.UpstandWidth, 16f, value => bridge.UpstandWidth = value, changed ) );
			} );
		}

		if ( Wanted( type, ArchBridgeGroup.Pier ) )
		{
			ArchSidebarSection.Disclosure( panel, "Bridge.piersize", "Pier size", true, pier =>
			{
				if ( bridge.Pier != PierStyle.Blade )
				{
					pier.Add( ArchPartUi.Number( "Columns", bridge.Columns, 2f, value => bridge.Columns = (int)MathF.Round( value ), changed ) );
					pier.Add( ArchPartUi.Number( "Width", bridge.PierWidth, 60f, value => bridge.PierWidth = value, changed ) );
				}

				pier.Add( ArchPartUi.Number( "Depth", bridge.PierDepth, 30f, value => bridge.PierDepth = value, changed ) );
				pier.Add( ArchPartUi.Number( "Batter", bridge.Batter, 4f, value => bridge.Batter = value, changed ) );
				pier.Add( ArchPartUi.Number( "Headstock", bridge.Headstock, 20f, value => bridge.Headstock = value, changed ) );
			} );
		}

		if ( bridge.Abutments && Wanted( type, ArchBridgeGroup.Ends ) )
		{
			ArchSidebarSection.Disclosure( panel, "Bridge.abutment", "Abutment", true, ends =>
			{
				ends.Add( ArchPartUi.Number( "Depth", bridge.AbutmentDepth, 72f, value => bridge.AbutmentDepth = value, changed ) );
				ends.Add( ArchPartUi.Number( "Wing wall", bridge.WingWall, 120f, value => bridge.WingWall = value, changed ) );
				ends.Add( ArchPartUi.Number( "Wing splay", bridge.WingSplay, 30f, value => bridge.WingSplay = value, changed ) );
			} );
		}

		if ( bridge.Parapet && Wanted( type, ArchBridgeGroup.Parapet ) )
		{
			ArchBarrierUi.Build( panel, bridge.Barrier, refresh, changed );
		}
	}

	static bool Wanted( ArchBridgeType type, ArchBridgeGroup group ) => type?.Wants( group ) ?? true;

	// A tunnel IS its structure - a type replaces it whole, never merges fields.
	public static void TunnelTypes( ToolSidebarWidget panel, ArchTool tool, string current, Action<ArchTunnelType> adopt )
	{
		var types = ArchRoadCatalogs.Tunnels();

		if ( types.Count == 0 )
		{
			panel.Layout.Add( ArchSidebarLayout.Advice( $"No types in Assets/{ArchRoadCatalogs.TunnelDirectoryPath}." ) );

			return;
		}

		ArchSidebarSection.Show( panel, "Tunnel.type", "Type", group =>
		{
			using ( var grid = ArchIconGrid.In( group ) )
			{
				foreach ( var type in types )
				{
					var captured = type;

					grid.Pick( $"{captured.Title} — {captured.Description}", $"tunnel_{captured.Name}", captured.Icon,
						string.Equals( current, captured.Name, StringComparison.OrdinalIgnoreCase ), () => adopt( captured ) );
				}
			}

			using var reload = ArchIconGrid.In( group );

			reload.Toggle( "Reload tunnel types from disk", "reload_tunnels", "refresh", false, _ =>
			{
				if ( ArchRoadCatalogs.Tunnel( current ) is { } again )
				{
					adopt( again );
				}
			} );
		} );

		panel.Layout.Add( ArchSidebarLayout.Advice( ArchRoadCatalogs.Tunnel( current ) is { } chosen
			? $"{chosen.Title} — {chosen.Description}"
			: "Hand-tuned — no type." ) );
	}

	// A bay belongs to the bore, not its kind - re-typing keeps the services carved off it.
	public static ArchTunnelPart Retype( ArchTool tool, ArchTunnelPart tunnel, ArchTunnelType type )
	{
		if ( tool.Plan.RoadHolding( tunnel ) is not { } road )
		{
			return tunnel;
		}

		var driven = type.Driving( tunnel.From, tunnel.To );

		driven.Id = tunnel.Id;
		driven.Name = tunnel.Name;

		road.Tunnels[road.Tunnels.IndexOf( tunnel )] = driven;

		return driven;
	}

	// An empty type gate means every group - a hand-tuned bore keeps the whole panel.
	public static void Tunnel( ToolSidebarWidget panel, ArchTunnelPart tunnel, ArchTunnelType type, Action refresh, Action changed )
	{
		// The bore's shape and its mouths read from the street. Its section is what the .tunneltype.json carries,
		// and it clears the road's own reach anyway, so it folds.
		if ( Wanted( type, ArchTunnelGroup.Bore ) )
		{
			ArchSidebarSection.Show( panel, "Tunnel.bore", "Bore", group =>
			{
				using var grid = ArchIconGrid.In( group );

				foreach ( var bore in Enum.GetValues<TunnelBore>() )
				{
					var captured = bore;

					grid.Pick( $"Bore: {captured}", $"bore_{captured}".ToLowerInvariant(), BoreGlyph( captured ),
						tunnel.Bore == captured, () => { tunnel.Bore = captured; refresh?.Invoke(); changed?.Invoke(); } );
				}
			} );
		}

		if ( Wanted( type, ArchTunnelGroup.Walkway ) )
		{
			ArchSidebarSection.Show( panel, "Tunnel.walkway", "Walkway", group =>
			{
				using ( var grid = ArchIconGrid.In( group ) )
				{
					foreach ( var side in Enum.GetValues<RoadSide>() )
					{
						var captured = side;

						grid.Pick( $"Walkway: {captured}", $"walk_{captured}".ToLowerInvariant(), SideGlyph( captured ),
							tunnel.Walkways == captured, () => { tunnel.Walkways = captured; changed?.Invoke(); } );
					}
				}

				if ( tunnel.Walkways == RoadSide.None )
				{
					return;
				}

				group.Add( ArchPartUi.Number( "Width", tunnel.WalkwayWidth, 48f, value => tunnel.WalkwayWidth = value, changed ) );
				group.Add( ArchPartUi.Number( "Height", tunnel.WalkwayHeight, 12f, value => tunnel.WalkwayHeight = value, changed ) );
			} );
		}

		if ( Wanted( type, ArchTunnelGroup.Portal ) )
		{
			ArchSidebarSection.Show( panel, "Tunnel.ends", "Ends", group =>
			{
				using var grid = ArchIconGrid.In( group );

				grid.Toggle( "Portal headwalls and wing walls at both mouths", "opt_portal", "door_front", tunnel.Portals,
					value => { tunnel.Portals = value; refresh?.Invoke(); changed?.Invoke(); } );
			} );
		}

		if ( Wanted( type, ArchTunnelGroup.Ground ) )
		{
			ArchSidebarSection.Show( panel, "Tunnel.ground", "Ground", group =>
			{
				using var grid = ArchIconGrid.In( group );

				grid.Toggle( "Bore: leave the ground standing over the span instead of carving a cut down to the road",
					"opt_bored", "landslide", tunnel.Bored, value => { tunnel.Bored = value; refresh?.Invoke(); changed?.Invoke(); } );
			} );
		}

		if ( Wanted( type, ArchTunnelGroup.Bore ) )
		{
			ArchSidebarSection.Disclosure( panel, "Tunnel.section", "Section", true, section =>
			{
				section.Add( ArchPartUi.Number( "Headroom", tunnel.Headroom, 216f, value => tunnel.Headroom = value, changed ) );
				section.Add( ArchPartUi.Number( "Springing", tunnel.Springing, 132f, value => tunnel.Springing = value, changed ) );
				section.Add( ArchPartUi.Number( "Clearance", tunnel.Clearance, 24f, value => tunnel.Clearance = value, changed ) );
				section.Add( ArchPartUi.Number( "Lining", tunnel.Lining, 18f, value => tunnel.Lining = value, changed ) );
				section.Add( ArchPartUi.Number( "Invert", tunnel.Invert, 14f, value => tunnel.Invert = value, changed ) );
				section.Add( ArchPartUi.Number( "Crown facets", tunnel.Segments, 6f, value => tunnel.Segments = (int)MathF.Round( value ), changed ) );
			} );
		}

		if ( tunnel.Portals && Wanted( type, ArchTunnelGroup.Portal ) )
		{
			ArchSidebarSection.Disclosure( panel, "Tunnel.portal", "Portal", true, portal =>
			{
				portal.Add( ArchPartUi.Number( "Depth", tunnel.PortalDepth, 36f, value => tunnel.PortalDepth = value, changed ) );
				portal.Add( ArchPartUi.Number( "Collar", tunnel.Collar, 72f, value => tunnel.Collar = value, changed ) );
				portal.Add( ArchPartUi.Number( "Rise", tunnel.PortalRise, 42f, value => tunnel.PortalRise = value, changed ) );
				portal.Add( ArchPartUi.Number( "Reach", tunnel.PortalReach, 72f, value => tunnel.PortalReach = value, changed ) );
				portal.Add( ArchPartUi.Number( "Hood", tunnel.Hood, 54f, value => tunnel.Hood = value, changed ) );
				portal.Add( ArchPartUi.Number( "Hood depth", tunnel.HoodDepth, 18f, value => tunnel.HoodDepth = value, changed ) );
				portal.Add( ArchPartUi.Number( "Coping", tunnel.Coping, 6f, value => tunnel.Coping = value, changed ) );
				portal.Add( ArchPartUi.Number( "Wing wall", tunnel.WingWall, 168f, value => tunnel.WingWall = value, changed ) );
				portal.Add( ArchPartUi.Number( "Wing splay", tunnel.WingSplay, 40f, value => tunnel.WingSplay = value, changed ) );
			} );
		}

		if ( Wanted( type, ArchTunnelGroup.Ground ) && tunnel.Bored )
		{
			panel.Layout.Add( ArchSidebarLayout.Advice( "Bored leaves the hill standing when the terrain is stamped. The hole through it is yours to paint in the terrain editor." ) );
		}
	}

	static bool Wanted( ArchTunnelType type, ArchTunnelGroup group ) => type?.Wants( group ) ?? true;

	static Widget Line( string label, string value, ArchKit kit, Action<string> assign, Action changed )
	{
		var holder = new Widget( null );
		holder.Layout = Layout.Row();
		holder.Layout.Spacing = 4;

		var name = new Label( label );
		name.MinimumWidth = 60;
		holder.Layout.Add( name );

		var combo = new ComboBox();
		combo.AddItem( "none", null, () => { assign( "" ); changed?.Invoke(); }, selected: string.IsNullOrWhiteSpace( value ) );

		foreach ( var line in kit.RoadLines )
		{
			var captured = line.Name;

			combo.AddItem( captured, null, () => { assign( captured ); changed?.Invoke(); }, selected: captured == value );
		}

		holder.Layout.Add( combo );

		return holder;
	}

	static string Slug( RoadSide side ) => $"side_{side}".ToLowerInvariant();

	static string SideGlyph( RoadSide side ) => side switch
	{
		RoadSide.None => "block",
		RoadSide.Left => "align_horizontal_left",
		RoadSide.Right => "align_horizontal_right",
		_ => "align_horizontal_center"
	};

	static string KerbGlyph( KerbStyle style ) => style switch
	{
		KerbStyle.Square => "crop_square",
		KerbStyle.Rolled => "rounded_corner",
		_ => "signal_cellular_4_bar"
	};

	static string PierGlyph( PierStyle style ) => style switch
	{
		PierStyle.Columns => "circle",
		PierStyle.Piers => "view_column",
		_ => "vertical_shades"
	};

	static string BoreGlyph( TunnelBore bore ) => bore switch
	{
		TunnelBore.Box => "crop_square",
		TunnelBore.Arch => "arch",
		_ => "vpn_lock"
	};

	static string CrossingGlyph( CrossingKind kind ) => kind switch
	{
		CrossingKind.Driveway => "garage",
		CrossingKind.Footpath => "directions_walk",
		_ => "menu"
	};
}