Editor/Services/ArchContacts.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Scope of a contact query - outside its reach a part has nothing it may join to.
public readonly struct ArchReach {
	public int? Storey { get; init; }
	public IReadOnlySet<int> Buildings { get; init; }

	public static ArchReach Anywhere => new();

	public static ArchReach Grouped( ArchPlan plan, int itemId ) => new() { Buildings = ArchLayerGroups.Reach( plan, itemId ) };

	public ArchReach OnStorey( int storey ) => new() { Storey = storey, Buildings = Buildings };

	public bool Holds( ArchBuilding building ) => Buildings is null || Buildings.Contains( building.Id );

	public bool Holds( ArchPlan plan, ArchRoom room ) {
		if ( Storey is { } floor && room.Floor != floor ) {
			return false;
		}

		if ( Buildings is null ) {
			return true;
		}

		return plan.OwnerOf( room ) is { } owner && Buildings.Contains( owner.Id );
	}
}

// One touched host, measured in that host's own frame.
public readonly record struct ArchTouch {
	public int HostId { get; init; }
	public ArchWall Wall { get; init; }
	public ArchRoom Room { get; init; }
	public float Along { get; init; }
	public float Span { get; init; }
	public float Height { get; init; }
	public float Distance { get; init; }
}

// What a part is against: the surface under it, the face in front of it, the walls it lies along.
public static class ArchContacts {
	// Bounded by a ceiling, not by a storey.
	public static float? Under( ArchPlan plan, ArchKit kit, ArchReach reach, Vector2 at, float ceiling ) {
		var seat = float.MinValue;

		foreach ( var surface in Surfaces( plan, kit, reach, at ) ) {
			if ( surface > seat && surface <= ceiling ) {
				seat = surface;
			}
		}

		return seat > float.MinValue ? seat : null;
	}

	// A negative Distance means the probe started inside the wall's thickness - flush, not nothing found.
	public static ArchTouch? Facing( ArchPlan plan, ArchKit kit, ArchReach reach, Vector2 from, Vector2 direction, float limit ) {
		ArchTouch? nearest = null;

		foreach ( var (room, wall) in Walls( plan, reach ) ) {
			if ( !Meets( wall, kit, from, direction, limit, out var face, out var along ) ) {
				continue;
			}

			if ( nearest is { } standing && standing.Distance <= face ) {
				continue;
			}

			nearest = new ArchTouch {
				HostId = wall.Id,
				Wall = wall,
				Room = room,
				Along = along,
				Span = wall.Length,
				Distance = face
			};
		}

		return nearest;
	}

	// Overlap expressed in each wall's own axis.
	public static IEnumerable<ArchTouch> Along( ArchPlan plan, ArchKit kit, ArchReach reach, Vector2 from, Vector2 to ) {
		var span = to - from;
		var length = span.Length;

		if ( length < 1f ) {
			yield break;
		}

		var direction = span / length;

		foreach ( var (room, wall) in Walls( plan, reach ) ) {
			if ( wall.Length < 1f || !ArchWallJoins.Aligned( direction, from, wall, kit.WallThickness ) ) {
				continue;
			}

			var a = Vector2.Dot( from - wall.Start, wall.Direction );
			var b = Vector2.Dot( to - wall.Start, wall.Direction );
			var near = MathF.Max( MathF.Min( a, b ), 0f );
			var far = MathF.Min( MathF.Max( a, b ), wall.Length );

			if ( far - near < 4f ) {
				continue;
			}

			yield return new ArchTouch {
				HostId = wall.Id,
				Wall = wall,
				Room = room,
				Along = (near + far) * 0.5f,
				Span = far - near,
				Height = ArchWallSection.Height( wall, room, kit )
			};
		}
	}

	public static IEnumerable<(ArchRoom Room, ArchWall Wall)> Walls( ArchPlan plan, ArchReach reach ) {
		foreach ( var room in plan?.AllRooms() ?? Enumerable.Empty<ArchRoom>() ) {
			if ( room.Spans || !ArchLayerGate.On( room ) || !reach.Holds( plan, room ) ) {
				continue;
			}

			foreach ( var wall in room.Walls ) {
				yield return (room, wall);
			}
		}
	}

	// Tests out to the outer wall face, not the centreline.
	static IEnumerable<float> Surfaces( ArchPlan plan, ArchKit kit, ArchReach reach, Vector2 at ) {
		var answers = ArchAnswers.Load();

		foreach ( var building in plan?.Buildings ?? Enumerable.Empty<ArchBuilding>() ) {
			if ( !reach.Holds( building ) ) {
				continue;
			}

			var lift = answers.Lift( plan, building, kit );

			foreach ( var room in building.Rooms ) {
				if ( room.Spans || !ArchLayerGate.On( room ) || !reach.Holds( plan, room ) ) {
					continue;
				}

				if ( ArchFootprint.Encloses( ArchRegion.Shell( ArchRegion.Footprints( new[] { room }, false ), kit.WallThickness ), at ) ) {
					yield return room.BaseHeight;
				}
			}

			// Offset by lift - platforms are poured under the building's grade.
			foreach ( var platform in ArchLayerGate.Enabled( building.Platforms ) ) {
				if ( ArchFootprint.Contains( platform.Outline(), at ) ) {
					yield return ArchPlatformDeck.Seat( platform, at ) - lift;
				}
			}

			foreach ( var beam in building.Rooms.SelectMany( standing => standing.Beams ).Where( ArchLayerGate.On ) ) {
				if ( ArchFootprint.Contains( beam.Outline(), at ) ) {
					yield return beam.TopHeight;
				}
			}

			// Foundation plinth extends past the outer wall face.
			var foundationLevel = ArchFloorGen.FoundationLevel( building );

			if ( building.Rooms.FirstOrDefault( standing =>
					standing.Floor == foundationLevel && standing.HasFloor && ArchLayerGate.On( standing ) ) is not { } foundation ) {
				continue;
			}

			foreach ( var plinth in ArchFloorGen.FoundationOutline( plan, building, kit, foundationLevel ) ) {
				if ( ArchFootprint.Contains( plinth, at ) ) {
					yield return foundation.BaseHeight;
				}
			}
		}
	}

	static bool Meets( ArchWall wall, ArchKit kit, Vector2 from, Vector2 direction, float limit, out float face, out float along ) {
		face = 0f;
		along = 0f;

		var length = wall.Length;

		if ( length < 1f ) {
			return false;
		}

		// Only a wall square to the probe counts, or every side of everything reads walled.
		var facing = Vector2.Dot( wall.Normal, direction );

		if ( MathF.Abs( facing ) < 0.8f ) {
			return false;
		}

		var half = (wall.Thickness > 0f ? wall.Thickness : kit.WallThickness) * 0.5f;
		var travel = Vector2.Dot( wall.Normal, wall.Start - from ) / facing;

		face = travel - half;

		if ( face < -half - 1f || face > limit ) {
			return false;
		}

		along = Vector2.Dot( from + direction * travel - wall.Start, wall.Direction );

		return along >= -1f && along <= length + 1f;
	}
}