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

public static class ArchStairLanes {
	const float Nothing = 0.05f;

	public const float MinLane = 18f;

	public static float Climb( ArchStairCore core, IReadOnlyList<ArchStairLane> lanes ) {
		var pinned = 0f;

		foreach ( var lane in lanes ) {
			pinned += MathF.Max( 0f, lane.Rise );
		}

		return MathF.Max( MathF.Max( 1f, core?.Rise ?? 0f ), pinned );
	}

	// Pinned lanes take their exact rise; unpinned flights share the remainder proportionally
	public static float[] Climbs( ArchStairCore core, IReadOnlyList<ArchStairLane> lanes ) {
		var climbs = new float[lanes.Count];

		if ( lanes.Count == 0 ) {
			return climbs;
		}

		var pinned = 0f;
		var openSpan = 0f;
		var openCount = 0;

		foreach ( var lane in lanes ) {
			if ( lane.Rise > Nothing ) {
				pinned += lane.Rise;

				continue;
			}

			if ( !lane.Climbs ) {
				continue;
			}

			openSpan += lane.Length;
			openCount++;
		}

		var free = MathF.Max( 0f, Climb( core, lanes ) - pinned );

		for ( var index = 0; index < lanes.Count; index++ ) {
			var lane = lanes[index];

			if ( lane.Rise > Nothing ) {
				climbs[index] = lane.Rise;

				continue;
			}

			if ( !lane.Climbs ) {
				continue;
			}

			climbs[index] = openSpan > Nothing
				? free * lane.Length / openSpan
				: free / MathF.Max( 1, openCount );
		}

		return climbs;
	}

	// Whichever of riser or going needs more steps wins
	public static int Steps( float climb, float riser, float length, float going ) {
		var byRise = (int)MathF.Round( climb / MathF.Max( 1f, riser ) );
		var byRun = (int)MathF.Round( length / MathF.Max( 1f, going ) );

		return Math.Max( 1, Math.Max( byRise, Math.Min( byRun, (int)MathF.Floor( climb / ShallowestRiser ) ) ) );
	}

	public const float ShallowestRiser = 2f;

	public static Vector2 Local( ArchStairCore core, Vector2 point ) {
		var axes = core.Axes;
		var offset = point - core.Origin;

		return new Vector2( Vector2.Dot( offset, axes.Along ), Vector2.Dot( offset, axes.Across ) );
	}

	static (float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo) Bounds( IReadOnlyList<ArchStairLane> lanes ) {
		var alongFrom = float.MaxValue;
		var alongTo = float.MinValue;
		var acrossFrom = float.MaxValue;
		var acrossTo = float.MinValue;

		foreach ( var corner in lanes.SelectMany( lane => lane.Corners() ) ) {
			alongFrom = MathF.Min( alongFrom, corner.x );
			alongTo = MathF.Max( alongTo, corner.x );
			acrossFrom = MathF.Min( acrossFrom, corner.y );
			acrossTo = MathF.Max( acrossTo, corner.y );
		}

		return (alongFrom, alongTo, acrossFrom, acrossTo);
	}

	// Grows the core to fit its lanes — the core frames what's inside it
	public static bool Fit( ArchStairCore core, IReadOnlyList<ArchStairLane> lanes ) {
		if ( core is null || lanes.Count == 0 ) {
			return false;
		}

		var (alongFrom, alongTo, acrossFrom, acrossTo) = Bounds( lanes );

		alongFrom = MathF.Min( alongFrom, 0f );
		acrossFrom = MathF.Min( acrossFrom, 0f );
		alongTo = MathF.Max( alongTo, core.Length );
		acrossTo = MathF.Max( acrossTo, core.Width );

		if ( alongFrom > -Nothing && acrossFrom > -Nothing
			&& alongTo - core.Length < Nothing && acrossTo - core.Width < Nothing ) {
			return false;
		}

		core.Origin = core.Flat( alongFrom, acrossFrom );
		core.Length = alongTo - alongFrom;
		core.Width = acrossTo - acrossFrom;

		foreach ( var lane in lanes ) {
			lane.AlongFrom -= alongFrom;
			lane.AlongTo -= alongFrom;
			lane.AcrossFrom -= acrossFrom;
			lane.AcrossTo -= acrossFrom;
		}

		return true;
	}

	// Reseats the core tightly onto its lanes (Fit only grows; this also shrinks)
	public static void Reseat( ArchStairCore core, IReadOnlyList<ArchStairLane> lanes ) {
		if ( core is null || lanes.Count == 0 ) {
			return;
		}

		var (alongFrom, alongTo, acrossFrom, acrossTo) = Bounds( lanes );

		core.Origin = core.Flat( alongFrom, acrossFrom );
		core.Length = MathF.Max( MinLane, alongTo - alongFrom );
		core.Width = MathF.Max( MinLane, acrossTo - acrossFrom );

		foreach ( var lane in lanes ) {
			lane.AlongFrom -= alongFrom;
			lane.AlongTo -= alongFrom;
			lane.AcrossFrom -= acrossFrom;
			lane.AcrossTo -= acrossFrom;
		}
	}

	public static void Number( ArchPlan plan, ArchStairPart stair ) {
		foreach ( var lane in stair.Lanes.Where( lane => lane.Id == 0 ) ) {
			lane.Id = plan.AllocateId();
		}
	}

	public static bool DeriveLandings( ArchPlan plan, ArchStairPart stair ) {
		if ( stair.Core is not { } core || stair.Lanes.Count == 0 || stair.Lanes.Any( lane => !lane.Climbs ) ) {
			return false;
		}

		var settled = new List<ArchStairLane>();

		for ( var index = 0; index < stair.Lanes.Count; index++ ) {
			var lane = stair.Lanes[index];

			settled.Add( lane );

			if ( index + 1 >= stair.Lanes.Count || Between( core, lane, stair.Lanes[index + 1] ) is not { } pad ) {
				continue;
			}

			settled.Add( new ArchStairLane {
				Id = plan.AllocateId(),
				Step = StairStep.Landing,
				AlongFrom = pad.AlongFrom,
				AlongTo = pad.AlongTo,
				AcrossFrom = pad.AcrossFrom,
				AcrossTo = pad.AcrossTo,
				Walk = lane.Walk
			} );
		}

		foreach ( var lane in settled ) {
			lane.LandingDepth = 0f;
		}

		stair.Lanes.Clear();
		stair.Lanes.AddRange( settled );

		return true;
	}

	readonly struct ArchStairLanding {
		public float AlongFrom { get; init; }
		public float AlongTo { get; init; }
		public float AcrossFrom { get; init; }
		public float AcrossTo { get; init; }
	}

	static ArchStairLanding? Between( ArchStairCore core, ArchStairLane lane, ArchStairLane next ) {
		var head = lane.HeadEdge();
		var foot = next.FootEdge();

		var alongFrom = Least( head.AlongFrom, head.AlongTo, foot.AlongFrom, foot.AlongTo );
		var alongTo = Most( head.AlongFrom, head.AlongTo, foot.AlongFrom, foot.AlongTo );
		var acrossFrom = Least( head.AcrossFrom, head.AcrossTo, foot.AcrossFrom, foot.AcrossTo );
		var acrossTo = Most( head.AcrossFrom, head.AcrossTo, foot.AcrossFrom, foot.AcrossTo );

		var heading = lane.Heading;
		var alongAxis = MathF.Abs( heading.x ) > 0.5f;

		if ( alongAxis && alongTo - alongFrom < Nothing ) {
			var line = head.AlongFrom;
			var depth = Depth( lane, Reach( core, lane, next, line, heading.x, acrossFrom, acrossTo, true ) );

			if ( depth < Nothing ) {
				return null;
			}

			alongFrom = heading.x > 0f ? line : line - depth;
			alongTo = heading.x > 0f ? line + depth : line;
		} else if ( !alongAxis && acrossTo - acrossFrom < Nothing ) {
			var line = head.AcrossFrom;
			var depth = Depth( lane, Reach( core, lane, next, line, heading.y, alongFrom, alongTo, false ) );

			if ( depth < Nothing ) {
				return null;
			}

			acrossFrom = heading.y > 0f ? line : line - depth;
			acrossTo = heading.y > 0f ? line + depth : line;
		}

		if ( alongTo - alongFrom < Nothing || acrossTo - acrossFrom < Nothing ) {
			return null;
		}

		return new ArchStairLanding {
			AlongFrom = alongFrom,
			AlongTo = alongTo,
			AcrossFrom = acrossFrom,
			AcrossTo = acrossTo
		};
	}

	static float Depth( ArchStairLane lane, float reach ) {
		return lane.LandingDepth > Nothing ? MathF.Min( lane.LandingDepth, reach ) : reach;
	}

	static float Reach( ArchStairCore core, ArchStairLane lane, ArchStairLane next, float line, float direction, float from, float to, bool alongAxis ) {
		var limit = direction > 0f ? alongAxis ? core.Length : core.Width : 0f;

		foreach ( var candidate in new[] { lane, next } ) {
			var crossFrom = alongAxis ? candidate.AcrossFrom : candidate.AlongFrom;
			var crossTo = alongAxis ? candidate.AcrossTo : candidate.AlongTo;

			if ( crossTo <= from + Nothing || crossFrom >= to - Nothing ) {
				continue;
			}

			var near = alongAxis ? candidate.AlongFrom : candidate.AcrossFrom;
			var far = alongAxis ? candidate.AlongTo : candidate.AcrossTo;

			if ( direction > 0f && near >= line - Nothing ) {
				limit = MathF.Min( limit, near );
			}

			if ( direction < 0f && far <= line + Nothing ) {
				limit = MathF.Max( limit, far );
			}
		}

		return MathF.Max( 0f, MathF.Abs( limit - line ) );
	}

	public static (ArchStairCore Core, List<ArchStairLane> Lanes) FromLegs( IReadOnlyList<ArchStairLeg> legs ) {
		var core = new ArchStairCore { Yaw = legs.Count > 0 ? legs[0].Yaw : 0f, Origin = legs.Count > 0 ? legs[0].Start : Vector2.Zero };
		var lanes = new List<ArchStairLane>( legs.Count );

		if ( legs.Count == 0 ) {
			return (core, lanes);
		}

		var frame = core.Axes;
		var alongFrom = float.MaxValue;
		var alongTo = float.MinValue;
		var acrossFrom = float.MaxValue;
		var acrossTo = float.MinValue;
		var boxes = new List<(float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo, StairWalk Walk)>();

		foreach ( var leg in legs ) {
			var width = leg.Width > 1f ? leg.Width : 48f;
			var corners = leg.Axes.Rect( 0f, leg.Length, 0f, width );
			var lowAlong = float.MaxValue;
			var highAlong = float.MinValue;
			var lowAcross = float.MaxValue;
			var highAcross = float.MinValue;

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

				lowAlong = MathF.Min( lowAlong, along );
				highAlong = MathF.Max( highAlong, along );
				lowAcross = MathF.Min( lowAcross, across );
				highAcross = MathF.Max( highAcross, across );
			}

			var travel = leg.Span.IsNearZeroLength ? frame.Along : leg.Span.Normal;
			var forward = Vector2.Dot( travel, frame.Along );
			var sideways = Vector2.Dot( travel, frame.Across );

			boxes.Add( (lowAlong, highAlong, lowAcross, highAcross, MathF.Abs( sideways ) > MathF.Abs( forward )
				? sideways >= 0f ? StairWalk.Left : StairWalk.Right
				: forward >= 0f ? StairWalk.Ahead : StairWalk.Back) );

			alongFrom = MathF.Min( alongFrom, lowAlong );
			alongTo = MathF.Max( alongTo, highAlong );
			acrossFrom = MathF.Min( acrossFrom, lowAcross );
			acrossTo = MathF.Max( acrossTo, highAcross );
		}

		core.Origin = frame.Flat( alongFrom, acrossFrom );
		core.Length = MathF.Max( 1f, alongTo - alongFrom );
		core.Width = MathF.Max( 1f, acrossTo - acrossFrom );

		foreach ( var box in boxes ) {
			lanes.Add( new ArchStairLane {
				AlongFrom = box.AlongFrom - alongFrom,
				AlongTo = box.AlongTo - alongFrom,
				AcrossFrom = box.AcrossFrom - acrossFrom,
				AcrossTo = box.AcrossTo - acrossFrom,
				Walk = box.Walk
			} );
		}

		return (core, lanes);
	}

	static float Least( float a, float b, float c, float d ) => MathF.Min( MathF.Min( a, b ), MathF.Min( c, d ) );

	static float Most( float a, float b, float c, float d ) => MathF.Max( MathF.Max( a, b ), MathF.Max( c, d ) );
}