Editor/Modules/ArchTable.cs
namespace Sunless.Architecture;

// The scaffold every table shares: loaded per ask because hotload carries statics over, refusing a second claim on
// a key rather than letting the order they were written in pick the winner, and standing a copy with one key gone
// so a case can probe the shape a caller sees when nothing answers.
//
// Standing is written out by each table rather than scanned for. One assembly declares every entry, so a scan would
// only be a slower way of naming what the table already knows, and each type handle it walked outlives the edit.
public abstract class ArchTable<TSelf, TKey, TEntry>
	where TSelf : ArchTable<TSelf, TKey, TEntry>, new()
	where TEntry : class {
	readonly Dictionary<TKey, TEntry> held = new();
	readonly List<string> collisions = new();

	protected abstract TKey KeyOf( TEntry entry );

	protected abstract string Collision( TEntry standing, TEntry entry );

	protected abstract IEnumerable<TEntry> Standing();

	protected virtual bool Accepts( TEntry entry ) => true;

	public static TSelf Load() {
		var built = new TSelf();

		foreach ( var entry in built.Standing() ) {
			built.Enrol( entry );
		}

		// The game's own entries after the tool's, so a written-out row always wins a key an addon also claims.
		foreach ( var contributed in ArchAddons.Load().Contributed<TEntry>() ) {
			built.Enrol( contributed );
		}

		return built;
	}

	public TSelf Enrol( TEntry entry ) {
		if ( entry is null || !Accepts( entry ) ) {
			return (TSelf)this;
		}

		var key = KeyOf( entry );

		if ( held.TryGetValue( key, out var standing ) ) {
			collisions.Add( Collision( standing, entry ) );

			return (TSelf)this;
		}

		held[key] = entry;

		return (TSelf)this;
	}

	public TSelf Without( TKey key ) {
		var built = new TSelf();

		foreach ( var entry in held.Values ) {
			if ( !EqualityComparer<TKey>.Default.Equals( KeyOf( entry ), key ) ) {
				built.Enrol( entry );
			}
		}

		return built;
	}

	public IReadOnlyList<string> Collisions => collisions;

	protected TEntry Held( TKey key ) {
		return key is not null && held.TryGetValue( key, out var entry ) ? entry : null;
	}

	protected IReadOnlyCollection<TEntry> Entries => held.Values;
}