Editor/Data/ArchPlanStore.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;

namespace Sunless.Architecture;

public static class ArchPlanStore {
	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>();
	}

	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 );
	}

	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 ) );
	}

	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>();

	public static IEnumerable<ArchUnit> Units( this ArchPlan plan, ArchKind kind ) => plan.Units.Where( unit => unit.Kind == kind );

	public static IEnumerable<object> Filed( this ArchPlan plan, ArchKind kind, object host, ArchKinds table = null ) {
		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 );
	}

	public static IEnumerable<object> FiledOn( ArchKind kind, object host, ArchKinds table = null ) {
		return host is null ? Enumerable.Empty<object>() : Filed( null, kind, host, table );
	}

	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>();
	}

	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();
	}

	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() );
	}

	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;
	}

	// Index is within the kind's filed order, not the raw list
	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] );
	}

	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 ) );
	}

	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 ) );
	}

	// Reflected — never cached across hotloads
	public static int IdOf( object payload ) {
		return payload?.GetType().GetProperty( "Id" )?.GetValue( payload ) is int id ? id : 0;
	}

	public static bool SetId( object payload, int id ) {
		if ( payload?.GetType().GetProperty( "Id" ) is not { CanWrite: true } property || property.PropertyType != typeof( int ) ) {
			return false;
		}

		property.SetValue( payload, id );

		return true;
	}

	// Watermark from opaque payloads — prevents id reuse across unknown kinds
	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 ) );
	}

	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;
	}

	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;
	}
}