Editor/Pipe/ArchPipeUi.cs
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// Read by the Pipes & Cables subtool, by the Brackets subtool and by Select, off the same records - a draft
// and a placed run carry identical controls because they ARE the same records, and a knob that exists in one
// place cannot drift from the other two. Each form takes a Layout rather than a sidebar, so the tool can ask
// them one per step and the sheet can lay them all out in a row.
public static class ArchPipeUi {
	// A tick or a picker that GATES the fields under it has to rebuild the sheet as well as commit, or the
	// fields it reveals never arrive and the ones it hides stay put.
	static Action Both( Action changed, Action refresh ) {
		return () => {
			changed?.Invoke();
			refresh?.Invoke();
		};
	}

	public static void Content( Layout group, ArchPipePart part, Action refresh, Action changed ) {
		var both = Both( changed, refresh );

		using var grid = ArchIconGrid.In( group );

		grid.Pick( "Pipe — round stock, socketed and collared", "pipe_content_pipe", "water_damage",
			part.Content == PipeContent.Pipe, () => { part.Content = PipeContent.Pipe; both(); } );

		grid.Pick( "Conduit — small round electrical runs", "pipe_content_conduit", "power_input",
			part.Content == PipeContent.Conduit, () => { part.Content = PipeContent.Conduit; both(); } );

		grid.Pick( "Cable — bundles that loop lazily where they turn", "pipe_content_cable", "cable",
			part.Content == PipeContent.Cable, () => { part.Content = PipeContent.Cable; both(); } );

		grid.Pick( "Tray — an open channel with the cables lying in it", "pipe_content_tray", "view_stream",
			part.Content == PipeContent.Tray, () => { part.Content = PipeContent.Tray; both(); } );

		grid.Pick( "Duct — square section, fabricated in lengths", "pipe_content_duct", "crop_square",
			part.Content == PipeContent.Duct, () => { part.Content = PipeContent.Duct; both(); } );
	}

	// Which surface of the volume the lanes seat against. Left on "where you drag", the face the cursor came
	// to rest on decides - looking down works the floor, looking up at a ceiling hangs off it.
	public static void Mount( Layout group, PipeMount? picked, Action<PipeMount?> chosen ) {
		using var grid = ArchIconGrid.In( group );

		grid.Pick( "Where you drag — the floor looking down, the ceiling looking up at it", "pipe_mount_dragged", "ads_click",
			picked is null, () => chosen( null ) );

		grid.Pick( "Ceiling — hung under the soffit", "pipe_mount_ceiling", "border_top",
			picked == PipeMount.Ceiling, () => chosen( PipeMount.Ceiling ) );

		grid.Pick( "Floor — laid on the slab", "pipe_mount_floor", "border_bottom",
			picked == PipeMount.Floor, () => chosen( PipeMount.Floor ) );

		grid.Pick( "Wall — stacked up the nearest elevation", "pipe_mount_wall", "wallpaper",
			picked == PipeMount.Wall, () => chosen( PipeMount.Wall ) );
	}

	// Stock sizes as picks with the exact figure under them, so the usual answer is one click and an odd one is
	// still typeable.
	public static void Section( Layout group, ArchPipePart part, Action refresh, Action changed ) {
		using ( var grid = ArchIconGrid.In( group ) ) {
			Diameter( grid, part, 0.5f, "0.5 in", refresh, changed );
			Diameter( grid, part, 1f, "1 in", refresh, changed );
			Diameter( grid, part, 2f, "2 in", refresh, changed );
			Diameter( grid, part, 4f, "4 in", refresh, changed );
			Diameter( grid, part, 6f, "6 in", refresh, changed );
			Diameter( grid, part, 8f, "8 in", refresh, changed );
			Diameter( grid, part, 12f, "12 in", refresh, changed );
		}

		group.Add( ArchPartUi.Number( "Diameter", part.Diameter, 4f, value => part.Diameter = MathF.Max( 0.5f, value ), changed ) );
	}

	static void Diameter( ArchIconGrid grid, ArchPipePart part, float diameter, string label, Action refresh, Action changed ) {
		grid.Pick( $"{label} — outside diameter", $"pipe_diameter_{diameter * 10f:0}", "radio_button_unchecked",
			MathF.Abs( part.Diameter - diameter ) < 0.01f, () => { part.Diameter = diameter; changed?.Invoke(); refresh?.Invoke(); } );
	}

	// The asset pickers hang off the PARENT widget rather than off the group, because a group inside a workflow
	// step is torn down on the next refresh and a dialog parented to one would go with it.
	public static void Materials( Widget parent, Layout group, ArchPipePart part, Action refresh, Action changed ) {
		var both = Both( changed, refresh );

		for ( var index = 0; index < part.Materials.Count; index++ ) {
			group.Add( Material( parent, part, index, both ) );
		}

		group.Add( new Button( "Add material slot", "add" ) {
			Clicked = () => {
				part.Materials.Add( "" );
				both();
			}
		} );

		group.Add( ArchPartUi.Integer( "Run index", part.RunMaterialIndex, 0,
			value => part.RunMaterialIndex = MaterialIndex( value, part.Materials.Count ), changed ) );
		group.Add( ArchPartUi.Integer( "Fitting index", part.FittingMaterialIndex, 0,
			value => part.FittingMaterialIndex = MaterialIndex( value, part.Materials.Count ), changed ) );
		group.Add( ArchPartUi.Integer( "Support index", part.SupportMaterialIndex, 0,
			value => part.SupportMaterialIndex = MaterialIndex( value, part.Materials.Count ), changed ) );
	}

	static Widget Material( Widget parent, ArchPipePart part, int index, Action changed ) {
		var holder = new Widget( parent );
		holder.Layout = Layout.Row();
		holder.Layout.Spacing = 4;

		var label = new Label( $"[{index}]" );
		label.MinimumWidth = 28;
		holder.Layout.Add( label );

		var path = part.Materials[index];
		var value = new Label( string.IsNullOrWhiteSpace( path ) ? "(pick material)" : System.IO.Path.GetFileNameWithoutExtension( path ) );
		value.MinimumWidth = 110;
		holder.Layout.Add( value );

		holder.Layout.Add( new Button( "", "image" ) {
			Clicked = () => {
				var picker = AssetPicker.Create( parent, AssetType.Material );
				picker.Window.Title = $"Pipe material [{index}]";
				picker.OnAssetPicked = assets => {
					var asset = assets.FirstOrDefault();

					if ( asset is null ) {
						return;
					}

					part.Materials[index] = asset.Path;
					ArchStyle.InvalidateCache();
					changed?.Invoke();
				};
				picker.Show();
			}
		} );

		holder.Layout.Add( new Button( "", "close" ) {
			Clicked = () => {
				part.Materials.RemoveAt( index );
				part.RunMaterialIndex = ShiftedIndex( part.RunMaterialIndex, index, part.Materials.Count );
				part.FittingMaterialIndex = ShiftedIndex( part.FittingMaterialIndex, index, part.Materials.Count );
				part.SupportMaterialIndex = ShiftedIndex( part.SupportMaterialIndex, index, part.Materials.Count );
				ArchStyle.InvalidateCache();
				changed?.Invoke();
			}
		} );

		holder.Layout.AddStretchCell();

		return holder;
	}

	static int ShiftedIndex( int value, int removed, int count ) {
		if ( count == 0 ) {
			return 0;
		}

		if ( value > removed ) {
			return value - 1;
		}

		return Math.Min( value, count - 1 );
	}

	static int MaterialIndex( int value, int count ) {
		return Math.Clamp( value, 0, Math.Max( 0, count - 1 ) );
	}

	// What happens where another corridor is in the way. WHICH of two crossing runs steps is not here and
	// never will be - it is the stack's own order, so the answer is to drag one row past the other.
	public static void Crossings( Layout group, ArchPipePart part, Action refresh, Action changed ) {
		var both = Both( changed, refresh );

		using ( var grid = ArchIconGrid.In( group ) ) {
			grid.Pick( "Over — steps out of the surface to clear it, then comes back", "pipe_dodge_away", "trending_up",
				part.Dodge == PipeDodge.Away, () => { part.Dodge = PipeDodge.Away; both(); } );

			grid.Pick( "Under — dives back toward the surface instead", "pipe_dodge_toward", "trending_down",
				part.Dodge == PipeDodge.Toward, () => { part.Dodge = PipeDodge.Toward; both(); } );

			grid.Pick( "Around — steps sideways out of the corridor", "pipe_dodge_side", "swap_horiz",
				part.Dodge == PipeDodge.Side, () => { part.Dodge = PipeDodge.Side; both(); } );

			grid.Pick( "Straight through — it ignores what it crosses", "pipe_dodge_none", "close",
				part.Dodge == PipeDodge.None, () => { part.Dodge = PipeDodge.None; both(); } );
		}

		group.Add( ArchPartUi.Wrapped( "The LATER run steps over the earlier one. Drag a row past another in Plan Layers to swap which gives way." ) );

		if ( part.Dodge == PipeDodge.None ) {
			return;
		}

		group.Add( ArchPartUi.Number( "Clearance", part.Clearance, 2f, value => part.Clearance = MathF.Max( 0f, value ), changed ) );
	}

	// Whether it hangs its own supports and WHAT they are is one answer: a kind picked is a run that hangs, and
	// none is a run that does not.
	public static void Supports( Layout group, ArchPipePart part, Action refresh, Action changed ) {
		var both = Both( changed, refresh );

		using ( var grid = ArchIconGrid.In( group ) ) {
			grid.Pick( "None — the run hangs off nothing of its own", "bracket_none", "block",
				!part.Supports, () => { part.Supports = false; both(); } );

			Kinds( grid, part.SupportKind, kind => { part.SupportKind = kind; part.Supports = true; both(); }, part.Supports );
		}

		if ( !part.Supports ) {
			return;
		}

		group.Add( ArchPartUi.Number( "Spacing", part.SupportSpacing, 96f, value => part.SupportSpacing = MathF.Max( 12f, value ), changed ) );
	}

	public static void Kinds( Layout group, BracketKind kind, Action<BracketKind> chosen ) {
		using var grid = ArchIconGrid.In( group );

		Kinds( grid, kind, chosen, true );
	}

	static void Kinds( ArchIconGrid grid, BracketKind kind, Action<BracketKind> chosen, bool standing ) {
		grid.Pick( "Trapeze — two drops and a cross-piece under the bundle", "bracket_trapeze", "table_rows",
			standing && kind == BracketKind.Trapeze, () => chosen( BracketKind.Trapeze ) );

		grid.Pick( "Rod — one centre drop with a short cross-piece", "bracket_rod", "vertical_align_bottom",
			standing && kind == BracketKind.Rod, () => chosen( BracketKind.Rod ) );

		grid.Pick( "Clamp — a collar round each run and a tab back to the surface", "bracket_clamp", "radio_button_unchecked",
			standing && kind == BracketKind.Clamp, () => chosen( BracketKind.Clamp ) );

		grid.Pick( "Strap — a flat bar out of the face to each run", "bracket_strap", "horizontal_rule",
			standing && kind == BracketKind.Strap, () => chosen( BracketKind.Strap ) );
	}

	// THE GEOMETRY BUDGET. Every knob in here buys triangles and nothing else - a run detailed all the way
	// down still crosses, clears and hangs exactly where the detailed one did, which is what makes it safe to
	// turn a whole backdrop block down in one pass.
	public static void Detail( Layout group, ArchPipeDetail detail, Action refresh, Action changed ) {
		group.Add( ArchPartUi.Integer( "Section sides", detail.Sides, 8,
			value => detail.Sides = Math.Max( ArchPipeSection.LeastSides, value ), changed ) );

		group.Add( ArchPartUi.Integer( "Bend segments", detail.BendSegments, 3,
			value => detail.BendSegments = Math.Max( 0, value ), changed ) );

		group.Add( ArchPartUi.Wrapped( "Zero mitres every elbow into one edge. The bend RADIUS comes off the section, so a wide step still comes out as two proper elbows joined by a straight." ) );

		group.Add( ArchPartUi.Check( "Collars down the run", detail.Couplings, value => detail.Couplings = value, Both( changed, refresh ) ) );

		if ( detail.Couplings ) {
			group.Add( ArchPartUi.Number( "Collar spacing", detail.CouplingSpacing, 96f,
				value => detail.CouplingSpacing = MathF.Max( 8f, value ), changed ) );
		}

		group.Add( ArchPartUi.Check( "A joint at every turn", detail.BendCollars, value => detail.BendCollars = value, changed ) );
	}

	// The whole run sheet, which is what Select shows and what the tool seeds a new drag from.
	public static void Run( ToolSidebarWidget panel, ArchPipePart part, Action refresh, Action changed ) {
		Content( panel.AddGroup( "Content" ), part, refresh, changed );
		Section( panel.AddGroup( "Section" ), part, refresh, changed );
		Materials( panel, panel.AddGroup( "Material indexes" ), part, refresh, changed );
		Crossings( panel.AddGroup( "Crossings" ), part, refresh, changed );
		Supports( panel.AddGroup( "Supports" ), part, refresh, changed );

		ArchSidebarSection.Disclosure( panel, $"Pipe.detail.{part.Id}", "Geometry detail", true, group =>
			Detail( group, part.Detail, refresh, changed ) );
	}

	public static void Spacing( Layout group, ArchPipeBracketPart part, Action changed ) {
		group.Add( ArchPartUi.Number( "Spacing", part.Spacing, 96f, value => part.Spacing = MathF.Max( 12f, value ), changed ) );
	}

	// Margins around what the box already found, never sizes: the cross-piece spans whatever crosses it.
	public static void Members( Layout group, ArchPipeBracketPart part, Action changed ) {
		group.Add( ArchPartUi.Number( "Member width", part.MemberWidth, 2f, value => part.MemberWidth = MathF.Max( 0.5f, value ), changed ) );
		group.Add( ArchPartUi.Number( "Clearance", part.Clearance, 0.5f, value => part.Clearance = MathF.Max( 0f, value ), changed ) );
		group.Add( ArchPartUi.Number( "Overhang", part.Overhang, 3f, value => part.Overhang = MathF.Max( 0f, value ), changed ) );
	}

	public static void Brackets( ToolSidebarWidget panel, ArchPipeBracketPart part, Action refresh, Action changed ) {
		var group = panel.AddGroup( "Hangers" );

		Kinds( group, part.Kind, kind => { part.Kind = kind; changed?.Invoke(); refresh?.Invoke(); } );

		Spacing( group, part, changed );
		Members( group, part, changed );

		ArchSidebarSection.Disclosure( panel, $"Bracket.detail.{part.Id}", "Geometry detail", true, group =>
			Detail( group, part.Detail, refresh, changed ) );
	}

	public static string KindGlyph( BracketKind kind ) => kind switch {
		BracketKind.Rod => "vertical_align_bottom",
		BracketKind.Clamp => "radio_button_unchecked",
		BracketKind.Strap => "horizontal_rule",
		_ => "table_rows"
	};

	public static string MountName( PipeMount? mount ) => mount switch {
		PipeMount.Ceiling => "Ceiling",
		PipeMount.Floor => "Floor",
		PipeMount.Wall => "Wall",
		_ => "Where you drag"
	};

	public static string MountGlyph( PipeMount? mount ) => mount switch {
		PipeMount.Ceiling => "border_top",
		PipeMount.Floor => "border_bottom",
		PipeMount.Wall => "wallpaper",
		_ => "ads_click"
	};
}