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

namespace Sunless.Architecture;

public readonly struct ArchEdge {
	public Vector2 From { get; init; }
	public Vector2 To { get; init; }
	public Vector2 Outward { get; init; }

	public Vector2 Midpoint => (From + To) * 0.5f;

	public float Length => (To - From).Length;
}

// One place for "what does this storey cover" - five generators used to re-derive it themselves.
public static class ArchRegion {
	// A non-rectilinear room is left out - the cell decomposition has no grid for it.
	public static List<List<Vector2>> Storey( ArchBuilding building, int level ) {
		return Footprints( building?.Rooms?.Where( room => room.Floor == level ) );
	}

	public static List<List<Vector2>> Footprints( IEnumerable<ArchRoom> rooms ) {
		return Footprints( rooms, true );
	}

	// Rectilinear test uses the storey's own bearing, not the world axes.
	public static List<List<Vector2>> Footprints( IEnumerable<ArchRoom> rooms, bool rectilinearOnly ) {
		if ( rooms is null ) {
			return new List<List<Vector2>>();
		}

		var loops = rooms.Select( ArchFloorGen.Footprint ).Where( loop => loop.Count >= 3 ).ToList();

		if ( !rectilinearOnly ) {
			return loops;
		}

		ArchFootprint.Bearing( loops, out var bearing );

		return loops.Where( loop => ArchFootprint.IsRectilinear( loop, bearing ) ).ToList();
	}

	// A room's footprint is its walls' CENTRELINE - grow by half a thickness or the shell reads small.
	public static List<List<Vector2>> Shell( IReadOnlyList<List<Vector2>> storey, float thickness ) {
		return storey.Count == 0 ? new List<List<Vector2>>() : ArchFootprint.Grow( storey, MathF.Max( 0f, thickness ) * 0.5f );
	}

	public static List<List<Vector2>> Minus( IReadOnlyList<Vector2> region, IReadOnlyList<IReadOnlyList<Vector2>> blockers ) {
		return ArchFootprint.Subtract( new[] { region }, blockers );
	}

	public static bool Covers( IReadOnlyList<IReadOnlyList<Vector2>> region, IReadOnlyList<Vector2> loop ) {
		return region.Count > 0 && Minus( loop, region ).Count == 0;
	}

	public static List<List<Vector2>> Open( IReadOnlyList<Vector2> loop, IReadOnlyList<IReadOnlyList<Vector2>> blockers ) {
		Split( loop, blockers, out var open, out _ );

		return open;
	}

	// Both halves of the same walk - deriving them separately let a deck and its flashing disagree.
	public static void Split(
		IReadOnlyList<Vector2> loop,
		IReadOnlyList<IReadOnlyList<Vector2>> blockers,
		out List<List<Vector2>> open,
		out List<ArchEdge> against ) {
		open = new List<List<Vector2>>();
		against = new List<ArchEdge>();

		var count = loop.Count;

		if ( count < 3 ) {
			return;
		}

		var leaning = new bool[count];

		for ( var index = 0; index < count; index++ ) {
			var from = loop[index];
			var to = loop[(index + 1) % count];
			var outward = Outward( from, to );

			leaning[index] = ArchProbe.Faces( blockers, from, to, outward );

			if ( leaning[index] ) {
				against.Add( new ArchEdge { From = from, To = to, Outward = outward } );
			}
		}

		var first = Array.IndexOf( leaning, true );

		// Nothing to lean on comes back as one closed run.
		if ( first < 0 ) {
			open.Add( loop.Append( loop[0] ).ToList() );
			return;
		}

		var run = new List<Vector2>();

		for ( var step = 1; step <= count; step++ ) {
			var index = (first + step) % count;

			if ( leaning[index] ) {
				Close( open, run );
				run = new List<Vector2>();
				continue;
			}

			if ( run.Count == 0 ) {
				run.Add( loop[index] );
			}

			run.Add( loop[(index + 1) % count] );
		}

		Close( open, run );
	}

	public static Vector2 Outward( Vector2 from, Vector2 to ) {
		var along = (to - from).Normal;

		return new Vector2( along.y, -along.x );
	}

	static void Close( List<List<Vector2>> open, List<Vector2> run ) {
		if ( run.Count >= 2 ) {
			open.Add( run );
		}
	}
}