Editor/Stair/ArchStairShape.cs
namespace Sunless.Architecture;

public sealed class ArchStairRun {
	public int Index { get; init; }
	public ArchStairAxes Axes { get; init; }
	public int Steps { get; init; }
	public float Going { get; init; }
	public float Rise { get; init; }
	public float Width { get; init; }
	public float BaseHeight { get; init; }
	public StairTurn Turn { get; init; }
	public bool Last { get; init; }
	public float Drawn { get; init; }
	public float DrawnWidth { get; init; }
	public int Level { get; init; }

	public bool WalledRight { get; init; }
	public bool WalledLeft { get; init; }

	// Resolved once — the run's own railing rows override the stair's Guard/WallRail
	public bool GuardLeft { get; init; }
	public bool GuardRight { get; init; }
	public bool HandrailLeft { get; init; }
	public bool HandrailRight { get; init; }

	public StairRailing RailingLeft { get; init; }
	public StairRailing RailingRight { get; init; }

	public Vector2 SpanLeft { get; init; } = new( 0f, 1f );
	public Vector2 SpanRight { get; init; } = new( 0f, 1f );

	public float Length => Steps * Going;

	public float TopHeight => BaseHeight + Steps * Rise;

	public float StepTop( int step ) => BaseHeight + (step + 1) * Rise;

	public float Rake( float along ) => BaseHeight + MathX.Clamp( along / MathF.Max( 1f, Length ), 0f, 1f ) * (TopHeight - BaseHeight);

	public float Nosing( float along ) => MathF.Min( Rake( along ) + Rise, TopHeight );

	public float Under( float clearance ) {
		return Length * ArchClearance.Cross( BaseHeight + clearance, TopHeight + clearance, TopHeight ).At;
	}

	public List<Vector2> Loop() => Loop( 0f );

	public List<Vector2> Loop( float from ) => Axes.Rect( Math.Clamp( from, 0f, MathF.Max( 0f, Length - 1f ) ), Length, 0f, Width );
}

public sealed class ArchStairPad {
	// -1 for synthesised arrival pads
	public int Index { get; init; } = -1;

	public ArchStairAxes Axes { get; init; }
	public float AlongFrom { get; set; }
	public float AlongTo { get; set; }
	public float AcrossFrom { get; set; }
	public float AcrossTo { get; set; }
	public float Height { get; init; }
	public StairTurn Turn { get; init; }
	public bool Arrival { get; init; }
	public int? Level { get; init; }
	public int Storey { get; init; }
	public bool WalledFrom { get; set; }
	public bool WalledTo { get; set; }
	public bool WalledHead { get; set; }

	// Null = landing defers to the turn direction
	public bool? RailedFrom { get; init; }
	public bool? RailedTo { get; init; }
	public bool? RailedHead { get; init; }

	public bool RailsFrom => RailedFrom ?? (!WalledFrom && Turn != StairTurn.Right);

	public bool RailsTo => RailedTo ?? (!WalledTo && Turn != StairTurn.Left);

	public bool RailsHead => RailedHead ?? (!WalledHead && Turn != StairTurn.None);

	public List<Vector2> Loop() => Axes.Rect( AlongFrom, AlongTo, AcrossFrom, AcrossTo );
}

public sealed class ArchStairWell {
	public int Level { get; init; }
	public float Height { get; init; }
	public ArchRoom Probe { get; init; }
	public List<List<Vector2>> Loops { get; } = new();
}

public sealed partial class ArchStairShape {
	public List<ArchStairRun> Runs { get; } = new();
	public List<ArchStairPad> Pads { get; } = new();
	public List<ArchStairWell> Levels { get; } = new();
	public List<List<Vector2>> AutomaticTopOpening { get; } = new();

	public float Width { get; init; }
	public float Going { get; init; }
	public float Rise { get; init; }
	public float BaseHeight { get; init; }
	public float TopHeight { get; private set; }
	public int TopLevel { get; private set; }

	public float OpenEdge { get; private set; }
	public float ClosedEdge { get; private set; }
	public bool GuardLeft { get; init; }
	public bool GuardRight { get; init; }

	public static float StoreyHeight( ArchBuilding building, ArchKit kit ) => ArchBuild.StoreyHeight( building, kit );

	public static float FloorOf( ArchBuilding building, ArchKit kit, int level ) => ArchBuild.FloorOf( building, kit, level );

	public static int FloorIndex( ArchBuilding building, ArchKit kit, float height ) {
		return Math.Clamp( (int)MathF.Round( (height - kit.GroundClearance) / MathF.Max( 1f, StoreyHeight( building, kit ) ) ), -64, 64 );
	}

	public static IReadOnlyList<ArchStairLane> Standing( ArchStairPart stair, ArchStairCore core ) {
		if ( stair.Lanes.Count > 0 ) {
			return stair.Lanes;
		}

		return new List<ArchStairLane>
		{
			new()
			{
				AlongFrom = 0f,
				AlongTo = MathF.Max( 1f, core.Length ),
				AcrossFrom = 0f,
				AcrossTo = MathF.Max( 1f, core.Width ),
				Walk = StairWalk.Ahead
			}
		};
	}

	public static ArchStairShape Resolve( ArchStairPart stair, ArchRoom room, ArchKit kit, IEnumerable<ArchRoom> storey = null, ArchBuilding building = null ) {
		var walls = storey ?? building?.Rooms;
		var core = stair.Core ?? new ArchStairCore();
		var lanes = Standing( stair, core );

		var going = stair.StepGoing > 0.5f ? stair.StepGoing : kit.StepGoing;
		// Stair's own riser beats kit — carved flights bend it to arrive at the host's coping
		var riser = stair.StepRise > 0.5f ? stair.StepRise : kit.StepRise;

		var guardLeft = stair.Guard is StairGuard.Left or StairGuard.Both;
		var guardRight = stair.Guard is StairGuard.Right or StairGuard.Both;
		var openLeft = guardLeft || !guardRight;
		var kinds = ArchKinds.Load();

		var climbs = ArchStairLanes.Climbs( core, lanes );
		var lead = lanes.FirstOrDefault( lane => lane.Climbs ) ?? lanes[0];

		var width = MathF.Max( 1f, lead.Width > 1f ? lead.Width : stair.Width > 1f ? stair.Width : kit.StairWidth );

		var housings = new ArchStairHousings( walls, kit, kinds );
		var fits = Fitting( core, lanes, climbs, housings, building, kit, stair.BaseHeight );
		var totalSteps = 0;

		for ( var index = 0; index < lanes.Count; index++ ) {
			if ( lanes[index].Climbs ) {
				totalSteps += ArchStairLanes.Steps( climbs[index], riser, fits[index].AlongTo - fits[index].AlongFrom, going );
			}
		}

		var shape = new ArchStairShape {
			Width = width,
			Going = going,
			Rise = ArchStairLanes.Climb( core, lanes ) / Math.Max( 1, totalSteps ),
			BaseHeight = stair.BaseHeight,
			GuardLeft = guardLeft,
			GuardRight = guardRight,
			OpenEdge = openLeft ? width : 0f,
			ClosedEdge = openLeft ? 0f : width
		};

		var startLevel = FloorIndex( building, kit, stair.BaseHeight );
		var baseProbe = Probe( room, walls, startLevel, room, kinds );
		var height = stair.BaseHeight;

		var frame = core.Axes;
		var arrived = (ArchStairRun)null;

		for ( var index = 0; index < lanes.Count; index++ ) {
			var lane = lanes[index];
			var level = FloorIndex( building, kit, height );
			var probe = Probe( room, walls, level, baseProbe, kinds );

			if ( !lane.Climbs ) {
				height += climbs[index];

				var stood = fits[index];

				var landing = new ArchStairPad {
					Index = index,
					RailedFrom = Railed( lane, StairEdge.Right ),
					RailedTo = Railed( lane, StairEdge.Left ),
					RailedHead = Railed( lane, StairEdge.Head ),
					Axes = frame,
					AlongFrom = stood.AlongFrom,
					AlongTo = stood.AlongTo,
					AcrossFrom = stood.AcrossFrom,
					AcrossTo = stood.AcrossTo,
					Height = height,
					Turn = arrived?.Turn ?? StairTurn.None,
					Arrival = NextFlight( lanes, index ) is null,
					Level = PadLevel( building, kit, height ),
					Storey = FloorIndex( building, kit, height )
				};

				Corner( landing, probe, kit, kinds );

				shape.Pads.Add( landing );

				continue;
			}

			var next = NextFlight( lanes, index );
			var last = next is null;
			var climb = climbs[index];
			var drawn = MathF.Max( 1f, lane.Length );
			var seated = fits[index];
			var length = seated.AlongTo - seated.AlongFrom;
			var steps = ArchStairLanes.Steps( climb, riser, length, going );
			var laneWidth = seated.AcrossTo - seated.AcrossFrom;
			var walked = lane.Axes( core );
			var axes = new ArchStairAxes { Origin = walked.Flat( 0f, seated.AcrossFrom ), Yaw = walked.Yaw };
			var walledRight = Walled( probe, kit, axes, length * 0.5f, 0f, -1f, kinds );
			var walledLeft = Walled( probe, kit, axes, length * 0.5f, laneWidth, 1f, kinds );

			var run = new ArchStairRun {
				Index = index,
				Axes = axes,
				Steps = steps,
				Drawn = drawn,
				DrawnWidth = MathF.Max( 1f, lane.Width ),
				Going = length / steps,
				Rise = climb / steps,
				Width = laneWidth,
				BaseHeight = height,
				Turn = last ? StairTurn.None : TurnBetween( core, lane, next ),
				Last = last,
				Level = level,
				WalledRight = walledRight,
				WalledLeft = walledLeft,
				GuardLeft = Carries( lane, StairEdge.Left ) ?? (stair.AutoRailings && guardLeft),
				GuardRight = Carries( lane, StairEdge.Right ) ?? (stair.AutoRailings && guardRight),
				RailingLeft = Kind( lane, StairEdge.Left ) ?? stair.LeftRailing,
				RailingRight = Kind( lane, StairEdge.Right ) ?? stair.RightRailing,
				HandrailLeft = walledLeft && (Carries( lane, StairEdge.Left, StairRailing.Handrail ) ?? (stair.AutoRailings && stair.WallRail)),
				HandrailRight = walledRight && (Carries( lane, StairEdge.Right, StairRailing.Handrail ) ?? (stair.AutoRailings && stair.WallRail)),
				SpanLeft = Spanned( lane, StairEdge.Left ),
				SpanRight = Spanned( lane, StairEdge.Right )
			};

			shape.Runs.Add( run );

			height = run.TopHeight;
			frame = axes;
			arrived = run;

			if ( last && stair.TopLanding && lanes[^1].Climbs ) {
				var arrival = housings.On( FloorIndex( building, kit, height ) )
					.Fit( axes, length, length + MathF.Max( going, stair.TopLandingDepth ), 0f, laneWidth );

				shape.Pads.Add( new ArchStairPad {
					Axes = axes,
					AlongFrom = arrival.AlongFrom,
					AlongTo = arrival.AlongTo,
					AcrossFrom = arrival.AcrossFrom,
					AcrossTo = arrival.AcrossTo,
					Height = height,
					Turn = StairTurn.None,
					Arrival = true,
					Level = PadLevel( building, kit, height ),
					Storey = FloorIndex( building, kit, height )
				} );
			}
		}

		shape.TopHeight = height;
		shape.TopLevel = FloorIndex( building, kit, height );

		if ( shape.Runs.Count > 0 && shape.Runs[0].WalledLeft != shape.Runs[0].WalledRight ) {
			var walledLeft = shape.Runs[0].WalledLeft;

			shape.ClosedEdge = walledLeft ? width : 0f;
			shape.OpenEdge = walledLeft ? 0f : width;
		}

		for ( var level = startLevel + 1; level <= shape.TopLevel; level++ ) {
			var well = new ArchStairWell {
				Level = level,
				Height = FloorOf( building, kit, level ),
				Probe = Probe( room, walls, level, baseProbe, kinds )
			};

			var soffit = well.Height - kit.FloorThickness;
			var headroom = MathF.Max( 1f, kit.StairHeadroom );

			foreach ( var run in shape.Runs ) {
				if ( run.BaseHeight + 0.5f >= well.Height ) {
					continue;
				}

				var reach = ArchClearance.Cross( run.BaseHeight + headroom, run.TopHeight + headroom, soffit );

				if ( reach.Clears ) {
					continue;
				}

				var openingFrom = reach.Blocked ? 0f : Riser( run, reach.At * run.Length );

				if ( level == shape.TopLevel ) {
					openingFrom = MathF.Max( 0f, run.Length - (run.Length - openingFrom) * 1.25f );
				}

				well.Loops.Add( run.Loop( openingFrom ) );
			}

			foreach ( var pad in shape.Pads ) {
				if ( PadWell( building, kit, pad ) == level && pad.Height + headroom > soffit ) {
					well.Loops.Add( pad.Loop() );
				}
			}

			if ( well.Loops.Count > 0 ) {
				shape.Levels.Add( well );
			}
		}

		if ( shape.Levels.Find( well => well.Level == shape.TopLevel ) is { } top ) {
			shape.AutomaticTopOpening.AddRange( top.Loops.Select( loop => loop.ToList() ) );

			if ( stair.TopOpening is { Count: >= 3 } ) {
				top.Loops.Clear();
				top.Loops.Add( stair.TopOpening.Select( point => core.Flat( point.x, point.y ) ).ToList() );
			}
		}

		return shape;
	}

	// Null when the lane has no explicit guards — falls through to stair-level settings
	static bool? Carries( ArchStairLane lane, StairEdge edge, StairRailing railing ) {
		if ( lane.Guards.Count == 0 ) {
			return null;
		}

		return lane.Guards.Any( guard => guard.Edge == edge && guard.Railing == railing );
	}

	static bool? Carries( ArchStairLane lane, StairEdge edge ) {
		if ( lane.Guards.Count == 0 ) {
			return null;
		}

		return lane.Guards.Any( guard => guard.Edge == edge );
	}

	static StairRailing? Kind( ArchStairLane lane, StairEdge edge ) {
		if ( Carries( lane, edge ) != true ) {
			return null;
		}

		return lane.Guards.Any( guard => guard.Edge == edge && guard.Railing == StairRailing.Balustrade )
			? StairRailing.Balustrade
			: StairRailing.Handrail;
	}

	static Vector2 Spanned( ArchStairLane lane, StairEdge edge ) {
		var start = 1f;
		var end = 0f;
		var found = false;

		foreach ( var guard in lane.Guards ) {
			if ( guard.Edge != edge || !guard.Stands ) {
				continue;
			}

			start = MathF.Min( start, guard.Start );
			end = MathF.Max( end, guard.End );
			found = true;
		}

		return found ? new Vector2( start, end ) : new Vector2( 0f, 1f );
	}

	static bool? Railed( ArchStairLane lane, StairEdge edge ) {
		return lane.Guards.Count == 0 ? null : lane.Guarding( edge );
	}

	static ArchStairLane NextFlight( IReadOnlyList<ArchStairLane> lanes, int after ) {
		for ( var index = after + 1; index < lanes.Count; index++ ) {
			if ( lanes[index].Climbs ) {
				return lanes[index];
			}
		}

		return null;
	}

	static void Framed( ArchStairAxes frame, ArchStairCore core, ArchStairLane landing, out float alongFrom, out float alongTo, out float acrossFrom, out float acrossTo ) {
		alongFrom = float.MaxValue;
		alongTo = float.MinValue;
		acrossFrom = float.MaxValue;
		acrossTo = float.MinValue;

		foreach ( var corner in landing.Loop( core ) ) {
			var local = corner - frame.Origin;
			var along = Vector2.Dot( local, frame.Along );
			var across = Vector2.Dot( local, frame.Across );

			alongFrom = MathF.Min( alongFrom, along );
			alongTo = MathF.Max( alongTo, along );
			acrossFrom = MathF.Min( acrossFrom, across );
			acrossTo = MathF.Max( acrossTo, across );
		}
	}

	static StairTurn TurnBetween( ArchStairCore core, ArchStairLane here, ArchStairLane next ) {
		return TurnKind( AngleDelta( next.Yaw( core ) - here.Yaw( core ) ) );
	}
}