Editor/ShaderGraphPlus/Compiler/GraphCompiler.ShaderFeatures.cs
using System.Text;
using ShaderGraphPlus.Internal;

namespace ShaderGraphPlus;

public struct FeatureRule : IValid
{
	/// <summary>
	/// Features bound to this rule.
	/// </summary>
	[InlineEditor( Label = false )]
	public List<string> Features { get; set; }

	/// <summary>
	/// Text hint when hovering over features
	/// </summary>
	public string HoverHint { get; set; }

	[Hide, JsonIgnore]
	public bool IsValid
	{
		get
		{
			if ( Features.Any() )
			{
				foreach ( var feature in Features )
				{
					if ( string.IsNullOrWhiteSpace( feature ) )
					{
						return false;
					}
					else
					{
						continue;
					}
				}

				return true;
			}
			else
			{
				return false;
			}
		}
	}

	public FeatureRule()
	{
		Features = new();
		HoverHint = string.Empty;
	}
}

public sealed partial class GraphCompiler
{
	private partial class CompileResult
	{
		public List<string> ShaderFeatureResultStrings { get; set; } = new();
	}

	/// <summary>
	/// Registerd ShaderFeatures
	/// </summary>
	public Dictionary<string, ShaderFeatureBase> ShaderFeatures = new();

	struct SwitchBlockResultHolder
	{
		public string GeneratedLocals { get; private set; }
		public NodeResult Result { get; private set; }

		public SwitchBlockResultHolder( string locals, NodeResult nodeResult )
		{
			GeneratedLocals = locals;
			Result = nodeResult;
		}
	}

	public void SetComboPreview( string comboName, int preview )
	{
		if ( IsNotPreview )
		{
			SGPLogger.Warning( $"{nameof( SetComboPreview )} was called when IsPreview is false!" );
			return;
		}

		Assert.True( comboName.StartsWith( $"D_" ), $"Cannot set a static combo \"{comboName}\"" );

		var dynamicComboWrapper = new DynamicComboWrapper( comboName, preview );
		OnAttribute?.Invoke( dynamicComboWrapper.ComboName, dynamicComboWrapper );
	}

	public NodeResult ResultFeatureSwitch( IEnumerable<NodeInput> inputs, ShaderFeatureBase shaderFeature, int previewInt )
	{
		var sb = new StringBuilder();
		var blockResults = new List<SwitchBlockResultHolder>();

		foreach ( var input in inputs )
		{
			if ( !input.IsValid )
			{
				blockResults.Add( new SwitchBlockResultHolder( $"float l_0 = 1.0f;", new NodeResult( ResultType.Float, "l_0" ) ) );
			}
			else
			{
				blockResults.Add( GenerateSwitchBlockCode( input, input.Identifier, shaderFeature ) );
			}
		}

		if ( !blockResults.Any() )
			return default;

		var resultType = blockResults.Select( x => x.Result.ResultType ).Where( x => x != ResultType.Invalid && !((int)x > 6) ).Max();
		var id = 0;

		while ( ShaderResult.ShaderFeatureResultStrings.Contains( $"{shaderFeature.Name}_result{id}" ) )
		{
			id++;
		}

		var resultAssignmentLocal = $"{shaderFeature.Name}_result{id}";
		var resultDataType = resultType switch
		{
			ResultType.Bool => "bool",
			ResultType.Int => "int",
			ResultType.Float => "float",
			ResultType.Vector2 => "float2",
			ResultType.Vector3 => "float3",
			ResultType.Vector4 => "float4",
			ResultType.Float2x2 => "float2x2",
			ResultType.Float3x3 => "float3x3",
			ResultType.Float4x4 => "float4x4",
			ResultType.Sampler => "SamplerState",
			ResultType.Texture2D => "Texture2D",
			ResultType.TextureCube => "TextureCube",
			ResultType.Gradient => "Gradient",
			_ => throw new Exception( $"Unsupported ResultType `{resultType}`" ),
		};
		var resultTypeComponents = blockResults.Select( x => x.Result.Components ).Max();

		foreach ( var (index, result) in blockResults.Index() )
		{
			if ( !result.Result.IsValid )
			{
				continue;
			}

			var lastResult = new NodeResult( resultType, result.Result.Cast( resultTypeComponents ) );

			if ( index == 0 )
			{
				sb.AppendLine( InitializeVariable( resultType, resultAssignmentLocal ) ); //$"{resultDataType} {resultLocal};" );

				if ( shaderFeature is ShaderFeatureBoolean boolFeature )
				{
					sb.AppendLine( $"#if ( {(IsPreview ? "D" : "S")}_{shaderFeature.Name.ToUpper()} == SWITCH_TRUE )" );
				}
				else
				{
					sb.AppendLine( $"#if ( {(IsPreview ? "D" : "S")}_{shaderFeature.Name.ToUpper()} == {index} )" );
				}

				BuildSwitchBlock( sb, result.GeneratedLocals, resultAssignmentLocal, lastResult );
			}

			if ( shaderFeature is ShaderFeatureBoolean )
			{
				if ( index == blockResults.Count - 1 )
				{
					sb.AppendLine( $"#else" );
					BuildSwitchBlock( sb, result.GeneratedLocals, resultAssignmentLocal, lastResult );
					sb.AppendLine( $"#endif" );
				}
			}
			else
			{
				if ( index != 0 && index != blockResults.Count - 1 )
				{
					sb.AppendLine( $"#elif ( {(IsPreview ? "D" : "S")}_{shaderFeature.Name.ToUpper()} == {index} )" );
					BuildSwitchBlock( sb, result.GeneratedLocals, resultAssignmentLocal, lastResult );
				}
				else if ( index == blockResults.Count - 1 )
				{
					sb.AppendLine( $"#elif ( {(IsPreview ? "D" : "S")}_{shaderFeature.Name.ToUpper()} == {index} )" );
					BuildSwitchBlock( sb, result.GeneratedLocals, resultAssignmentLocal, lastResult );
					sb.AppendLine( $"#endif" );
				}
			}
		}

		//SGPLog.Info( $"Generated Switch D_{shaderFeature.Name.ToUpper()}: \n {sb.ToString()}" );

		// TODO : Once SceneObject.Attributes.SetFeature is added. Replace SetComboPreview with something like SetFeaturePreview.
		if ( IsPreview )
		{
			SetComboPreview( shaderFeature.GetDynamicComboString(), previewInt );
		}

		var finalResult = new NodeResult( resultType, resultAssignmentLocal );
		finalResult.AddMetadataEntry( nameof( MetadataType.ComboSwitchBody ), sb.ToString() );

		if ( !ShaderResult.ShaderFeatureResultStrings.Contains( resultAssignmentLocal ) )
		{
			ShaderResult.ShaderFeatureResultStrings.Add( resultAssignmentLocal );
		}

		return finalResult;
	}

	private SwitchBlockResultHolder GenerateSwitchBlockCode( NodeInput input, string blockName, ShaderFeatureBase shaderFeature )
	{
		var outerResult = ShaderResult;
		var outerInputStack = InputStack;

		if ( IsVs )
		{
			VertexResult = new();
		}
		else
		{
			PixelResult = new();
		}

		InputStack = new();

		var result = Result( input );
		var codeBlock = GenerateLocals( true );

		foreach ( var samplerState in ShaderResult.SamplerStates )
		{
			outerResult.SamplerStates[samplerState.Key] = samplerState.Value;
		}

		foreach ( var textureInput in ShaderResult.TextureInputs )
		{
			outerResult.TextureInputs[textureInput.Key] = textureInput.Value;
		}

		foreach ( var gradient in ShaderResult.Gradients )
		{
			outerResult.Gradients[gradient.Key] = gradient.Value;
		}

		foreach ( var parameter in ShaderResult.Parameters )
		{
			outerResult.Parameters[parameter.Key] = parameter.Value;
		}

		foreach ( var attribute in ShaderResult.Attributes )
		{
			outerResult.Attributes[attribute.Key] = attribute.Value;
		}

		foreach ( var function in ShaderResult.Functions )
		{
			outerResult.Functions.Add( function );
		}

		if ( IsVs )
		{
			VertexResult = outerResult;
		}
		else
		{
			PixelResult = outerResult;
		}
		InputStack = outerInputStack;

		//SGPLogger.Info( $"GeneratedBlock {blockName} : \n {{ {IndentString( codeBlock, 1)} \n }}" );

		return new( codeBlock, result );
	}

	private static StringBuilder BuildSwitchBlock( StringBuilder sb, string generatedLocals, string resultAssignmentLocal, NodeResult lastResult )
	{
		sb.AppendLine( $"{{" );
		sb.AppendLine( $"{IndentString( generatedLocals, 1 )}" );
		sb.AppendLine( $"{IndentString( $"{resultAssignmentLocal} = {lastResult}", 1 )};" );
		sb.AppendLine( $"}}" );

		return sb;
	}

	private static string BuildFeatureOptionsBody( List<ShaderFeatureEnumOption> options )
	{
		var options_body = "";
		int count = 0;

		foreach ( var option in options )
		{
			if ( count == 0 ) // first option starts at 0 :)
			{
				options_body += $"0=\"{option.Name}\", ";
				count++;
			}
			else if ( count != (options.Count - 1) )  // These options dont get the privilege of being the first >:)
			{
				options_body += $"{count}=\"{option.Name}\", ";
				count++;
			}
			else // Last option in the list oh well...:(
			{
				options_body += $"{count}=\"{option.Name}\"";
			}
		}

		return options_body;
	}
}