Editor/Pillar/ArchPillarSoffit.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchPillarSoffit {
	const float Clear = 8f;

	public static float Over( ArchPlan plan, ArchKit kit, ArchRoom room, ArchPillarPart pillar, float floor ) {
		var host = plan?.OwnerOf( room );
		var plate = host is null
			? room.BaseHeight + ArchFloorGen.WallHeight( room, kit )
			: ArchFloorGen.Overhead( host, room, kit );

		var slab = Slab( plan, kit, pillar, floor );

		if ( slab >= float.MaxValue ) {
			return plate;
		}

		// Plate only counts where the storey actually stands over the column
		return Plated( plan, kit, room, Under( pillar ) ) ? MathF.Min( plate, slab ) : slab;
	}

	static bool Plated( ArchPlan plan, ArchKit kit, ArchRoom room, Vector2 under ) {
		foreach ( var standing in plan?.AllRooms() ?? Enumerable.Empty<ArchRoom>() ) {
			if ( standing.Floor == room.Floor && standing.HasFloor && ArchLayerGate.On( standing )
				&& ArchFootprint.Encloses( Shell( standing, kit ), under ) ) {
				return true;
			}
		}

		return false;
	}

	public static float Slab( ArchPlan plan, ArchKit kit, ArchPillarPart pillar, float floor ) {
		return Covering( plan, kit, Under( pillar ), floor );
	}

	public static float Covering( ArchPlan plan, ArchKit kit, Vector2 under, float floor ) {
		if ( plan is null ) {
			return float.MaxValue;
		}

		var soffit = float.MaxValue;
		var answers = ArchAnswers.Load();

		foreach ( var building in plan.Buildings ) {
			var lift = answers.Lift( plan, building, kit );

			// Measured by footprint, not storey — avoids counting the carried slab as the ceiling
			foreach ( var above in building.Rooms.Where( standing => standing.HasFloor && !standing.Spans && ArchLayerGate.On( standing ) ) ) {
				soffit = Lower( soffit, above.BaseHeight - kit.FloorThickness, floor, under, Shell( above, kit ) );

				if ( above.Floor > 0 ) {
					soffit = Lower( soffit, ArchOverhangGen.Soffit( above, kit ), floor, under,
						ArchOverhangGen.Unsupported( above, building, kit ) );
				}
			}

			// Hung only — poured platforms are something to stand ON
			foreach ( var platform in building.Platforms.Where( standing => standing.Hung && ArchLayerGate.On( standing ) ) ) {
				soffit = Lower( soffit, platform.GradeHeight - lift, floor, under, platform.Outline() );
			}

			foreach ( var beam in building.Rooms.SelectMany( standing => standing.Beams ).Where( ArchLayerGate.On ) ) {
				soffit = Lower( soffit, beam.Soffit, floor, under, beam.Outline() );
			}
		}

		return soffit;
	}

	// Tests at the centroid, not the footprint — overlap ≠ under
	static Vector2 Under( ArchPillarPart pillar ) {
		var bounds = Footprint( pillar );
		var centre = Vector2.Zero;

		foreach ( var corner in bounds ) {
			centre += corner;
		}

		return bounds.Count == 0 ? pillar.Origin : centre / bounds.Count;
	}

	public static List<Vector2> Footprint( ArchPillarPart pillar ) {
		var half = pillar.Seated;
		var min = new Vector2( float.MaxValue, float.MaxValue );
		var max = new Vector2( float.MinValue, float.MinValue );

		foreach ( var column in ArchPillarGen.Columns( pillar ) ) {
			var at = new Vector2( column.At.x, column.At.y );

			min = Vector2.Min( min, at - half );
			max = Vector2.Max( max, at + half );
		}

		return ArchFootprint.Rect( min, max );
	}

	// Out to the outer wall face, not the centreline
	static List<List<Vector2>> Shell( ArchRoom room, ArchKit kit ) {
		return ArchRegion.Shell( ArchRegion.Footprints( new[] { room }, false ), kit.WallThickness );
	}

	static float Lower( float soffit, float candidate, float floor, Vector2 under, IReadOnlyList<Vector2> outline ) {
		if ( candidate <= floor + Clear || candidate >= soffit || !ArchFootprint.Contains( outline, under ) ) {
			return soffit;
		}

		return candidate;
	}

	static float Lower( float soffit, float candidate, float floor, Vector2 under, IEnumerable<IReadOnlyList<Vector2>> outlines ) {
		foreach ( var outline in outlines ) {
			soffit = Lower( soffit, candidate, floor, under, outline );
		}

		return soffit;
	}
}