Editor/ShaderGraphPlus/Compiler/GraphCompiler.cs
using Editor;
using Microsoft.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text;
using ShaderGraphPlus.Nodes;

namespace ShaderGraphPlus;

public sealed partial class GraphCompiler
{
	public struct GraphIssue
	{
		public BaseNodePlus Node;
		public string Message;
		public bool IsWarning;
	}

	/// <summary>
	/// Current graph we're compiling
	/// </summary>
	public ShaderGraphPlus Graph { get; private set; }

	/// <summary>
	/// Current SubGraph
	/// </summary>
	private ShaderGraphPlus Subgraph = null;

	/// <summary>
	/// Current SubGraph node that we are in
	/// </summary>
	private SubgraphNode SubgraphNode = null;

	/// <summary>
	/// The loaded sub-graphs
	/// </summary>
	public List<ShaderGraphPlus> Subgraphs { get; private set; }

	private List<(SubgraphNode, ShaderGraphPlus)> SubgraphStack = new();

	public Dictionary<string, string> CompiledTextures { get; private set; } = new();

	/// <summary>
	/// Pixel Shader Stage Includes
	/// </summary>
	public HashSet<string> PixelIncludes { get; private set; } = new();

	/// <summary>
	/// Vertex Shader Stage Includes
	/// </summary>
	public HashSet<string> VertexIncludes { get; private set; } = new();

	public Dictionary<string, string> PreviewImages = new();

	/// <summary>
	/// Vertex Shader stage inputs
	/// </summary>
	public Dictionary<string, string> VertexInputs { get; private set; } = new();

	/// <summary>
	/// Pixel Shader stage inputs
	/// </summary>
	public Dictionary<string, string> PixelInputs { get; private set; } = new();

	public Dictionary<string, string> ShaderDefines { get; private set; } = new();

	public int VoidLocalCount { get; set; } = 0;

	/// <summary>
	/// Is this compile for just the preview or not, preview uses attributes for constant values
	/// </summary>
	public bool IsPreview { get; private set; }

	/// <summary>
	/// Is this compile for the final generated result
	/// </summary>
	public bool IsNotPreview => !IsPreview;

	private partial class CompileResult
	{
		public List<(NodeResult localResult, NodeResult funcResult)> Results = new();
		public Dictionary<NodeInput, NodeResult> InputResults = new();

		public Dictionary<string, Sampler> SamplerStates = new();
		public Dictionary<string, TextureInput> TextureInputs = new();
		public Dictionary<string, Gradient> Gradients = new();
		public Dictionary<string, (string Options, NodeResult Result)> Parameters = new();
		public Dictionary<string, string> Globals { get; private set; } = new();
		public Dictionary<string, object> Attributes { get; private set; } = new();
		public HashSet<string> Functions { get; private set; } = new();

		public string RepresentativeTexture { get; set; }

		internal void Replace( Dictionary<string, string> globals, Dictionary<string, object> attributes, HashSet<string> functions )
		{
			Globals = globals;
			Attributes = attributes;
			Functions = functions;
		}
	}

	public enum ShaderStage
	{
		Vertex,
		Pixel,
	}

	/// <summary>
	/// Current shader stage being evaluated.
	/// </summary>
	public ShaderStage Stage { get; private set; }

	public bool IsVs => Stage == ShaderStage.Vertex;
	public bool IsPs => Stage == ShaderStage.Pixel;
	private string StageName => Stage == ShaderStage.Vertex ? "vs" : "ps";
	private string CurrentResultInput;

	private CompileResult VertexResult { get; set; } = new();
	private CompileResult PixelResult { get; set; } = new();
	private CompileResult ShaderResult => Stage == ShaderStage.Vertex ? VertexResult : PixelResult;

	public Action<string, object> OnAttribute { get; set; }

	// Init to 1, 0 is reserved.
	public int PreviewID { get; internal set; } = 1;

	private List<NodeInput> InputStack = new();

	private List<int> VoidDataHashes = new();

	private readonly Dictionary<BaseNodePlus, List<string>> NodeErrors = new();
	private readonly Dictionary<BaseNodePlus, List<string>> NodeWarnings = new();

	/// <summary>
	/// Error list.
	/// </summary>
	public IEnumerable<GraphIssue> Errors => NodeErrors
		.Select( x => new GraphIssue { Node = x.Key, Message = x.Value.FirstOrDefault(), IsWarning = false } );

	/// <summary>
	/// Warning list.
	/// </summary>
	public IEnumerable<GraphIssue> Warnings => NodeWarnings
		.Select( x => new GraphIssue { Node = x.Key, Message = x.Value.FirstOrDefault(), IsWarning = true } );

	public GraphCompiler( ShaderGraphPlus graph, Dictionary<string, ShaderFeatureBase> shaderFeatures, bool preview )
	{
		Graph = graph;
		IsPreview = preview;
		Stage = ShaderStage.Pixel;
		Subgraphs = new();
		AddSubgraphs( Graph );
		ShaderFeatures = shaderFeatures;

		// Set the Initial Vertex and Pixel stage inputs from ShaderTemplate.
		VertexInputs = ShaderTemplate.VertexInputs;
		PixelInputs = ShaderTemplate.PixelInputs;
	}

	public bool TryGetPreviewImage( string name, out string imagePath )
	{
		return PreviewImages.TryGetValue( name, out imagePath );
	}

	public void SetAttribute<T>( string name, T value )
	{
		// Dont even try to set an attribute of the type if it
		// isnt able to be set in the first place.
		if ( !ShaderAttributeTypes.Contains( value.GetType() ) )
		{
			return;
		}

		if ( !ShaderResult.Attributes.ContainsKey( name ) )
		{
			OnAttribute?.Invoke( name, value );
			ShaderResult.Attributes[name] = value;
		}
	}

	/// <summary>
	/// Generates and compiles a texture and returns the path to it.
	/// </summary>
	public string CompileTexture( TextureInput textureInput )
	{
		if ( string.IsNullOrWhiteSpace( textureInput.DefaultTexture ) )
			return "";

		var resourceText = string.Format( ShaderTemplate.TextureDefinition,
			textureInput.DefaultTexture,
			textureInput.ColorSpace,
			textureInput.ImageFormat,
			textureInput.Processor
		);

		var assetPath = $"shadergraphplus/textures/{textureInput.DefaultTexture.Replace( ".", "_" )}_shadergraphplus.generated.vtex";

		var resourcePath = Editor.FileSystem.Root.GetFullPath( "/.source2/temp" );
		resourcePath = System.IO.Path.Combine( resourcePath, assetPath );

		if ( AssetSystem.CompileResource( resourcePath, resourceText ) )
		{
			return assetPath;
		}
		else
		{
			SGPLogger.Warning( $"Failed to compile {textureInput.DefaultTexture}" );
			return "";
		}
	}

	private void AddSubgraphs( ShaderGraphPlus graph )
	{
		if ( graph != Graph )
		{
			if ( Subgraphs.Contains( graph ) )
				return;

			Subgraphs.Add( graph );
		}

		foreach ( var node in graph.Nodes )
		{
			if ( node is SubgraphNode subgraphNode )
			{
				AddSubgraphs( subgraphNode.Subgraph );
			}
		}
	}

	public void RegisterVertexInput( string type, string name, string semantic )
	{
		name = CleanName( name );

		if ( ShaderTemplate.InternalVertexInputs.ContainsKey( name ) )
		{
			SGPLogger.Error( $"VetexInput \"{name}\" is reserved by the GraphCompiler" );

			return;
		}

		var vertexInput = $"{type} {name} : {semantic};";

		if ( !VertexInputs.TryAdd( name, vertexInput ) )
		{
			SGPLogger.Warning( $"VertexInputs already contains key \"{name}\"", IsNotPreview );
		}
	}

	public void RegisterPixelInput( string type, string name, string semantic )
	{
		name = CleanName( name );

		if ( ShaderTemplate.InternalPixelInputs.ContainsKey( name ) )
		{
			SGPLogger.Error( $"PixelInput \"{name}\" is reserved by the GraphCompiler" );

			return;
		}

		var pixelInput = $"{type} {name} : {semantic};";

		if ( !PixelInputs.TryAdd( name, pixelInput ) )
		{
			SGPLogger.Warning( $"PixelInputs already contains key \"{name}\"", IsNotPreview );
		}
	}

	internal string RegisterCustomCode( string nodeID, string functionName, string functionInputs, Dictionary<string, string> outputResults, CustomCodeNodeMode mode )
	{
		if ( !ShaderResult.VoidFunctionLocals.ContainsKey( nodeID ) )
		{
			var outputData = new List<VoidFunctionResult>();
			var functionOutputsSb = new StringBuilder();

			foreach ( var output in outputResults.Index() )
			{
				var userAssignedname = output.Item.Key;
				var id = VoidLocalCount++;
				var compilerName = $"ol_{id}";

				functionOutputsSb.Append( (output.Index + 1) == outputResults.Count ? $"{compilerName}" : $" {compilerName}, " );

				VoidFunctionResult data = new(
					userAssignedname,
					compilerName,
					GetResultTypeFromHLSLDataType( output.Item.Value )
				);

				outputData.Add( data );
			}

			// Assemble the function call
			var funcCall = "";
			if ( mode == CustomCodeNodeMode.Generate )
			{
				funcCall = ResultHLSLFunction( functionName, $"{functionInputs}, {functionOutputsSb}" );
			}
			else if ( mode == CustomCodeNodeMode.File )
			{
				funcCall = $"{functionName}( {string.Join( ", ", $"{functionInputs}, {functionOutputsSb}" )} )";
			}
			else
			{
				throw new NotImplementedException( $"Unknown CustomFunction mode '{mode}'" );
			}

			var voidData = new VoidFunctionInfo(
				outputData,
				funcCall,
				SubgraphNode == null ? nodeID : SubgraphNode.Identifier,
				mode == CustomCodeNodeMode.File
			);

			ShaderResult.VoidFunctionLocals.Add( nodeID, voidData );

			return funcCall;
		}

		return ShaderResult.VoidFunctionLocals[nodeID].FunctionCall;
	}

	/// <summary>
	/// Register a hlsl shader include.
	/// </summary>
	/// <param name="path"></param>
	public void RegisterInclude( string path )
	{
		var list = IsVs ? VertexIncludes : PixelIncludes;

		if ( list.Contains( path ) )
			return;

		list.Add( path );
	}

	/// <summary>
	/// Register a hlsl define directive and then return the name of the define converted to all uppercase.
	/// </summary>
	public string RegisterDefine( string name, string value )
	{
		name = CleanName( name ).ToUpper();

		if ( ShaderDefines.ContainsKey( name ) )
			return name;

		ShaderDefines.Add( name, value );

		return name;
	}

	/// <summary>
	/// Register some generic global parameter for a node to use.
	/// </summary>
	public void RegisterGlobal( string name, string global )
	{
		var result = ShaderResult;
		if ( result.Globals.ContainsKey( name ) )
			return;

		result.Globals.Add( name, global );
	}

	/// <summary>
	/// Emit a scalar <c>float</c> expression. If <paramref name="r"/> is float2/float3/float4, uses swizzle
	/// so wiring a vector into a <c>float</c> HLSL parameter does not rely on implicit truncation (-Wconversion).
	/// </summary>
	public static string EmitScalarFloat( NodeResult r, float fallback )
	{
		if ( !r.IsValid || !r.IsFloatTypeResult() )
			return $"{fallback}";
		return r.Components > 1 ? r.Cast( 1 ) : r.Code;
	}

	public string ResultHLSLFunction( string name, params string[] args )
	{
		if ( !GraphHLSLFunctions.HasFunction( name ) )
			return null;

		var result = ShaderResult;
		if ( !result.Functions.Contains( name ) )
			result.Functions.Add( name );

		return $"{name}( {string.Join( ", ", args )} )";
	}

	public string RegisterHLSLFunction( string code, [CallerArgumentExpression( nameof( code ) )] string functionName = "", bool overrideFunction = false )
	{
		if ( !GraphHLSLFunctions.HasFunction( functionName ) || overrideFunction )
		{
			GraphHLSLFunctions.RegisterFunction( functionName, code, overrideFunction );
		}

		return functionName;
	}

	/// <summary>
	/// Loops through ShaderResult.Gradients to find the matching key then returns the corresponding Gradient.
	/// </summary>
	public Gradient GetGradient( string gradientName )
	{
		var result = ShaderResult;

		Gradient searchResult = new();

		foreach ( var gradient in result.Gradients )
		{
			if ( gradient.Key == gradientName )
			{
				searchResult = gradient.Value;
			}
		}

		return searchResult;
	}

	/// <summary>
	/// Register a gradient and return the name of the graident. A generic name is returned instead if the gradient name is empty.
	/// </summary>
	public string RegisterGradient( Gradient gradient, string gradientName )
	{
		var result = ShaderResult;

		var name = !string.IsNullOrWhiteSpace( gradientName ) ? CleanName( gradientName ) : $"Gradient_{result.Gradients.Count}";

		if ( !result.Gradients.ContainsKey( name ) )
		{
			result.Gradients.Add( name, gradient );
		}

		return name;
	}

	/// <summary>
	/// Register a sampler and return the name of it
	/// </summary>
	public string ResultSampler( string name, Sampler sampler )
	{
		name = !string.IsNullOrWhiteSpace( name ) ? CleanName( name ) : $"Sampler_{sampler.GetHashCode():X8}";

		if ( IsPreview )
		{
			return ResultValue( sampler, $"g_s{name}", previewNameOverride: true ).Code;
		}
		else
		{
			if ( !ShaderResult.SamplerStates.ContainsKey( name ) )
			{
				ShaderResult.SamplerStates.Add( name, sampler );
			}

			return $"g_s{name}";
		}
	}

	/// <summary>
	/// Register a texture and return the global name of it
	/// </summary>
	public string ResultTexture( TextureInput input, Texture texture, bool storePreviewImage = false )
	{
		var name = CleanName( input.Name );
		var result = ShaderResult;
		var globalName = "";

		name = string.IsNullOrWhiteSpace( name ) ? $"Texture_{StageName}_{ShaderResult.TextureInputs.Count}" : name;

		if ( !result.TextureInputs.ContainsKey( name ) )
			result.TextureInputs.Add( name, input );

		if ( storePreviewImage )
		{
			if ( !PreviewImages.ContainsKey( $"g_t{name}" ) )
			{
				PreviewImages.Add( $"g_t{name}", input.DefaultTexture );
			}
		}

		if ( texture != null )
			SetAttribute( name, texture );

		globalName = $"g_t{name}";

		if ( CurrentResultInput == "Albedo" && !input.IsAttribute )
		{
			result.RepresentativeTexture = globalName;
		}

		return globalName;
	}

	/// <summary>
	/// Get result of a sampler input with an optional default sampelr value if it failed to resolve
	/// </summary>
	public string ResultSamplerOrDefault( NodeInput samplerInput, Sampler defaultSampler )
	{
		var resultSampler = Result( samplerInput );
		return resultSampler.IsValid && resultSampler.ResultType == ResultType.Sampler ? resultSampler.Code : ResultSampler( $"Sampler_{defaultSampler.GetHashCode():X8}", defaultSampler );
	}

	/// <summary>
	/// Get result of an input with an optional default value if it failed to resolve
	/// </summary>
	public NodeResult ResultOrDefault<T>( NodeInput input, T defaultValue )
	{
		var result = Result( input );
		return result.IsValid ? result : ResultValue( defaultValue );
	}

	/// <summary>
	/// Get result of an named reroute
	/// </summary>
	internal NodeResult ResultNamedReroute( string name )
	{
		var node = Graph.FindNamedRerouteDeclarationNode( name );

		if ( node != null )
		{
			var result = node.Result.Invoke( this );

			return result;
		}

		throw new Exception( $"Could not find named reroute with name \"{name}\"" );
	}

	/// <summary>
	/// Get result of an input
	/// </summary>
	public NodeResult Result( NodeInput input )
	{
		void InvalidNodeResult( ref BaseNodePlus node, NodeResult funcResult )
		{
			if ( funcResult.IsValid )
				return;

			if ( !NodeErrors.TryGetValue( node, out var errors ) )
			{
				errors = new();
				NodeErrors.Add( node, errors );
			}

			if ( funcResult.Errors is null || funcResult.Errors.Length == 0 )
			{
				errors.Add( $"Missing input" );
			}
			else
			{
				node.HasError = true;
				node.ErrorMessage = funcResult.Errors.FirstOrDefault();

				foreach ( var error in funcResult.Errors )
					errors.Add( error );
			}
		}

		void NodeError( ref BaseNodePlus node, string error )
		{
			node.HasError = true;
			node.ErrorMessage = error;

			NodeErrors[node] = [node.ErrorMessage];
		}

		if ( !input.IsValid )
			return default;

		BaseNodePlus node = null;
		if ( string.IsNullOrEmpty( input.Subgraph ) )
		{
			if ( Subgraph is not null )
			{
				var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) );

				return Result( new()
				{
					Identifier = input.Identifier,
					Output = input.Output,
					Subgraph = Subgraph.Path,
					SubgraphNode = nodeId
				} );
			}

			node = Graph.FindNode( input.Identifier );
		}
		else
		{
			var subgraph = Subgraphs.FirstOrDefault( x => x.Path == input.Subgraph );
			if ( subgraph is not null )
			{
				node = subgraph.FindNode( input.Identifier );
			}
		}

		if ( ShaderResult.InputResults.TryGetValue( input, out var result ) )
		{
			return result;
		}

		if ( node == null )
		{
			return default;
		}

		var nodeType = node.GetType();
		var property = nodeType.GetProperty( input.Output );

		if ( property == null )
		{
			// Search for alias
			var allProperties = nodeType.GetProperties();
			foreach ( var prop in allProperties )
			{
				var alias = prop.GetCustomAttribute<AliasAttribute>();
				if ( alias is null ) continue;
				foreach ( var al in alias.Value )
				{
					if ( al == input.Output )
					{
						property = prop;
						break;
					}
				}
				if ( property != null )
					break;
			}
		}

		object value = null;

		if ( node is not IRerouteNode && node is not CustomFunctionNode && InputStack.Contains( input ) )
		{
			NodeErrors[node] = new List<string> { "Circular reference detected" };
			return default;
		}

		InputStack.Add( input );

		if ( Subgraph is not null && node.Graph != Subgraph )
		{
			if ( node.Graph != Graph )
			{
				Subgraph = node.Graph as ShaderGraphPlus;
			}
			else
			{
				Subgraph = null;
			}
		}

		if ( node is CustomFunctionNode customFunctionNode )
		{
			node.ClearError();

			if ( customFunctionNode.Mode == CustomCodeNodeMode.Generate || customFunctionNode.Mode == CustomCodeNodeMode.File )
			{
				var funcResult = customFunctionNode.GetResult( this );

				if ( !funcResult.IsValid )
				{
					InvalidNodeResult( ref node, funcResult );

					InputStack.Remove( input );
					return default;
				}

				if ( ShaderResult.VoidFunctionLocals.TryGetValue( node.Identifier, out VoidFunctionInfo data ) ) //&& !data.IsAlreadyPostProcessed )
				{
					funcResult.SetVoidLocalTargetID( node.Identifier );

					var userAssignedVariableName = input.Output;
					var compilerAssignedVariableName = data.GetCompilerAssignedName( userAssignedVariableName );

					var resultType = data.GetResultResultType( compilerAssignedVariableName );
					var localResult = new NodeResult( resultType, $"{compilerAssignedVariableName}", false, funcResult.Metadata );
					localResult.SetVoidLocalTargetID( funcResult.VoidLocalTargetID );

					if ( Subgraph != null )
					{
						if ( data.IsAlreadyPostProcessed )
						{
							InputStack.Remove( input );
							return localResult;
						}
					}
					else
					{
						// return the localResult if we are getting a result from a node that we have already evaluated. 
						foreach ( var inputResult in ShaderResult.InputResults )
						{
							if ( inputResult.Key.Identifier == input.Identifier )
							{
								InputStack.Remove( input );
								return localResult;
							}
						}
					}

					ShaderResult.InputResults.Add( input, localResult );
					ShaderResult.Results.Add( (localResult, funcResult) );
					ShaderResult.VoidFunctionLocals[node.Identifier] = data with { IsAlreadyPostProcessed = true };

					InputStack.Remove( input );

					return localResult;
				}


				SGPLogger.Error( "Failed to get localResult" );
			}
			else
			{
				throw new NotImplementedException( $"Unknown mode '{customFunctionNode.Mode}'" );
			}

		}

		if ( node is SubgraphNode subgraphNode )
		{
			var newStack = (subgraphNode, Subgraph);
			var lastNode = SubgraphNode;

			SubgraphStack.Add( newStack );
			Subgraph = subgraphNode.Subgraph;
			SubgraphNode = subgraphNode;

			if ( !Subgraphs.Contains( Subgraph ) )
			{
				Subgraphs.Add( Subgraph );
			}

			var resultNode = Subgraph.Nodes.OfType<SubgraphOutput>().Where( x => x.OutputName == input.Output ).FirstOrDefault();
			var resultInput = resultNode.Inputs.FirstOrDefault( x => x.Identifier == input.Output );
			if ( resultInput?.ConnectedOutput is not null )
			{
				var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) );
				var newConnection = new NodeInput()
				{
					Identifier = resultInput.ConnectedOutput.Node.Identifier,
					Output = resultInput.ConnectedOutput.Identifier,
					Subgraph = Subgraph.Path,
					SubgraphNode = nodeId
				};
				var newResult = Result( newConnection );

				if ( NodeErrors.Any() )
				{
					InputStack.Remove( input );
					return default;
				}

				InputStack.Remove( input );
				SubgraphStack.RemoveAt( SubgraphStack.Count - 1 );
				Subgraph = newStack.Item2;
				SubgraphNode = lastNode;

				return newResult;
			}
			else
			{
				//value = GetDefaultValue( subgraphNode, input.Output, resultInput.Type );

				switch ( resultNode.PortType )
				{
					case SubgraphPortType.Bool:
						value = false;
						break;
					case SubgraphPortType.Int:
						value = 1;
						break;
					case SubgraphPortType.Float:
						value = 1;
						break;
					case SubgraphPortType.Vector2:
						value = Vector2.One;
						break;
					case SubgraphPortType.Vector3:
						value = Vector3.One;
						break;
					case SubgraphPortType.Vector4:
						value = Vector4.One;
						break;
					case SubgraphPortType.Color:
						value = Color.White;
						break;
				}

				if ( value == null )
				{
					NodeError( ref node, $"Missing internal input \'{resultInput.DisplayInfo.Name}\' in node \'{Subgraph.Path}\'" );

					SGPLogger.Error( subgraphNode.ErrorMessage );

					return default;
				}
				else
				{
					SGPLogger.Error( $"Missing Internal Input \'{resultInput.DisplayInfo.Name}\' in node \'{Subgraph.Path}\' falling back to default value \'{value}\'" );
				}

				SubgraphStack.RemoveAt( SubgraphStack.Count - 1 );
				Subgraph = newStack.Item2;
				SubgraphNode = lastNode;
			}
		}
		else
		{
			if ( Subgraph is not null )
			{

				if ( node is SubgraphInput subgraphInput && !string.IsNullOrWhiteSpace( subgraphInput.Name ) )
				{
					var newResult = ResolveSubgraphInput( subgraphInput, ref value, out var error );

					if ( !string.IsNullOrWhiteSpace( error.ErrorString ) )
					{
						NodeErrors.Add( error.Node, new List<string> { error.ErrorString } );

						InputStack.Remove( input );
						return default;
					}

					if ( newResult.IsValid )
					{
						InputStack.Remove( input );
						return newResult;
					}
				}
			}
			else if ( Graph.IsSubgraph )
			{
				if ( node is SubgraphInput subgraphInput )
				{
					if ( subgraphInput.PreviewInput.IsValid )
					{
						var subgraphInputResult = Result( subgraphInput.PreviewInput );

						InputStack.Remove( input );
						return subgraphInputResult;
					}
				}
			}

			if ( value is null )
			{
				if ( property == null )
				{
					InputStack.Remove( input );
					return default;
				}

				value = property.GetValue( node );
			}

			if ( value == null )
			{
				InputStack.Remove( input );
				return default;
			}
		}

		if ( value is NodeResult nodeResult )
		{
			InputStack.Remove( input );
			return nodeResult;
		}
		else if ( value is NodeInput nodeInput )
		{
			if ( nodeInput == input )
			{
				InputStack.Remove( input );
				return default;
			}

			var newResult = Result( nodeInput );

			InputStack.Remove( input );
			return newResult;
		}
		else if ( value is NodeResult.Func resultFunc )
		{
			var funcResult = resultFunc.Invoke( this );

			if ( !funcResult.IsValid )
			{
				InvalidNodeResult( ref node, funcResult );

				InputStack.Remove( input );
				return default;
			}

			funcResult.SetVoidLocalTargetID( node.Identifier );

			if ( IsPreview )
			{
				funcResult.SetPreviewID( node.PreviewID );
				funcResult.ShouldPreview = node.CanPreview;
			}
			else
			{
				funcResult.ShouldPreview = false;
			}

			//if ( subgraphResult )
			//{
			//	funcResult.SetPreviewID( SubgraphNode.PreviewID );
			//}

			// We can return this result without making it a local variable because it's constant
			if ( funcResult.Constant )
			{
				InputStack.Remove( input );

				return funcResult;
			}

			int id = ShaderResult.InputResults.Count;
			var varName = $"l_{id}";
			var localResult = new NodeResult( funcResult.ResultType, varName, false, funcResult.Metadata );
			localResult.SetPreviewID( funcResult.PreviewID );
			localResult.SetVoidLocalTargetID( funcResult.VoidLocalTargetID );
			localResult.ShouldPreview = funcResult.ShouldPreview;

			if ( !ShaderResult.InputResults.ContainsKey( input ) )
			{
				ShaderResult.InputResults.Add( input, localResult );
			}

			ShaderResult.Results.Add( (localResult, funcResult) );

			InputStack.Remove( input );

			return localResult;
		}

		var resultVal = ResultValue( value );

		InputStack.Remove( input );

		return resultVal;
	}

	/// <summary>
	/// Get result of two inputs and cast to the largest component of the two (a float2 and float3 will both become float3 results)
	/// </summary>
	public (NodeResult, NodeResult) Result( NodeInput a, NodeInput b, float defaultA = 0.0f, float defaultB = 1.0f )
	{
		var resultA = ResultOrDefault( a, defaultA );
		var resultB = ResultOrDefault( b, defaultB );

		if ( resultA.Components == resultB.Components )
			return (resultA, resultB);

		if ( resultA.Components < resultB.Components )
			return (new( resultB.ResultType, resultA.Cast( resultB.Components ) ), resultB);

		return (resultA, new( resultA.ResultType, resultB.Cast( resultA.Components ) ));
	}

	private static object GetDefaultValue( SubgraphNode node, string name, Type type )
	{
		if ( !node.DefaultValues.TryGetValue( name, out var value ) )
		{
			switch ( type )
			{
				case Type t when t == typeof( bool ):
					return false;
				case Type t when t == typeof( int ):
					return 1;
				case Type t when t == typeof( float ):
					return 1.0f;
				case Type t when t == typeof( Vector2 ):
					return Vector2.One;
				case Type t when t == typeof( Vector3 ):
					return Vector3.One;
				case Type t when t == typeof( Vector4 ):
					return Vector4.One;
				case Type t when t == typeof( Color ):
					return Color.White;
				case Type t when t == typeof( Float2x2 ):
					return new Float2x2();
				case Type t when t == typeof( Float3x3 ):
					return new Float3x3();
				case Type t when t == typeof( Float4x4 ):
					return new Float4x4();
				case Type t when t == typeof( Sampler ):
					return new Sampler();
				case Type t when t == typeof( Texture ):
					var inputType = node.InputReferences.FirstOrDefault( x => x.Value.inputNode.Name == name ).Value.inputNode.PortType;

					return new TextureInput() { Type = (inputType == SubgraphPortType.Texture2DObject ? TextureType.Tex2D : TextureType.TexCube) };
			}

		}

		if ( value is JsonElement el )
		{
			if ( type == typeof( bool ) )
			{
				value = el.GetBoolean();
			}
			else if ( type == typeof( int ) )
			{
				value = el.GetInt32();
			}
			else if ( type == typeof( float ) )
			{
				value = el.GetSingle();
			}
			else if ( type == typeof( Vector2 ) )
			{
				value = Vector2.Parse( el.GetString() );
			}
			else if ( type == typeof( Vector3 ) )
			{
				value = Vector3.Parse( el.GetString() );
			}
			else if ( type == typeof( Vector4 ) )
			{
				value = Vector4.Parse( el.GetString() );
			}
			else if ( type == typeof( Color ) )
			{
				value = Color.Parse( el.GetString() ) ?? Color.White;
			}
			else if ( type == typeof( Float2x2 ) )
			{
				value = JsonSerializer.Deserialize<Float2x2>( el, ShaderGraphPlus.SerializerOptions() );
			}
			else if ( type == typeof( Float3x3 ) )
			{
				value = JsonSerializer.Deserialize<Float3x3>( el, ShaderGraphPlus.SerializerOptions() );
			}
			else if ( type == typeof( Float4x4 ) )
			{
				value = JsonSerializer.Deserialize<Float4x4>( el, ShaderGraphPlus.SerializerOptions() );
			}
			else if ( type == typeof( Sampler ) )
			{
				value = JsonSerializer.Deserialize<Sampler>( el, ShaderGraphPlus.SerializerOptions() );
			}
			else if ( type == typeof( Texture ) )
			{
				var inputType = node.InputReferences.FirstOrDefault( x => x.Value.inputNode.Name == name ).Value.inputNode.PortType;
				var textureInput = JsonSerializer.Deserialize<TextureInput>( el, ShaderGraphPlus.SerializerOptions() );

				value = textureInput with { Type = (inputType == SubgraphPortType.Texture2DObject ? TextureType.Tex2D : TextureType.TexCube) };
			}
		}

		return value;
	}

	/// <summary>
	/// Get result of a value that can be set in material editor
	/// </summary>
	public NodeResult ResultParameter<T>( T parameter ) where T : IBlackboardParameter
	{
		var name = parameter.Name;
		var value = parameter.GetValue();
		object min = default;
		object max = default;
		var isRange = false;
		var isAttribute = false;
		IParameterUI parameterUI = default;

		if ( parameter is IBlackboardMaterialParameter materialParameter )
		{
			isAttribute = materialParameter.IsAttribute;
			parameterUI = materialParameter.GetParameterUI();
		}

		if ( parameter is IRangedBlackboardMaterialParameter rangedParameter )
		{
			min = rangedParameter.GetRangeMin();
			max = rangedParameter.GetRangeMax();
			isRange = min != max;
		}

		return ResultParameter( name, value, min, max, isRange, isAttribute, parameterUI );
	}

	/// <summary>
	/// Get result of a value that can be set in material editor
	/// </summary>
	public NodeResult ResultParameter<T>( string name, T value, T min = default, T max = default, bool isRange = false, bool isAttribute = false, IParameterUI ui = default )
	{
		if ( IsPreview || string.IsNullOrWhiteSpace( name ) || Subgraph is not null )
			return ResultValue( value );

		var attribName = name;
		name = CleanName( name );

		var prefix = value switch
		{
			bool _ => "g_b",
			int _ => "g_n",
			float _ => "g_fl",
			Vector2 _ => "g_v",
			Vector2Int _ => "g_v",
			Vector3 _ => "g_v",
			Vector3Int _ => "g_v",
			Vector4 _ => "g_v",
			Color _ => "g_v",
			Float2x2 _ => "g_m",
			Float3x3 _ => "g_m",
			Float4x4 _ => "g_m",
			Texture _ => "g_t",
			Sampler _ => "g_s",
			_ => throw new Exception( $"Unknown type \"{value.GetType()}\"" )
		};

		// Make sure the type T is can have a Default();
		bool canHaveDefualt = typeof( T ) switch
		{
			Type t when t == typeof( Float2x2 ) => false,
			Type t when t == typeof( Float3x3 ) => false,
			Type t when t == typeof( Float4x4 ) => false,
			_ => true,
		};

		if ( !name.StartsWith( prefix ) )
			name = prefix + name;

		if ( ShaderResult.Parameters.TryGetValue( name, out var parameter ) )
			return parameter.Result;

		parameter.Result = ResultValue( value, name );

		var options = new StringWriter();

		// If we're an attribute, we don't care about the UI options
		if ( isAttribute )
		{
			options.Write( $"Attribute( \"{attribName}\" ); " );

			if ( value is bool boolValue )
			{
				options.Write( $"Default( {(boolValue ? 1 : 0)} ); " );
			}
			else if ( canHaveDefualt )
			{
				options.Write( $"Default{parameter.Result.Components}( {value} );" );
			}
		}
		else if ( MaterialParameterTypes.Contains( value.GetType() ) )
		{
			if ( ui is FloatParameterUI floatParameterUI )
			{
				if ( floatParameterUI.Type != UIType.Default )
				{
					options.Write( $"UiType( {floatParameterUI.Type} ); " );
				}

				if ( floatParameterUI.Step > 0.0f )
				{
					options.Write( $"UiStep( {floatParameterUI.Step} ); " );
				}
			}
			else if ( value is int )
			{
				options.Write( $"UiType( Slider ); " );

			}
			else if ( value is bool )
			{
				options.Write( $"UiType( CheckBox ); " );
			}
			else if ( value is Color )
			{
				options.Write( $"UiType( Color ); " );
			}

			options.Write( $"UiGroup( \"{ui.UIGroup}\" ); " );

			if ( value is bool boolValue )
			{
				options.Write( $"Default( {(boolValue ? 1 : 0)} ); " );
			}
			else if ( canHaveDefualt )
			{
				options.Write( $"Default{parameter.Result.Components}( {value} ); " );
			}

			if ( value is not bool && parameter.Result.Components > 0 && isRange )
			{
				options.Write( $"Range{parameter.Result.Components}( {min}, {max} ); " );
			}
		}

		parameter.Options = options.ToString().Trim();

		// Avoid adding types that arnt exposed to the material editor.
		if ( MaterialParameterTypes.Contains( value.GetType() ) )
		{
			ShaderResult.Parameters.Add( name, parameter );
		}

		return parameter.Result;
	}

	/// <summary>
	/// Get result of a value, in preview mode an attribute will be registered and returned
	/// </summary>
	public NodeResult ResultValue<T>( T value, string name = null, bool previewOverride = false, bool previewNameOverride = false )
	{
		if ( value is NodeInput nodeInput ) return Result( nodeInput );

		bool isConstant = IsPreview && !previewOverride;
		bool isNamed = isConstant || !string.IsNullOrWhiteSpace( name );
		name = isConstant && !previewNameOverride ? $"g_{StageName}_{ShaderResult.Attributes.Count}" : name;

		if ( isConstant )
		{
			SetAttribute( name, value );
		}

		return value switch
		{
			bool v => isNamed ? new NodeResult( ResultType.Bool, $"{name}" ) : new NodeResult( ResultType.Bool, $"{v.ToString().ToLower()}" ) { },
			int v => isNamed ? new NodeResult( ResultType.Int, $"{name}" ) : new NodeResult( ResultType.Int, $"{v}", true ),
			float v => isNamed ? new NodeResult( ResultType.Float, $"{name}" ) : new NodeResult( ResultType.Float, $"{v}", true ),
			Vector2 v => isNamed ? new NodeResult( ResultType.Vector2, $"{name}" ) : new NodeResult( ResultType.Vector2, $"float2( {v.x}, {v.y} )" ),
			Vector3 v => isNamed ? new NodeResult( ResultType.Vector3, $"{name}" ) : new NodeResult( ResultType.Vector3, $"float3( {v.x}, {v.y}, {v.z} )" ),
			Vector4 v => isNamed ? new NodeResult( ResultType.Vector4, $"{name}" ) : new NodeResult( ResultType.Vector4, $"float4( {v.x}, {v.y}, {v.z}, {v.w} )" ),
			Color v => isNamed ? new NodeResult( ResultType.Vector4, $"{name}" ) : new NodeResult( ResultType.Vector4, $"float4( {v.r}, {v.g}, {v.b}, {v.a} )" ),
			Float2x2 v => isNamed ? new NodeResult( ResultType.Float2x2, $"{value}" ) : new NodeResult( ResultType.Float2x2, $"float2x2( {v.M11}, {v.M12}, {v.M21}, {v.M22} )" ),
			Float3x3 v => isNamed ? new NodeResult( ResultType.Float3x3, $"{value}" ) : new NodeResult( ResultType.Float3x3, $"float3x3( {v.M11}, {v.M12}, {v.M13}, {v.M21}, {v.M22}, {v.M23}, {v.M31}, {v.M32}, {v.M33} )" ),
			Float4x4 v => isNamed ? new NodeResult( ResultType.Float4x4, $"{value}" ) : new NodeResult( ResultType.Float4x4, $"float4x4( {v.M11}, {v.M12}, {v.M13}, {v.M14}, {v.M21}, {v.M22}, {v.M23}, {v.M24}, {v.M31}, {v.M32}, {v.M33}, {v.M34}, {v.M41}, {v.M42}, {v.M43}, {v.M44} )" ),
			Sampler v => new NodeResult( ResultType.Sampler, $"{name}", true ),
			_ => throw new ArgumentException( $"Unsupported attribute type `{value.GetType()}`" )
		};
	}

	private NodeResult ResolveSubgraphInput( SubgraphInput inputNode, ref object value, out (SubgraphNode Node, string ErrorString) error )
	{
		var lastStack = SubgraphStack.LastOrDefault();
		var lastNodeEntered = lastStack.Item1;
		error = new();

		if ( lastNodeEntered != null )
		{
			var parentInput = lastNodeEntered.InputReferences.FirstOrDefault( x => x.Key.Identifier == inputNode.Name );
			if ( parentInput.Key is not null )
			{
				var lastSubgraph = Subgraph;
				var lastNode = SubgraphNode;
				Subgraph = lastStack.Item2;
				SubgraphNode = (Subgraph is null) ? null : lastNodeEntered;
				SubgraphStack.RemoveAt( SubgraphStack.Count - 1 );

				var connectedPlug = parentInput.Key.ConnectedOutput;
				if ( connectedPlug is not null )
				{
					var nodeId = string.Join( ',', SubgraphStack.Select( x => x.Item1.Identifier ) );
					var newResult = Result( new()
					{
						Identifier = connectedPlug.Node.Identifier,
						Output = connectedPlug.Identifier,
						Subgraph = Subgraph?.Path,
						SubgraphNode = nodeId
					} );
					SubgraphStack.Add( lastStack );
					Subgraph = lastSubgraph;
					SubgraphNode = lastNode;
					return newResult;
				}
				else
				{
					value = GetDefaultValue( lastNodeEntered, inputNode.Name, parentInput.Value.inputNodeValueType );

					SubgraphStack.Add( lastStack );
					Subgraph = lastSubgraph;
					SubgraphNode = lastNode;

					var inputNodeInputType = parentInput.Value.inputNode.PortType;
					if ( inputNodeInputType == SubgraphPortType.Texture2DObject || inputNodeInputType == SubgraphPortType.TextureCubeObject )
					{
						var resultType = (inputNodeInputType == SubgraphPortType.Texture2DObject ? ResultType.Texture2D : ResultType.TextureCube);
						var textureInput = ((TextureInput)value) with { Type = (inputNodeInputType == SubgraphPortType.Texture2DObject ? TextureType.Tex2D : TextureType.TexCube) };
						var texurePath = CompileTexture( textureInput );
						var textureGlobal = ResultTexture( textureInput, Texture.Load( texurePath ), true );

						return new NodeResult( resultType, textureGlobal, true );
					}
					else if ( value is Sampler sampler )
					{
						var samplerResult = ResultSampler( $"Sampler_{sampler.GetHashCode():X8}", sampler );
						return new NodeResult( ResultType.Sampler, samplerResult, constant: true );
					}
				}
			}
		}

		return new();
	}

	// TODO : Fix this up
	/*
	public string GeneratePostProcessingComponent( PostProcessingComponentInfo postProcessiComponentInfo, string className, string shaderPath )
	{
		var ppcb = new PostProcessingComponentBuilder( postProcessiComponentInfo );
		var type = "";

		foreach ( var parameter in ShaderResult.Parameters )
		{
			type = parameter.Value.Result.ComponentType.ToSimpleString();

			if ( type is "System.Boolean" )
			{
				ppcb.AddBoolProperty( parameter.Key, parameter.Value.Options );
			}
			if ( type is "float" )
			{
				ppcb.AddFloatProperty( type, parameter.Key, parameter.Value.Options );
			}
			if ( type is "Vector2" )
			{
				ppcb.AddVector2Property( type, parameter.Key, parameter.Value.Options );
			}
			if ( type is "Vector3" )
			{
				ppcb.AddVector3Property( type, parameter.Key, parameter.Value.Options );
			}
			if ( type is "Vector4" )
			{
				ppcb.AddVector4Property( type, parameter.Key, parameter.Value.Options );
			}
			if ( type is "Color" )
			{
				ppcb.AddVector4Property( type, parameter.Key, parameter.Value.Options );
			}
		}

		return ppcb.Finish( className, shaderPath );
	}
	*/

	/// <summary>
	/// Generate shader code, will evaluate the graph if it hasn't already.
	/// Different code is generated for preview and not preview.
	/// </summary>
	public string Generate()
	{
		if ( Graph.IsSubgraph )
		{
			if ( !Graph.Nodes.OfType<SubgraphOutput>().Any() )
			{
				NodeErrors.Add( new DummyNode(), [$"There must be atleast one Subgraph Output node!"] );
			}
		}

		// May have already evaluated and there's errors
		if ( Errors.Any() )
			return null;

		/*
		if ( Graph.MaterialDomain == MaterialDomain.BlendingSurface )
		{
			VertexInputs.Add( "vColorBlendValues", "float4 vColorBlendValues : TEXCOORD4 < Semantic( VertexPaintBlendParams ); >;" );
			VertexInputs.Add( "vColorPaintValues", "float4 vColorPaintValues : TEXCOORD5 < Semantic( VertexPaintTintColor ); >;" );
		
			PixelInputs.Add( "vBlendValues", "float4 vBlendValues : TEXCOORD14;" );
			PixelInputs.Add( "vPaintValues", "float4 vPaintValues : TEXCOORD15;" );
		}
		*/

		// Pre-Register anything before we Generate anything. Shouldn't cause any issues i hope.
		foreach ( var node in Graph.Nodes.OfType<IPreRegisterNodeData>() )
		{
			node.PreRegister( this );
		}

		var material = GenerateMaterial();
		var pixelOutput = GeneratePixelOutput();

		// If we have any errors after evaluating, no point going further
		if ( Errors.Any() )
			return null;

		var shaderTemplate = ShaderTemplate.Code;

		if ( Graph.Domain is ShaderDomain.BlendingSurface )
		{
			shaderTemplate = ShaderTemplateBlending.Code;
		}

		return string.Format( shaderTemplate,
			Graph.Description, // {0}
			IndentString( GenerateFeatures(), 1 ), // {1}
			IndentString( GenerateCommon(), 1 ), // {2}
			IndentString( GenerateStageInputs( ShaderStage.Vertex ), 1 ), // {3}
			IndentString( GenerateStageInputs( ShaderStage.Pixel ), 1 ), // {4}
			IndentString( GenerateGlobals(), 1 ), // {5}
			IndentString( GenerateLocals(), 2 ), // {6}
			IndentString( material, 2 ), // {7}
			IndentString( GenerateVertex(), 2 ), // {8}
			IndentString( GenerateGlobals(), 1 ), // {9}
			IndentString( GenerateVertexComboRules(), 1 ), // {10}
			IndentString( GeneratePixelComboRules(), 1 ),  // {11}
			IndentString( GenerateFunctions( PixelResult ), 1 ),  // {12}
			IndentString( GenerateFunctions( VertexResult ), 1 ),  // {13}
			IndentString( GeneratePixelInit(), 2 ), // {14}
			IndentString( pixelOutput, 2 ) // {15}
		);
	}

	private string GenerateMaterial()
	{
		Stage = ShaderStage.Pixel;
		Subgraph = null;
		SubgraphStack.Clear();

		if ( Graph.ShadingModel != ShadingModel.Lit || Graph.Domain == ShaderDomain.PostProcess ) return "";

		var resultNode = Graph.Nodes.OfType<BaseResult>().FirstOrDefault();

		if ( resultNode == null )
			return null;

		var sb = new StringBuilder();
		var visited = new HashSet<string>();

		foreach ( var property in GetNodeInputProperties( resultNode.GetType() ) )
		{
			if ( property.Name == "PositionOffset" )
				continue;

			CurrentResultInput = property.Name;
			visited.Add( property.Name );

			NodeResult result;

			if ( property.GetValue( resultNode ) is NodeInput connection && connection.IsValid() )
			{
				result = Result( connection );
			}
			else
			{
				var editorAttribute = property.GetCustomAttribute<BaseNodePlus.NodeValueEditorAttribute>();
				if ( editorAttribute == null )
					continue;

				var valueProperty = resultNode.GetType().GetProperty( editorAttribute.ValueName );
				if ( valueProperty == null )
					continue;

				result = ResultValue( valueProperty.GetValue( resultNode ) );

			}

			if ( Errors.Any() )
				return null;

			if ( !result.IsValid() )
				continue;

			if ( string.IsNullOrWhiteSpace( result.Code ) )
				continue;

			var inputAttribute = property.GetCustomAttribute<BaseNodePlus.InputAttribute>();
			var componentCount = GetComponentCount( inputAttribute.Type );

			sb.AppendLine( $"m.{property.Name} = {result.Cast( componentCount )};" );
		}

		if ( Graph.IsSubgraph )
		{
			var subgraphOutputs = Graph.Nodes.OfType<SubgraphOutput>();
			var reservedPreview = new Dictionary<SubgraphOutputPreviewType, SubgraphOutput>();

			foreach ( var subgraphOutput in subgraphOutputs )
			{
				subgraphOutput.ClearError();

				if ( reservedPreview.ContainsKey( subgraphOutput.Preview ) )
				{
					subgraphOutput.HasError = true;
					subgraphOutput.ErrorMessage = $"SubgraphOutput \"{reservedPreview[subgraphOutput.Preview].OutputName}\" has already claimed the \"{subgraphOutput.Preview}\" preview type";

					NodeErrors.Add( subgraphOutput, [subgraphOutput.ErrorMessage] );
					continue;
				}

				if ( subgraphOutput.Preview == SubgraphOutputPreviewType.None )
					continue;

				reservedPreview.Add( subgraphOutput.Preview, subgraphOutput );

				subgraphOutput.AddMaterialOutput( this, sb, subgraphOutput.Preview, out var errors );

				if ( errors.Any() )
				{
					NodeErrors.Add( new DummyNode(), errors );

					return "";
				}
			}

			reservedPreview.Clear();
		}

		visited.Clear();
		CurrentResultInput = null;

		return sb.ToString();
	}

	private string GenerateVertex()
	{
		Stage = ShaderStage.Vertex;

		var resultNode = Graph.Nodes.OfType<BaseResult>().FirstOrDefault();
		if ( resultNode == null )
			return null;

		var positionOffsetInput = resultNode.GetPositionOffset();

		var sb = new StringBuilder();
		var sb2 = new StringBuilder();

		foreach ( var vertexInput in VertexInputs )
		{
			sb2.AppendLine( $"i.{vertexInput.Key} = v.{vertexInput.Key};" );
		}

		switch ( Graph.Domain )
		{
			case ShaderDomain.Surface:
				sb.AppendLine( $@"
PixelInput i = ProcessVertex( v );
i.vPositionOs = v.vPositionOs.xyz;

{sb2.ToString()}
ExtraShaderData_t extraShaderData = GetExtraPerInstanceShaderData( v.nInstanceTransformID );
i.vTintColor = extraShaderData.vTint;

VS_DecodeObjectSpaceNormalAndTangent( v, i.vNormalOs, i.vTangentUOs_flTangentVSign );
		" );
				break;
			case ShaderDomain.BlendingSurface:
				sb.AppendLine( $@"
PixelInput i = ProcessVertex( v );

{sb2.ToString()}
i.vBlendValues = v.vColorBlendValues;
i.vPaintValues = v.vColorPaintValues;
" );
				break;
			case ShaderDomain.PostProcess:
				sb.AppendLine( $@"
PixelInput i;

{sb2.ToString()}
i.vPositionPs = float4( v.vPositionOs.xy, 0.0f, 1.0f );
i.vPositionWs = float3( v.vTexCoord, 0.0f );
" );
				break;
		}

		NodeResult result;

		if ( positionOffsetInput is NodeInput connection && connection.IsValid() )
		{
			result = Result( connection );

			if ( !Errors.Any() && result.IsValid() && !string.IsNullOrWhiteSpace( result.Code ) )
			{
				var componentCount = GetComponentCount( typeof( Vector3 ) );

				GenerateLocalResults( ref sb, ShaderResult.Results, out _, true, 0 );

				sb.AppendLine( $"i.vPositionWs.xyz += {result.Cast( componentCount )};" );
				sb.AppendLine( "i.vPositionPs.xyzw = Position3WsToPs( i.vPositionWs.xyz );" );
			}
		}

		switch ( Graph.Domain )
		{
			case ShaderDomain.Surface:
				sb.AppendLine( "return FinalizeVertex( i );" );
				break;
			case ShaderDomain.BlendingSurface:
				sb.AppendLine( "return FinalizeVertex( i );" );
				break;
			case ShaderDomain.PostProcess:
				sb.AppendLine( "return i;" );
				break;
		}
		return sb.ToString();
	}

	private string GeneratePixelOutput()
	{
		Stage = ShaderStage.Pixel;

		Subgraph = null;
		SubgraphStack.Clear();

		if ( Graph.ShadingModel == ShadingModel.Unlit || Graph.Domain == ShaderDomain.PostProcess )
		{
			var resultNode = Graph.Nodes.OfType<BaseResult>().FirstOrDefault();
			if ( resultNode == null )
				return null;

			var albedoResult = resultNode.GetAlbedoResult( this );
			string albedo = "float3( 1.0f, 1.0f, 1.0f )";
			if ( albedoResult.IsValid )
			{
				albedo = albedoResult.Cast( GetComponentCount( typeof( Vector3 ) ) ) ?? "float3( 1.0f, 1.0f, 1.0f )";
			}

			var opacityResult = resultNode.GetOpacityResult( this );
			string opacity = "1.0f";
			if ( opacityResult.IsValid )
			{
				opacity = opacityResult.Cast( 1 ) ?? "1.0f";
			}

			return $"return float4( {albedo}, {opacity} );";
		}
		else if ( Graph.ShadingModel == ShadingModel.Lit )
		{
			return ShaderTemplate.Material_output;
		}

		return null;
	}

	private string GenerateFeatures()
	{
		var sb = new StringBuilder();
		var result = ShaderResult;

		sb.AppendLine( "#include \"common/features.hlsl\"" );

		// Register any Graph level Shader Features...
		//RegisterShaderFeatures( Graph.shaderFeatureNodeResults );

		if ( Graph.Domain is ShaderDomain.BlendingSurface )
		{
			sb.AppendLine( "Feature( F_MULTIBLEND, 0..3 ( 0=\"1 Layers\", 1=\"2 Layers\", 2=\"3 Layers\", 3=\"4 Layers\", 4=\"5 Layers\" ), \"Number Of Blendable Layers\" );" );
		}

		foreach ( var feature in ShaderFeatures )
		{
			if ( feature.Value is ShaderFeatureBoolean boolFeature )
			{
				sb.AppendLine( $"Feature( {boolFeature.GetFeatureString()}, 0..1, \"{boolFeature.HeaderName}\" );" );
			}
			else if ( feature.Value is ShaderFeatureEnum enumFeature )
			{
				var optionsBody = BuildFeatureOptionsBody( enumFeature.Options );
				sb.AppendLine( $"Feature( {enumFeature.GetFeatureString()}, 0..{enumFeature.Options.Count - 1} ( {optionsBody} ), \"{enumFeature.HeaderName}\" );" );
			}
		}

		// TODO :
		//if ( Graph.FeatureRules.Any() )
		//{
		//	foreach ( var rule in Graph.FeatureRules )
		//	{
		//		if ( rule.IsValid )
		//		{
		//			sb.AppendLine( $"FeatureRule(Allow1( {String.Join( ", ", rule.Features )} ), \"{rule.HoverHint}\");" );
		//		}
		//	}
		//}

		return sb.ToString();
	}

	private string GenerateCommon()
	{
		var sb = new StringBuilder();

		if ( IsPreview )
		{
			sb.AppendLine( $"#ifndef SHADERGRAPHPLUS_PREVIEW" );
			sb.AppendLine( IndentString( $"#define SHADERGRAPHPLUS_PREVIEW", 1 ) );
			sb.AppendLine( $"#endif" );
		}

		if ( ShaderFeatures.Any() )
		{
			sb.AppendLine( $"#ifndef SWITCH_TRUE" );
			sb.AppendLine( IndentString( $"#define SWITCH_TRUE 1", 1 ) );
			sb.AppendLine( $"#endif" );

			sb.AppendLine( $"#ifndef SWITCH_FALSE" );
			sb.AppendLine( IndentString( $"#define SWITCH_FALSE 0", 1 ) );
			sb.AppendLine( $"#endif" );

			sb.AppendLine();
		}

		var blendMode = Graph.BlendMode;
		var alphaTest = blendMode == BlendMode.Masked ? 1 : 0;
		var translucent = blendMode == BlendMode.Translucent ? 1 : 0;

		sb.AppendLine( $"#ifndef S_ALPHA_TEST" );
		sb.AppendLine( IndentString( $"#define S_ALPHA_TEST {alphaTest}", 1 ) );
		sb.AppendLine( $"#endif" );

		sb.AppendLine( $"#ifndef S_TRANSLUCENT" );
		sb.AppendLine( IndentString( $"#define S_TRANSLUCENT {translucent}", 1 ) );
		sb.AppendLine( $"#endif" );

		sb.AppendLine();

		sb.AppendLine( $"#include \"common/shared.hlsl\"" );
		sb.AppendLine( $"#include \"common/gradient.hlsl\"" );
		sb.AppendLine( $"#include \"procedural.hlsl\"" );

		sb.AppendLine();

		sb.Append( $"#define S_UV2 1" );

		if ( ShaderDefines.Any() )
		{
			sb.AppendLine();
			sb.AppendLine();

			foreach ( var define in ShaderDefines )
			{
				sb.AppendLine( $"#ifndef {define.Key}" );
				sb.AppendLine( IndentString( $"#define {define.Key} {define.Value}", 1 ) );
				sb.AppendLine( $"#endif" );
			}
		}

		return sb.ToString();
	}

	private string GenerateStageInputs( ShaderStage shaderStage )
	{
		var sb = new StringBuilder();

		if ( shaderStage == ShaderStage.Vertex )
		{
			sb.AppendLine();

			foreach ( var vertexInput in VertexInputs )
			{
				if ( !ShaderTemplate.InternalVertexInputs.ContainsValue( vertexInput.Value ) )
				{
					sb.AppendLine( vertexInput.Value );
				}
			}
		}
		else
		{
			sb.AppendLine();

			foreach ( var pixelInput in PixelInputs )
			{
				if ( !ShaderTemplate.InternalPixelInputs.ContainsValue( pixelInput.Value ) )
				{
					sb.AppendLine( pixelInput.Value );
				}
			}
		}

		return sb.ToString();
	}

	private string GenerateGlobals()
	{
		var sb = new StringBuilder();

		var includes = IsVs ? VertexIncludes : PixelIncludes;

		if ( IsPs && Graph.Domain == ShaderDomain.PostProcess )
		{
			sb.AppendLine( "#include \"postprocess/functions.hlsl\"" );
			sb.AppendLine( "#include \"postprocess/common.hlsl\"" );
		}

		foreach ( var include in includes )
		{
			sb.AppendLine( $"#include \"{include}\"" );
		}

		if ( includes.Any() )
		{
			sb.AppendLine();
		}

		foreach ( var global in ShaderResult.Globals )
		{
			sb.AppendLine( global.Value );
		}

		foreach ( var feature in ShaderFeatures )
		{
			if ( IsPreview )
			{
				sb.AppendLine( $"DynamicCombo( {feature.Value.GetDynamicComboString()}, {feature.Value.GetOptionRangeString()}, Sys( ALL ) );" );
			}
			else
			{
				sb.AppendLine( $"StaticCombo( {feature.Value.GetStaticComboString()}, {feature.Value.GetFeatureString()}, Sys( ALL ) );" );
			}

			sb.AppendLine();
		}

		// Support for color buffer in post-process shaders
		if ( IsPs && Graph.Domain is ShaderDomain.PostProcess )
		{
			sb.AppendLine( "Texture2D g_tColorBuffer < Attribute( \"ColorBuffer\" ); SrgbRead ( true ); >;" );
		}

		if ( IsPreview )
		{
			foreach ( var result in ShaderResult.TextureInputs )
			{
				sb.Append( $"{result.Value.CreateTexture( result.Key )} <" )
				  .Append( $" Attribute( \"{result.Key}\" );" )
				  .Append( $" SrgbRead( {result.Value.SrgbRead} ); >;" )
				  .AppendLine();
			}

			foreach ( var result in ShaderResult.Attributes )
			{
				if ( result.Value is Texture || result.Value == null || !ShaderAttributeTypes.Contains( result.Value.GetType() ) )
					continue;

				var typeName = result.Value switch
				{
					bool _ => "bool",
					int _ => "int",
					float _ => "float",
					Vector2 _ => "float2",
					Vector3 _ => "float3",
					Vector4 _ or Color _ => "float4",
					Float2x2 _ => "float2x2",
					Float3x3 _ => "float3x3",
					Float4x4 _ => "float4x4",
					Sampler _ => "SamplerState",
					_ => null
				};

				sb.AppendLine( $"{typeName} {result.Key} < Attribute( \"{result.Key}\" ); >;" );
			}

			sb.AppendLine( "float g_flPreviewTime < Attribute( \"g_flPreviewTime\" ); >;" );
			sb.AppendLine( $"int g_iStageId < Attribute( \"g_iStageId\" ); >;" );
		}
		else
		{
			foreach ( var sampler in ShaderResult.SamplerStates )
			{
				if ( sampler.Value.IsAttribute )
				{
					sb.AppendLine( $"SamplerState g_s{sampler.Key} < Attribute( \"{sampler.Key}\" ); >;" );
				}
				else
				{
					sb.Append( $"SamplerState g_s{sampler.Key} <" )
					  .Append( $" Filter( {sampler.Value.Filter.ToString().ToUpper()} );" )
					  .Append( $" AddressU( {sampler.Value.AddressModeU.ToString().ToUpper()} );" )
					  .Append( $" AddressV( {sampler.Value.AddressModeV.ToString().ToUpper()} );" )
					  .Append( $" AddressW( {sampler.Value.AddressModeW.ToString().ToUpper()} );" )
					  .Append( $" MaxAniso( {sampler.Value.MaxAnisotropy.ToString()} ); >;" )
					  .AppendLine();
				}
			}

			foreach ( var result in ShaderResult.TextureInputs )
			{
				// If we're an attribute, we don't care about texture inputs
				if ( result.Value.IsAttribute )
					continue;

				var defaultTex = result.Value.DefaultTexture;
				sb.Append( $"{result.Value.CreateInput}( {result.Key}, {result.Value.ColorSpace}, 8," )
				  .Append( $" \"{result.Value.Processor.ToString()}\"," )
				  .Append( $" \"_{result.Value.ExtensionString.ToLower()}\"," )
				  .Append( $" \"{result.Value.UIGroup}\"," )
				  .Append( string.IsNullOrEmpty( defaultTex )
							? $" Default4( {result.Value.DefaultColor} ) );"
							: $" DefaultFile( \"{defaultTex}\" ) );" )
				  .AppendLine();
			}

			foreach ( var result in ShaderResult.TextureInputs )
			{
				// If we're an attribute, we don't care about the UI options
				if ( result.Value.IsAttribute )
				{
					sb.AppendLine( $"{result.Value.CreateTexture( result.Key )} < Attribute( \"{result.Key}\" ); >;" );
				}
				else
				{
					sb.Append( $"{result.Value.CreateTexture( result.Key )} < Channel( RGBA, Box( {result.Key} ), {(result.Value.SrgbRead ? "Srgb" : "Linear")} );" )
					  .Append( $" OutputFormat( {result.Value.ImageFormat} );" )
					  .Append( $" SrgbRead( {result.Value.SrgbRead} ); >;" )
					  .AppendLine();
				}
			}

			if ( !string.IsNullOrWhiteSpace( ShaderResult.RepresentativeTexture ) )
			{
				sb.AppendLine( $"TextureAttribute( LightSim_DiffuseAlbedoTexture, {ShaderResult.RepresentativeTexture} )" );
				sb.AppendLine( $"TextureAttribute( RepresentativeTexture, {ShaderResult.RepresentativeTexture} )" );
			}

			foreach ( var parameter in ShaderResult.Parameters )
			{
				sb.AppendLine( $"{parameter.Value.Result.TypeName} {parameter.Key} < {parameter.Value.Options} >;" );
			}
		}

		if ( sb.Length > 0 )
		{
			sb.Insert( 0, "\n" );
		}

		return sb.ToString();
	}

	private string GenerateLocals( bool noPreviewOverride = false )
	{
		var sb = new StringBuilder();

		if ( ShaderResult.Results.Any() )
		{
			sb.AppendLine();
		}

		foreach ( var gradient in ShaderResult.Gradients )
		{
			sb.AppendLine( $"Gradient {gradient.Key} = Gradient::Init();" );
			sb.AppendLine();

			var colorindex = 0;
			var alphaindex = 0;

			sb.AppendLine( $"{gradient.Key}.colorsLength = {gradient.Value.Colors.Count};" );
			sb.AppendLine( $"{gradient.Key}.alphasLength = {gradient.Value.Alphas.Count};" );

			foreach ( var color in gradient.Value.Colors )
			{
				if ( ConCommands.VerboseDebgging )
				{
					SGPLogger.Info( $"{gradient.Key} Gradient Color {colorindex} : {color.Value} Time : {color.Time}" );
				}

				// All good with time as the 4th component?
				sb.AppendLine( $"{gradient.Key}.colors[{colorindex++}] = float4( {color.Value.r}, {color.Value.g}, {color.Value.b}, {color.Time} );" );
			}

			foreach ( var alpha in gradient.Value.Alphas )
			{
				sb.AppendLine( $"{gradient.Key}.alphas[{alphaindex++}] = float( {alpha.Value} );" );
			}

			sb.AppendLine();
		}

		GenerateLocalResults( ref sb, ShaderResult.Results, out _, noPreviewOverride, 0 );

		return sb.ToString();
	}

	private void GenerateLocalResults( ref StringBuilder sb, IEnumerable<(NodeResult localResult, NodeResult funcResult)> shaderResults, out (NodeResult localResult, NodeResult funcResult) lastResult, bool noPreviewOverride = false, int indentLevel = 0 )
	{
		lastResult = (new NodeResult(), new NodeResult());

		foreach ( var result in shaderResults )
		{
			lastResult = result;
			var comboBodyStr = "";

			if ( result.funcResult.TryGetMetaData<string>( nameof( MetadataType.ComboSwitchBody ), out var comboSwitchBody ) )
			{
				comboBodyStr = comboSwitchBody;

				if ( !string.IsNullOrWhiteSpace( comboSwitchBody ) )
				{
					sb.AppendLine( IndentString( comboSwitchBody, indentLevel ) );
				}
			}

			// CustomFunction node uses this primarly.
			if ( ShaderResult.VoidFunctionLocals.Any() && ShaderResult.VoidFunctionLocals.ContainsKey( result.funcResult.VoidLocalTargetID ) )
			{
				var data = ShaderResult.VoidFunctionLocals[result.funcResult.VoidLocalTargetID];
				sb.AppendLine();

				if ( !VoidDataHashes.Contains( data.GetHashCode() ) )
				{
					VoidDataHashes.Add( data.GetHashCode() );

					// Init all the output results.
					foreach ( var outResult in data.TargetResults )
					{
						sb.AppendLine( IndentString( InitializeVariable( outResult.ResultType, outResult.CompilerAssignedName ), indentLevel ) );
					}

					sb.AppendLine( IndentString( $"{data.FunctionCall};", indentLevel ) );
					sb.AppendLine();
				}
			}

			if ( result.funcResult.ResultType != ResultType.VoidFunction )
			{
				if ( result.funcResult.ResultType == ResultType.Float2x2 ||
					 result.funcResult.ResultType == ResultType.Float3x3 ||
					 result.funcResult.ResultType == ResultType.Float4x4 )
				{
					if ( IsPreview )
					{
						sb.AppendLine( IndentString( $"{result.funcResult.TypeName} {result.localResult} = {result.funcResult.TypeName}( {result.funcResult.Code} );", indentLevel ) );
					}
					else
					{
						sb.AppendLine( IndentString( $"{result.funcResult.TypeName} {result.localResult} = {result.funcResult.Code};", indentLevel ) );
					}
				}
				else
				{
					sb.AppendLine( IndentString( $"{result.funcResult.TypeName} {result.localResult} = {result.funcResult.Code};", indentLevel ) );
				}
			}

			if ( !noPreviewOverride )
			{
				if ( IsPs && IsPreview && string.IsNullOrWhiteSpace( comboBodyStr ) )
				{
					if ( result.localResult.ResultType == ResultType.Bool )
					{
						// TODO
					}
					else if ( result.localResult.CanPreview && result.localResult.ShouldPreview && result.localResult.PreviewID != ShaderGraphPlusGlobals.GraphCompiler.NoNodePreviewID )
					{
						sb.AppendLine( IndentString( $"if ( g_iStageId == {result.localResult.PreviewID} ) return {result.localResult.Cast( 4, 1.0f )};", indentLevel ) );
					}
				}
			}
		}
	}

	private string GenerateVertexComboRules()
	{
		var sb = new StringBuilder();

		if ( IsNotPreview )
			return sb.ToString();

		sb.AppendLine();
		return sb.ToString();
	}

	private string GeneratePixelComboRules()
	{
		var sb = new StringBuilder();

		if ( !IsNotPreview )
		{
			sb.AppendLine();
			sb.AppendLine( "DynamicCombo( D_RENDER_BACKFACES, 0..1, Sys( ALL ) );" );
			if ( Graph.RenderFace == RenderFace.Front )
				sb.AppendLine( "RenderState( CullMode, D_RENDER_BACKFACES ? NONE : BACK );" );
			else if ( Graph.RenderFace == RenderFace.Back )
				sb.AppendLine( "RenderState( CullMode, D_RENDER_BACKFACES ? NONE : FRONT );" );
			else
				sb.AppendLine( "RenderState( CullMode, NONE );" );
		}
		else
		{
			sb.AppendLine();
			if ( Graph.RenderFace == RenderFace.Front )
				sb.AppendLine( "RenderState( CullMode, F_RENDER_BACKFACES ? NONE : BACK );" );
			else if ( Graph.RenderFace == RenderFace.Back )
				sb.AppendLine( "RenderState( CullMode, F_RENDER_BACKFACES ? NONE : FRONT );" );
			else
				sb.AppendLine( "RenderState( CullMode, NONE );" );
		}

		return sb.ToString();
	}

	private string GeneratePixelInit()
	{
		Stage = ShaderStage.Pixel;
		if ( Graph.ShadingModel == ShadingModel.Lit && Graph.Domain != ShaderDomain.PostProcess )
			return ShaderTemplate.Material_init;
		return "";
	}

	public static string IndentString( string input, int tabCount )
	{
		if ( tabCount == 0 ) return input;

		if ( string.IsNullOrWhiteSpace( input ) )
			return input;

		var tabs = new string( '\t', tabCount );
		var lines = input.Split( '\n' );

		for ( int i = 0; i < lines.Length; i++ )
		{
			lines[i] = tabs + lines[i];
		}

		return string.Join( "\n", lines );
	}

	public static string CleanName( string name )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			return "";

		name = name.Trim();
		name = new string( name.Where( x => char.IsLetter( x ) || char.IsNumber( x ) || x == '_' ).ToArray() );

		return name;
	}

	private static int GetComponentCount( Type inputType )
	{
		return inputType switch
		{
			Type t when t == typeof( int ) => 1,
			Type t when t == typeof( float ) => 1,
			Type t when t == typeof( Vector2 ) => 2,
			Type t when t == typeof( Vector3 ) => 3,
			Type t when t == typeof( Vector4 ) || t == typeof( Color ) => 4,
			Type t when t == typeof( Float2x2 ) => 4,
			Type t when t == typeof( Float3x3 ) => 6,
			Type t when t == typeof( Float4x4 ) => 16,
			_ => 0
		};
	}

	private static string InitializeVariable( ResultType resultType, string name )
	{
		switch ( resultType )
		{
			case ResultType.Bool:
				return $"bool {name} = false;";
			case ResultType.Int:
				return $"int {name} = 0;";
			case ResultType.Float:
				return $"float {name} = 0.0f;";
			case ResultType.Vector2:
				return $"float2 {name} = float2( 0.0f, 0.0f );";
			case ResultType.Vector3:
				return $"float3 {name} = float3( 0.0f, 0.0f, 0.0f );";
			case ResultType.Vector4:
				return $"float4 {name} = float4( 0.0f, 0.0f, 0.0f, 0.0f );";
			case ResultType.Float2x2:
				return $"float2x2 {name} = float2x2( 0.0f, 0.0f, 0.0f, 0.0f );";
			case ResultType.Float3x3:
				return $"float3x3 {name} = float3x3( 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f );";
			case ResultType.Float4x4:
				return $"float4x4 {name} = float4x4( 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f );";
			default:
				throw new NotImplementedException( $"Unknown ResultType \"{resultType}\"" );
		}
	}

	private static ResultType GetResultTypeFromHLSLDataType( string DataType )
	{
		return DataType switch
		{
			"bool" => ResultType.Bool,
			"int" => ResultType.Int,
			"float" => ResultType.Float,
			"float2" => ResultType.Vector2,
			"float3" => ResultType.Vector3,
			"float4" => ResultType.Vector4,
			"float2x2" => ResultType.Float2x2,
			"float3x3" => ResultType.Float3x3,
			"float4x4" => ResultType.Float4x4,
			"Texture2D" => ResultType.Texture2D,
			"TextureCube" => ResultType.TextureCube,
			"SamplerState" => ResultType.Sampler,
			_ => throw new ArgumentException( $"Unknown DataType `{DataType}`" )
		};
	}

	private static IEnumerable<PropertyInfo> GetNodeInputProperties( Type type )
	{
		return type.GetProperties( BindingFlags.Instance | BindingFlags.Public )
			.Where( property => property.GetSetMethod() != null &&
			property.PropertyType == typeof( NodeInput ) &&
			property.IsDefined( typeof( BaseNodePlus.InputAttribute ), false ) );
	}

	private static string GenerateFunctions( CompileResult result )
	{
		if ( !result.Functions.Any() )
			return null;

		var sb = new StringBuilder();
		foreach ( var function in result.Functions )
		{
			if ( GraphHLSLFunctions.TryGetFunction( function, out var code ) )
			{
				sb.AppendLine();
				sb.Append( code );
			}
		}

		return sb.ToString();
	}
}