Editor/Services/ArchOwned.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sunless.Architecture;

// Effects keyed by (host, ordinal) slot so a re-derive keeps existing ids.
public sealed record ArchOwnedHosts<THost, TEffect>(
	Func<ArchPlan, IEnumerable<THost>> All,
	Func<THost, int> IdOf,
	Func<THost, List<TEffect>> Effects,
	Func<TEffect, int> OwnerOf,
	Action<TEffect, int, int> Stamp );

// One effect an owner wants. Dress writes everything but identity, so it may run over an effect that has
// been standing since the plan was authored.
public readonly record struct ArchWant<THost, TEffect>( THost Host, Action<TEffect> Dress );

public static class ArchOwned {
	// Never a static field: a held delegate answers with the code the editor had before the last hotload.
	public static ArchOwnedHosts<ArchWall, ArchOpening> Openings => new(
		// AllWalls, not rooms: a wall also hosts on a roof parapet, and an effect left there could never be closed.
		plan => plan.AllWalls(),
		wall => wall.Id,
		wall => wall.Openings,
		opening => opening.OwnerId,
		( opening, id, owner ) => {
			opening.Id = id;
			opening.OwnerId = owner;
		} );

	// Reconcile standing effects against wants: keep occupied slots, mint empty, remove unwanted.
	public static bool Settle<THost, TEffect>( ArchPlan plan, ArchOwnedHosts<THost, TEffect> family, int ownerId,
		IReadOnlyList<ArchWant<THost, TEffect>> wants, Func<TEffect> mint ) where TEffect : class {
		if ( plan is null || family is null || wants is null ) {
			return false;
		}

		var standing = Standing( plan, family, ownerId );
		var taken = new HashSet<(int Host, int Ordinal)>();
		var ordinals = new Dictionary<int, int>();
		var moved = false;

		foreach ( var want in wants ) {
			var host = family.IdOf( want.Host );

			ordinals.TryGetValue( host, out var ordinal );
			ordinals[host] = ordinal + 1;

			var slot = (host, ordinal);

			if ( standing.TryGetValue( slot, out var effect ) ) {
				taken.Add( slot );
			} else {
				effect = mint();

				family.Stamp( effect, plan.AllocateId(), ownerId );
				family.Effects( want.Host ).Add( effect );

				moved = true;
			}

			want.Dress( effect );
		}

		var vanished = standing
			.Where( entry => !taken.Contains( entry.Key ) )
			.Select( entry => entry.Value )
			.ToList();

		if ( vanished.Count == 0 ) {
			return moved;
		}

		foreach ( var host in family.All( plan ) ) {
			family.Effects( host ).RemoveAll( vanished.Contains );
		}

		return true;
	}

	// Everything an owner has standing, wherever it stands - the other half of being able to re-derive.
	public static int Close<THost, TEffect>( ArchPlan plan, ArchOwnedHosts<THost, TEffect> family, int ownerId ) {
		if ( plan is null || family is null ) {
			return 0;
		}

		return family.All( plan ).Sum( host => family.Effects( host ).RemoveAll( effect => family.OwnerOf( effect ) == ownerId ) );
	}

	static Dictionary<(int Host, int Ordinal), TEffect> Standing<THost, TEffect>( ArchPlan plan,
		ArchOwnedHosts<THost, TEffect> family, int ownerId ) {
		var standing = new Dictionary<(int, int), TEffect>();

		foreach ( var host in family.All( plan ) ) {
			var id = family.IdOf( host );
			var ordinal = 0;

			foreach ( var effect in family.Effects( host ).Where( effect => family.OwnerOf( effect ) == ownerId ) ) {
				standing[(id, ordinal++)] = effect;
			}
		}

		return standing;
	}
}