Code/Systems/VNScript/Nodes.cs
using System;
using System.IO;
using System.Linq;
using System.Collections;
using System.Collections.Generic;

namespace VNScript;

/// <summary>
/// Exception thrown when a variable is not found in the environment.
/// </summary>
public class UndefinedVariableException : Exception
{
	public UndefinedVariableException( string name ) : base( $"Failed to find variable {name}!" )
	{
		base.Data["Missing Variable"] = name;
	}
}

/// <summary>
/// Exception thrown when the parameters passed to a function are invalid.
/// </summary>
public sealed class InvalidParametersException : Exception
{
	public InvalidParametersException( IEnumerable<Value> values ) : base( $"Invalid parameter types: {FormatValues( values )}" )
	{
		Data["Values"] = values;
	}
	
	public InvalidParametersException( string functionName, string expected, IEnumerable<Value> values ) : base( $"Error in `{functionName}`\n" + $"Expected: {expected}\n" + $"Got: {FormatValues(values)}" )
	{
		Data["Function"] = functionName;
		Data["Expected"] = expected;
		Data["Values"] = values;
	}
	
	private static string FormatValues( IEnumerable<Value> values )
	{
		return string.Join( ", ", values.Select( v => v.ToString() ) );
	}
}

/// <summary>
/// Exception that is thrown if a required resource is unable to be found.
/// </summary>
public class ResourceNotFoundException : FileNotFoundException
{
	private string ResourceName { get; set; }
	
	public ResourceNotFoundException( string message, string? resourceName = null, string? fileName = null, Exception? innerException = null ) : base( message, fileName )
	{
		ResourceName = resourceName ?? string.Empty;
		
		if ( innerException is not null )
		{
			base.Data["InnerException"] = innerException;
		}
	}
	
	public override string Message => $"{base.Message} Resource: {ResourceName}, File: {FileName ?? "N/A"}";
}

internal static class ParamError
{
	public static InvalidParametersException Wrong( string name, string expected, IEnumerable<Value> values )
	{
		return new InvalidParametersException( name, expected, values );
	}
}

/// <summary>
/// A list of values that can be parsed from a string.
/// </summary>
public class SParen : IReadOnlyList<Value>
{
	/// <summary>
	/// A token that can be parsed from a string.
	/// </summary>
	public abstract record Token
	{
		public record OpenParen : Token;
		
		public record Symbol( string Name ) : Token;
		
		public record String( string Text ) : Token;
		
		public record Number( string Value ) : Token;
		
		public record CloseParen : Token;
	}
	
	private readonly List<Value> _backingList;
	
	private SParen( List<Value> backingList )
	{
		_backingList = backingList;
	}
	
	public IEnumerator<Value> GetEnumerator()
	{
		return _backingList.GetEnumerator();
	}
	
	IEnumerator IEnumerable.GetEnumerator()
	{
		return ((IEnumerable)_backingList).GetEnumerator();
	}
	
	public int Count => _backingList.Count;
	
	public Value this[ int index ] => _backingList[index];
	
	/// <summary>
	/// Tokenizes a string into a list of tokens.
	/// </summary>
	/// <param name="text">The string to tokenize.</param>
	private static IEnumerable<Token> TokenizeText( string text )
	{
		var symbolStart = 0;
		var isInQuote = false;
		var isInSingleLineComment = false;
		var isInMultiLineComment = false;
		
		for ( var i = 0; i < text.Length; i++ )
		{
			if ( isInSingleLineComment )
			{
				if ( text[i] == '\n' )
				{
					// End of single-line comment
					isInSingleLineComment = false;
					symbolStart = i + 1;
				}
				
				continue;
			}
			
			if ( isInMultiLineComment )
			{
				if ( text[i] == '*' && i + 1 < text.Length && text[i + 1] == '/' )
				{
					// End of multi-line comment
					isInMultiLineComment = false;
					i++; // Skip '/'
					symbolStart = i + 1;
				}
				
				continue;
			}
			
			if ( isInQuote )
			{
				// Check for escaped quote
				if ( text[i] == '\\' && i + 1 < text.Length && text[i + 1] == '"' )
				{
					i++; // Skip the escaped quote
					continue;
				}
				
				if ( text[i] != '"' )
				{
					continue;
				}
				
				if ( text[i] == '"' )
				{
					// Extract string content without quotes
					var stringContent = text.Substring( symbolStart, i - symbolStart );
					// Unescape any escaped quotes
					stringContent = stringContent.Replace( "\\\"", "\"" );
					yield return new Token.String( stringContent );
					symbolStart = i + 1;
					isInQuote = false;
				}
			}
			else
			{
				if ( char.IsWhiteSpace( text[i] ) )
				{
					if ( i != symbolStart )
					{
						var sym = text[symbolStart..i];
						
						if ( IsValidNumber( sym ) )
						{
							yield return new Token.Number( sym );
						}
						else
						{
							yield return new Token.Symbol( sym );
						}
					}
					
					symbolStart = i + 1;
					
					continue;
				}
				
				switch ( text[i] )
				{
					case '"': 
						isInQuote = true;
						symbolStart = i + 1; // Start after the opening quote
						continue; // Skip rest of loop iteration
					case '/' when i + 1 < text.Length && text[i + 1] == '/':
						isInSingleLineComment = true;
						i++; // Skip '/'
						symbolStart = i + 1;
						continue;
					case '/' when i + 1 < text.Length && text[i + 1] == '*':
						isInMultiLineComment = true;
						i++; // Skip '*'
						symbolStart = i + 1;
						continue;
				}
			}
			
			if ( symbolStart != i && IsValidSymbolName( text[symbolStart] ) && !IsValidSymbolName( text[i] ) )
			{
				var sym = text[symbolStart..i];
				
				if ( IsValidNumber( sym ) )
				{
					yield return new Token.Number( sym );
				}
				else
				{
					yield return new Token.Symbol( sym );
				}
				
				symbolStart = i + 1;
			}
			
			if ( text[i] == '(' )
			{
				yield return new Token.OpenParen();
				symbolStart = i + 1;
			}
			
			if ( text[i] != ')' )
			{
				continue;
			}
			
			yield return new Token.CloseParen();
			symbolStart = i + 1;
		}
		
		if ( symbolStart >= text.Length )
		{
			yield break;
		}
		
		// New scope for sym.
		{
			var sym = text[symbolStart..];
			
			if ( IsValidNumber( sym ) )
			{
				yield return new Token.Number( sym );
			}
			else
			{
				yield return new Token.Symbol( sym );
			}
		}
	}
	
	private static bool IsValidNumber( string str )
	{
		if ( string.IsNullOrEmpty( str ) )
		{
			return false;
		}
		
		// A valid number must contain at least one digit
		if ( !str.Any( char.IsDigit ) )
		{
			return false;
		}
		
		// Try parsing to validate
		return decimal.TryParse( str, out _ );
	}
	
	private static bool IsValidSymbolName( char character )
	{
		return char.IsLetterOrDigit( character ) || character is '=' or '<' or '>' or '-' or '+' or '/' or '*' or '.' or '_';
	}
	
	public static IEnumerable<SParen> ParseText( string text )
	{
		var tokenList = TokenizeText( text ).ToList();
		
		foreach ( var token in ProcessTokens( tokenList ) )
		{
			yield return token;
		}
	}
	
	private static IEnumerable<SParen> ProcessTokens( List<Token> tokenList )
	{
		SParen? currentParen = null;
		var tokenDepth = 0;
		
		for ( var tokenIndex = 0; tokenIndex < tokenList.Count; tokenIndex++ )
		{
			var token = tokenList[tokenIndex];
			
			switch ( token )
			{
				case Token.CloseParen:
					tokenDepth--;
					
					if ( tokenDepth == 0 && currentParen is not null )
					{
						yield return currentParen;
						currentParen = null;
					}
					
					break;
				case Token.OpenParen:
					tokenDepth++;
					
					if ( tokenDepth == 1 )
					{
						currentParen = new SParen( [] );
					}
					else
					{
						var subDepth = 1;
						var subToken = tokenIndex + 1;
						
						for ( ; subToken < tokenList.Count; subToken++ )
						{
							subDepth = tokenList[subToken] switch
							{
								Token.CloseParen => subDepth - 1, Token.OpenParen => subDepth + 1, _ => subDepth
							};
							
							if ( subDepth == 0 ) break;
						}
						
						foreach ( var sub in ProcessTokens( tokenList.GetRange( tokenIndex, subToken - tokenIndex + 1 ) ) )
						{
							currentParen!._backingList.Add( new Value.ListValue( sub ) );
						}
						
						tokenDepth--;
						tokenIndex = subToken;
					}
					
					break;
				case Token.Number number:
					currentParen!._backingList.Add( new Value.NumberValue( decimal.Parse( number.Value ) ) ); break;
				case Token.String str:
					// String text no longer includes quotes
					currentParen!._backingList.Add( new Value.StringValue( str.Text ) ); break;
				case Token.Symbol symbol:
					currentParen!._backingList.Add( new Value.VariableReferenceValue( symbol.Name ) ); break;
			}
		}
	}
	
	public Value Execute( IEnvironment environment )
	{
		return new Value.ListValue( this ).Evaluate( environment );
	}
}