Editor/Stair/ArchStairAffector.cs
using System.Collections.Generic;
using System.Linq;

namespace Sunless.Architecture;

// A flight that walks out of the building it was filed in is an affector on the one it arrives in, so
// it obeys the same rules a link does: its shaft and its archways are re-cut from where it stands now
// on every commit, and it is grouped with the buildings it actually opened. The group is then the scope
// ArchStairShape.Pierce reads back, so a flight can no longer reach across the whole plan by accident.
public static class ArchStairAffector {
	public static int Resolve( ArchPlan plan, ArchKit kit, ArchBuilding building, ArchRoom room, ArchStairPart stair ) {
		if ( building is null || stair is null ) {
			return 0;
		}

		var kinds = ArchKinds.Load();

		ArchStairShape.Pierce( plan, building, room, stair, kit );
		Regroup( plan, building, stair, kinds );

		return 1;
	}

	public static int Resolve( ArchPlan plan, ArchKit kit ) {
		var recut = 0;
		var kinds = ArchKinds.Load();

		// Only flights that pierced something: one drawn on grade opens no slab and has no shaft to keep
		// in step, and standing one for it would put a hole under every ground-floor stair in the plan.
		var pierced = plan.Buildings
			.SelectMany( building => building.Cutouts )
			.Where( cutout => cutout.OwnerId != 0 )
			.Select( cutout => cutout.OwnerId )
			.ToHashSet();

		foreach ( var building in plan.Buildings.ToList() ) {
			foreach ( var room in building.Rooms.ToList() ) {
				foreach ( var stair in plan.Filed<ArchStairPart>( room, kinds ).Where( stair => pierced.Contains( stair.Id ) ).ToList() ) {
					ArchStairShape.Pierce( plan, building, room, stair, kit );
					Regroup( plan, building, stair, kinds );

					recut++;
				}
			}
		}

		return recut;
	}

	// The buildings the flight actually opened, gathered with it and with the one it was filed in. A
	// flight that stays inside its own building is no relationship worth a folder, so it forms none -
	// and one that stops reaching out leaves the group, the way a link that reaches nothing does.
	static void Regroup( ArchPlan plan, ArchBuilding home, ArchStairPart stair, ArchKinds kinds ) {
		var reached = plan.Buildings
			.Where( host => host != home && Opened( plan, host, stair, kinds ) )
			.ToList();

		if ( reached.Count == 0 ) {
			ArchLayerGroups.Leave( plan, stair.Id );

			return;
		}

		var join = ArchLayerGroups.Holding( plan, stair.Id )
			?? ArchLayerGroups.Create( plan, ArchLayerGroups.Named( plan ), ArchAssemblyKind.SharedCore, System.Array.Empty<int>() );

		// The affector is filed last, because it acts on what sits above it in the stack.
		var members = reached.Select( host => host.Id ).Prepend( home.Id ).Append( stair.Id ).ToList();

		foreach ( var member in members ) {
			ArchLayerGroups.Leave( plan, member );
		}

		join.Children.Clear();
		join.Children.AddRange( members );
	}

	static bool Opened( ArchPlan plan, ArchBuilding host, ArchStairPart stair, ArchKinds kinds ) {
		return host.Cutouts.Any( cutout => cutout.OwnerId == stair.Id )
			|| host.Rooms
				.SelectMany( room => plan.Filed( ArchKind.Wall, room, kinds ) )
				.SelectMany( wall => plan.Filed( ArchKind.Opening, wall, kinds ) )
				.Any( opening => opening is ArchOpening archway && archway.OwnerId == stair.Id );
	}

	// Deleting the flight takes its shaft, its archways and its own membership - never the group.
	public static void Remove( ArchPlan plan, ArchStairPart stair ) {
		var kinds = ArchKinds.Load();

		foreach ( var building in plan.Buildings ) {
			building.Cutouts.RemoveAll( cutout => cutout.OwnerId == stair.Id );
		}

		foreach ( var wall in plan.AllRooms().SelectMany( room => plan.Filed( ArchKind.Wall, room, kinds ) ) ) {
			plan.RemoveAll( ArchKind.Opening, wall, opening => opening is ArchOpening archway && archway.OwnerId == stair.Id, kinds );
		}

		ArchLayerGroups.Leave( plan, stair.Id );
	}
}