Editor/Prism/Nodes/ParameterNodes.cs

Editor Prism node implementations for graph parameters, constants and keyword-based branching. Defines PrismConstantKind enum, PrismParameters helper functions (parameter/keyword lookup, symbol generation, component helpers), and node classes: ParameterReferenceNode, ConstantNode, KeywordBranchNode, KeywordValueNode, StaticSwitchNode, ShaderConstantNode. They validate, define ports, and emit IR/global declarations for use by the compiler/backends.

Reflection
using Editor.Prism.Compiler;
using Editor.Prism.Compiler.Backends;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
using Editor.Prism.Model;

namespace Editor.Prism.Nodes;

// ---------------------------------------------------------------------------------------------------
// Parameters, constants and combos.
//
// A parameter node stores a ParamId, never a name: renaming a blackboard entry must not break a graph,
// and the clipboard has to be able to remap the reference exactly when a fragment is pasted into a
// document that already has that id. The uniform symbol is derived through
// GraphCompiler.ParameterSymbol so the model, the compiler and both backends agree on one spelling.
// ---------------------------------------------------------------------------------------------------

/// <summary>The shapes a constant node can take.</summary>
public enum PrismConstantKind
{
	/// <summary>A single float.</summary>
	Float,
	/// <summary>Two floats.</summary>
	Float2,
	/// <summary>Three floats.</summary>
	Float3,
	/// <summary>Four floats.</summary>
	Float4,
	/// <summary>A colour, edited through a colour picker and split into RGBA.</summary>
	Color,
	/// <summary>A boolean.</summary>
	Bool,
	/// <summary>A signed integer.</summary>
	Int,
	/// <summary>A 4x4 matrix, authored as four rows.</summary>
	Matrix
}

/// <summary>Naming and typing rules shared by every node that references a blackboard entry.</summary>
public static class PrismParameters
{
	/// <summary>The declaration the compiler emits for a parameter. Matches what the compiler builds itself.</summary>
	public static GlobalDecl Declare( IGraphParameter parameter )
	{
		if ( parameter is null ) return null;

		var name = GraphCompiler.ParameterSymbol( parameter );

		if ( string.IsNullOrEmpty( name ) ) return null;

		return new GlobalDecl( name, parameter.Type, KindFor( parameter.Type ) )
		{
			Parameter = parameter.Id,
			AttributeName = parameter.AttributeName,
			Default = parameter.Type.IsNumeric ? parameter.DefaultValue : null,
			DefaultAsset = parameter.DefaultAsset,
			Srgb = parameter.Srgb,
			Ui = parameter.Ui
		};
	}

	/// <summary>Which kind of module-level declaration a type becomes.</summary>
	public static GlobalKind KindFor( ShaderType type )
	{
		if ( type.IsSampler ) return GlobalKind.Sampler;
		if ( type.IsTexture ) return type.IsWritable ? GlobalKind.RwTexture : GlobalKind.Texture;
		if ( type.IsBuffer ) return type.IsWritable ? GlobalKind.RwBuffer : GlobalKind.Buffer;

		return GlobalKind.Uniform;
	}

	/// <summary>Find a blackboard parameter by id. Null when the graph does not have it.</summary>
	public static IGraphParameter Find( IPrismGraph graph, ParamId id )
	{
		if ( graph is not ICompilableGraph compilable || compilable.Parameters is null ) return null;
		if ( !id.IsValid ) return null;

		foreach ( var parameter in compilable.Parameters )
		{
			if ( parameter is not null && parameter.Id == id ) return parameter;
		}

		return null;
	}

	/// <summary>Find a keyword by id. Null when the graph does not have it.</summary>
	public static IGraphKeyword FindKeyword( IPrismGraph graph, ParamId id )
	{
		if ( graph is not ICompilableGraph compilable || compilable.Keywords is null ) return null;
		if ( !id.IsValid ) return null;

		foreach ( var keyword in compilable.Keywords )
		{
			if ( keyword is not null && keyword.Id == id ) return keyword;
		}

		return null;
	}

	/// <summary>
	/// The symbol a combo is readable under from inside a program. A feature is only visible through
	/// the static combo it is bound to, so <c>F_PUDDLES</c> is read as <c>S_PUDDLES</c>.
	/// </summary>
	public static string ComboSymbol( IGraphKeyword keyword )
	{
		if ( keyword is null || string.IsNullOrWhiteSpace( keyword.Name ) ) return null;

		return keyword.Kind == ComboKind.Feature
			? SboxShaderWriter.StaticNameFor( keyword.Name )
			: keyword.Name;
	}

	/// <summary>
	/// The symbol a combo is readable under from a standalone Slang module. The Slang backend lowers
	/// every graph keyword to a link-time specialization constant named after the keyword itself, so a
	/// feature is read as <c>F_PUDDLES</c> there and as <c>S_PUDDLES</c> in a <c>.shader</c>.
	/// </summary>
	public static string SlangComboSymbol( IGraphKeyword keyword ) =>
		string.IsNullOrWhiteSpace( keyword?.Name ) ? null : keyword.Name;

	/// <summary>
	/// A helper that reads a combo as a boolean.
	/// <para>
	/// The HLSL body is guarded by <c>#if defined</c>, because a <c>.shader</c> can legitimately be
	/// compiled without the combo — a probe build, or a pass it was never wired into. The Slang body is
	/// not: a module always declares every keyword as a specialization constant, and a constant is
	/// invisible to the preprocessor, so the guard would make the branch permanently false.
	/// </para>
	/// </summary>
	public static HelperFunction ComboTest( string symbol, string slangSymbol = null )
	{
		var name = "Prism_Combo_" + symbol;

		return new HelperFunction( name, ShaderType.Bool, Array.Empty<HelperParam>() )
		{
			Hlsl = TestBody( name, symbol, true ),
			Slang = TestBody( name, string.IsNullOrEmpty( slangSymbol ) ? symbol : slangSymbol, false )
		};
	}

	/// <summary>
	/// A helper that reads a combo as an integer, for a multi-valued combo used as a mode selector.
	/// </summary>
	public static HelperFunction ComboValue( string symbol, string slangSymbol = null )
	{
		var name = "Prism_ComboValue_" + symbol;

		return new HelperFunction( name, ShaderType.Int, Array.Empty<HelperParam>() )
		{
			Hlsl = ValueBody( name, symbol, true ),
			Slang = ValueBody( name, string.IsNullOrEmpty( slangSymbol ) ? symbol : slangSymbol, false )
		};
	}

	static string TestBody( string name, string symbol, bool guarded ) =>
		Body( "bool", name, $"return ( {symbol} ) != 0;", "return false;", symbol, guarded );

	static string ValueBody( string name, string symbol, bool guarded ) =>
		Body( "int", name, $"return (int)( {symbol} );", "return 0;", symbol, guarded );

	static string Body( string type, string name, string taken, string fallback, string symbol, bool guarded )
	{
		if ( !guarded ) return $"{type} {name}()\r\n{{\r\n\t{taken}\r\n}}";

		return
			$"{type} {name}()\r\n" +
			"{\r\n" +
			$"#if defined( {symbol} )\r\n" +
			$"\t{taken}\r\n" +
			"#else\r\n" +
			$"\t{fallback}\r\n" +
			"#endif\r\n" +
			"}";
	}

	/// <summary>The component labels a type's split outputs use.</summary>
	public static IReadOnlyList<string> ComponentNames( ShaderType type, bool asColor )
	{
		var count = Math.Clamp( type.Components, 0, 4 );

		if ( count <= 1 || type.IsMatrix ) return Array.Empty<string>();

		var names = asColor ? new[] { "R", "G", "B", "A" } : new[] { "X", "Y", "Z", "W" };

		return names.Take( count ).ToArray();
	}

	/// <summary>The swizzle mask that selects one component by index.</summary>
	public static string ComponentMask( int index ) => index switch
	{
		1 => "y",
		2 => "z",
		3 => "w",
		_ => "x"
	};
}

/// <summary>
/// Reads a blackboard parameter.
/// <para>
/// The reference is a <see cref="ParamId"/>, so renaming the parameter, reordering the blackboard or
/// pasting the node into another document never silently binds it to something else. The uniform is
/// declared once per module however many nodes reference it.
/// </para>
/// </summary>
[NodeInfo( Id = ParameterReferenceNode.TypeId, Title = "Parameter", Category = "Parameter",
	Icon = "tune", Keywords = new[] { "parameter", "uniform", "property", "blackboard", "material" },
	Description = "Reads a parameter from the graph's blackboard. Drag a parameter onto the canvas to " +
		"create one of these already bound." )]
[NodeVersion( 1 )]
public sealed class ParameterReferenceNode : PrismNode
{
	/// <summary>The stable type id. Referenced by the legacy importer when it promotes a constant.</summary>
	public const string TypeId = "prism.parameter.ref";

	ParamId _parameter;
	string _parameterType = ShaderType.Float.ToString();

	/// <summary>
	/// Which blackboard parameter this node reads.
	/// <para>
	/// Typed as <see cref="ParamId"/> per addendum B4, so the clipboard's paste-time renumbering can
	/// find and rewrite it. <c>PrismJson</c> writes it as the bare id string, which is what the document
	/// schema shows and what the legacy importer emits.
	/// </para>
	/// </summary>
	public ParamId Parameter
	{
		get => _parameter;
		set
		{
			if ( _parameter == value ) return;

			_parameter = value;
			SyncType();
			RebuildPorts();
		}
	}

	/// <summary>
	/// The parameter's type, mirrored onto the node.
	/// <para>
	/// Kept so the node still has the right ports — and its wires still draw — when the parameter it
	/// points at has been deleted, or when the node is being inspected outside a document.
	/// </para>
	/// </summary>
	public string ParameterType
	{
		get => _parameterType;
		set
		{
			var text = string.IsNullOrWhiteSpace( value ) ? ShaderType.Float.ToString() : value.Trim();

			if ( string.Equals( _parameterType, text, StringComparison.Ordinal ) ) return;

			_parameterType = text;
			RebuildPorts();
		}
	}

	/// <summary>True when the referenced parameter is a colour, so its channels are labelled RGBA.</summary>
	public bool IsColor { get; set; }

	/// <summary>The type this node resolves to right now.</summary>
	public ShaderType ResolvedParameterType =>
		ShaderType.TryParse( _parameterType, out var type ) && !type.IsVoid ? type : ShaderType.Float;

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		SyncType();

		var type = ResolvedParameterType;

		b.Output( "Out", type.ToString(), string.Empty );

		var components = PrismParameters.ComponentNames( type, IsColor );

		for ( int i = 0; i < components.Count; i++ )
		{
			b.Output( components[i], type.ScalarType.ToString(), components[i], order: i + 1 );
		}
	}

	/// <summary>Pull the parameter's type and colour-ness off the blackboard, when there is one.</summary>
	void SyncType()
	{
		var parameter = PrismParameters.Find( Graph, _parameter );

		if ( parameter is null ) return;

		_parameterType = parameter.Type.ToString();
		IsColor = parameter.Ui is { Control: UiControl.Color };
	}

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null ) return;

		if ( !_parameter.IsValid )
		{
			ctx.Error( "This node is not bound to a parameter", null, DiagnosticCode.UnresolvedParameter );
			return;
		}

		if ( PrismParameters.Find( ctx.Graph, _parameter ) is not null ) return;

		ctx.Error( $"The blackboard has no parameter '{_parameter}' any more",
			null, DiagnosticCode.UnresolvedParameter );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var parameter = PrismParameters.Find( ctx.Graph, _parameter );

		if ( parameter is null )
		{
			ctx.Error( _parameter.IsValid
				? $"The blackboard has no parameter '{_parameter}'"
				: "This node is not bound to a parameter" );

			ctx.Out( "Out", IrValue.Invalid );
			return;
		}

		var declaration = PrismParameters.Declare( parameter );

		if ( declaration is null )
		{
			ctx.Error( $"Parameter '{parameter.Name}' cannot be declared" );
			ctx.Out( "Out", IrValue.Invalid );
			return;
		}

		var value = ctx.Global( declaration );

		ctx.Out( "Out", value );

		if ( !value.IsValid ) return;

		var components = PrismParameters.ComponentNames( parameter.Type, IsColor );

		for ( int i = 0; i < components.Count; i++ )
		{
			ctx.Out( components[i], ctx.Swizzle( value, PrismParameters.ComponentMask( i ) ) );
		}
	}
}

/// <summary>
/// A typed literal.
/// <para>
/// In preview mode a constant becomes a live uniform rather than a baked number, so dragging its
/// value updates the frame without recompiling anything; in a final build it folds away entirely.
/// </para>
/// </summary>
[NodeInfo( Id = ConstantNode.TypeId, Title = "Constant", Category = "Parameter",
	Icon = "looks_one", Keywords = new[] { "constant", "value", "float", "vector", "colour", "color", "literal" },
	Description = "A literal of any shape: scalar, vector, colour, boolean, integer or matrix." )]
[NodeVersion( 1 )]
public sealed class ConstantNode : PrismNode
{
	/// <summary>The stable type id. Every legacy Float/Float2/Float3/Float4 node imports to this.</summary>
	public const string TypeId = "prism.parameter.constant";

	PrismConstantKind _kind = PrismConstantKind.Float;

	/// <summary>What shape the literal takes.</summary>
	public PrismConstantKind Kind
	{
		get => _kind;
		set
		{
			if ( _kind == value ) return;

			_kind = value;
			RebuildPorts();
		}
	}

	/// <summary>The value, when the kind is <see cref="PrismConstantKind.Float"/>.</summary>
	public float ScalarValue { get; set; }

	/// <summary>The value, when the kind is <see cref="PrismConstantKind.Float2"/>.</summary>
	public Vector2 Vector2Value { get; set; }

	/// <summary>The value, when the kind is <see cref="PrismConstantKind.Float3"/>.</summary>
	public Vector3 Vector3Value { get; set; }

	/// <summary>The value, when the kind is <see cref="PrismConstantKind.Float4"/>.</summary>
	public Vector4 Vector4Value { get; set; }

	/// <summary>The value, when the kind is <see cref="PrismConstantKind.Color"/>.</summary>
	public Color ColorValue { get; set; } = Color.White;

	/// <summary>The value, when the kind is <see cref="PrismConstantKind.Bool"/>.</summary>
	public bool BoolValue { get; set; }

	/// <summary>The value, when the kind is <see cref="PrismConstantKind.Int"/>.</summary>
	public int IntValue { get; set; }

	/// <summary>First row, when the kind is <see cref="PrismConstantKind.Matrix"/>.</summary>
	public Vector4 Row0 { get; set; } = new( 1f, 0f, 0f, 0f );

	/// <summary>Second row, when the kind is <see cref="PrismConstantKind.Matrix"/>.</summary>
	public Vector4 Row1 { get; set; } = new( 0f, 1f, 0f, 0f );

	/// <summary>Third row, when the kind is <see cref="PrismConstantKind.Matrix"/>.</summary>
	public Vector4 Row2 { get; set; } = new( 0f, 0f, 1f, 0f );

	/// <summary>Fourth row, when the kind is <see cref="PrismConstantKind.Matrix"/>.</summary>
	public Vector4 Row3 { get; set; } = new( 0f, 0f, 0f, 1f );

	/// <summary>The shader type this constant produces.</summary>
	public ShaderType ValueType => _kind switch
	{
		PrismConstantKind.Float2 => ShaderType.Float2,
		PrismConstantKind.Float3 => ShaderType.Float3,
		PrismConstantKind.Float4 or PrismConstantKind.Color => ShaderType.Float4,
		PrismConstantKind.Bool => ShaderType.Bool,
		PrismConstantKind.Int => ShaderType.Int,
		PrismConstantKind.Matrix => ShaderType.Float4x4,
		_ => ShaderType.Float
	};

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b )
	{
		if ( b is null ) return;

		var type = ValueType;

		b.Output( "Out", type.ToString(), string.Empty );

		var components = PrismParameters.ComponentNames( type, _kind == PrismConstantKind.Color );

		for ( int i = 0; i < components.Count; i++ )
		{
			b.Output( components[i], type.ScalarType.ToString(), components[i], order: i + 1 );
		}
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var value = _kind switch
		{
			PrismConstantKind.Float2 => ctx.Const( Vector2Value ),
			PrismConstantKind.Float3 => ctx.Const( Vector3Value ),
			PrismConstantKind.Float4 => ctx.Const( Vector4Value ),
			PrismConstantKind.Color => ctx.Const( ColorValue ),
			PrismConstantKind.Bool => ctx.Const( BoolValue ),
			PrismConstantKind.Int => ctx.Const( IntValue ),
			PrismConstantKind.Matrix => ctx.Construct( ShaderType.Float4x4,
				ctx.Const( Row0 ), ctx.Const( Row1 ), ctx.Const( Row2 ), ctx.Const( Row3 ) ),
			_ => ctx.Const( ScalarValue )
		};

		ctx.Out( "Out", value );

		if ( !value.IsValid ) return;

		var components = PrismParameters.ComponentNames( ValueType, _kind == PrismConstantKind.Color );

		for ( int i = 0; i < components.Count; i++ )
		{
			ctx.Out( components[i], ctx.Swizzle( value, PrismParameters.ComponentMask( i ) ) );
		}
	}
}

/// <summary>
/// Chooses between two branches on the state of a graph keyword.
/// <para>
/// A feature or static combo is resolved when the shader is compiled, so the branch that loses costs
/// nothing at runtime — the driver folds the whole <c>select</c> away because the condition is a
/// compile-time constant. A dynamic combo resolves per draw call instead.
/// </para>
/// </summary>
[NodeInfo( Id = KeywordBranchNode.TypeId, Title = "Keyword Branch", Category = "Parameter",
	Icon = "call_split", Keywords = new[] { "keyword", "combo", "feature", "static switch", "branch", "#if" },
	Description = "Picks one of two values based on a graph keyword. Features and static combos are " +
		"resolved at compile time." )]
[NodeVersion( 1 )]
public sealed class KeywordBranchNode : PrismNode
{
	/// <summary>The stable type id.</summary>
	public const string TypeId = "prism.parameter.keyword";

	/// <summary>
	/// Which keyword decides the branch. Typed as <see cref="ParamId"/> per addendum B4 so the
	/// clipboard can renumber it; written to disk as the bare id string.
	/// </summary>
	public ParamId Keyword { get; set; }

	/// <summary>The value used while the keyword is on.</summary>
	[In( "T", Name = "On" )] public PortRef On { get; set; }

	/// <summary>The value used while the keyword is off.</summary>
	[In( "T", Name = "Off" )] public PortRef Off { get; set; }

	/// <summary>The chosen value.</summary>
	[Out( "T", Name = "" )] public PortRef Out { get; set; }

	/// <summary>The value used when <see cref="On"/> is unconnected.</summary>
	[InlineValue( nameof( On ) )] public float DefaultOn { get; set; } = 1f;

	/// <summary>The value used when <see cref="Off"/> is unconnected.</summary>
	[InlineValue( nameof( Off ) )] public float DefaultOff { get; set; }

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null ) return;

		if ( !Keyword.IsValid )
		{
			ctx.Error( "This node is not bound to a keyword", null, DiagnosticCode.UnresolvedParameter );
			return;
		}

		if ( PrismParameters.FindKeyword( ctx.Graph, Keyword ) is not null ) return;

		ctx.Error( $"The graph has no keyword '{Keyword}' any more", null, DiagnosticCode.UnresolvedParameter );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var (on, off) = ctx.InPair( nameof( On ), nameof( Off ) );

		if ( !on.IsValid || !off.IsValid )
		{
			ctx.Out( nameof( Out ), on.IsValid ? on : off );
			return;
		}

		var keyword = PrismParameters.FindKeyword( ctx.Graph, Keyword );
		var symbol = PrismParameters.ComboSymbol( keyword );

		if ( string.IsNullOrEmpty( symbol ) )
		{
			ctx.Error( "This node is not bound to a keyword, so the Off branch was used" );
			ctx.Out( nameof( Out ), off );
			return;
		}

		// Resolve the branch with the preprocessor when the backend can. Only the taken side is then
		// compiled at all, so the other side's texture samples and loops cost nothing — as opposed to a
		// run-time select, which evaluates both and throws one away.
		if ( ctx is NodeEmitContext emit && emit.KeywordSelect( symbol, on, off ) is { IsValid: true } picked )
		{
			ctx.Out( nameof( Out ), picked );
			return;
		}

		var condition = ctx.Helper( PrismParameters.ComboTest( symbol,
			PrismParameters.SlangComboSymbol( keyword ) ) );

		if ( !condition.IsValid )
		{
			ctx.Out( nameof( Out ), off );
			return;
		}

		ctx.Out( nameof( Out ), ctx.Select( condition, on, off ) );
	}
}

/// <summary>
/// Reads a multi-valued keyword as an integer, for a combo used as a mode selector rather than a
/// toggle.
/// </summary>
[NodeInfo( Id = "prism.parameter.keywordValue", Title = "Keyword Value", Category = "Parameter",
	Icon = "list", Tier = NodeTier.Advanced,
	Keywords = new[] { "keyword", "combo", "value", "mode", "enum" },
	Description = "The integer a graph keyword currently holds. Compare it, or feed it to a switch." )]
[NodeVersion( 1 )]
public sealed class KeywordValueNode : PrismNode
{
	/// <summary>
	/// Which keyword to read. Typed as <see cref="ParamId"/> per addendum B4 so the clipboard can
	/// renumber it; written to disk as the bare id string.
	/// </summary>
	public ParamId Keyword { get; set; }

	/// <summary>The keyword's value.</summary>
	[Out( "int", Name = "Value" )] public PortRef Out { get; set; }

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null ) return;
		if ( PrismParameters.FindKeyword( ctx.Graph, Keyword ) is not null ) return;

		ctx.Error( Keyword.IsValid
			? $"The graph has no keyword '{Keyword}' any more"
			: "This node is not bound to a keyword", null, DiagnosticCode.UnresolvedParameter );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var keyword = PrismParameters.FindKeyword( ctx.Graph, Keyword );
		var symbol = PrismParameters.ComboSymbol( keyword );

		if ( string.IsNullOrEmpty( symbol ) )
		{
			ctx.Error( "This node is not bound to a keyword" );
			ctx.Out( nameof( Out ), ctx.Const( 0 ) );
			return;
		}

		ctx.Out( nameof( Out ), ctx.Helper( PrismParameters.ComboValue( symbol,
			PrismParameters.SlangComboSymbol( keyword ) ) ) );
	}
}

/// <summary>
/// An author-time switch. The branch that loses is never traversed, so nothing behind it reaches the
/// shader at all — unlike a runtime branch, which compiles both sides.
/// </summary>
[NodeInfo( Id = "prism.parameter.staticSwitch", Title = "Static Switch", Category = "Parameter",
	Icon = "toggle_on", Keywords = new[] { "static", "switch", "toggle", "variant", "compile" },
	Description = "Picks one of two inputs while authoring. Only the chosen branch is compiled." )]
[NodeVersion( 1 )]
public sealed class StaticSwitchNode : PrismNode
{
	/// <summary>Which branch wins.</summary>
	public bool Value { get; set; } = true;

	/// <summary>The value used while the switch is on.</summary>
	[In( "T", Name = "On" )] public PortRef On { get; set; }

	/// <summary>The value used while the switch is off.</summary>
	[In( "T", Name = "Off" )] public PortRef Off { get; set; }

	/// <summary>The chosen value.</summary>
	[Out( "T", Name = "" )] public PortRef Out { get; set; }

	/// <summary>The value used when <see cref="On"/> is unconnected.</summary>
	[InlineValue( nameof( On ) )] public float DefaultOn { get; set; } = 1f;

	/// <summary>The value used when <see cref="Off"/> is unconnected.</summary>
	[InlineValue( nameof( Off ) )] public float DefaultOff { get; set; }

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		ctx.Out( nameof( Out ), ctx.In( Value ? nameof( On ) : nameof( Off ) ) );
	}
}

/// <summary>
/// Declares a named module-level constant and reads it back.
/// <para>
/// This is the readable form of a <c>#define</c>: the generated shader gets
/// <c>static const float3 g_vMyName = float3( … );</c> once, and every node that reads it refers to the
/// name rather than repeating the literal. Nothing about the compiled binary changes — it exists so
/// generated code stays legible and so a magic number has one place to live.
/// </para>
/// </summary>
[NodeInfo( Id = "prism.parameter.define", Title = "Shader Constant", Category = "Parameter",
	Icon = "code", Tier = NodeTier.Advanced,
	Keywords = new[] { "define", "constant", "static const", "macro", "named" },
	Description = "A named module-level constant. Emitted once, referenced by name." )]
[NodeVersion( 1 )]
public sealed class ShaderConstantNode : PrismNode
{
	PrismConstantKind _kind = PrismConstantKind.Float;

	/// <summary>The constant's name, before the engine's hungarian prefix is applied.</summary>
	public string Name { get; set; } = "Constant";

	/// <summary>What shape the constant takes. Matrices are not supported here.</summary>
	public PrismConstantKind Kind
	{
		get => _kind;
		set
		{
			var kind = value == PrismConstantKind.Matrix ? PrismConstantKind.Float4 : value;

			if ( _kind == kind ) return;

			_kind = kind;
			RebuildPorts();
		}
	}

	/// <summary>The value, packed into four components. Unused components are ignored.</summary>
	public Vector4 Value { get; set; }

	/// <summary>The shader type this constant declares.</summary>
	public ShaderType ValueType => _kind switch
	{
		PrismConstantKind.Float2 => ShaderType.Float2,
		PrismConstantKind.Float3 => ShaderType.Float3,
		PrismConstantKind.Float4 or PrismConstantKind.Color => ShaderType.Float4,
		PrismConstantKind.Bool => ShaderType.Bool,
		PrismConstantKind.Int => ShaderType.Int,
		_ => ShaderType.Float
	};

	/// <summary>The symbol the constant is declared under.</summary>
	public string Symbol => GraphCompiler.SymbolPrefix( ValueType ) + Parameter.Sanitize( Name );

	/// <inheritdoc/>
	protected override void OnDefinePorts( PortBuilder b ) =>
		b?.Output( "Out", ValueType.ToString(), string.Empty );

	/// <inheritdoc/>
	public override void OnValidate( ValidationContext ctx )
	{
		if ( ctx is null || !string.IsNullOrWhiteSpace( Name ) ) return;

		ctx.Error( "A shader constant needs a name", null, DiagnosticCode.GlobalCollision );
	}

	/// <inheritdoc/>
	public override void Emit( EmitContext ctx )
	{
		if ( ctx is null ) return;

		var type = ValueType;

		var declaration = new GlobalDecl( Symbol, type, GlobalKind.Constant )
		{
			Default = ConstValue.From( Value )
		};

		ctx.Out( "Out", ctx.Global( declaration ) );
	}
}