Editor/Stair/ArchApproachPlacement.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly record struct ArchApproachDrag( ArchApproachPart Placed, ArchRoom Room, ArchBuilding On, string Refused ) {
	public bool Stands => Placed is not null;
}

public static class ArchApproachPlacement {
	public static ArchApproachDrag Dragged(
		ArchPlan plan, ArchKit kit, int level, Vector2 from, Vector2 to,
		ApproachKind kind, float width, bool kerbs, bool rails, float splay,
		ApproachAgainst against = ApproachAgainst.Auto ) {
		var centre = (from + to) * 0.5f;
		var buildings = (plan?.Buildings ?? Enumerable.Empty<ArchBuilding>()).ToList();

		ArchWall wall = null;
		ArchBuilding building = null;
		ArchRoom room = null;
		var along = 0f;
		var toWall = float.MaxValue;
		var outward = Vector2.Zero;

		if ( against != ApproachAgainst.Platform ) {
			wall = NearestExteriorWall( buildings, level, centre,
				out building, out room, out along, out toWall, out outward );
		}

		var seat = against == ApproachAgainst.Foundation ? default : NearestPlatformEdge( buildings, level, centre );

		if ( seat.Stands && (wall is null || seat.Gap <= toWall) ) {
			return Onto( kit, seat, from, to, kind, width, kerbs, rails, splay, against );
		}

		if ( wall is null || room is null ) {
			return new ArchApproachDrag( null, null, null, against == ApproachAgainst.Platform
				? $"no platform on floor {level} — switch back to the foundation, or stand a podium to drag onto"
				: $"no exterior wall on floor {level} — an approach comes off a building, so drag near one" );
		}

		var grid = new ArchGridService();
		var drag = to - from;
		var minimum = grid.SubgridSize( 4 );
		var run = MathF.Abs( Vector2.Dot( drag, outward ) );

		var span = Spanned( grid, wall.Length,
			Vector2.Dot( from - wall.Start, wall.Direction ),
			Vector2.Dot( to - wall.Start, wall.Direction ), width );

		var placed = Seat( wall, room, building, kit, span.Station, kind, span.Width,
			run < minimum ? 0f : run,
			kerbs, rails, splay );

		placed.Against = against;

		return new ArchApproachDrag( placed, room, building, null );
	}

	// Ends snap flush to host corners within a cell; width is the span between them
	public readonly record struct ArchApproachSpan( float Width, float Station );

	static ArchApproachSpan Spanned( ArchGridService grid, float length, float lo, float hi, float typed ) {
		var unit = grid.SubgridSize( 4 );
		var cell = grid.BaseSize;
		var run = MathF.Max( unit, length );

		if ( hi < lo ) {
			(lo, hi) = (hi, lo);
		}

		lo = Math.Clamp( lo, 0f, run );
		hi = Math.Clamp( hi, 0f, run );

		if ( hi - lo < unit ) {
			var wide = Math.Clamp( grid.Subgrid( MathF.Max( unit, typed ), 4 ), unit, run );

			return new ArchApproachSpan( wide,
				Math.Clamp( grid.Subgrid( (lo + hi) * 0.5f, 4 ), wide * 0.5f, run - wide * 0.5f ) );
		}

		lo = lo <= cell ? ArchLap.Shy : grid.Subgrid( lo, 4 );
		hi = run - hi <= cell ? run - ArchLap.Shy : grid.Subgrid( hi, 4 );

		return new ArchApproachSpan( MathF.Max( unit, hi - lo ), (lo + hi) * 0.5f );
	}

	public readonly record struct ArchPodiumEdge(
		ArchBuilding On, ArchRoom Room, ArchPlatformPart Platform,
		Vector2 Start, Vector2 Along, float Length, Vector2 Outward, float Station, float Gap ) {
		public bool Stands => Platform is not null;
	}

	// Outward is probed, not derived from winding — works regardless of loop direction
	public static ArchPodiumEdge NearestPlatformEdge( IEnumerable<ArchBuilding> buildings, int level, Vector2 point ) {
		var best = default( ArchPodiumEdge );
		var nearest = float.MaxValue;

		foreach ( var building in buildings ) {
			var room = building.Rooms.FirstOrDefault( standing => standing.Floor == level && ArchLayerGate.On( standing ) )
				?? building.Rooms.FirstOrDefault( standing => ArchLayerGate.On( standing ) );

			foreach ( var platform in ArchLayerGate.Enabled( building.Platforms ) ) {
				if ( platform.Level != level ) {
					continue;
				}

				var outline = platform.Outline();

				for ( var index = 0; index < outline.Count; index++ ) {
					var start = outline[index];
					var span = outline[(index + 1) % outline.Count] - start;
					var length = span.Length;

					if ( length < 1f ) {
						continue;
					}

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

					if ( gap >= nearest ) {
						continue;
					}

					nearest = gap;
					best = new ArchPodiumEdge( building, room, platform, start, along, length,
						Off( outline, start + along * length * 0.5f, new Vector2( -along.y, along.x ) ),
						station, gap );
				}
			}
		}

		return best;
	}

	static Vector2 Off( IReadOnlyList<Vector2> outline, Vector2 middle, Vector2 normal ) {
		return ArchFootprint.Contains( outline, middle + normal ) ? -normal : normal;
	}

	static ArchApproachDrag Onto(
		ArchKit kit, ArchPodiumEdge seat, Vector2 from, Vector2 to,
		ApproachKind kind, float width, bool kerbs, bool rails, float splay, ApproachAgainst against ) {
		var grid = new ArchGridService();
		var unit = grid.SubgridSize( 4 );
		var drag = to - from;
		var run = MathF.Abs( Vector2.Dot( drag, seat.Outward ) );

		var span = Spanned( grid, seat.Length,
			Vector2.Dot( from - seat.Start, seat.Along ),
			Vector2.Dot( to - seat.Start, seat.Along ), width );

		var placed = new ArchApproachPart {
			Kind = kind,
			// Podium outline IS its face — no half-thickness offset
			Origin = seat.Start + seat.Along * span.Station,
			Yaw = MathF.Atan2( seat.Outward.y, seat.Outward.x ).RadianToDegree(),
			Width = span.Width,
			Run = run > unit ? grid.Subgrid( run, 4 ) : 0f,
			Kerbs = kerbs,
			Rails = rails,
			Splay = splay,
			Against = against,
			PlatformId = seat.Platform.Id
		};

		return new ArchApproachDrag( placed, seat.Room, seat.On, null );
	}

	public static ArchApproachPart Seat(
		ArchWall wall,
		ArchRoom room,
		ArchBuilding building,
		ArchKit kit,
		float along,
		ApproachKind kind,
		float width,
		float run,
		bool kerbs,
		bool rails,
		float splay ) {
		var grid = new ArchGridService();
		var unit = grid.SubgridSize( 4 );
		var maximumWidth = MathF.Max( unit, wall.Length );
		var snappedWidth = Math.Clamp( MathF.Max( unit, width ), unit, maximumWidth );
		var centreAlong = Math.Clamp( along, snappedWidth * 0.5f, MathF.Max( snappedWidth * 0.5f, wall.Length - snappedWidth * 0.5f ) );
		var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;
		var outward = ArchWallFaces.Outward( wall, room, building );

		return new ArchApproachPart {
			Kind = kind,
			Origin = wall.PointAt( centreAlong ) + outward * (thickness * 0.5f + MathF.Max( 0f, kit.FoundationOversize )),
			Yaw = MathF.Atan2( outward.y, outward.x ).RadianToDegree(),
			Width = snappedWidth,
			Run = run > 0f ? MathF.Max( unit, grid.Subgrid( run, 4 ) ) : 0f,
			Kerbs = kerbs,
			Rails = rails,
			Splay = splay
		};
	}

	public static ArchWall NearestExteriorWall(
		IEnumerable<ArchBuilding> buildings,
		int level,
		Vector2 point,
		out ArchBuilding building,
		out ArchRoom room,
		out float along,
		out float distance,
		out Vector2 outward ) {
		ArchWall best = null;
		building = null;
		room = null;
		along = 0f;
		distance = float.MaxValue;
		outward = default;

		foreach ( var candidateBuilding in buildings ) {
			foreach ( var candidateRoom in candidateBuilding.Rooms ) {
				if ( candidateRoom.Floor != level ) {
					continue;
				}

				foreach ( var candidateWall in candidateRoom.Walls ) {
					var length = candidateWall.Length;

					if ( length < 0.5f || !ArchWallFaces.TryOutward( candidateWall, candidateRoom, candidateBuilding, out var candidateOutward ) ) {
						continue;
					}

					var projected = Math.Clamp( Vector2.Dot( point - candidateWall.Start, candidateWall.Direction ), 0f, length );
					var gap = (point - candidateWall.PointAt( projected )).Length;

					if ( gap >= distance ) {
						continue;
					}

					best = candidateWall;
					building = candidateBuilding;
					room = candidateRoom;
					along = projected;
					distance = gap;
					outward = candidateOutward;
				}
			}
		}

		return best;
	}
}