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

// A whole subtool contributed by the GAME around the tool - its kind, its generator, its sheet, its widgets. The
// third host hook, and scanned for the same reason the other two are: this library cannot name a type in an
// assembly it does not reference.
//
// Everything the tool itself stands is still written out, and adding a hook here does not change that. What it
// buys is the one thing writing out cannot: a tool the game owns, hidden and shown beside the built-in ones,
// living in the game's own Editor folder where the game can edit it without touching a published package.
public interface IArchAddon {
	// Named in the shelf settings dialog and in a collision report, so a clash says which addon brought it.
	string Name { get; }

	IEnumerable<ArchSubtoolEntry> Subtools();

	// Whatever the tool's tables ask for: an IArchKindManifest, an IArchDrawer, an IArchFiler, an IArchSheet, an
	// IArchHandler, an IArchExtentProvider, an IArchPicker, an IArchPreview, an IArchGuide. Each table filters this
	// by the contract it holds, so an addon answers one method however many tables it reaches.
	//
	// Build these with plain constructors. Asking a table from in here re-enters the scan that is asking you, and
	// the recursion has no floor.
	IEnumerable<object> Contributes();
}

// THE ONE TABLE IN THE TOOL THAT IS HELD, and the only one that may be: every other builds per ask because hotload
// carries a static over, and this is released BY the hotload. A scan is far too expensive to run per ask - a table
// loads many times a frame while the layer stack paints - and an addon's entries are answered once, bucketed by the
// contract each table asks for, and handed back as the same arrays for the rest of the assembly's life.
//
// The pinned-assembly problem the written-out tables exist to avoid is answered by Forget: nothing enrolled here
// outlives the edit that replaced it, so the game's pre-edit types are collectable the moment they are stale.
public sealed class ArchAddons {
	static volatile ArchAddons held;

	[EditorEvent.Hotload]
	static void Forget() {
		held = null;
	}

	// Read through a local, the way AssetSystem's compiler cache is: an asset compile can ask off the main thread
	// while a hotload on it clears the field.
	public static ArchAddons Load() {
		return held ?? (held = Discovered());
	}

	readonly IArchAddon[] enrolled;
	readonly ArchSubtoolEntry[] subtools;
	readonly object[] contributions;
	readonly Dictionary<Type, Array> byContract = new();

	ArchAddons( IArchAddon[] enrolled, ArchSubtoolEntry[] subtools, object[] contributions ) {
		this.enrolled = enrolled;
		this.subtools = subtools;
		this.contributions = contributions;
	}

	public IReadOnlyList<IArchAddon> Enrolled => enrolled;

	// An array, not a query: a table foreaches this on every load, and an interface handle would box the
	// enumerator each time where the array's own is a struct.
	public ArchSubtoolEntry[] Subtools() => subtools;

	public T[] Contributed<T>() where T : class {
		if ( contributions.Length == 0 ) {
			return Array.Empty<T>();
		}

		lock ( byContract ) {
			if ( byContract.TryGetValue( typeof( T ), out var bucketed ) ) {
				return (T[])bucketed;
			}

			var bucket = contributions.OfType<T>().ToArray();

			byContract[typeof( T )] = bucket;

			return bucket;
		}
	}

	static ArchAddons Discovered() {
		var found = ArchDiscovery.EnrolledByName<IArchAddon>().ToArray();

		if ( found.Length == 0 ) {
			return new ArchAddons( Array.Empty<IArchAddon>(), Array.Empty<ArchSubtoolEntry>(), Array.Empty<object>() );
		}

		var shelved = new List<ArchSubtoolEntry>();
		var given = new List<object>();

		foreach ( var addon in found ) {
			shelved.AddRange( Offered( addon ) );
			given.AddRange( Given( addon ) );
		}

		return new ArchAddons( found, shelved.ToArray(), given.ToArray() );
	}

	// An addon that throws is reported and walked past, never taken down the table asking it: one broken game
	// hook must not be the tool refusing to open.
	static IEnumerable<ArchSubtoolEntry> Offered( IArchAddon addon ) {
		try {
			return addon.Subtools()?.Where( entry => entry is not null ) ?? Enumerable.Empty<ArchSubtoolEntry>();
		} catch ( Exception fault ) {
			Log.Warning( $"Architecture addon {addon.Name} threw while naming its subtools: {fault.Message}" );

			return Enumerable.Empty<ArchSubtoolEntry>();
		}
	}

	static IEnumerable<object> Given( IArchAddon addon ) {
		try {
			return addon.Contributes()?.Where( entry => entry is not null ) ?? Enumerable.Empty<object>();
		} catch ( Exception fault ) {
			Log.Warning( $"Architecture addon {addon.Name} threw while contributing: {fault.Message}" );

			return Enumerable.Empty<object>();
		}
	}
}