Editor/Prism/Model/Port.cs

Editor-side model for node ports in a graph editor. Defines port enums and flags, immutable PortDef declarations built by reflection or programmatically, live Port/InputPort/OutputPort types that carry solver state, a PortBuilder to assemble declarations (including reflection-based extraction), and PortCollection to instantiate live ports and preserve state across rebuilds.

Reflection
using Editor.Prism.Core;
using System.ComponentModel;
using System.Reflection;

namespace Editor.Prism.Model;

/// <summary>Which side of a node a port lives on.</summary>
public enum PortDirection
{
	/// <summary>Consumes a value. At most one incoming edge.</summary>
	Input,
	/// <summary>Produces a value. Any number of outgoing edges.</summary>
	Output
}

/// <summary>Behavioural flags on a port.</summary>
[Flags]
public enum PortFlags
{
	/// <summary>Nothing special.</summary>
	None = 0,
	/// <summary>Leaving this input unconnected with no inline value is an error.</summary>
	Required = 1 << 0,
	/// <summary>Not drawn on the card. Still connectable programmatically.</summary>
	Hidden = 1 << 1,
	/// <summary>Never draw an inline value pill for this input.</summary>
	NoInlineEditor = 1 << 2,
	/// <summary>Part of a variadic group; the node grows another socket as this one is filled.</summary>
	Variadic = 1 << 3,
	/// <summary>Opts out of type inference — the value passes through unchanged (reroute, custom code).</summary>
	Passthrough = 1 << 4,
	/// <summary>Drawn in the node's title bar rather than a port row.</summary>
	InTitleBar = 1 << 5,
	/// <summary>This input accepts more than one incoming edge (variadic sums, subgraph fan-in).</summary>
	AllowMultiple = 1 << 6,
	/// <summary>Created by <see cref="PortBuilder"/> at runtime rather than by an attribute.</summary>
	Dynamic = 1 << 7
}

/// <summary>
/// A reference to one port of one node. This is the property type used by <c>[In]</c> and
/// <c>[Out]</c> declarations, and the shape both ends of an <see cref="Edge"/> serialize as.
/// </summary>
public readonly record struct PortRef( NodeId Node, PortId Port )
{
	/// <summary>The unset reference.</summary>
	public static readonly PortRef None = default;

	/// <summary>True when both halves are set.</summary>
	public bool IsValid => Node.IsValid && Port.IsValid;

	/// <summary>Build a reference from raw strings.</summary>
	public static PortRef Parse( string node, string port ) => new( NodeId.Parse( node ), PortId.Parse( port ) );

	/// <inheritdoc/>
	public override string ToString() => IsValid ? $"{Node}.{Port}" : "<none>";
}

/// <summary>
/// The immutable declaration of a port: what it is called, what type it claims to be and how it
/// behaves. Produced by reflection over <c>[In]</c>/<c>[Out]</c> properties and then optionally
/// amended by <see cref="PortBuilder"/> inside <c>PrismNode.OnDefinePorts</c>.
/// </summary>
public sealed record PortDef( PortId Id, string DisplayName, string DeclaredType, PortDirection Direction )
{
	/// <summary>Optional collapsible group on the card.</summary>
	public string Group { get; init; }

	/// <summary>Tooltip shown on the handle and the label.</summary>
	public string Tooltip { get; init; }

	/// <summary>Behavioural flags.</summary>
	public PortFlags Flags { get; init; }

	/// <summary>Sort key within the node. Ties keep declaration order.</summary>
	public int Order { get; init; }

	/// <summary>Name of the <c>[In]</c>/<c>[Out]</c> property that declared this port, when there is one.</summary>
	public string PropertyName { get; init; }

	/// <summary>Name of the <c>[InlineValue]</c> property that supplies the unconnected value, when there is one.</summary>
	public string InlineValueProperty { get; init; }

	/// <summary>Former ids that must still deserialize into this port.</summary>
	public IReadOnlyList<string> FormerIds { get; init; }

	/// <summary>True when the declared type is a type variable rather than a concrete spelling.</summary>
	public bool IsGeneric => TypeRules.IsTypeVariable( DeclaredType );

	/// <summary>The concrete declared type, or <see cref="ShaderType.Void"/> when the port is generic.</summary>
	public ShaderType FixedType => ShaderType.Parse( DeclaredType );

	/// <summary>True when leaving this input unconnected is an error.</summary>
	public bool Required => ( Flags & PortFlags.Required ) != 0;

	/// <summary>True when the port should not be drawn.</summary>
	public bool Hidden => ( Flags & PortFlags.Hidden ) != 0;

	/// <inheritdoc/>
	public override string ToString() => $"{Direction} {Id}:{DeclaredType}";
}

/// <summary>A live port on a live node. Carries the declaration plus everything the solver resolves.</summary>
public abstract class Port
{
	/// <summary>Build a port from its declaration.</summary>
	protected Port( PrismNode node, PortDef def )
	{
		Node = node;
		Def = def;
	}

	/// <summary>
	/// The node this port belongs to.
	/// <para>
	/// Hidden from reflection-driven UI: this is a back-reference, so a <c>SerializedObject</c> walk
	/// that reaches a port would loop <c>port -&gt; Node -&gt; Inputs -&gt; port</c> forever. That is an
	/// uncatchable <c>StackOverflowException</c> inside engine code, which kills the whole editor.
	/// </para>
	/// </summary>
	[Hide, Browsable( false ), JsonIgnore]
	public PrismNode Node { get; }

	/// <summary>The declaration this port was built from.</summary>
	public PortDef Def { get; internal set; }

	/// <summary>Stable id, unique within the node.</summary>
	public PortId Id => Def.Id;

	/// <summary>Display label. May be empty for an unlabelled socket.</summary>
	public string DisplayName => Def.DisplayName;

	/// <summary>The declared type spelling, concrete or generic.</summary>
	public string DeclaredType => Def.DeclaredType;

	/// <summary>Optional port group.</summary>
	public string Group => Def.Group;

	/// <summary>Tooltip text.</summary>
	public string Tooltip => Def.Tooltip;

	/// <summary>Behavioural flags.</summary>
	public PortFlags Flags => Def.Flags;

	/// <summary>True when leaving this input unconnected is an error.</summary>
	public bool Required => Def.Required;

	/// <summary>Which side of the node this port is on.</summary>
	public abstract PortDirection Direction { get; }

	/// <summary>Position within the node's port list. Assigned when the collection is built.</summary>
	public int Index { get; internal set; }

	/// <summary>
	/// The concrete type assigned by the type solver. Void until the first successful solve;
	/// for a non-generic port it always ends up equal to <see cref="PortDef.FixedType"/>.
	/// </summary>
	public ShaderType ResolvedType { get; set; }

	/// <summary>The best type we know: the resolved one when solved, otherwise the declared one.</summary>
	public ShaderType EffectiveType => ResolvedType.IsVoid ? Def.FixedType : ResolvedType;

	/// <inheritdoc/>
	public override string ToString() => $"{Node?.Id}.{Id}";
}

/// <summary>An input port. At most one incoming edge unless <see cref="PortFlags.AllowMultiple"/> is set.</summary>
public sealed class InputPort : Port
{
	/// <summary>Build an input port.</summary>
	public InputPort( PrismNode node, PortDef def ) : base( node, def ) { }

	/// <inheritdoc/>
	public override PortDirection Direction => PortDirection.Input;

	/// <summary>
	/// The literal used when nothing is connected. Boxed because it may be any of the value shapes
	/// <c>ValueCodec</c> understands; the node's <c>[InlineValue]</c> property is the authored source
	/// when <see cref="PortDef.InlineValueProperty"/> is set.
	/// </summary>
	public object InlineValue { get; set; }

	/// <summary>True when an edge terminates on this port.</summary>
	public bool IsConnected =>
		Node?.Graph is { } graph && graph.TryGetIncomingEdge( Node.Id, Id, out _ );
}

/// <summary>An output port. May fan out to any number of inputs.</summary>
public sealed class OutputPort : Port
{
	/// <summary>Build an output port.</summary>
	public OutputPort( PrismNode node, PortDef def ) : base( node, def ) { }

	/// <inheritdoc/>
	public override PortDirection Direction => PortDirection.Output;

	/// <summary>True when at least one edge starts at this port.</summary>
	public bool IsConnected =>
		Node?.Graph is { } graph && graph.GetOutgoingEdges( Node.Id, Id ).Any();
}

/// <summary>
/// Builds the port list for a node: first from reflection over <c>[In]</c>/<c>[Out]</c> properties,
/// then amended by the node's <c>OnDefinePorts</c> override. Ports that cannot be expressed as
/// properties — variadic sockets, subgraph signatures, mode-dependent sets — are added here.
/// </summary>
public sealed class PortBuilder
{
	readonly List<PortDef> _inputs = new();
	readonly List<PortDef> _outputs = new();

	/// <summary>Input declarations, in socket order.</summary>
	public IReadOnlyList<PortDef> Inputs => _inputs;

	/// <summary>Output declarations, in socket order.</summary>
	public IReadOnlyList<PortDef> Outputs => _outputs;

	/// <summary>Append an input port.</summary>
	public PortBuilder Input( string id, string type = "float", string name = null, string group = null,
		PortFlags flags = PortFlags.None, string tooltip = null, int order = 0 )
	{
		_inputs.Add( new PortDef( PortId.Parse( id ), name ?? id, type ?? "float", PortDirection.Input )
		{
			Group = group,
			Tooltip = tooltip,
			Flags = flags | PortFlags.Dynamic,
			Order = order
		} );

		return this;
	}

	/// <summary>Append an output port.</summary>
	public PortBuilder Output( string id, string type = "float", string name = null, string group = null,
		PortFlags flags = PortFlags.None, string tooltip = null, int order = 0 )
	{
		_outputs.Add( new PortDef( PortId.Parse( id ), name ?? id, type ?? "float", PortDirection.Output )
		{
			Group = group,
			Tooltip = tooltip,
			Flags = flags | PortFlags.Dynamic,
			Order = order
		} );

		return this;
	}

	/// <summary>Append a declaration built elsewhere.</summary>
	public PortBuilder Add( PortDef def )
	{
		if ( def is null ) return this;

		if ( def.Direction == PortDirection.Input ) _inputs.Add( def );
		else _outputs.Add( def );

		return this;
	}

	/// <summary>Remove a port by id from whichever side it is on.</summary>
	public PortBuilder Remove( string id )
	{
		var portId = PortId.Parse( id );
		_inputs.RemoveAll( x => x.Id == portId );
		_outputs.RemoveAll( x => x.Id == portId );
		return this;
	}

	/// <summary>Change a port's declared type.</summary>
	public PortBuilder Retype( string id, string declaredType )
	{
		Mutate( id, def => def with { DeclaredType = declaredType } );
		return this;
	}

	/// <summary>Change a port's display label.</summary>
	public PortBuilder Rename( string id, string displayName )
	{
		Mutate( id, def => def with { DisplayName = displayName } );
		return this;
	}

	/// <summary>Add flags to a port.</summary>
	public PortBuilder SetFlags( string id, PortFlags flags )
	{
		Mutate( id, def => def with { Flags = def.Flags | flags } );
		return this;
	}

	/// <summary>True when a port with this id exists on either side.</summary>
	public bool Has( string id )
	{
		var portId = PortId.Parse( id );
		return _inputs.Any( x => x.Id == portId ) || _outputs.Any( x => x.Id == portId );
	}

	/// <summary>Drop every declaration. Used by nodes that build their entire signature dynamically.</summary>
	public PortBuilder Clear()
	{
		_inputs.Clear();
		_outputs.Clear();
		return this;
	}

	void Mutate( string id, Func<PortDef, PortDef> mutate )
	{
		var portId = PortId.Parse( id );

		for ( int i = 0; i < _inputs.Count; i++ )
		{
			if ( _inputs[i].Id == portId ) _inputs[i] = mutate( _inputs[i] );
		}

		for ( int i = 0; i < _outputs.Count; i++ )
		{
			if ( _outputs[i].Id == portId ) _outputs[i] = mutate( _outputs[i] );
		}
	}

	/// <summary>Build a builder pre-populated with the reflected declarations of a node type.</summary>
	public static PortBuilder FromReflection( Type nodeType )
	{
		var builder = new PortBuilder();
		var (inputs, outputs) = Reflect( nodeType );

		builder._inputs.AddRange( inputs );
		builder._outputs.AddRange( outputs );

		return builder;
	}

	/// <summary>
	/// The port declarations implied by a node type's <c>[In]</c>/<c>[Out]</c> properties, in
	/// declaration order (base class first). Cached per type; call <see cref="FlushCache"/> on hotload.
	/// </summary>
	public static (IReadOnlyList<PortDef> Inputs, IReadOnlyList<PortDef> Outputs) Reflect( Type nodeType )
	{
		if ( nodeType is null ) return ( Array.Empty<PortDef>(), Array.Empty<PortDef>() );

		lock ( s_cacheLock )
		{
			if ( s_cache.TryGetValue( nodeType, out var cached ) ) return cached;
		}

		var inputs = new List<PortDef>();
		var outputs = new List<PortDef>();

		var properties = nodeType
			.GetProperties( BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy )
			.OrderBy( DeclarationDepth )
			.ThenBy( x => x.MetadataToken )
			.ToArray();

		// Map port name -> the [InlineValue] property that feeds it.
		var inlineValues = new Dictionary<string, string>();

		foreach ( var property in properties )
		{
			var inline = property.GetCustomAttribute<InlineValueAttribute>();
			if ( inline is null || string.IsNullOrEmpty( inline.PortName ) ) continue;

			inlineValues[inline.PortName] = property.Name;
		}

		foreach ( var property in properties )
		{
			var formerly = property.GetCustomAttributes<FormerlyKnownAsAttribute>()
				.Select( x => x.OldName )
				.Where( x => !string.IsNullOrEmpty( x ) )
				.ToArray();

			if ( property.GetCustomAttribute<InAttribute>() is { } input )
			{
				inlineValues.TryGetValue( property.Name, out var inlineProperty );

				inputs.Add( new PortDef( PortId.Parse( property.Name ), input.Name ?? property.Name,
					input.Type ?? "float", PortDirection.Input )
				{
					Group = input.Group,
					Tooltip = input.Tooltip,
					Flags = input.Required ? PortFlags.Required : PortFlags.None,
					Order = input.Order,
					PropertyName = property.Name,
					InlineValueProperty = inlineProperty,
					FormerIds = formerly.Length > 0 ? formerly : null
				} );
			}

			if ( property.GetCustomAttribute<OutAttribute>() is { } output )
			{
				outputs.Add( new PortDef( PortId.Parse( property.Name ), output.Name ?? property.Name,
					output.Type ?? "float", PortDirection.Output )
				{
					Group = output.Group,
					Tooltip = output.Tooltip,
					Order = output.Order,
					PropertyName = property.Name,
					FormerIds = formerly.Length > 0 ? formerly : null
				} );
			}
		}

		var result = ( (IReadOnlyList<PortDef>)StableSort( inputs ), (IReadOnlyList<PortDef>)StableSort( outputs ) );

		lock ( s_cacheLock )
		{
			s_cache[nodeType] = result;
		}

		return result;
	}

	/// <summary>Drop the reflection cache. Must run on hotload — <see cref="PortDef"/>s outlive the assembly otherwise.</summary>
	public static void FlushCache()
	{
		lock ( s_cacheLock )
		{
			s_cache.Clear();
		}
	}

	/// <summary>
	/// Sort by explicit order, keeping declaration order for ties, and drop duplicate ids — a derived
	/// class that redeclares a base port wins, because its declaration is the more specific one.
	/// </summary>
	static PortDef[] StableSort( List<PortDef> defs )
	{
		var deduped = new List<PortDef>( defs.Count );

		for ( int i = 0; i < defs.Count; i++ )
		{
			var later = false;

			for ( int j = i + 1; j < defs.Count; j++ )
			{
				if ( defs[j].Id != defs[i].Id ) continue;

				later = true;
				break;
			}

			if ( !later ) deduped.Add( defs[i] );
		}

		return deduped.OrderBy( x => x.Order ).ToArray();
	}

	static int DeclarationDepth( PropertyInfo property )
	{
		var depth = 0;
		var type = property.DeclaringType;

		while ( type is not null && type != typeof( object ) )
		{
			depth++;
			type = type.BaseType;
		}

		return depth;
	}

	static readonly Dictionary<Type, (IReadOnlyList<PortDef> Inputs, IReadOnlyList<PortDef> Outputs)> s_cache = new();
	static readonly object s_cacheLock = new();
}

/// <summary>
/// The live ports of one node. Rebuilding preserves the resolved type and inline value of every
/// port whose id survives; ports that disappear leave their edges to be converted into
/// <see cref="BrokenEdge"/> ghosts by the graph, never silently deleted.
/// </summary>
public sealed class PortCollection
{
	readonly List<InputPort> _inputs = new();
	readonly List<OutputPort> _outputs = new();

	/// <summary>Input ports, in socket order.</summary>
	public IReadOnlyList<InputPort> Inputs => _inputs;

	/// <summary>Output ports, in socket order.</summary>
	public IReadOnlyList<OutputPort> Outputs => _outputs;

	/// <summary>Find an input port by id.</summary>
	public InputPort FindInput( PortId id ) => _inputs.FirstOrDefault( x => x.Id == id );

	/// <summary>Find an output port by id.</summary>
	public OutputPort FindOutput( PortId id ) => _outputs.FirstOrDefault( x => x.Id == id );

	/// <summary>Find a port of either direction by id.</summary>
	public Port Find( PortId id ) => (Port)FindInput( id ) ?? FindOutput( id );

	/// <summary>
	/// Replace the port set with the declarations in <paramref name="builder"/>, carrying over state
	/// from ports whose ids survive. Returns the ids that disappeared.
	/// <para>
	/// Duplicate ids are tolerated rather than fatal: a node whose <c>OnDefinePorts</c> re-declares a
	/// reflected port keeps the last declaration, matching the "more specific wins" rule the reflection
	/// pass already uses. A malformed node must never take the document down.
	/// </para>
	/// </summary>
	public IReadOnlyList<PortId> Apply( PrismNode node, PortBuilder builder )
	{
		var removed = new List<PortId>();

		var oldInputs = ToLookup( _inputs );
		var oldOutputs = ToLookup( _outputs );

		_inputs.Clear();
		_outputs.Clear();

		foreach ( var def in Dedupe( builder.Inputs ) )
		{
			var port = new InputPort( node, def ) { Index = _inputs.Count };

			if ( oldInputs.TryGetValue( def.Id, out var old ) )
			{
				port.ResolvedType = old.ResolvedType;
				port.InlineValue = old.InlineValue;
				oldInputs.Remove( def.Id );
			}

			_inputs.Add( port );
		}

		foreach ( var def in Dedupe( builder.Outputs ) )
		{
			var port = new OutputPort( node, def ) { Index = _outputs.Count };

			if ( oldOutputs.TryGetValue( def.Id, out var old ) )
			{
				port.ResolvedType = old.ResolvedType;
				oldOutputs.Remove( def.Id );
			}

			_outputs.Add( port );
		}

		removed.AddRange( oldInputs.Keys );
		removed.AddRange( oldOutputs.Keys );

		return removed;
	}

	/// <summary>Keep the last declaration for each id, preserving declaration order otherwise.</summary>
	static List<PortDef> Dedupe( IReadOnlyList<PortDef> defs )
	{
		var result = new List<PortDef>( defs.Count );

		for ( int i = 0; i < defs.Count; i++ )
		{
			var later = false;

			for ( int j = i + 1; j < defs.Count; j++ )
			{
				if ( defs[j].Id != defs[i].Id ) continue;

				later = true;
				break;
			}

			if ( !later ) result.Add( defs[i] );
		}

		return result;
	}

	static Dictionary<PortId, T> ToLookup<T>( List<T> ports ) where T : Port
	{
		var map = new Dictionary<PortId, T>();

		foreach ( var port in ports )
		{
			map[port.Id] = port;
		}

		return map;
	}
}