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

namespace Sunless.Architecture;

// House-facing pieces read Footprint; garden-facing pieces walk Open - no special case at the corner.
public sealed class ArchPorchShape {
	public List<Vector2> Footprint { get; init; }
	// Runs broken wherever the perimeter meets the house.
	public List<ArchRunPath> Open { get; init; }
	public List<ArchPorchWall> Walls { get; init; }

	public float Grade { get; init; }
	public float Deck { get; init; }
	public float Head { get; init; }
	public float Ceiling { get; init; }

	public bool IsUsable => Footprint is { Count: >= 4 } && Open.Count > 0;

	public float Reach => ArchBays.Reach( Open );

	// Depth is what the roof rises over - the flashing here has to know it.
	public readonly struct ArchPorchWall {
		public Vector2 From { get; init; }
		public Vector2 To { get; init; }
		public Vector2 Outward { get; init; }
		public float Depth { get; init; }
	}

	public static ArchPorchShape Resolve( ArchPorchPart porch, ArchRoom room, ArchBuilding building, ArchKit kit ) {
		var legs = porch.Legs?.Where( leg => leg.Max.x - leg.Min.x > 1f && leg.Max.y - leg.Min.y > 1f ).ToList();

		if ( legs is null || legs.Count == 0 ) {
			return new ArchPorchShape { Footprint = new List<Vector2>(), Open = new List<ArchRunPath>(), Walls = new List<ArchPorchWall>() };
		}

		var merged = ArchFootprint.Union( legs.Select( leg => ArchFootprint.Rect( leg.Min, leg.Max ) ) );

		if ( merged.Count != 1 ) {
			Log.Info( $"Architecture: porch {porch.Name} has legs that do not join into one deck, so only the first is built." );
			merged = new List<List<Vector2>> { ArchFootprint.Rect( legs[0].Min, legs[0].Max ) };
		}

		var footprint = ArchFootprint.Wind( merged[0] );
		// Free leans on nothing, so nothing is subtracted and every edge comes back open - which is the only
		// way a deck stands inside a room, where the shell subtraction leaves no apron at all.
		var house = porch.Standing == PorchStanding.Against
			? ArchPorch.Shell( building, room, kit )
			: new List<List<Vector2>>();

		ArchRegion.Split( footprint, house, out var open, out var against );

		var walls = against.Select( edge => new ArchPorchWall {
			From = edge.From,
			To = edge.To,
			Outward = edge.Outward,
			Depth = LegDepth( legs, edge.Midpoint, edge.Outward )
		} ).ToList();

		// The clamp reads the walls it just resolved rather than resolving the porch a second time, so the
		// head the generator builds to, the head the ghost draws and the head the roof seats on are one number.
		var head = porch.BaseHeight + ArchPorchCover.Headroom( porch, building, kit,
			walls.Count > 0 ? walls.Max( wall => wall.Depth ) : 0f );

		return new ArchPorchShape {
			Footprint = footprint,
			Open = Perimeter( open, porch.BaseHeight ),
			Walls = walls,
			Grade = porch.GradeHeight,
			Deck = porch.BaseHeight,
			Head = head,
			Ceiling = head + MathF.Max( 3f, kit.PorchBeamDepth )
		};
	}

	// Leaning on nothing, the run returns to its own start and closes like any other.
	static List<ArchRunPath> Perimeter( IEnumerable<List<Vector2>> open, float height ) {
		var runs = new List<ArchRunPath>();

		foreach ( var run in open ) {
			runs.Add( ArchRunPath.Normalized( run, height ) );
		}

		return runs;
	}

	static float LegDepth( IReadOnlyList<ArchPorchLeg> legs, Vector2 midpoint, Vector2 outward ) {
		var inward = -outward;
		var best = 0f;

		foreach ( var leg in legs ) {
			var probe = midpoint + inward * ArchProbe.Step;

			if ( probe.x < leg.Min.x || probe.x > leg.Max.x || probe.y < leg.Min.y || probe.y > leg.Max.y ) {
				continue;
			}

			var size = leg.Max - leg.Min;

			best = MathF.Max( best, MathF.Abs( inward.x ) > MathF.Abs( inward.y ) ? size.x : size.y );
		}

		return best;
	}
}