Editor/Walkway/ArchWalkwayGeometryService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;


public sealed class ArchWalkwayGeometryService {
	readonly ArchConnections connections;

	public ArchWalkwayGeometryService( ArchConnections connections ) {
		this.connections = connections;
	}

	// ignore excludes the link being re-dragged so its own footprint doesn't close the gap it spans.
	public ArchWalkwaySpan Resolve( int level, int rise, Vector2 dragFrom, Vector2 dragMin, Vector2 dragMax, float width, ArchRoom ignore = null ) {
		ArchWalkway.Shape( dragMin, dragMax, width, out var shapedMin, out var shapedMax, out var alongX );

		var shells = connections.Storey( level )
			.Concat( connections.Storey( level + rise ) )
			.Where( room => !ReferenceEquals( room, ignore ) && !room.Spans )
			.Select( room => ArchFloorGen.Footprint( room ) )
			.Where( loop => loop.Count >= 3 && ArchFootprint.Bearing( loop, out _ ) )
			.ToList();
		var bounds = ResolveGap( shapedMin, shapedMax, ArchRegion.Shell( shells, connections.Kit.WallThickness ), alongX );

		return bounds is null ? null : Seat( level, rise, dragFrom, bounds.Value.Min, bounds.Value.Max, alongX, ignore );
	}

	// Editing reseats ends only — gap search and width-filling are placement-only answers.
	public ArchWalkwaySpan Reseat( int level, int rise, Vector2 min, Vector2 max, ArchRoom link ) {
		return Seat( level, rise, min, min, max, ArchWalkway.RunsAlongX( min, max ), link );
	}

	// One seating for both, or a reshape and a placement disagree about where the same end lands.
	ArchWalkwaySpan Seat( int level, int rise, Vector2 dragFrom, Vector2 min, Vector2 max, bool alongX, ArchRoom ignore ) {
		if ( Reach( min, max, alongX ) < ArchWalkway.MinSpan ) {
			return null;
		}

		// Half a wall in seats each end on the host's inner face.
		var half = connections.Kit.WallThickness * 0.5f;
		var reach = connections.Kit.WallThickness * 2f;
		var centre = (min + max) * 0.5f;
		// The drag starts at the low end: that side opens at the current storey, the far side climbs.
		var lowAtMin = alongX ? dragFrom.x <= centre.x : dragFrom.y <= centre.y;
		var west = ResolveEnd( level, rise, lowAtMin, alongX ? new Vector2( min.x, centre.y ) : new Vector2( centre.x, min.y ), reach, ignore );
		var east = ResolveEnd( level, rise, !lowAtMin, alongX ? new Vector2( max.x, centre.y ) : new Vector2( centre.x, max.y ), reach, ignore );

		// Slab to inner face (meets host slab), deck to centreline (wall covers the rest).
		var deckFrom = alongX ? min.x : min.y;
		var deckTo = alongX ? max.x : max.y;

		if ( west is not null && west.HasFootprint ) {
			ArchFootprint.Bounds( ArchFloorGen.Footprint( west ), out _, out var roomMax );

			deckFrom = alongX ? roomMax.x : roomMax.y;
			min = alongX ? new Vector2( deckFrom - half, min.y ) : new Vector2( min.x, deckFrom - half );
		}

		if ( east is not null && east.HasFootprint ) {
			ArchFootprint.Bounds( ArchFloorGen.Footprint( east ), out var roomMin, out _ );

			deckTo = alongX ? roomMin.x : roomMin.y;
			max = alongX ? new Vector2( deckTo + half, max.y ) : new Vector2( max.x, deckTo + half );
		}

		var deckMin = alongX ? new Vector2( deckFrom, min.y ) : new Vector2( min.x, deckFrom );
		var deckMax = alongX ? new Vector2( deckTo, max.y ) : new Vector2( max.x, deckTo );

		// The mouths open at the passage's width - the walkway's own walls are the jambs.
		var mouths = alongX
			? new[] { (new Vector2( min.x, min.y + half ), new Vector2( min.x, max.y - half )), (new Vector2( max.x, max.y - half ), new Vector2( max.x, min.y + half )) }
			: new[] { (new Vector2( max.x - half, min.y ), new Vector2( min.x + half, min.y )), (new Vector2( min.x + half, max.y ), new Vector2( max.x - half, max.y )) };

		return new ArchWalkwaySpan { Min = min, Max = max, DeckMin = deckMin, DeckMax = deckMax, AlongX = alongX, Mouths = mouths, EndRooms = new[] { west, east } };
	}

	// The low end (where the drag started) opens at the current storey; the far end climbs to it.
	// Never the link itself: its own end stands on its own boundary, so an end pulled clear of every
	// host would otherwise seat on the link it belongs to and take its extent from that.
	ArchRoom ResolveEnd( int level, int rise, bool lowEnd, Vector2 point, float reach, ArchRoom ignore ) {
		if ( !lowEnd && rise > 0 ) {
			return connections.RoomNear( level + rise, point, reach, ignore )
				?? connections.RoomNear( level, point, reach, ignore );
		}

		return connections.RoomNear( level, point, reach, ignore )
			?? connections.RoomNear( level + rise, point, reach, ignore );
	}

	static (Vector2 Min, Vector2 Max)? ResolveGap( Vector2 dragMin, Vector2 dragMax, IReadOnlyList<List<Vector2>> shells, bool alongX ) {
		if ( shells.Count == 0 ) {
			return (dragMin, dragMax);
		}

		// Buildings subtracted out of the drag rectangle: the walkway spans the gap left between them.
		var gap = ArchRegion.Minus( ArchFootprint.Rect( dragMin, dragMax ), shells );
		var longest = ArchFootprint.Outer( gap )
			.OrderByDescending( loop => Extent( loop, alongX ) )
			.FirstOrDefault();

		if ( longest is null ) {
			return null;
		}

		ArchFootprint.Bounds( longest, out var min, out var max );

		return (min, max);
	}

	static float Extent( IReadOnlyList<Vector2> loop, bool alongX ) {
		ArchFootprint.Bounds( loop, out var min, out var max );

		return Reach( min, max, alongX );
	}

	static float Reach( Vector2 min, Vector2 max, bool alongX ) {
		return alongX ? max.x - min.x : max.y - min.y;
	}
}