Editor/Data/ArchPlanStore.cs

Editor-side helpers for reading and mutating an ArchPlan. Provides traversal (Everything, Parts, Units), find/host lookups, filing/unfiling/insert/remove operations, id utilities, and JSON highest-id scanning used when plan parts lack runtime types.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

namespace Sunless.Architecture;

// The accessors that replace fourteen bespoke finders, each of which was a copy of the same
// SelectMany(...).FirstOrDefault(x => x.Id == id). The caller names the type it wants; the manifest that claims
// that type says where it is filed, so core reaches a module's parts without naming one. Every generic member has an
// ArchKind-keyed twin beside it, for a caller holding an ident off disk and no type to go with it.
public static class ArchPlanStore
{
	// Every authored object in the plan, walked through the manifests rather than through a hand-listed traversal
	// that forgets whatever was added last. A part nothing claims is simply not walked - that is a kind this build
	// has no manifest for, and its bytes ride in the payload bag untouched.
	public static IEnumerable<object> Everything( this ArchPlan plan, ArchKinds table = null )
	{
		if ( plan is null )
		{
			yield break;
		}

		var kinds = table ?? ArchKinds.Load();
		var seen = new HashSet<object>( ReferenceEqualityComparer.Instance );
		var pending = new Queue<object>();

		foreach ( var root in plan.Units.Cast<object>().Concat( plan.Assemblies ).Concat( plan.Instances ) )
		{
			pending.Enqueue( root );
		}

		while ( pending.Count > 0 )
		{
			var host = pending.Dequeue();

			if ( host is null || !seen.Add( host ) )
			{
				continue;
			}

			yield return host;

			foreach ( var filed in kinds.All.Where( manifest => Hosts( manifest, host ) ).SelectMany( manifest => manifest.Filed( plan, host ) ) )
			{
				pending.Enqueue( filed );
			}
		}
	}

	public static IEnumerable<T> Parts<T>( this ArchPlan plan, ArchKinds table = null ) where T : class
	{
		return plan.Everything( table ).OfType<T>();
	}

	// The same sweep keyed on a kind rather than on a type. A kind stands at the plan root, on hosts, or on both, so
	// the two are walked together and reference identity keeps whatever overlaps from coming back twice.
	public static IEnumerable<object> Parts( this ArchPlan plan, ArchKind kind, ArchKinds table = null )
	{
		if ( plan is null )
		{
			yield break;
		}

		var kinds = table ?? ArchKinds.Load();
		var manifest = kinds.For( kind );

		if ( manifest is null )
		{
			yield break;
		}

		var seen = new HashSet<object>( ReferenceEqualityComparer.Instance );

		foreach ( var part in manifest.Filed( plan, null ).Where( seen.Add ) )
		{
			yield return part;
		}

		foreach ( var host in plan.Everything( kinds ).Where( candidate => Hosts( manifest, candidate ) ) )
		{
			foreach ( var part in manifest.Filed( plan, host ).Where( seen.Add ) )
			{
				yield return part;
			}
		}
	}

	public static T Find<T>( this ArchPlan plan, int id, ArchKinds table = null ) where T : class
	{
		return id == 0 ? null : plan.Parts<T>( table ).FirstOrDefault( part => IdOf( part ) == id );
	}

	public static object Find( this ArchPlan plan, ArchKind kind, int id, ArchKinds table = null )
	{
		return id == 0 ? null : plan.Parts( kind, table ).FirstOrDefault( part => IdOf( part ) == id );
	}

	// An id arriving with no kind beside it, which is how a selection, a link and an mcp argument all arrive.
	public static object Find( this ArchPlan plan, int id, ArchKinds table = null )
	{
		return id == 0 ? null : plan.Everything( table ).FirstOrDefault( part => IdOf( part ) == id );
	}

	public static THost HostOf<THost>( this ArchPlan plan, object part, ArchKinds table = null ) where THost : class
	{
		if ( part is null )
		{
			return null;
		}

		var kinds = table ?? ArchKinds.Load();

		return plan.Everything( kinds ).OfType<THost>().FirstOrDefault( host => Files( plan, kinds, host, part ) );
	}

	// Neither end of the question named: the part is an object and so is what comes back.
	public static object HostOf( this ArchPlan plan, object part, ArchKinds table = null )
	{
		if ( part is null )
		{
			return null;
		}

		var kinds = table ?? ArchKinds.Load();

		return plan.Everything( kinds ).FirstOrDefault( host => Files( plan, kinds, host, part ) );
	}

	public static IEnumerable<T> Units<T>( this ArchPlan plan ) where T : ArchUnit => plan.Units.OfType<T>();

	// Read off the unit's own stored kind, so a unit whose module is not installed still answers as itself.
	public static IEnumerable<ArchUnit> Units( this ArchPlan plan, ArchKind kind ) => plan.Units.Where( unit => unit.Kind == kind );

	// One host's own parts of one kind, in the order the manifest files them - which is the order the stack applies
	// them in, so an index taken from this read is the index to seat against.
	public static IEnumerable<object> Filed( this ArchPlan plan, ArchKind kind, object host, ArchKinds table = null )
	{
		// Only the rootward ask walks the document; a host answers off its own list, so demanding a plan there
		// would force one through helpers that hold a host and nothing else, purely to be ignored.
		if ( host is null && plan is null )
		{
			return Enumerable.Empty<object>();
		}

		var manifest = (table ?? ArchKinds.Load()).For( kind );

		return manifest is null ? Enumerable.Empty<object>() : manifest.Filed( plan, host );
	}

	// For a helper that holds the host and no plan. Named apart from the extension so the reader sees that nothing
	// is being asked of the document.
	public static IEnumerable<object> FiledOn( ArchKind kind, object host, ArchKinds table = null )
	{
		return host is null ? Enumerable.Empty<object>() : Filed( null, kind, host, table );
	}

	// For a call site that can still name what it is after: the kind is found from the type rather than given, and a
	// type no kind claims falls back to whatever this host holds of it.
	public static IEnumerable<T> Filed<T>( this ArchPlan plan, object host, ArchKinds table = null ) where T : class
	{
		var kinds = table ?? ArchKinds.Load();

		return kinds.Claiming( typeof( T ) ) is { } manifest
			? manifest.Filed( plan, host ).OfType<T>()
			: kinds.All.Where( entry => Hosts( entry, host ) ).SelectMany( entry => entry.Filed( plan, host ) ).OfType<T>();
	}

	// Counted where the list can count itself, so asking whether a host holds anything never builds a list to
	// measure - the ten Count reads a HasContent used to make were ten free answers.
	public static int CountFiled( this ArchPlan plan, ArchKind kind, object host, ArchKinds table = null )
	{
		return plan.Filed( kind, host, table ).Count();
	}

	public static int CountFiled<T>( this ArchPlan plan, object host, ArchKinds table = null ) where T : class
	{
		return plan.Filed<T>( host, table ).Count();
	}

	// Stops at the first kind that holds something, which is all HasContent ever wanted to know.
	public static bool AnyFiled( this ArchPlan plan, object host, ArchKinds table = null )
	{
		if ( plan is null )
		{
			return false;
		}

		var kinds = table ?? ArchKinds.Load();

		return kinds.All.Where( manifest => Hosts( manifest, host ) ).Any( manifest => manifest.Filed( plan, host ).Any() );
	}

	// Appended through the manifest, the only thing that knows which of a host's several lists this part belongs in.
	public static bool File( this ArchPlan plan, ArchKind kind, object host, object payload, ArchKinds table = null )
	{
		return (table ?? ArchKinds.Load()).For( kind )?.File( host, payload ) == true;
	}

	// The index counts within the KIND's filed order, not within the list underneath it: two kinds can share one
	// list, and a walkway seated before the second walkway must not land before the second room.
	public static bool Insert( this ArchPlan plan, ArchKind kind, object host, int index, object payload, ArchKinds table = null )
	{
		var manifest = plan is null || payload is null ? null : (table ?? ArchKinds.Load()).For( kind );
		var folder = manifest?.Folder( plan, host );

		if ( folder is null )
		{
			return false;
		}

		var seat = SeatFor( folder, manifest.Filed( plan, host ).ToList(), index );

		if ( !manifest.File( host, payload ) )
		{
			return false;
		}

		var landed = SeatOf( folder, payload );

		if ( landed >= 0 && landed != seat )
		{
			folder.RemoveAt( landed );
			folder.Insert( seat, payload );
		}

		return true;
	}

	public static bool RemoveAt( this ArchPlan plan, ArchKind kind, object host, int index, ArchKinds table = null )
	{
		var manifest = plan is null || index < 0 ? null : (table ?? ArchKinds.Load()).For( kind );
		var filed = manifest?.Filed( plan, host ).ToList();

		return filed is not null && index < filed.Count && manifest.Unfile( plan, filed[index] );
	}

	// Materialised before anything is dropped, or the predicate walks a list being edited under it.
	public static int RemoveAll( this ArchPlan plan, ArchKind kind, object host, Func<object, bool> predicate, ArchKinds table = null )
	{
		var manifest = plan is null || predicate is null ? null : (table ?? ArchKinds.Load()).For( kind );

		if ( manifest is null )
		{
			return 0;
		}

		return manifest.Filed( plan, host ).Where( predicate ).ToList().Count( doomed => manifest.Unfile( plan, doomed ) );
	}

	// Keyed on the payload, because a part is dropped by a caller holding the thing rather than its kind. A payload
	// no manifest claims is offered round the table, since two kinds sharing one type leave one of them unclaimed.
	public static bool Unfile( this ArchPlan plan, object payload, ArchKinds table = null )
	{
		if ( plan is null || payload is null )
		{
			return false;
		}

		var kinds = table ?? ArchKinds.Load();

		return kinds.Claiming( payload )?.Unfile( plan, payload ) == true
			|| kinds.All.Any( manifest => manifest.Unfile( plan, payload ) );
	}

	// Never cached: hotload carries a PropertyInfo from the assembly the editor had before the edit.
	public static int IdOf( object payload )
	{
		return payload?.GetType().GetProperty( "Id" )?.GetValue( payload ) is int id ? id : 0;
	}

	// The id watermark inside bytes no type in this editor claims. Without this pass the parts of a kind this build
	// cannot read are invisible to the allocator, which hands their ids out again - and two objects sharing an id
	// are one object in the scene, standing where two were meant to.
	public static int HighestIdIn( IReadOnlyDictionary<string, JsonElement> payloads )
	{
		return payloads is null ? 0 : payloads.Values.Select( HighestIdIn ).DefaultIfEmpty( 0 ).Max();
	}

	public static int HighestIdIn( JsonElement element )
	{
		switch ( element.ValueKind )
		{
			case JsonValueKind.Object:
				var highest = 0;

				foreach ( var property in element.EnumerateObject() )
				{
					var found = property.NameEquals( "Id" ) && property.Value.ValueKind == JsonValueKind.Number
						&& property.Value.TryGetInt32( out var id )
							? id
							: HighestIdIn( property.Value );

					highest = System.Math.Max( highest, found );
				}

				return highest;

			case JsonValueKind.Array:
				return element.EnumerateArray().Select( HighestIdIn ).DefaultIfEmpty( 0 ).Max();

			default:
				return 0;
		}
	}

	static bool Hosts( IArchKindManifest manifest, object host )
	{
		return manifest.Hosts.Any( type => type.IsInstanceOfType( host ) );
	}

	static bool Files( ArchPlan plan, ArchKinds kinds, object host, object part )
	{
		return kinds.All.Where( manifest => Hosts( manifest, host ) )
			.SelectMany( manifest => manifest.Filed( plan, host ) )
			.Any( filed => ReferenceEquals( filed, part ) );
	}

	// Where in the folder the kind's own index lands. Past the end of what this kind holds is an append, which is
	// also what a folder shared with another kind gives back when nothing of ours stands at that index.
	static int SeatFor( System.Collections.IList folder, IReadOnlyList<object> filed, int index )
	{
		var seat = index >= 0 && index < filed.Count ? SeatOf( folder, filed[index] ) : -1;

		return seat < 0 ? folder.Count : seat;
	}

	// By reference, because a part is identified by being that object - two parts that compare equal are still two.
	static int SeatOf( System.Collections.IList folder, object payload )
	{
		for ( var seat = 0; seat < folder.Count; seat++ )
		{
			if ( ReferenceEquals( folder[seat], payload ) )
			{
				return seat;
			}
		}

		return -1;
	}
}