Editor/Porch/ArchBays.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// A stretch of the walk that wants no infill - where a gate swings, or where a flight comes down through.
public readonly struct ArchBayGate {
	public float At { get; init; }
	public float Width { get; init; }

	public bool Spans( float from, float to ) => Width > 0f && to > At && from < At + Width;
}

// A gated bay keeps its posts and loses its infill - the gap a gate or a flight uses.
public readonly struct ArchBay {
	public ArchStation From { get; init; }
	public ArchStation To { get; init; }
	public bool Gated { get; init; }
	public bool Last { get; init; }

	public float Span => (To.Point - From.Point).Length;

	public ArchBay Narrowed( float by ) {
		return new ArchBay {
			From = ArchStation.Between( From.Point, To.Point, by, From.Height, From.Distance + by ),
			To = ArchStation.Between( From.Point, To.Point, MathF.Max( by, Span - by ), To.Height, To.Distance - by ),
			Gated = Gated,
			Last = Last
		};
	}
}

// Distance runs across the WHOLE walk, never restarted per edge.
public static class ArchBays {
	public static List<ArchBay> Over( IEnumerable<ArchRunPath> runs, float spacing, float minimum, IReadOnlyList<ArchBayGate> gates = null ) {
		var bays = new List<ArchBay>();
		var travelled = 0f;

		foreach ( var run in runs ) {
			for ( var edge = 0; edge < run.Edges; edge++ ) {
				var from = run.At( edge );
				var to = run.At( edge + 1 );
				var length = (to - from).Length;

				// A leg too short to post still counts against the tape.
				if ( length < MathF.Max( 0.05f, minimum ) ) {
					travelled += length;
					continue;
				}

				var division = ArchDivide.AtMost( length, spacing );
				var closing = !run.Closed && edge == run.Edges - 1;

				for ( var index = 0; index < division.Count; index++ ) {
					var (start, end) = division.Bay( index );

					bays.Add( new ArchBay {
						From = ArchStation.Between( from, to, start, run.Height, travelled + start ),
						To = ArchStation.Between( from, to, end, run.Height, travelled + end ),
						Gated = Opened( gates, travelled + start, travelled + end ),
						Last = closing && index == division.Count - 1
					} );
				}

				travelled += length;
			}
		}

		return bays;
	}

	static bool Opened( IReadOnlyList<ArchBayGate> gates, float from, float to ) {
		if ( gates is null ) {
			return false;
		}

		foreach ( var gate in gates ) {
			if ( gate.Spans( from, to ) ) {
				return true;
			}
		}

		return false;
	}

	// A closed run needs no closing post - the first stands on the return point.
	public static IEnumerable<ArchStation> Posts( IEnumerable<ArchBay> bays ) {
		foreach ( var bay in bays ) {
			yield return bay.From;

			if ( bay.Last ) {
				yield return bay.To;
			}
		}
	}

	public static float Reach( IEnumerable<ArchRunPath> runs ) => runs.Sum( run => run.Length );

	// The inverse of Station, and it lives beside it: a flight already standing on the deck says where the
	// balustrade must open, so the gap and the steps cannot be two opinions about one gesture.
	public static bool Nearest( IEnumerable<ArchRunPath> runs, Vector2 point, out ArchStation station ) {
		var travelled = 0f;
		var closest = float.MaxValue;

		station = default;

		foreach ( var run in runs ) {
			for ( var edge = 0; edge < run.Edges; edge++ ) {
				var from = run.At( edge );
				var to = run.At( edge + 1 );
				var span = to - from;
				var length = span.Length;

				if ( length < 0.05f ) {
					continue;
				}

				var along = Math.Clamp( Vector2.Dot( point - from, span / length ), 0f, length );
				var gap = (from + span / length * along - point).Length;

				if ( gap < closest ) {
					closest = gap;
					station = ArchStation.Between( from, to, along, run.Height, travelled + along );
				}

				travelled += length;
			}
		}

		return closest < float.MaxValue;
	}

	public static bool Station( IEnumerable<ArchRunPath> runs, float distance, out ArchStation station ) {
		var travelled = 0f;

		foreach ( var run in runs ) {
			if ( run.Station( distance - travelled, out var found ) ) {
				station = found with { Distance = distance };

				return true;
			}

			travelled += run.Length;
		}

		station = default;

		return false;
	}
}