Editor/Data/ArchCatalogs.cs

Editor-side catalog management types. Defines ArchShelf role identifiers, the IArchCatalog interface, an ArchCatalogs table that aggregates specific catalog implementations, and helpers to list shipped/authored entries and merge/save catalog items.

File Access
namespace Sunless.Architecture;

// Which shelf a catalog stands on. A role rather than an interface per catalog, because the catalogs differ only in
// what they hold - a module brings its own with new ArchShelf( role ), and the reservation only stops two of them
// meaning the same thing.
public readonly record struct ArchShelf( string Role )
{
	public static readonly ArchShelf Openings = new( "openings" );
	public static readonly ArchShelf Walls = new( "walls" );
	public static readonly ArchShelf Pillars = new( "pillars" );
	public static readonly ArchShelf RoadLines = new( "roadlines" );
	public static readonly ArchShelf Profiles = new( "profiles" );
	public static readonly ArchShelf Types = new( "types" );

	public override string ToString() => Role;
}

// One catalog of authored types, owned by the module whose subtool authors them. The TYPE itself stays in core: a
// porch fits an opening preset it never authored, so a preset is shared geometry by the same rule a payload is,
// while the catalog around it belongs to one module alone.
public interface IArchCatalog
{
	ArchShelf Shelf { get; }

	string Named( object entry );

	// What the module brings with it; an authored entry of the same name wins, so a tuned preset survives upgrades.
	IEnumerable<object> Shipped();

	// What stands on disk beside it, which tops what ships.
	IEnumerable<object> Authored();

	bool Save( object entry );
}

public sealed class ArchCatalogs : ArchTable<ArchCatalogs, ArchShelf, IArchCatalog>
{
	protected override IEnumerable<IArchCatalog> Standing()
	{
		yield return new ArchArchetypeCatalog();
		yield return new ArchProfileCatalog();
		yield return new ArchOpeningCatalog();
		yield return new ArchWallCatalog();
		yield return new ArchPillarCatalog();
		yield return new ArchRoadLineCatalog();
	}

	protected override ArchShelf KeyOf( IArchCatalog catalog ) => catalog.Shelf;

	protected override bool Accepts( IArchCatalog catalog ) => !string.IsNullOrWhiteSpace( catalog.Shelf.Role );

	protected override string Collision( IArchCatalog standing, IArchCatalog catalog )
	{
		return $"{catalog.Shelf} is claimed by both {standing.GetType().Name} and {catalog.GetType().Name}";
	}

	// A shelf no catalog claims stands empty rather than erroring.
	public IArchCatalog On( ArchShelf shelf ) => Held( shelf );

	public List<T> Ships<T>( ArchShelf shelf ) where T : class
	{
		return On( shelf ) is { } catalog ? catalog.Shipped().OfType<T>().ToList() : new List<T>();
	}

	// For a catalog whose authored folder IS the offering - a picker that browses what an author has on disk.
	public List<T> Authors<T>( ArchShelf shelf ) where T : class
	{
		return On( shelf ) is { } catalog ? catalog.Authored().OfType<T>().ToList() : new List<T>();
	}

	// What ships never displaces what is already held, and what is authored on disk always does - so an upgrade
	// brings new presets in beside the tuned ones and a written file is the last word on its own name.
	public void Stock<T>( ArchShelf shelf, List<T> saved ) where T : class
	{
		if ( saved is null || On( shelf ) is not { } catalog )
		{
			return;
		}

		foreach ( var entry in catalog.Shipped().OfType<T>() )
		{
			if ( !saved.Any( existing => Same( catalog.Named( existing ), catalog.Named( entry ) ) ) )
			{
				saved.Add( entry );
			}
		}

		foreach ( var entry in catalog.Authored().OfType<T>() )
		{
			var index = saved.FindIndex( existing => Same( catalog.Named( existing ), catalog.Named( entry ) ) );

			if ( index >= 0 )
			{
				saved[index] = entry;
			}
			else
			{
				saved.Add( entry );
			}
		}
	}

	public bool Save( ArchShelf shelf, object entry ) => On( shelf ) is { } catalog && catalog.Save( entry );

	static bool Same( string first, string second ) => string.Equals( first, second, StringComparison.OrdinalIgnoreCase );
}

// For a one-off caller holding no table of its own. Reaching through it in a loop loads the shelf every time.
public static class ArchShelved
{
	public static List<T> Ships<T>( ArchShelf shelf ) where T : class => ArchCatalogs.Load().Ships<T>( shelf );

	public static List<T> Authors<T>( ArchShelf shelf ) where T : class => ArchCatalogs.Load().Authors<T>( shelf );

	public static bool Save( ArchShelf shelf, object entry ) => ArchCatalogs.Load().Save( shelf, entry );
}