Editor/Tool/Subtools/ArchPartUi.cs
using System;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// One layout per kind, read by both creator and Select subtool.
public static class ArchPartUi {
	public const float PanelWidth = 208f;

	public static void Sheet( ToolSidebarWidget panel, ArchTool tool, ArchSelection picked, Action refresh ) {
		Action changed = () => tool.Touch( $"Edit {picked.Describe()}" );
		var sheet = ArchSheets.Load().For( picked.Item );

		if ( sheet is not null ) {
			sheet.Build( panel, tool, picked, refresh, changed );

			return;
		}

			switch ( picked.Item ) {
			case ArchBuilding building:
				Building( panel, building, tool?.Plan, refresh, changed );
				break;

			case ArchOpening opening:
				Opening( panel, opening, picked.Room, refresh, changed );
				break;

			case ArchFencePart fence:
				Fence( panel, fence, refresh, changed );
				break;

			case ArchDownpipePart pipe:
				Downpipe( panel, pipe, tool.Kit, changed );
				break;

			case ArchTrimPart trim:
				Trim( panel, trim, tool.Kit, changed );
				break;

			case ArchRoom room:
				Room( panel, room, tool, refresh, changed );
				break;
		}
	}

	static void Building( ToolSidebarWidget panel, ArchBuilding building, ArchPlan plan, Action refresh, Action changed ) {
		ArchSidebarSection.Show( panel, "Building.shell", "Shell", group => {
			var name = new LineEdit( building.Name );

			name.TextEdited += text => {
				building.Name = text;
				changed?.Invoke();
			};

			group.Add( name );
		} );

		ArchSidebarSection.Show( panel, "Building.rotation", "Rotation", group => {
			group.Add( Angle( "Angle", building.Facing, degrees => Turn( building, degrees, plan ), changed ) );
			group.Add( Increment( refresh ) );
		} );
	}

	static void Opening( ToolSidebarWidget panel, ArchOpening opening, ArchRoom room, Action refresh, Action changed ) {
		ArchSidebarSection.Show( panel, "Opening.hole", "Hole", size => {
			size.Add( Number( "Along the wall", opening.Offset, 0f, value => opening.Offset = value, changed ) );
			size.Add( Number( "Width", opening.Width, 0f, value => opening.Width = value, changed ) );
			size.Add( Number( "Start height", opening.SillHeight, 0f, value => SetOpeningStart( opening, value ), changed ) );
			size.Add( Number( "End height", opening.Top, 0f, value => SetOpeningEnd( opening, value ), changed ) );
		} );

		// Only what this kind actually generates - controls the generator never reads would do nothing.
		ArchSidebarSection.Show( panel, "Opening.unit", "Unit", group => {
			using var grid = ArchIconGrid.In( group );

			grid.Toggle( "Casing round the opening", "opt_cased", "filter_frames", opening.Cased,
				value => { opening.Cased = value; changed?.Invoke(); } );

			if ( opening.Kind.Sills() ) {
				grid.Toggle( "Sill board under it", "opt_sill", "border_bottom", opening.HasSill,
					value => { opening.HasSill = value; changed?.Invoke(); } );
			}

			if ( opening.Kind.Hangs() ) {
				grid.Toggle( "Hang a leaf in it", "opt_leaf", "door_front", opening.Leaf,
					value => { opening.Leaf = value; refresh?.Invoke(); changed?.Invoke(); } );
			}

			if ( opening.IsHung ) {
				grid.Toggle( "Hinge on the other side", "opt_flip_hinge", "flip", opening.FlipHinge,
					value => { opening.FlipHinge = value; changed?.Invoke(); } );

				grid.Toggle( "Standing open when the level starts", "opt_start_open", "door_open", opening.StartOpen,
					value => { opening.StartOpen = value; changed?.Invoke(); } );
			}

			if ( opening.Kind.Glazes() ) {
				grid.Toggle( "Glazed — sashes and panes in the hole", "opt_glazed", "window", opening.Glazed,
					value => { opening.Glazed = value; refresh?.Invoke(); changed?.Invoke(); } );
			}

			ArchOpeningUi.Furniture( grid, opening.Kind, opening.Furniture,
				value => { opening.Furniture = value; changed?.Invoke(); } );

			ArchOpeningUi.Hood( grid, opening.Kind, opening.Hood,
				value => { opening.Hood = value; changed?.Invoke(); } );
		} );

		if ( !opening.IsGlazed ) {
			BasementWell( panel, opening, room, changed );
			return;
		}

		// The sash arrangement is authored on the preset and rarely re-cut on one standing unit, so it folds.
		ArchSidebarSection.Disclosure( panel, "Opening.glazing", "Glazing", true, glazing => {
			using ( var grid = ArchIconGrid.In( glazing ) ) {
				grid.Toggle( "Fit mullions and sashes to the resized frame", "opt_window_auto_layout", "auto_awesome", opening.AutoLayout,
					value => { opening.AutoLayout = value; changed?.Invoke(); } );

				grid.Toggle( "Already broken", "opt_broken", "broken_image", opening.Glass == GlassState.Broken,
					value => { opening.Glass = value ? GlassState.Broken : GlassState.Intact; changed?.Invoke(); } );

				grid.Toggle( "Breakable — panes on their own object", "opt_breakable", "shield", opening.Breakable,
					value => { opening.Breakable = value; changed?.Invoke(); } );
			}

			glazing.Add( Integer( "Lights", opening.Lights, 1,
				value => { opening.Lights = value; opening.AutoLayout = false; }, changed ) );
			glazing.Add( Integer( "Sashes", opening.Sashes, 2,
				value => { opening.Sashes = value; opening.AutoLayout = false; }, changed ) );
			glazing.Add( Integer( "Pane columns", opening.PanesAcross, 2, value => opening.PanesAcross = value, changed ) );
			glazing.Add( Integer( "Pane rows", opening.PanesDown, 3, value => opening.PanesDown = value, changed ) );
		} );

		BasementWell( panel, opening, room, changed );
	}

	static void BasementWell( ToolSidebarWidget panel, ArchOpening opening, ArchRoom room, Action changed ) {
		if ( room?.Floor >= 0 || opening.Kind != OpeningKind.Window ) {
			return;
		}

		ArchSidebarSection.Show( panel, "Opening.basement", "Basement", group => {
			group.Add( Check( "Foundation light well", opening.BasementWell, value => opening.BasementWell = value, changed ) );

			if ( opening.BasementWell ) {
				group.Add( Number( "Well depth", opening.BasementWellDepth, 36f, value => opening.BasementWellDepth = MathF.Max( 18f, value ), changed ) );
			}
		} );
	}

	static void SetOpeningStart( ArchOpening opening, float value ) {
		var top = opening.Top;
		opening.SillHeight = MathF.Min( value, top - ArchGridService.FinestSize );
		opening.Height = MathF.Max( ArchGridService.FinestSize, top - opening.SillHeight );
	}

	static void SetOpeningEnd( ArchOpening opening, float value ) {
		opening.Height = MathF.Max( ArchGridService.FinestSize, value - opening.SillHeight );
	}

	static void Fence( ToolSidebarWidget panel, ArchFencePart fence, Action refresh, Action changed ) {
		ArchSidebarSection.Show( panel, "Fence.run", "Run", group => {
			using var grid = ArchIconGrid.In( group );

			grid.Toggle( "Closed loop", "opt_closed", "crop_square", fence.Closed,
				value => { fence.Closed = value; changed?.Invoke(); } );
		} );

		ArchBarrierUi.Build( panel, fence.Barrier, refresh, changed );
	}

	public static void Downpipe( ToolSidebarWidget panel, ArchDownpipePart pipe, ArchKit kit, Action changed ) {
		ArchSidebarSection.Show( panel, "Downpipe.section", "Section", section => {
			using ( var kinds = ArchIconGrid.In( section ) ) {
				kinds.Pick( "Rainwater — taps the gutter and swans over to the wall", "pipe_rainwater", "water_drop",
					pipe.Run == PipeRun.Rainwater, () => { pipe.Run = PipeRun.Rainwater; changed?.Invoke(); } );

				kinds.Pick( "Conduit — a bracketed service run straight down the elevation", "pipe_conduit", "power_input",
					pipe.Run == PipeRun.Conduit, () => { pipe.Run = PipeRun.Conduit; changed?.Invoke(); } );
			}

			using ( var grid = ArchIconGrid.In( section ) ) {
				foreach ( var entry in kit.Profiles.Where( entry => entry.Name.Contains( "pipe", StringComparison.OrdinalIgnoreCase ) ) ) {
					var name = entry.Name;

					grid.Pick( name, ArchIcons.ProfileSlug( name ), ArchIcons.ProfileGlyph, pipe.Profile == name,
						() => { pipe.Profile = name; changed?.Invoke(); } );
				}
			}

			using var fittings = ArchIconGrid.In( section );

			fittings.Toggle( "Shoe at the bottom", "opt_shoe", "south", pipe.Shoe,
				value => { pipe.Shoe = value; changed?.Invoke(); } );
		} );
	}

	public static void Trim( ToolSidebarWidget panel, ArchTrimPart trim, ArchKit kit, Action changed ) {
		ArchSidebarSection.Show( panel, "Trim.profile", "Profile", group => {
			using ( var grid = ArchIconGrid.In( group ) ) {
				foreach ( var entry in kit.Profiles ) {
					var name = entry.Name;

					grid.Pick( name, ArchIcons.ProfileSlug( name ), ArchIcons.ProfileGlyph, trim.Profile == name,
						() => { trim.Profile = name; changed?.Invoke(); } );
				}
			}

			if ( !trim.Follows ) {
				return;
			}

			using var reach = ArchIconGrid.In( group );

			reach.Pick( "Floor edge only", "trim_floor", "horizontal_rule", trim.Reach == ArchTrimReach.Floor,
				() => { trim.Reach = ArchTrimReach.Floor; changed?.Invoke(); } );
			reach.Pick( "Wall jambs only", "trim_walls", "vertical_align_center", trim.Reach == ArchTrimReach.Walls,
				() => { trim.Reach = ArchTrimReach.Walls; changed?.Invoke(); } );
			reach.Pick( "One continuous run over the floor edge and wall jambs", "trim_both", "polyline", trim.Reach == ArchTrimReach.Both,
				() => { trim.Reach = ArchTrimReach.Both; changed?.Invoke(); } );
		} );

		ArchSidebarSection.Disclosure( panel, "Trim.role", "Material role", true, group => {
			using var roles = ArchIconGrid.In( group );

			foreach ( var value in new[] { ArchSurface.Trim, ArchSurface.Gutter, ArchSurface.Baseboard, ArchSurface.WallCap, ArchSurface.Pillar } ) {
				var captured = value;

				roles.Pick( captured.ToString(), $"surface_{captured}".ToLowerInvariant(), TrimRoleGlyph( captured ),
					trim.Surface == captured, () => { trim.Surface = captured; changed?.Invoke(); } );
			}
		} );
	}

	// A room has no tool of its own - a shell stands one and a wall sections one - so this sheet is the only
	// place its own numbers live, and everything it had stays reachable here.
	static void Room( ToolSidebarWidget panel, ArchRoom room, ArchTool tool, Action refresh, Action changed ) {
		ArchSidebarSection.Show( panel, "Room.include", "Include", group => {
			using var grid = ArchIconGrid.In( group );

			grid.Toggle( "Own floor slab", "opt_own_floor", "layers", room.HasFloor,
				value => { room.HasFloor = value; changed?.Invoke(); } );

			grid.Toggle( "Ceiling body under the plate", "opt_ceiling", "border_top", room.HasCeiling,
				value => { room.HasCeiling = value; changed?.Invoke(); } );

			grid.Toggle( "Boards over the slab", "opt_floor_boards", "view_stream", room.FloorBoards,
				value => { room.FloorBoards = value; changed?.Invoke(); } );

			grid.Toggle( "Stands on a raised foundation", "opt_raised", "foundation", room.RaisedFoundation,
				value => { room.RaisedFoundation = value; changed?.Invoke(); } );

			grid.Toggle( "Facade — no inside, so no far face on any of its walls", "opt_facade", "filter_1", room.Facade,
				value => { room.Facade = value; changed?.Invoke(); } );

			grid.Toggle( "Posts under whatever this storey oversails", "opt_overhang_posts", "view_column", room.OverhangPosts,
				value => { room.OverhangPosts = value; refresh?.Invoke(); changed?.Invoke(); } );
		} );

		OverhangPosts( panel, room, tool, refresh, changed );

		ArchSidebarSection.Show( panel, "Room.storey", "Storey", group => {
			group.Add( Number( "Wall height", room.WallHeight, 0f, value => room.WallHeight = value, changed ) );
			group.Add( Number( "Wall thickness", room.Walls.FirstOrDefault()?.Thickness ?? 0f, tool?.Kit.WallThickness ?? 8f, value => {
				foreach ( var wall in room.Walls ) { wall.Thickness = MathF.Max( 1f, value ); }
			}, changed ) );
			group.Add( Number( "Board angle", room.FloorBoardYaw, 0f, value => room.FloorBoardYaw = value, changed ) );

			if ( room.HasCeiling ) {
				group.Add( Number( "Ceiling depth", room.CeilingDepth, 0f, value => room.CeilingDepth = value, changed ) );
			}
		} );
	}

	// A prop under an overhang IS a pillar, so it is dressed by the Pillars tool's own forms - the type shelf, the
	// section, the footing. No span: nothing bridges between two props holding one storey up.
	static void OverhangPosts( ToolSidebarWidget panel, ArchRoom room, ArchTool tool, Action refresh, Action changed ) {
		if ( !room.OverhangPosts || tool is null ) {
			return;
		}

		room.OverhangPost ??= new ArchPillarPart();

		var offered = tool.PillarTypes;
		var type = offered.FirstOrDefault( standing => string.Equals( standing.Name, room.OverhangPost.Type, StringComparison.OrdinalIgnoreCase ) );

		ArchPillarUi.Kinds( panel, offered, room.OverhangPost.Type, adopted => {
			room.OverhangPost = adopted.Standing( Vector2.Zero, 0f, 0f );
			refresh?.Invoke();
			changed?.Invoke();
		} );

		ArchPillarUi.Build( panel, room.OverhangPost, type, refresh, changed, spans: false );
	}

	static string TrimRoleGlyph( ArchSurface value ) => value switch {
		ArchSurface.Gutter => "water_damage",
		ArchSurface.Baseboard => "border_bottom",
		ArchSurface.WallCap => "border_top",
		ArchSurface.Pillar => "view_column",
		_ => "timeline"
	};

	// Value lands per keystroke; `changed` only applies - it debounces.
	// An absolute bearing, where the plan holds only turned coordinates: the box says where the shell faces and
	// assigning one turns it by the DIFFERENCE, so naming an angle and nudging it are the same edit.
	static void Turn( ArchBuilding building, float degrees, ArchPlan plan ) {
		if ( ArchHandles.Pivot( building, out var about ) ) {
			ArchCarry.Turn( building, about, degrees - building.Facing, plan );
		}
	}

	// Dragged in whole steps of the angle ladder, and slowly - a bearing is aimed, not flicked. Free swing
	// (a ladder of zero) still drags in whole degrees, because a box with no step at all cannot be pulled.
	public static Widget Angle( string label, float value, Action<float> assign, Action changed = null ) {
		var step = ArchGridService.AngleStep;

		// An unreadable box is left ALONE rather than read as zero: this is an absolute bearing, so clearing it
		// to retype would swing the whole house back to north on the way.
		return Pull( label, value != 0f ? value.ToString( "0.##" ) : "", "0",
			step > 0f ? step : 1f, 0.5f, "0.##",
			text => {
				if ( !float.TryParse( text, out var parsed ) ) {
					return;
				}

				assign( parsed );
				changed?.Invoke();
			} );
	}

	// The ladder itself, beside what it steps: it is the editor's, so every turn in the tool ratchets onto it.
	// The sheet is rebuilt behind it, or the angle box beside it keeps dragging on the step it was built with.
	public static Widget Increment( Action refresh ) {
		return Pull( "Increment", ArchGridService.AngleStep.ToString( "0.##" ), "0",
			1f, 0.25f, "0.##",
			text => {
				ArchGridService.AngleStep = float.TryParse( text, out var parsed ) ? parsed : 0f;
				refresh?.Invoke();
			} );
	}

	public static Widget Number( string label, float value, float placeholder, Action<float> assign, Action changed = null ) {
		return Pull( label, value != 0f ? value.ToString( "0.##" ) : "", placeholder != 0f ? placeholder.ToString( "0.##" ) : "0",
			ArchGridService.FinestSize, 1f, "0.##",
			text => { assign( float.TryParse( text, out var parsed ) ? parsed : 0f ); changed?.Invoke(); } );
	}

	// Whole numbers only - a count that jumps several at a flick can't be aimed.
	public static Widget Integer( string label, int value, int placeholder, Action<int> assign, Action changed = null ) {
		return Pull( label, value != 0 ? value.ToString() : "", placeholder != 0 ? placeholder.ToString() : "0",
			1f, 0.25f, "0",
			text => { assign( int.TryParse( text, out var parsed ) ? parsed : 0 ); changed?.Invoke(); } );
	}

	public static Widget OptionalNumber( string label, float? value, float placeholder, Action<float?> assign, Action changed = null ) {
		return Pull( label, value?.ToString( "0.##" ) ?? "", placeholder.ToString( "0.##" ),
			ArchGridService.FinestSize, 1f, "0.##",
			text => { assign( float.TryParse( text, out var parsed ) ? parsed : null ); changed?.Invoke(); } );
	}

	static Widget Pull( string label, string text, string placeholder, float snap, float perPixel, string format, Action<string> edited ) {
		var holder = new Widget( null );
		holder.Layout = Layout.Row();
		holder.Layout.Spacing = 4;

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

		var edit = new ArchNumberEdit( text, snap, perPixel, format, value => edited( value.ToString( format ) ) ) {
			PlaceholderText = placeholder
		};

		edit.TextEdited += edited;

		holder.Layout.Add( edit );

		return holder;
	}

	// A named thing rather than a measured one - an asset a part points at. Committed on edit, so nothing is lost
	// by clicking away from it.
	public static Widget Text( string label, string value, string placeholder, Action<string> assign ) {
		var holder = new Widget( null );
		holder.Layout = Layout.Row();
		holder.Layout.Spacing = 4;

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

		var edit = new LineEdit( value ?? "" ) { PlaceholderText = placeholder };

		edit.TextEdited += assign;

		holder.Layout.Add( edit );

		return holder;
	}

	public static Widget Check( string label, bool value, Action<bool> assign, Action changed = null ) {
		var box = new Checkbox( label ) { Value = value };

		box.Toggled += () => {
			assign( box.Value );
			changed?.Invoke();
		};

		return box;
	}

	// Qt won't size wrappable labels; estimate and pin the line count.
	public static Widget Wrapped( string text ) {
		var label = new Label( text );
		label.WordWrap = true;
		label.FixedWidth = PanelWidth;
		label.MinimumHeight = MathF.Ceiling( text.Length / 32f ) * 16f;

		return label;
	}

	// The answer a workflow row prints for a step that is a row of independent toggles: what is on, in order.
	public static string Listed( params (bool On, string Name)[] parts ) {
		var standing = parts.Where( part => part.On ).Select( part => part.Name ).ToList();

		return standing.Count == 0 ? "None" : string.Join( ", ", standing );
	}
}