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

namespace Sunless.Architecture;

[Title( "Stair" ), Icon( "stairs" ), Group( "10" )]
public sealed class ArchStairSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	protected override ArchKind? DraftKind => ArchKind.Stair;

	enum DrivewayConnectionKind {
		Wall,
		Street
	}

	sealed class DrivewayConnection {
		public DrivewayConnectionKind Kind { get; init; }
		public Vector2 Position { get; init; }
		public Vector2 Direction { get; init; }
		public float Gap { get; init; }
		public ArchBuilding Building { get; init; }
		public ArchRoom Room { get; init; }
		public ArchWall Wall { get; init; }
		public float Along { get; init; }
		public ArchRoadPart Road { get; init; }
		public ArchFrame RoadFrame { get; init; }
	}

	public override ArchSurface[] Surfaces => new[] { ArchSurface.StairTread, ArchSurface.StairRiser, ArchSurface.StairStringer, ArchSurface.Railing };

	public override ArchViewAxis[] Works => new[] { ArchViewAxis.Free, ArchViewAxis.Top, ArchViewAxis.Front, ArchViewAxis.Side };

	// Only adjust this tool's own kinds — other selections' gizmos would eat placement gestures
	protected override bool Adjusts => Owner.Picked?.Item is ArchStairPart or ArchApproachPart;

	readonly ArchStairPart draft = new();

	readonly ArchApproachPart approachDraft = new();
	readonly List<ArchCurveNode> drivewayNodes = new();

	readonly List<Vector3> walk = new();

	bool splineDriveway;
	DrivewayConnection drivewayStart;
	DrivewayConnection drivewayEnd;
	bool enterDown;
	bool escapeDown;

	ApproachKind? approach;

	bool carving;

	bool walking = true;

	ArchStairPart continuing;

	StairFill fill;
	bool snapStoreys = true;
	int stories = 1;
	bool cutStairwell = true;
	bool seededRise;
	float flightRise = 128f;

	protected override bool UsesDrag => !walking && (approach != ApproachKind.Ramp || !splineDriveway);

	protected override bool TakesFreeClick => walking;

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

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

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

		var enter = global::Editor.Application.IsKeyDown( KeyCode.Enter ) || global::Editor.Application.IsKeyDown( KeyCode.Return );
		var escape = global::Editor.Application.IsKeyDown( KeyCode.Escape );

		if ( walking ) {
			if ( enter && !enterDown ) {
				FinishWalk();
			}

			if ( escape && !escapeDown ) {
				CancelWalk();
			}
		}

		if ( approach == ApproachKind.Ramp && splineDriveway ) {
			if ( enter && !enterDown ) {
				TryFinishSplineDriveway();
			}

			if ( escape && !escapeDown ) {
				CancelSplineDriveway();
			}
		}

		enterDown = enter;
		escapeDown = escape;
	}

	protected override string Title() => walking ? continuing is null ? "Stair" : "Continue Stair" : carving ? "Concrete Steps" : approach switch {
		ApproachKind.Steps => "Foundation Steps",
		ApproachKind.Ramp when splineDriveway => "Spline Driveway",
		ApproachKind.Ramp => "Driveway Ramp",
		_ => "Stair"
	};

	protected override ArchRunState Run() {
		if ( walking ) {
			return new ArchRunState( walk.Count > 0, $"{walk.Count} point{(walk.Count == 1 ? "" : "s")}" );
		}

		var spline = approach == ApproachKind.Ramp && splineDriveway;

		return new ArchRunState( spline && drivewayNodes.Count > 0,
			$"{drivewayNodes.Count} point{(drivewayNodes.Count == 1 ? "" : "s")}" );
	}

	protected override void FinishRun() {
		if ( walking ) {
			FinishWalk();

			return;
		}

		TryFinishSplineDriveway();
	}

	protected override void CancelRun() {
		if ( walking ) {
			CancelWalk();

			return;
		}

		CancelSplineDriveway();
	}

	protected override string Advice() => walking
		? WalkAdvice()
		: carving
		? "Drag across a platform to cut the flight out of it."
		: approach switch {
			ApproachKind.Steps => "Drag the footprint outside a door; it snaps onto the nearest wall.",
			ApproachKind.Ramp when splineDriveway => "Click control points from one connection to the other.",
			ApproachKind.Ramp => "Drag the footprint at a garage; it snaps square onto the nearest wall.",
			_ => "Drag the stair's shaft the way you drag a building."
		};

	void DrawApproachPreview() {
		var drag = Draft( DragStart, DragCurrent );

		if ( drag.Placed is not { } draft ) {
			ArchGhost.Note( new Vector3( DragCurrent.x, DragCurrent.y, Owner.LevelHeight ), drag.Refused );

			return;
		}

		var room = drag.Room ?? new ArchRoom { Floor = Owner.Level, BaseHeight = Owner.LevelHeight };
		var building = Owner.Plan.OwnerOf( drag.Room ) ?? drag.On ?? Owner.ActiveBuilding();
		var rise = ArchApproachGen.Rise( room, building, Owner.Kit, Owner.Plan );
		var lift = ArchAsks.Lift( Owner.Plan, building, Owner.Kit );

		var outward = ArchApproachAxes.Outward( draft );
		var across = new Vector2( -outward.y, outward.x ) * (draft.Width * 0.5f);
		var foot = draft.Origin + outward * draft.Run;

		ArchGhost.Ring( new List<Vector2>
		{
			draft.Origin + across,
			foot + across,
			foot - across,
			draft.Origin - across
		}, room.BaseHeight + lift - rise );

		var change = rise < 0f ? $"drops {MathF.Abs( rise ):0}" : $"rises {rise:0}";

		ArchGhost.Note( new Vector3( foot.x, foot.y, room.BaseHeight + lift ),
			$"{draft.Kind} · {change} · run {draft.Run:0} · {draft.Width:0} wide" );
	}

	ArchApproachDrag Draft( Vector2 from, Vector2 to ) {
		var splay = Owner.FindArchetype( Owner.ActiveBuilding()?.Archetype )?.DriveSplay ?? approachDraft.Splay;

		return ArchApproachPlacement.Dragged( Owner.Plan, Owner.Kit, Owner.Level, from, to,
			approach ?? ApproachKind.Steps, approachDraft.Width, approachDraft.Kerbs, approachDraft.Rails, splay,
			approachDraft.Against );
	}

	bool PlaceApproach( Vector2 from, Vector2 to ) {
		var drag = Draft( from, to );

		if ( drag.Placed is not { } draft ) {
			Log.Info( $"Architecture: {drag.Refused}." );

			return true;
		}

		var room = drag.Room ?? Owner.EnsureRoom( drag.On );

		draft.Id = Owner.Plan.AllocateId();
		draft.Name = $"{draft.Kind}{room.Approaches.Count + 1}";

		room.Approaches.Add( draft );
		FinishDraft( draft.Id );
		Owner.Commit( $"Place {draft.Kind}" );

		return true;
	}

	float ChainRise() => snapStoreys ? stories * Owner.StoreyHeight : MathF.Max( 1f, flightRise );

	ArchStairPart FlightPart( ArchStairCore core, List<ArchStairLane> lanes, ArchRoom room ) {
		return new ArchStairPart {
			BaseHeight = room?.BaseHeight ?? Owner.LevelHeight,
			Core = core,
			Lanes = lanes,
			Width = draft.Width,
			StepRise = draft.StepRise,
			StepGoing = draft.StepGoing,
			WellGap = draft.WellGap,
			Support = draft.Support,
			AutoRailings = draft.AutoRailings,
			LeftRailing = draft.LeftRailing,
			RightRailing = draft.RightRailing,
			Under = draft.Under,
			Guard = draft.Guard,
			WallRail = draft.WallRail,
			Wrap = draft.Wrap,
			WrapThickness = draft.WrapThickness
		};
	}

	bool Aimed( out Vector3 point ) {
		var ray = Gizmo.CurrentRay;

		if ( Owner.FacesUnder( ray ).FirstOrDefault() is { } face ) {
			var facing = Vector3.Dot( face.Normal, ray.Forward );

			if ( MathF.Abs( facing ) > 0.0001f ) {
				var along = Vector3.Dot( face.Centre - ray.Position, face.Normal ) / facing;

				return Seated( ray.Position + ray.Forward * along, out point );
			}
		}

		var trace = Owner.Scene.Trace.Ray( ray.Position, ray.Position + ray.Forward * 32768f ).Run();

		if ( trace.Hit ) {
			return Seated( trace.EndPosition, out point );
		}

		if ( Owner.Cursor( out var cursor ) ) {
			point = new Vector3( cursor.Plan.x, cursor.Plan.y, cursor.Height );

			return true;
		}

		point = default;

		return false;
	}

	bool Seated( Vector3 hit, out Vector3 point ) {
		var flat = Owner.Snap( new Vector2( hit.x, hit.y ) );

		var storey = Owner.Plan.AllRooms().Where( entry => entry.Floor == Owner.Level );

		if ( Owner.SnapsToWalls
			&& ArchWallSnap.NearestWallFace( storey, flat, Owner.Kit.WallThickness, out var distance, out var onFace ) is not null
			&& distance < Owner.SnapReach().Face ) {
			flat = onFace;
		}

		point = new Vector3( flat.x, flat.y, ArchGridService.Fine( hit.z ) );

		return true;
	}

	ArchStairPart Continued( Vector3 point ) {
		var reach = MathF.Max( 24f, Owner.Kit.GridSize );

		foreach ( var room in Owner.Plan.AllRooms() ) {
			foreach ( var stair in room.Stairs ) {
				if ( ArchStairWalk.Head( stair, out var head ) && (head - point).Length <= reach ) {
					return stair;
				}
			}
		}

		return null;
	}

	ArchStairPart Previewed( IReadOnlyList<Vector3> line, ArchRoom room ) {
		if ( continuing is null ) {
			var walked = ArchStairWalk.Sketch( line, draft.Width, ChainRise() );

			return walked.Stands ? FlightPart( walked.Core, walked.Lanes, room ) : null;
		}

		var copy = FlightPart( continuing.Core.Copy(), continuing.Lanes.Select( lane => lane.Copy() ).ToList(), room );

		copy.BaseHeight = continuing.BaseHeight;

		return ArchStairWalk.Extend( copy, line, draft.Width ) ? copy : null;
	}

	protected override void DrawFreeHover() {
		if ( !Aimed( out var point ) ) {
			return;
		}

		var line = new List<Vector3>( walk ) { point };
		var marker = MathF.Max( 6f, draft.Width * 0.12f );

		for ( var index = 0; index < line.Count; index++ ) {
			var node = line[index];
			var flat = new Vector2( node.x, node.y );

			ArchGhost.Quad(
				ArchFootprint.Rect( flat - new Vector2( marker, marker ), flat + new Vector2( marker, marker ) ),
				node.z,
				ArchGhost.Nth( index ) );
		}

		if ( walk.Count == 0 ) {
			ArchGhost.Note( point, Continued( point ) is { } stair ? $"continue {stair.Name}" : "click the foot of the stair" );

			return;
		}

		var room = HostRoom( new Vector2( line[0].x, line[0].y ) );

		if ( room is null || Previewed( line, room ) is not { } preview ) {
			ArchGhost.Note( point, room is null
				? "no building here — walk the stair inside or against one"
				: "walk further — a flight needs a run" );

			return;
		}

		var shape = ArchStairShape.Resolve( preview, room, Owner.Kit );
		var flights = preview.Lanes.Count( lane => lane.Climbs );
		var pads = preview.Lanes.Count - flights;

		ArchStairGhost.Blueprint( preview );

		Gizmo.Draw.Color = ArchGhost.Ground;

		foreach ( var well in shape.Levels ) {
			foreach ( var outline in well.Loops ) {
				ArchGhost.Ring( outline, well.Height );
			}
		}

		Gizmo.Draw.Color = ArchGhost.Line;
		ArchGhost.Note( point, $"{flights} flight{(flights == 1 ? "" : "s")}"
			+ (pads > 0 ? $" · {pads} platform{(pads == 1 ? "" : "s")}" : "")
			+ $" · {preview.TotalRise:0} climb" );
	}

	protected override void OnFreeClick() {
		if ( !Aimed( out var point ) ) {
			return;
		}

		if ( walk.Count == 0 ) {
			continuing = Continued( point );

			if ( continuing is not null && ArchStairWalk.Head( continuing, out var head ) ) {
				walk.Add( head );

				return;
			}
		}

		Owner.EnsureTarget();
		walk.Add( point );
	}

	void FinishWalk() {
		if ( walk.Count < 2 ) {
			CancelWalk();

			return;
		}

		if ( continuing is not null ) {
			ExtendWalk();

			return;
		}

		if ( HostRoom( new Vector2( walk[0].x, walk[0].y ) ) is not { } room ) {
			Log.Info( "Architecture: the walk is outside every building — start it inside or against one so the stair has a room to stand in." );
			CancelWalk();

			return;
		}

		if ( Previewed( walk, room ) is not { } stair ) {
			Log.Info( $"Architecture: that walk is too short — a flight needs {ArchStairLanes.MinLane:0} inches of run." );
			CancelWalk();

			return;
		}

		stair.Id = Owner.Plan.AllocateId();
		stair.Name = $"Stair{room.Stairs.Count + 1}";
		stair.Core.Fill = fill;

		ArchStairLanes.Number( Owner.Plan, stair );

		room.Stairs.Add( stair );

		if ( cutStairwell ) {
			ArchStairShape.Pierce( Owner.Plan, Owner.Plan.OwnerOf( room ) ?? Owner.ActiveBuilding(), room, stair, Owner.Kit );
		}

		FinishDraft( stair.Id );
		Owner.Picked = new ArchSelection { Item = stair, Room = room, Building = Owner.Plan.OwnerOf( room ) };
		Drew( stair );
		Owner.Commit( "Walk Stair" );
		CancelWalk();

		ArchStairWindow.OpenOnWalk( Owner, stair );
	}

	void ExtendWalk() {
		if ( !ArchStairWalk.Extend( continuing, walk, draft.Width ) ) {
			CancelWalk();

			return;
		}

		ArchStairLanes.Number( Owner.Plan, continuing );

		FinishDraft( continuing.Id );
		Owner.Commit( "Continue Stair" );
		CancelWalk();
	}

	void CancelWalk() {
		walk.Clear();
		continuing = null;
	}

	protected override void OnClick( Vector2 point ) {
		if ( approach != ApproachKind.Ramp || !splineDriveway ) {
			return;
		}

		if ( drivewayStart is null ) {
			if ( ResolveDrivewayConnection( point, out drivewayStart ) ) {
				drivewayNodes.Add( DrivewayNode( drivewayStart.Position ) );
			}

			return;
		}

		if ( ResolveDrivewayConnection( point, out var end ) && end.Kind != drivewayStart.Kind ) {
			drivewayNodes.Add( DrivewayNode( end.Position ) );
			drivewayEnd = end;
			return;
		}

		drivewayNodes.Add( DrivewayNode( point ) );
		drivewayEnd = null;
	}

	protected override void DrawHover( Vector2 point ) {
		if ( approach != ApproachKind.Ramp || !splineDriveway ) {
			base.DrawHover( point );

			return;
		}

		base.DrawHover( point );

		if ( drivewayStart is null ) {
			var message = ResolveDrivewayConnection( point, out var connection )
				? $"start at {connection.Kind.ToString().ToLowerInvariant()}"
				: "start on an exterior wall or street edge";

			ArchGhost.Note( new Vector3( point.x, point.y, Owner.LevelHeight ), message );
			return;
		}

		var target = point;
		var completing = ResolveDrivewayConnection( point, out var end ) && end.Kind != drivewayStart.Kind;

		if ( completing ) {
			target = end.Position;
		}

		var line = drivewayNodes.Select( node => node.Copy() ).ToList();
		line.Add( DrivewayNode( target ) );

		if ( drivewayStart.Kind == DrivewayConnectionKind.Street ) {
			line.Reverse();
		}

		var curve = ArchCurve.Of( line );
		var frames = curve.Walk( MathF.Max( 8f, Owner.Kit.GridSize * 0.5f ) );

		ArchGhost.Curve( curve );
		DrawDrivewayWidth( frames, approachDraft.Width );

		var note = completing ? $"connect to {end.Kind.ToString().ToLowerInvariant()}" : "click a control point";
		ArchGhost.Note( new Vector3( target.x, target.y, Owner.LevelHeight ), note );
	}

	void FinishSplineDriveway( DrivewayConnection end ) {
		var wall = drivewayStart.Kind == DrivewayConnectionKind.Wall ? drivewayStart : end;
		var street = drivewayStart.Kind == DrivewayConnectionKind.Street ? drivewayStart : end?.Kind == DrivewayConnectionKind.Street ? end : null;

		if ( wall is null || drivewayNodes.Count < 2 ) {
			return;
		}

		var line = drivewayNodes.Select( node => node.Copy() ).ToList();

		if ( drivewayStart.Kind == DrivewayConnectionKind.Street ) {
			line.Reverse();
		}

		var splay = Owner.FindArchetype( wall.Building?.Archetype )?.DriveSplay ?? approachDraft.Splay;
		var placed = ArchApproachPlacement.Seat(
			wall.Wall, wall.Room, wall.Building, Owner.Kit, wall.Along, ApproachKind.Ramp,
			approachDraft.Width, 0f, approachDraft.Kerbs, false, splay );

		line[0].Position = new Vector3( placed.Origin.x, placed.Origin.y, wall.Room.BaseHeight );

		if ( street is not null ) {
			line[^1].Position = new Vector3( street.Position.x, street.Position.y, 0f );
		}

		GradeSplineDriveway( line, wall, street );
		AlignSplineDrivewayEnds( line, wall.Direction, street?.Direction ?? FreeEndDirection( line ) );

		placed.Id = Owner.Plan.AllocateId();
		placed.Name = $"SplineRamp{wall.Room.Approaches.Count + 1}";
		placed.RoadId = street?.Road.Id ?? 0;
		placed.Nodes = line;
		placed.Run = placed.Curve().Length;

		wall.Room.Approaches.Add( placed );
		FinishDraft( placed.Id );
		Owner.Commit( "Place Spline Driveway" );

		CancelSplineDriveway();
	}

	void TryFinishSplineDriveway() {
		if ( drivewayStart is null || drivewayNodes.Count < 2 ) {
			return;
		}

		if ( drivewayStart.Kind == DrivewayConnectionKind.Street && drivewayEnd?.Kind != DrivewayConnectionKind.Wall ) {
			return;
		}

		FinishSplineDriveway( drivewayEnd );
	}

	void GradeSplineDriveway( List<ArchCurveNode> line, DrivewayConnection wall, DrivewayConnection street ) {
		var lift = ArchAsks.Lift( Owner.Plan, wall.Building, Owner.Kit );
		var startHeight = wall.Room.BaseHeight;
		var endPoint = Flat( line[^1] );
		var endHeight = street is not null
			? street.RoadFrame.Position.z + (ArchAnswers.Load().Asks<IArchApproachLanding>()?.Back( street.Road, Owner.Kit ) ?? 0f) - lift
			: ArchTerrain.Sample( Owner.Scene, endPoint, out var terrainHeight ) ? terrainHeight - lift : -lift;
		var distances = new float[line.Count];

		for ( var index = 1; index < line.Count; index++ ) {
			distances[index] = distances[index - 1] + (Flat( line[index] ) - Flat( line[index - 1] )).Length;
		}

		var length = MathF.Max( 1f, distances[^1] );

		for ( var index = 0; index < line.Count; index++ ) {
			var position = line[index].Position;
			position.z = ArchGridService.Fine( MathX.Lerp( startHeight, endHeight, distances[index] / length ) );
			line[index].Position = position;
		}
	}

	void AlignSplineDrivewayEnds( List<ArchCurveNode> line, Vector2 wallDirection, Vector2 streetDirection ) {
		if ( line.Count < 2 ) {
			return;
		}

		var grid = Owner.Grid;
		var firstReach = MathF.Max( grid.SubgridSize( 4 ), (Flat( line[1] ) - Flat( line[0] )).Length / 3f );
		var lastReach = MathF.Max( grid.SubgridSize( 4 ), (Flat( line[^1] ) - Flat( line[^2] )).Length / 3f );
		var flatLength = MathF.Max( 1f, line.Zip( line.Skip( 1 ), ( from, to ) => (Flat( to ) - Flat( from )).Length ).Sum() );
		var slope = (line[^1].Position.z - line[0].Position.z) / flatLength;

		line[0].Mode = ArchTangentMode.Split;
		line[0].Out = new Vector3( wallDirection.x * firstReach, wallDirection.y * firstReach, slope * firstReach );
		line[^1].Mode = ArchTangentMode.Split;
		line[^1].In = new Vector3( -streetDirection.x * lastReach, -streetDirection.y * lastReach, -slope * lastReach );
	}

	static Vector2 FreeEndDirection( IReadOnlyList<ArchCurveNode> line ) {
		var direction = Flat( line[^1] ) - Flat( line[^2] );

		return direction.IsNearZeroLength ? new Vector2( 1f, 0f ) : direction.Normal;
	}

	bool ResolveDrivewayConnection( Vector2 point, out DrivewayConnection connection ) {
		var wallFound = ResolveDrivewayWall( point, out var wall );
		var streetFound = ResolveDrivewayStreet( point, out var street );

		if ( wallFound && (!streetFound || wall.Gap <= street.Gap) ) {
			connection = wall;
			return true;
		}

		connection = street;

		return streetFound;
	}

	bool ResolveDrivewayWall( Vector2 point, out DrivewayConnection connection ) {
		connection = null;

		var wall = ArchApproachPlacement.NearestExteriorWall(
			Owner.Plan.Buildings, Owner.Level, point,
			out var building, out var room, out var along, out var distance, out var outward );

		if ( wall is null || distance > MathF.Max( 48f, Owner.Kit.GridSize ) ) {
			return false;
		}

		var seated = ArchApproachPlacement.Seat(
			wall, room, building, Owner.Kit, along, ApproachKind.Ramp,
			approachDraft.Width, 0f, approachDraft.Kerbs, false, approachDraft.Splay );

		connection = new DrivewayConnection {
			Kind = DrivewayConnectionKind.Wall,
			Position = seated.Origin,
			Direction = outward,
			Gap = distance,
			Building = building,
			Room = room,
			Wall = wall,
			Along = along
		};

		return true;
	}

	bool ResolveDrivewayStreet( Vector2 point, out DrivewayConnection connection ) {
		connection = null;
		var nearest = float.MaxValue;

		foreach ( var road in Owner.Plan.Roads() ) {
			var curve = Owner.Resolved( road.Id, road.Curve );

			if ( !curve.Nearest( point, out var frame, out var centreGap ) ) {
				continue;
			}

			var across = new Vector2( frame.Across.x, frame.Across.y );
			var right = Vector2.Dot( point - frame.Flat, across ) >= 0f;
			var reach = road.Reach( right, frame.WidthScale );
			var edgeGap = MathF.Abs( centreGap - reach );

			if ( centreGap > reach + MathF.Max( 48f, Owner.Kit.GridSize ) || edgeGap >= nearest ) {
				continue;
			}

			var station = Owner.Grid.Subgrid( frame.Distance, 4 );

			if ( !curve.Sample( station, out frame ) ) {
				continue;
			}

			var side = right ? 1f : -1f;
			var edge = frame.Side( side * road.Reach( right, frame.WidthScale ) );

			nearest = edgeGap;
			connection = new DrivewayConnection {
				Kind = DrivewayConnectionKind.Street,
				Position = ArchGridService.Fine( new Vector2( edge.x, edge.y ) ),
				Direction = right ? -across : across,
				Gap = edgeGap,
				Road = road,
				RoadFrame = frame
			};
		}

		return connection is not null;
	}

	ArchCurveNode DrivewayNode( Vector2 point ) {
		var snapped = Owner.Grid.CurveControl( new Vector3( point.x, point.y, Owner.LevelHeight ) );

		return ArchCurveNode.At( snapped );
	}

	void CancelSplineDriveway() {
		drivewayNodes.Clear();
		drivewayStart = null;
		drivewayEnd = null;
	}

	static Vector2 Flat( ArchCurveNode node ) => new( node.Position.x, node.Position.y );

	static void DrawDrivewayWidth( IReadOnlyList<ArchFrame> frames, float width ) {
		for ( var index = 0; index < frames.Count - 1; index++ ) {
			var fromHalf = MathF.Max( 12f, width * frames[index].WidthScale ) * 0.5f;
			var toHalf = MathF.Max( 12f, width * frames[index + 1].WidthScale ) * 0.5f;

			Gizmo.Draw.Line( frames[index].Side( -fromHalf ), frames[index + 1].Side( -toHalf ) );
			Gizmo.Draw.Line( frames[index].Side( fromHalf ), frames[index + 1].Side( toHalf ) );
		}
	}

	protected override void DrawPreview() {
		if ( carving ) {
			DrawCarvePreview();
			return;
		}

		DrawApproachPreview();
	}

	protected override void OnDrag( Vector2 from, Vector2 to ) {
		if ( carving ) {
			CarveSteps( from, to );
			return;
		}

		PlaceApproach( from, to );
	}

	void DrawCarvePreview() {
		var drag = ArchCarveHosts.Aim( Owner.Plan, Owner.Level, DragStart, DragCurrent );
		var note = new Vector3( DragCurrent.x, DragCurrent.y, drag.Host?.TopHeight ?? Owner.LevelHeight );

		if ( !drag.Lands ) {
			ArchGhost.Plate( Min( DragStart, DragCurrent ), Max( DragStart, DragCurrent ), Owner.LevelHeight, 8 );
			ArchGhost.Note( note, "nothing here to carve into — stand a platform first" );

			return;
		}

		ArchGhost.Ring( drag.Host.Outline(), drag.Host.TopHeight );

		if ( ArchStairCarve.Sketch( drag, Owner.Kit, draft ) is not { } carved ) {
			ArchGhost.Note( note, $"{drag.Host.Name} — drag further to carve steps into it" );
			return;
		}

		ArchStairGhost.Flight( ArchStairCarve.Resolve( drag.Host, carved, Owner.ActiveBuilding(), Owner.Kit ), drag.Host.TopHeight );
		ArchGhost.Note( note, $"Concrete steps · {carved.Lanes.Count} flight carved into {drag.Host.Name}" );
	}

	void CarveSteps( Vector2 from, Vector2 to ) {
		var drag = ArchCarveHosts.Aim( Owner.Plan, Owner.Level, from, to );

		if ( !drag.Lands ) {
			Log.Info( "Architecture: Concrete steps are cut OUT of a platform's concrete — stand one with Boolean Modifiers first, then drag across it." );
			return;
		}

		if ( ArchStairCarve.Cut( Owner.Plan, drag, Owner.Kit, draft ) is not { } carved ) {
			Log.Info( $"Architecture: that run is shorter than {ArchStairCarve.MinRun} inches, so there is nothing to cut." );
			return;
		}

		FinishDraft( carved.Id );
		Owner.Commit( "Carve Steps" );
	}

	void PunchToTop() {
		var building = Owner.ActiveBuilding();

		if ( building is null ) {
			return;
		}

		var top = building.Rooms.Count > 0 ? building.Rooms.Max( entry => entry.Floor ) : Owner.Level;
		var head = ArchStairShape.FloorOf( building, Owner.Kit, top + 1 );

		flightRise = MathF.Max( 1f, head - Owner.LevelHeight );
		stories = Math.Max( 1, (int)MathF.Round( flightRise / MathF.Max( 1f, Owner.StoreyHeight ) ) );

		Refresh();
	}

	ArchRoom HostRoom( Vector2 point ) {
		var snap = MathF.Max( 48f, Owner.Kit.GridSize );

		if ( ArchApproachPlacement.NearestExteriorWall( Owner.Plan.Buildings, Owner.Level, point,
			out var building, out var room, out _, out var distance, out _ ) is not null && distance < snap && room is not null ) {
			return room;
		}

		return Owner.RoomOnLevel( point ) ?? Owner.ActiveRoom();
	}

	protected override void BuildOptions( ToolSidebarWidget panel ) {
		SeedRise();

		approachDraft.Kind = approach ?? ApproachKind.Steps;

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

		flow.Step( "Kind", KindName, KindGlyph, PickKind );

		if ( walking ) {
			WalkSteps( flow );

			return;
		}

		if ( carving ) {
			flow.Step( "Flight", FlightName, "straighten", BuildCarved );

			return;
		}

		ApproachSteps( flow );
	}

	string KindName => walking ? "Stair" : carving ? "Concrete steps" : approach == ApproachKind.Ramp ? "Driveway ramp" : "Foundation steps";

	string KindGlyph => walking ? "stairs" : carving ? "content_cut" : approach == ApproachKind.Ramp ? "trending_up" : "stairs_2";

	void PickKind( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Stair — click the foot, then each point it turns or arrives at; the clicks are the pattern", "stair_straight", "stairs", walking,
			() => Mode( () => walking = true, step ) );

		grid.Pick( "Concrete steps — cut a flight OUT of a platform you already stood", "stair_carve", "content_cut", carving,
			() => Mode( () => carving = true, step ) );

		grid.Pick( "Foundation steps — climb from grade onto the plinth at a door", "approach_steps", "stairs_2",
			approach == ApproachKind.Steps, () => Mode( () => approach = ApproachKind.Steps, step ) );

		grid.Pick( "Driveway ramp — slope from grade onto the plinth at a garage", "approach_ramp", "trending_up",
			approach == ApproachKind.Ramp, () => Mode( () => approach = ApproachKind.Ramp, step ) );
	}

	void Mode( Action chosen, ArchWorkflowStep step ) {
		CancelSplineDriveway();
		CancelWalk();

		walking = false;
		carving = false;
		approach = null;

		chosen();

		step.Chose();
	}

	void WalkSteps( ArchWorkflow flow ) {
		flow.Step( "Built of", Solid ? "Solid" : "Open", Solid ? "foundation" : "stairs", PickFill );

		flow.Step( "Carriage", CarriageName, ArchStairUi.Fallback( draft.Support ), BuildCarriage );

		flow.Step( "Balustrade", GuardName, ArchStairUi.Fallback( draft.Guard ), BuildBalustrade );

		// A stepped mass has no soffit to open, which is what makes the row absent rather than dead.
		if ( !Solid ) {
			flow.Step( "Underside", ArchStairLabels.Describe( draft.Under ), ArchStairLabels.Fallback( draft.Under ), BuildUnderside );
		}

		flow.Step( "Wrap", draft.Wrap == StairWrap.Walls ? "Walled well" : "Open", draft.Wrap == StairWrap.Walls ? "square_foot" : "crop_free", BuildWrap );

		flow.Step( "Climb", ClimbName, snapStoreys ? "stairs" : "straighten", BuildClimb );

		flow.Step( "Flight", FlightName, "straighten", BuildFlight );
	}

	bool Solid => fill == StairFill.Solid;

	void PickFill( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Open — treads on stringers over an open raked soffit, the joinery stair", "fill_open", "stairs",
			!Solid, () => Built( StairFill.Open, step ) );

		grid.Pick( "Solid — every flight a stepped mass standing on the shaft floor, the concrete stair", "fill_solid", "foundation",
			Solid, () => Built( StairFill.Solid, step ) );
	}

	void Built( StairFill wanted, ArchWorkflowStep step ) {
		fill = wanted;

		step.Chose();
	}

	string CarriageName => draft.Support == StairSupport.OpenString ? "Open string" : "Closed string";

	void BuildCarriage( ArchWorkflowStep step ) => ArchStairUi.Carriage( step.Layout, draft, step.Chose, null );

	string GuardName => draft.Guard switch {
		StairGuard.None => "None",
		StairGuard.Left => $"Left, {WornName}",
		StairGuard.Right => $"Right, {WornName}",
		_ => $"Both sides, {WornName}"
	};

	StairRailing Worn => draft.Guard == StairGuard.Right ? draft.RightRailing : draft.LeftRailing;

	string WornName => Worn == StairRailing.Handrail ? "rake" : "balustrade";

	void BuildBalustrade( ArchWorkflowStep step ) {
		ArchStairUi.Guard( step.Layout, draft, step.Chose, null );

		if ( draft.Guard == StairGuard.None ) {
			return;
		}

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

		grid.Pick( "Balustrade — newels, balusters and the handrail over them", "railing_balustrade", "fence",
			Worn == StairRailing.Balustrade, () => Wears( StairRailing.Balustrade ) );

		grid.Pick( "Rake — the handrail on its newels, nothing under it", "railing_handrail", "remove",
			Worn == StairRailing.Handrail, () => Wears( StairRailing.Handrail ) );
	}

	void Wears( StairRailing railing ) {
		draft.LeftRailing = railing;
		draft.RightRailing = railing;
	}

	void BuildUnderside( ArchWorkflowStep step ) => ArchStairUi.Underneath( step.Layout, draft, step.Chose, null );

	void BuildWrap( ArchWorkflowStep step ) => ArchStairUi.Wrap( step.Layout, draft, step.Chose, null );

	string ClimbName => snapStoreys
		? $"{stories} floor{(stories == 1 ? "" : "s")}{(cutStairwell ? ", well" : "")}"
		: $"{ChainRise():0} climb{(cutStairwell ? ", well" : "")}";

	void BuildClimb( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Storeys — the climb is whole floors and lands level with one", "bp_storeys", "stairs",
			snapStoreys, () => Measure( true, step ) );

		grid.Pick( "To a height — the climb reaches a head you type", "bp_head", "straighten",
			!snapStoreys, () => Measure( false, step ) );

		grid.Toggle( "Cut a stairwell through every floor the climb reaches", "opt_pierce_floor", "crop_free",
			cutStairwell, value => cutStairwell = value );
	}

	void Measure( bool storeys, ArchWorkflowStep step ) {
		snapStoreys = storeys;

		step.Chose();
	}

	string FlightName => $"{Sized( draft.Width, Owner.Kit.StairWidth ):0} wide, {Sized( draft.StepRise, Owner.Kit.StepRise ):0} rise";

	static float Sized( float value, float stock ) => value > 0f ? value : stock;

	void BuildFlight( ArchWorkflowStep step ) {
		if ( snapStoreys ) {
			step.Layout.Add( ArchPartUi.Integer( "Climbs floors", stories, 1, value => stories = Math.Clamp( value, 1, 12 ) ) );
		} else {
			var baseHeight = Owner.LevelHeight;

			step.Layout.Add( ArchPartUi.Number( "Head", baseHeight + flightRise, baseHeight + Owner.StoreyHeight,
				value => flightRise = MathF.Max( 1f, value - baseHeight ) ) );
		}

		step.Layout.Add( new Button( "To the top", "vertical_align_top" ) { Clicked = PunchToTop } );

		step.Layout.Add( ArchPartUi.Number( "Flight width", draft.Width, Owner.Kit.StairWidth, value => draft.Width = MathF.Max( ArchStairLanes.MinLane, value ) ) );
		step.Layout.Add( ArchPartUi.Number( "Riser", draft.StepRise, Owner.Kit.StepRise, value => draft.StepRise = value ) );
		step.Layout.Add( ArchPartUi.Number( "Going", draft.StepGoing, Owner.Kit.StepGoing, value => draft.StepGoing = value ) );
	}

	void BuildCarved( ArchWorkflowStep step ) {
		step.Layout.Add( ArchPartUi.Number( "Width", draft.Width, Owner.Kit.StairWidth, value => draft.Width = value ) );
		step.Layout.Add( ArchPartUi.Number( "Riser", draft.StepRise, Owner.Kit.StepRise, value => draft.StepRise = value ) );
		step.Layout.Add( ArchPartUi.Number( "Going", draft.StepGoing, Owner.Kit.StepGoing, value => draft.StepGoing = value ) );
	}

	void ApproachSteps( ArchWorkflow flow ) {
		if ( approachDraft.Kind == ApproachKind.Ramp ) {
			flow.Step( "Path", splineDriveway ? "Spline" : "Straight", splineDriveway ? "gesture" : "horizontal_rule", PickPath );
		}

		if ( !splineDriveway ) {
			flow.Step( "Lands on", AgainstName, AgainstGlyph, PickAgainst );
		}

		flow.Step( "Fittings", FittingsName, "fence", BuildFittings );
	}

	void PickPath( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Straight — locked square to the supporting wall", "driveway_straight", "horizontal_rule",
			!splineDriveway, () => Path( false, step ) );

		grid.Pick( "Spline — snapped control points between a wall and street", "driveway_spline", "gesture",
			splineDriveway, () => Path( true, step ) );
	}

	void Path( bool spline, ArchWorkflowStep step ) {
		CancelSplineDriveway();

		splineDriveway = spline;

		step.Chose();
	}

	string AgainstName => approachDraft.Against switch {
		ApproachAgainst.Foundation => "Foundation",
		ApproachAgainst.Platform => "Platform",
		_ => "Auto"
	};

	string AgainstGlyph => approachDraft.Against switch {
		ApproachAgainst.Foundation => "foundation",
		ApproachAgainst.Platform => "deck",
		_ => "auto_awesome"
	};

	void PickAgainst( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Auto — whichever of the two is nearer to the drag", "approach_auto", "auto_awesome",
			approachDraft.Against == ApproachAgainst.Auto, () => Against( ApproachAgainst.Auto, step ) );

		grid.Pick( "Foundation — the house's own plinth, climbing to its floor", "approach_foundation", "foundation",
			approachDraft.Against == ApproachAgainst.Foundation, () => Against( ApproachAgainst.Foundation, step ) );

		grid.Pick( "Platform — a podium's edge, climbing to its deck; its coping breaks to let the drive in",
			"approach_platform", "deck", approachDraft.Against == ApproachAgainst.Platform,
			() => Against( ApproachAgainst.Platform, step ) );
	}

	void Against( ApproachAgainst wanted, ArchWorkflowStep step ) {
		approachDraft.Against = wanted;

		step.Chose();
	}

	string FittingsName {
		get {
			if ( approachDraft.Kind != ApproachKind.Ramp ) {
				return approachDraft.Rails ? "Handrails" : "Bare";
			}

			var worn = new List<string>();

			if ( approachDraft.Kerbs ) {
				worn.Add( "Kerbs" );
			}

			if ( approachDraft.Skirted ) {
				worn.Add( "skirted" );
			}

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

	void BuildFittings( ArchWorkflowStep step ) {
		if ( approachDraft.Kind == ApproachKind.Ramp && splineDriveway ) {
			step.Layout.Add( ArchPartUi.Number( "Width", approachDraft.Width, 96f,
				value => approachDraft.Width = Owner.Grid.Subgrid( value, 4 ) ) );
		}

		ArchSidebarLayout.Form( step.Layout, form => ArchStairUi.Fittings( form, approachDraft, null ) );
	}

	void SeedRise() {
		if ( seededRise ) {
			return;
		}

		flightRise = Owner.StoreyHeight;
		seededRise = true;
	}

	string WalkAdvice() {
		if ( continuing is not null ) {
			return $"Walking {continuing.Name} on from its head. Click, then Enter.";
		}

		var destination = snapStoreys ? $"reaches floor {Owner.Level + stories + 1}" : $"{ChainRise():0} of climb";

		return $"Click the foot, then each turn, then Enter — {destination}.";
	}
}