Editor/Stair/ArchStairUi.cs
using System;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchStairUi {
	// Every group here folds, and remembers being folded (SidebarGroupWidget keys its own cookie off the title). A
	// stair carries five kinds of setting and an author is working on one of them, so the sheet opens as a list of
	// headings rather than as a page you have to scroll to the end of to find out what is on it. The Stair tool
	// asks the same five as five steps, which is why each is reachable on its own.
	public static void Build( ToolSidebarWidget panel, ArchStairPart stair, Action refresh, Action changed ) {
		Carriage( panel.AddGroup( "Carriage", collapsible: true ), stair, refresh, changed );

		var balustrade = panel.AddGroup( "Balustrade", collapsible: true );

		Guard( balustrade, stair, refresh, changed );
		Rails( balustrade, stair, refresh, changed );

		Wrap( panel.AddGroup( "Wall wrap", collapsible: true ), stair, refresh, changed );
		BuiltOf( panel.AddGroup( "Built of", collapsible: true ), stair, refresh, changed );

		if ( Solid( stair ) ) {
			return;
		}

		Underneath( panel.AddGroup( "Under the stairs", collapsible: true ), stair, refresh, changed );
	}

	// A stepped mass has no soffit to open, which is what makes the question below it absent rather than dead.
	public static bool Solid( ArchStairPart stair ) => stair.Core?.Fill == StairFill.Solid;

	public static void Carriage( Layout group, ArchStairPart stair, Action refresh, Action changed ) {
		using var grid = ArchIconGrid.In( group );

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

			grid.Pick( Describe( captured ), $"stair_{captured}".ToLowerInvariant(), Fallback( captured ),
				stair.Support == captured, () => { stair.Support = captured; changed?.Invoke(); refresh?.Invoke(); } );
		}
	}

	// Flanks on one row, the opt-out under them: the enum's own order strands Both alone below None, Left and Right.
	static readonly StairGuard[] Guards = { StairGuard.Both, StairGuard.Left, StairGuard.Right, StairGuard.None };

	// The one exclusive answer - which flanks are guarded - kept apart from what those flanks WEAR, so a sequence
	// can advance on the first and leave the second on a plain relayout.
	public static void Guard( Layout group, ArchStairPart stair, Action refresh, Action changed ) {
		using var grid = ArchIconGrid.In( group );

		foreach ( var value in Guards ) {
			var captured = value;

			grid.Pick( Describe( captured ), $"guard_{captured}".ToLowerInvariant(), Fallback( captured ),
				stair.Guard == captured, () => { stair.Guard = captured; changed?.Invoke(); refresh?.Invoke(); } );
		}
	}

	public static void Rails( Layout group, ArchStairPart stair, Action refresh, Action changed ) {
		if ( stair.Guard is StairGuard.Left or StairGuard.Both ) {
			Railing( group, "left", stair.LeftRailing, value => { stair.LeftRailing = value; changed?.Invoke(); } );
		}

		if ( stair.Guard is StairGuard.Right or StairGuard.Both ) {
			Railing( group, "right", stair.RightRailing, value => { stair.RightRailing = value; changed?.Invoke(); } );
		}

		using var rails = ArchIconGrid.In( group );

		rails.Toggle( "Handrail on walled sides — the rail bolted to the plaster where no balustrade stands", "opt_wall_rail", "handyman",
			stair.WallRail, value => { stair.WallRail = value; changed?.Invoke(); } );

		// Off, the two settings above stop deriving anything and only the railings actually placed on a step
		// stand - which is what makes each of them a thing you put somewhere and drag.
		rails.Toggle( "Automatic railings — off, only the railings you place by hand stand", "opt_auto_rails", "auto_fix_high",
			stair.AutoRailings, value => { stair.AutoRailings = value; changed?.Invoke(); refresh?.Invoke(); } );
	}

	public static void Wrap( Layout group, ArchStairPart stair, Action refresh, Action changed ) {
		using ( var wrap = ArchIconGrid.In( group ) ) {
			wrap.Pick( "Open — the flight stands in the room it was drawn in", "wrap_open", "crop_free",
				stair.Wrap == StairWrap.None, () => { stair.Wrap = StairWrap.None; changed?.Invoke(); refresh?.Invoke(); } );

			wrap.Pick( "Walls round the well — the flight carries its own core past whatever it started in", "wrap_walls", "square_foot",
				stair.Wrap == StairWrap.Walls, () => { stair.Wrap = StairWrap.Walls; changed?.Invoke(); refresh?.Invoke(); } );
		}

		if ( stair.Wrap != StairWrap.Walls ) {
			return;
		}

		group.Add( ArchPartUi.Number( "Wall thickness", stair.WrapThickness, 8f, value => stair.WrapThickness = value, changed ) );
	}

	public static void BuiltOf( Layout group, ArchStairPart stair, Action refresh, Action changed ) {
		using var fill = ArchIconGrid.In( group );

		fill.Pick( "Open — treads on stringers over an open raked soffit, the joinery stair", "fill_open", "stairs",
			!Solid( stair ), () => { Filled( stair, StairFill.Open ); changed?.Invoke(); refresh?.Invoke(); } );

		fill.Pick( "Solid — every flight a stepped mass standing on the shaft floor, the concrete stair", "fill_solid", "foundation",
			Solid( stair ), () => { Filled( stair, StairFill.Solid ); changed?.Invoke(); refresh?.Invoke(); } );
	}

	public static void Underneath( Layout group, ArchStairPart stair, Action refresh, Action changed ) {
		using var under = ArchIconGrid.In( group );

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

			under.Pick( Describe( captured ), $"under_{captured}".ToLowerInvariant(), Fallback( captured ),
				stair.Under == captured, () => { stair.Under = captured; changed?.Invoke(); refresh?.Invoke(); } );
		}
	}

	// What the guard on one side IS, once that side has one - the pair of choices only stands where the balustrade
	// setting above has already opened that flank, so a stair guarded on one side is never asked about the other.
	static void Railing( Layout group, string side, StairRailing railing, Action<StairRailing> assign ) {
		using var grid = ArchIconGrid.In( group );

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

			grid.Pick( Describe( captured, side ), $"railing_{side}_{captured}".ToLowerInvariant(), Fallback( captured ),
				railing == captured, () => assign( captured ) );
		}
	}

	static void Filled( ArchStairPart stair, StairFill fill ) {
		stair.Core ??= new ArchStairCore();
		stair.Core.Fill = fill;
	}

	public static void Sizes( ToolSidebarWidget panel, ArchStairPart stair, Action changed ) {
		var core = stair.Core ??= new ArchStairCore();
		var group = panel.AddGroup( "Shaft", collapsible: true );

		// The shaft's footprint is dragged on its own box and has no number here, but the climb keeps one: a
		// stair told to reach exactly one storey is a figure you type, not a height you aim at.
		group.Add( ArchPartUi.Number( "Climb", core.Rise, 128f, value => core.Rise = MathF.Max( 4f, value ), changed ) );
		group.Add( ArchPartUi.Number( "Flight width", stair.Width, 48f, value => stair.Width = value, changed ) );
		group.Add( ArchPartUi.Number( "Step rise", stair.StepRise, 8f, value => stair.StepRise = value, changed ) );
		group.Add( ArchPartUi.Number( "Step going", stair.StepGoing, 12f, value => stair.StepGoing = value, changed ) );
		group.Add( ArchPartUi.Number( "Nosing", stair.TreadNosing, 1f, value => stair.TreadNosing = value, changed ) );
		group.Add( ArchPartUi.Number( "Well gap", stair.WellGap, 4f, value => stair.WellGap = value, changed ) );
		group.Add( ArchPartUi.Number( "Arrival depth", stair.TopLandingDepth, 48f, value => stair.TopLandingDepth = value, changed ) );

		using ( var grid = ArchIconGrid.In( panel.AddGroup( "Fit", collapsible: true ) ) ) {
			grid.Toggle( "Railing round the well on the storey above", "opt_well_guard", "vertical_align_top", stair.WellGuard,
				value => { stair.WellGuard = value; changed?.Invoke(); } );

			grid.Toggle( "Landing at the top", "opt_top_landing", "horizontal_rule", stair.TopLanding,
				value => { stair.TopLanding = value; changed?.Invoke(); } );
		}

		// No per-flight rows here. A step's climb is its lift pin, its row in the drawing and its own sheet - three
		// ways in already - and a list that grows a field and a button per step turns a stair of eight segments
		// into a sheet nobody can scroll to the end of.
		var flow = panel.AddGroup( "Flow", collapsible: true ).AddRow();

		flow.Spacing = 4;
		flow.Add( new Button( "Invert climb", "swap_vert" ) { Clicked = () => { stair.ReverseChain(); changed?.Invoke(); } } );
		flow.Add( new Button( "Reset top opening", "restart_alt" ) { Clicked = () => { stair.TopOpening = null; stair.TopOpeningSnap = null; changed?.Invoke(); } } );
	}

	public static void Approach( ToolSidebarWidget panel, ArchApproachPart approach, ArchGridService gridService, Action refresh, Action changed ) {
		if ( !approach.IsSpline ) {
			using var grid = ArchIconGrid.In( panel.AddGroup( "Kind" ) );

			grid.Pick( "Foundation steps — climb from grade onto the plinth at a door", "approach_steps", "stairs_2",
				approach.Kind == ApproachKind.Steps, () => { approach.Kind = ApproachKind.Steps; refresh?.Invoke(); changed?.Invoke(); } );

			grid.Pick( "Driveway ramp — slope from grade onto the plinth at a garage", "approach_ramp", "trending_up",
				approach.Kind == ApproachKind.Ramp, () => { approach.Kind = ApproachKind.Ramp; refresh?.Invoke(); changed?.Invoke(); } );
		}

		Fittings( panel, approach, changed );

		var group = panel.AddGroup( "Reach" );

		if ( approach.IsSpline ) {
			for ( var index = 0; index < approach.Nodes.Count; index++ ) {
				var captured = index;
				var label = index == 0 ? "Start width" : index == approach.Nodes.Count - 1 ? "End width" : $"Middle {index} width";
				var width = ArchApproachAxes.WidthAt( approach, index );

				group.Add( ArchPartUi.Number( label, width, approach.Width, value => {
					var snapped = MathF.Max( gridService.SubgridSize( 4 ), gridService.Subgrid( value, 4 ) );
					approach.Nodes[captured].WidthScale = snapped / MathF.Max( 12f, approach.Width );
				}, changed ) );
			}
		} else {
			group.Add( ArchPartUi.Number( "Width", approach.Width, 48f,
				value => approach.Width = gridService.Subgrid( value, 4 ), changed ) );
			group.Add( ArchPartUi.Number( "Run", approach.Run, 0f,
				value => approach.Run = gridService.Subgrid( value, 4 ), changed ) );
		}

		if ( approach.Kind != ApproachKind.Ramp ) {
			return;
		}

		// The MATERIAL's question, not the geometry's: cast concrete follows the rake, brick is laid level.
		using ( var mapping = ArchIconGrid.In( panel.AddGroup( "UV Options" ) ) ) {
			mapping.Pick( "Raked — courses follow the drive's own top line, the way the trim over them does",
				"uv_raked", "trending_down", approach.Mapping == ApproachMapping.Raked,
				() => { approach.Mapping = ApproachMapping.Raked; changed?.Invoke(); } );

			mapping.Pick( "Level — courses stay horizontal, as a bricklayer would lay them",
				"uv_level", "horizontal_rule", approach.Mapping == ApproachMapping.Level,
				() => { approach.Mapping = ApproachMapping.Level; changed?.Invoke(); } );
		}

		var crossover = panel.AddGroup( "Crossover" );

		crossover.Add( ArchPartUi.Number( "Splay", approach.Splay, 36f,
			value => approach.Splay = gridService.Subgrid( value, 4 ), changed ) );
		crossover.Add( ArchPartUi.Number( "Segments", approach.SplaySegments, 0f,
			value => approach.SplaySegments = (int)MathF.Round( value ), changed ) );
	}

	public static void Fittings( ToolSidebarWidget panel, ArchApproachPart approach, Action changed ) {
		using var grid = ArchIconGrid.In( panel.AddGroup( approach.Kind == ApproachKind.Ramp ? "Ramp" : "Steps" ) );

		if ( approach.Kind == ApproachKind.Ramp ) {
			grid.Toggle( "Kerbs down both sides", "opt_kerbs", "align_horizontal_center", approach.Kerbs,
				value => { approach.Kerbs = value; changed?.Invoke(); } );

			grid.Toggle( "Skirted flanks — the sides wear the skirt of whatever it ran off, not the drive's own surface",
				"opt_skirted", "layers", approach.Skirted,
				value => { approach.Skirted = value; changed?.Invoke(); } );

			return;
		}

		grid.Toggle( "Handrails", "opt_rails", "fence", approach.Rails,
			value => { approach.Rails = value; changed?.Invoke(); } );
	}

	public static string Describe( StairSupport value ) => value switch {
		StairSupport.OpenString => "Open string — stringers, no risers",
		_ => "Closed string — stringers and risers"
	};

	public static string Describe( StairUnder value ) => value switch {
		StairUnder.Filled => "Filled — a solid raked wall under the flight, faced both sides",
		StairUnder.Cupboard => "Cupboard — filled, with a door into the space under the stairs",
		_ => "Open underneath"
	};

	public static string Describe( StairGuard value ) => value switch {
		StairGuard.Left => "Balustrade on the left going up",
		StairGuard.Right => "Balustrade on the right going up",
		StairGuard.None => "No balustrade",
		_ => "Balustrade both sides"
	};

	public static string Describe( StairRailing value, string side ) => value switch {
		StairRailing.Handrail => $"{Title( side )} rake — the handrail on its newels, nothing under it",
		_ => $"{Title( side )} balustrade — newels, balusters and the handrail over them"
	};

	static string Title( string side ) => char.ToUpperInvariant( side[0] ) + side[1..];

	public static string Fallback( StairRailing value ) => value switch {
		StairRailing.Handrail => "remove",
		_ => "fence"
	};

	public static string Fallback( StairSupport value ) => value switch {
		StairSupport.OpenString => "density_small",
		_ => "stairs"
	};

	public static string Fallback( StairUnder value ) => value switch {
		StairUnder.Filled => "square",
		StairUnder.Cupboard => "door_front",
		_ => "check_box_outline_blank"
	};

	public static string Fallback( StairGuard value ) => value switch {
		StairGuard.Left => "align_horizontal_left",
		StairGuard.Right => "align_horizontal_right",
		StairGuard.None => "block",
		_ => "fence"
	};
}