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

namespace Sunless.Architecture;

[Title( "Walkway" ), Icon( "linear_scale" ), Group( "07" )]
public sealed class ArchWalkwaySubtool( ArchTool owner ) : ArchSubtool( owner ) {
	protected override ArchKind? DraftKind => ArchKind.Walkway;

	public override ArchSurface[] Surfaces => new[]
	{
		ArchSurface.WallExterior, ArchSurface.WallInterior, ArchSurface.Floor, ArchSurface.Roof,
		ArchSurface.Ceiling, ArchSurface.Baseboard, ArchSurface.Trim, ArchSurface.Pillar, ArchSurface.PillarCap
	};

	float width = 72f;
	float height = 96f;
	int rise;
	bool piers = true;
	WalkwayInterior interior;
	bool cornice = true;
	bool skirting = true;

	// Held here so the pier type is answered BEFORE the drag, not only after.
	ArchPillarPart draft = new();

	// Re-dresses the newest link in the plan; authoring anything else moves the counter.
	int placed;
	int stamp;

	protected override string Title() => "Walkway";

	protected override string Advice() => "Drag the link between two shells. Both ends open into whatever wall they land on.";

	ArchRoom Placed => placed != 0 && (adopted || Owner.Plan.NextId == stamp) ? Owner.Plan.FindRoom( placed ) : null;

	// A link taken from the stack is edited live like one just dragged - but it stays editable
	// through commits, where the freshly-placed one is only current until the plan moves on.
	bool adopted;

	public override bool Adopts => true;

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

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

	protected override void Adopt( ArchSelection picked ) {
		adopted = picked?.Item is ArchRoom { Spans: true };
		placed = adopted ? ((ArchRoom)picked.Item).Id : 0;
		stamp = Owner.Plan.NextId;

		if ( Placed is { } link ) {
			height = MathF.Max( 60f, (Deck( link )?.BaseHeight ?? link.BaseHeight + link.WallHeight) - link.BaseHeight );
		}
	}

	ArchRoofPart Deck( ArchRoom link ) {
		return Owner.Plan.Buildings
			.SelectMany( building => building.Roofs )
			.FirstOrDefault( roof => roof.Level == link.Floor && MathF.Abs( roof.BaseHeight - (link.BaseHeight + height) ) < link.WallHeight );
	}

	bool Rising( ArchRoom link ) => link is not null && link.WalkwayTop > link.BaseHeight + 1f;

	// The drag squared onto its long axis and widened to the Width box.
	protected override void DrawPreview() {
		ArchWalkway.Shape( Min( DragStart, DragCurrent ), Max( DragStart, DragCurrent ), width, out var min, out var max, out var alongX );

		var deck = Owner.LevelHeight;
		var head = deck + MathF.Max( 60f, height );

		ArchGhost.Volume( min, max, deck, head );
		ArchGhost.Plate( min, max, deck, 8 );

		var run = alongX ? max.x - min.x : max.y - min.y;
		var across = alongX ? max.y - min.y : max.x - min.x;

		ArchGhost.Note( new Vector3( max.x, max.y, head ), $"walkway — {run:0} x {across:0}" );
	}

	protected override void OnDrag( Vector2 from, Vector2 to ) {
		var link = ArchWalkway.Add(
			Owner.Plan, Owner.ActiveBuilding(), Owner.Kit, Owner.Level, Owner.LevelHeight,
			Min( from, to ), Max( from, to ), from, width, height, piers ? draft : null,
			rise, interior, cornice, skirting );

		if ( link is null ) {
			Log.Info( "Architecture: that drag leaves nothing to span - it needs to reach across the gap between two shells." );
			return;
		}

		Owner.ActiveBuildingId = link.Host.Id;
		Owner.ActiveRoomId = link.Room.Id;
		FinishDraft( link.Room.Id );

		// The link is a site connection: it forms the group holding it and both houses it joined,
		// so the tree shows the join and the mouths know which walls they are allowed to stand in.
		Owner.InsertionTarget = null;
		ArchWalkwayConnection.Form( Owner.Plan, link );

		Owner.Commit( "Create Walkway" );

		placed = link.Room.Id;
		stamp = Owner.Plan.NextId;

		Refresh();

		if ( link.Breached == 0 ) {
			Log.Info( "Architecture: the walkway reached no wall at either end, so it opens into nothing yet." );
		}
	}

	void Seeded() {
		if ( draft.Type.Length > 0 || Owner.DefaultPillar() is not { } first ) {
			return;
		}

		draft = first.Standing( default, 0f, 0f );
	}

	void Reheight( ArchRoom link, float value ) {
		height = value;

		ArchWalkway.Reheight( link, Deck( link ), Owner.Kit, value );
		Owner.Touch( "Walkway Height" );
	}

	// Refreshed, unlike the height: the reshape clamps a width the passage cannot take and re-seats both
	// ends, so the box has to show back what the link actually settled on.
	void Rewidth( ArchRoom link, float value ) {
		width = value;

		ArchWalkway.SetWidth( Owner.Plan, link, Owner.Kit, value );
		Owner.Touch( "Walkway Width" );
		Refresh();
	}

	// How far it climbs, what carries the climb, what the street sees under it, what stands beneath it, what it
	// wears, then how big it is - every pick before the two typed sizes, which can never answer their own step.
	//
	// Each step is asked only where the link can have one: the rise seeds the next drag and a placed link changes
	// storeys by being redrawn, the underside exists only once a flight has been cut, and a link sitting on grade
	// has no gap to stand piers in and says so instead of offering a control the support service would refuse.
	protected override void BuildOptions( ToolSidebarWidget panel ) {
		Seeded();

		var link = Placed;

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

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

		if ( link is null ) {
			flow.Step( "Rise", RiseName, RiseGlyph, PickRise );
		}

		if ( link is null ? rise > 0 : Rising( link ) ) {
			flow.Step( "Climb", ClimbName( link ), ClimbGlyph( link ), step => PickClimb( link, step ) );

			if ( Flight( link ) is not null ) {
				flow.Step( "Underside", UndersideName( link ), UndersideGlyph( link ), step => PickUnderside( link, step ) );
			}
		}

		flow.Step( "Piers", PiersName( link ), PiersGlyph( link ), step => PickPiers( link, step ) );

		flow.Step( "Finish", FinishName( link ), "format_paint", step => BuildFinish( link, step ) );

		flow.Step( "Span", SpanName( link ), "straighten", step => BuildSpan( link, step ) );
	}

	static string Storeys( int count ) => count == 1 ? "1 storey" : $"{count} storeys";

	string RiseName => rise == 0 ? "Level" : Storeys( rise );

	string RiseGlyph => rise == 0 ? "horizontal_rule" : "trending_up";

	// A rise is three legal answers and level, so it is picked rather than typed - and picked first, because
	// everything below it is asked only of a link that climbs.
	void PickRise( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Level — both ends open at the same storey", "rise_level", "horizontal_rule",
			rise == 0, () => Raise( 0, step ) );

		for ( var storeys = 1; storeys <= 3; storeys++ ) {
			var captured = storeys;

			grid.Pick( $"{Storeys( captured )} — the far end opens that much higher", $"rise_{captured}", "trending_up",
				rise == captured, () => Raise( captured, step ) );
		}
	}

	void Raise( int storeys, ArchWorkflowStep step ) {
		rise = storeys;

		step.Chose();
	}

	WalkwayInterior Inside( ArchRoom link ) => link?.Interior ?? interior;

	string ClimbName( ArchRoom link ) => Inside( link ) switch {
		WalkwayInterior.Stairs => "Stairs",
		WalkwayInterior.Ramp => "Ramp",
		_ => "Slope"
	};

	string ClimbGlyph( ArchRoom link ) => Inside( link ) switch {
		WalkwayInterior.Stairs => "stairs",
		WalkwayInterior.Ramp => "trending_up",
		_ => "landscape"
	};

	void PickClimb( ArchRoom link, ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Slope — a plain grade to walk up", "opt_slope", "landscape",
			Inside( link ) == WalkwayInterior.None, () => ChooseInterior( link, WalkwayInterior.None, step ) );

		grid.Pick( "Stairs — a flight cut inside the link", "opt_stairs", "stairs",
			Inside( link ) == WalkwayInterior.Stairs, () => ChooseInterior( link, WalkwayInterior.Stairs, step ) );

		grid.Pick( "Ramp — a ramp with kerbs", "opt_ramp", "trending_up",
			Inside( link ) == WalkwayInterior.Ramp, () => ChooseInterior( link, WalkwayInterior.Ramp, step ) );
	}

	void ChooseInterior( ArchRoom link, WalkwayInterior value, ArchWorkflowStep step ) {
		if ( link is null ) {
			interior = value;
		} else {
			ArchWalkway.SetInterior( Owner.Plan, link, Owner.Kit, value );
			Owner.Touch( "Walkway Interior" );
		}

		step.Chose();
	}

	// A link stands clear of the ground, so what is under its flight is a face the street sees.
	ArchStairPart Flight( ArchRoom link ) => link?.Stairs.FirstOrDefault( stair => stair.Name == "WalkwayFlight" );

	string UndersideName( ArchRoom link ) => Flight( link ) is { } flight ? ArchStairLabels.Describe( flight.Under ) : "";

	string UndersideGlyph( ArchRoom link ) => Flight( link ) is { } flight ? ArchStairLabels.Fallback( flight.Under ) : "crop_free";

	void PickUnderside( ArchRoom link, ArchWorkflowStep step ) {
		if ( Flight( link ) is not { } flight ) {
			return;
		}

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

		foreach ( var value in new[] { StairUnder.Filled, StairUnder.Open } ) {
			var captured = value;

			grid.Pick( ArchStairLabels.Describe( captured ), $"under_{captured}".ToLowerInvariant(), ArchStairLabels.Fallback( captured ),
				flight.Under == captured, () => Underneath( flight, captured, step ) );
		}
	}

	void Underneath( ArchStairPart flight, StairUnder under, ArchWorkflowStep step ) {
		flight.Under = under;

		Owner.Touch( "Walkway Underside" );

		step.Chose();
	}

	bool Grounded( ArchRoom link ) => link is not null && !ArchWalkway.Raised( link, Owner.Kit );

	string Carrying( ArchRoom link ) => link is null ? piers ? draft.Type : null : link.Pillars.FirstOrDefault()?.Type;

	string PiersName( ArchRoom link ) {
		if ( Grounded( link ) ) {
			return "On grade";
		}

		return Carrying( link ) is { Length: > 0 } name ? Owner.FindPillarType( name )?.Title ?? name : "None";
	}

	string PiersGlyph( ArchRoom link ) {
		if ( Grounded( link ) ) {
			return "horizontal_rule";
		}

		return Carrying( link ) is { Length: > 0 } ? "view_column" : "block";
	}

	// One question - what carries the link - where a toggle used to govern a folded type list. A pier IS a
	// pillar, so its section, footing and bays are the Pillar tool's to author and the pier's own sheet in
	// Select to retune; standing them here from the same support service the drag uses gives a link given piers
	// after the fact the bays the drag would have given it.
	void PickPiers( ArchRoom link, ArchWorkflowStep step ) {
		if ( Grounded( link ) ) {
			step.Layout.Add( Wrapped( "This link sits on grade, so there is no gap under it to stand piers in. Raise it and they can go in." ) );

			return;
		}

		var offered = Owner.OfferedPillars();

		if ( offered.Count == 0 ) {
			step.Layout.Add( ArchSidebarLayout.Advice( "No pillar types are authored — add one to Assets/arch/pillars." ) );

			return;
		}

		var carrying = Carrying( link );

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

		grid.Pick( "None — the link spans clear", "opt_no_piers", "block",
			string.IsNullOrEmpty( carrying ), () => Stand( link, null, step ) );

		foreach ( var type in offered ) {
			var captured = type;

			grid.Pick( $"{captured.Title} — {captured.Description}", ArchIcons.PillarSlug( captured.Name ), captured.Icon,
				string.Equals( carrying, captured.Name, StringComparison.OrdinalIgnoreCase ), () => Stand( link, captured, step ) );
		}
	}

	void Stand( ArchRoom link, ArchPillarType type, ArchWorkflowStep step ) {
		var source = type?.Standing( default, 0f, 0f );

		if ( link is null ) {
			piers = source is not null;

			if ( source is not null ) {
				draft = source;
			}
		} else {
			ArchWalkway.SetPiers( Owner.Plan, link, Owner.Kit, source );
			Owner.Touch( "Walkway Piers" );
		}

		step.Chose();
	}

	bool Corniced( ArchRoom link ) => link is null ? cornice : Deck( link )?.CeilingCornice ?? true;

	bool Skirted( ArchRoom link ) => link is null ? skirting : link.WalkwaySkirting;

	string FinishName( ArchRoom link ) {
		var worn = new List<string>();

		if ( Corniced( link ) ) {
			worn.Add( "Cornice" );
		}

		if ( Skirted( link ) ) {
			worn.Add( "skirting" );
		}

		return worn.Count == 0 ? "Bare" : string.Join( ", ", worn );
	}

	// Cornice and skirting are dress on the placed link; without one they seed the next drag.
	void BuildFinish( ArchRoom link, ArchWorkflowStep step ) {
		var deck = link is null ? null : Deck( link );

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

		grid.Toggle( "Cornice under the deck, along the walls", "opt_cornice", "border_top", Corniced( link ),
			value => {
				if ( link is null ) {
					cornice = value;
				} else if ( deck is not null ) {
					deck.CeilingCornice = value;
					Owner.Touch( "Walkway Cornice" );
				}
			} );

		grid.Toggle( "Skirting boards along the walls", "opt_skirting", "format_align_center", Skirted( link ),
			value => {
				if ( link is null ) {
					skirting = value;
				} else {
					link.WalkwaySkirting = value;
					Owner.Touch( "Walkway Skirting" );
				}
			} );
	}

	string SpanName( ArchRoom link ) {
		var across = link is null ? width : ArchWalkway.WidthOf( link );

		return $"{across:0} wide, {MathF.Max( 60f, height ):0} high";
	}

	void BuildSpan( ArchRoom link, ArchWorkflowStep step ) {
		step.Layout.Add( ArchPartUi.Number( "Width", link is null ? width : ArchWalkway.WidthOf( link ), 72f,
			value => { if ( link is null ) { width = value; } else { Rewidth( link, value ); } } ) );

		step.Layout.Add( ArchPartUi.Number( "Height (floor to ceiling)", height, 96f,
			value => { if ( link is null ) { height = value; } else { Reheight( link, value ); } } ) );
	}
}