Components/DialogueParser.cs
using System;

namespace Sandbox;

/*
Dialogue format, yarn-like:

title: BuyItem
tags:
---
{$NpcName}: That is a [blue]{$ItemName}[/blue]. It costs [green]{$ItemPrice}[/green] clovers. Do you want to buy it?
-> Yes
    <<if $PlayerClovers >= $ItemPrice>>
        <<DoBuyItem>>
        {$NpcName}: Thank you for your purchase!
    <<else>>
        {$NpcName}: Oh... You don't seem to have enough clovers to buy that. See anything else you like?
    <<endif>>
-> No
    {$NpcName}: Alright, let me know if you change your mind.
*/

public class DialogueParser : Component
{
	public const string SampleDialogue = @"title: BuyItem
tags:
---
{$NpcName}: That is a [blue]{$ItemName}[/blue]. It costs [green]{$ItemPrice}[/green] clovers. Do you want to buy it?
-> Yes
    <<if $PlayerClovers >= $ItemPrice>>
        <<DoBuyItem>>
        {$NpcName}: Thank you for your purchase!
    <<else>>
        {$NpcName}: Oh... You don't seem to have enough clovers to buy that. See anything else you like?
    <<endif>>
-> No
    {$NpcName}: Alright, let me know if you change your mind.";


	public class BaseNode
	{
		public DialogueLine Line { get; set; }
		public int Indent { get; set; }
		public string Label { get; set; }
		public string Speaker { get; set; }

		public BaseNode( DialogueLine line )
		{
			Line = line;
		}
	}

	public class TextNode : BaseNode
	{
		public string Body { get; set; }

		public TextNode( DialogueLine line ) : base( line )
		{
		}
	}

	public class ChoiceNode : BaseNode
	{
		public string Text { get; set; }
		public List<Choice> Choices { get; set; } = new();

		public ChoiceNode( DialogueLine line ) : base( line )
		{
		}
	}

	public class LogicNode : BaseNode
	{
		/// <summary>
		///  Line index and logic string.
		///  If the logic is true, jump to the line index, otherwise continue to the next logic line
		/// </summary>
		public Dictionary<int, string> Logic { get; set; } = new();

		public enum LogicType
		{
			If,
			Else,
			ElseIf,
			EndIf
		}

		public enum ComparisonOperator
		{
			Equal,
			NotEqual,
			GreaterThan,
			LessThan,
			GreaterThanOrEqual,
			LessThanOrEqual
		}

		public class Statement
		{
			public virtual bool Evaluate( Dictionary<string, object> variables )
			{
				return false;
			}
		}

		public class IfStatement : Statement
		{
			public string Variable1 { get; set; }
			public string Variable2 { get; set; }
			public ComparisonOperator Operator { get; set; }

			public override bool Evaluate( Dictionary<string, object> variables )
			{
				if ( !variables.ContainsKey( Variable1 ) || !variables.ContainsKey( Variable2 ) )
				{
					Log.Error( $"Variable not found: {Variable1} or {Variable2}" );
					return false;
				}

				var value1 = (int)variables[Variable1];
				var value2 = (int)variables[Variable2];

				switch ( Operator )
				{
					case ComparisonOperator.Equal:
						return value1 == value2;
					case ComparisonOperator.NotEqual:
						return value1 != value2;
					case ComparisonOperator.GreaterThan:
						return value1 > value2;
					case ComparisonOperator.LessThan:
						return value1 < value2;
					case ComparisonOperator.GreaterThanOrEqual:
						return value1 >= value2;
					case ComparisonOperator.LessThanOrEqual:
						return value1 <= value2;
					default:
						return false;
				}
			}
		}

		public class ElseStatement : Statement
		{
		}

		public class ElseIfStatement : IfStatement
		{
		}

		public class EndIfStatement : Statement
		{
		}

		public List<Statement> Statements { get; set; } = new();

		/// <summary>
		///  Run the logic and return the line index to jump to
		/// </summary>
		/// <returns></returns>
		public int Run()
		{
			ParseLogic();

			var currentStatement = 0;
			var currentLine = Line.Index;
			var variables = Line.Parser.Variables;
			while ( currentStatement < Statements.Count )
			{
				var statement = Statements[currentStatement];
				if ( statement is ElseIfStatement elseIfStatement )
				{
					if ( elseIfStatement.Evaluate( variables ) )
					{
						currentStatement++;
						continue;
					}
				}
				else if ( statement is ElseStatement )
				{
					// jump to endif
					while ( !(Statements[currentStatement] is EndIfStatement) )
					{
						currentStatement++;
					}
				}
				else if ( statement is EndIfStatement )
				{
					// done
					break;
				}
				else if ( statement is IfStatement ifStatement )
				{
					if ( ifStatement.Evaluate( variables ) )
					{
						currentStatement++;
						continue;
					}
				}

				currentStatement++;
			}

			return currentLine;
		}

		private void ParseLogic()
		{
			foreach ( var logic in Logic )
			{
				if ( logic.Value.StartsWith( "if " ) )
				{
					var parts = logic.Value.Split( ' ' );
					if ( parts.Length != 4 )
					{
						Log.Error( $"Invalid if statement: {logic.Value}" );
						continue;
					}

					var variable1 = parts[1];
					var op = parts[2];
					var variable2 = parts[3];

					Statements.Add( new IfStatement
					{
						Variable1 = variable1, Operator = ParseOperator( op ), Variable2 = variable2
					} );
				}
				else if ( logic.Value == "else" )
				{
					Statements.Add( new ElseStatement() );
				}
				else if ( logic.Value.StartsWith( "elseif " ) )
				{
					var parts = logic.Value.Split( ' ' );
					if ( parts.Length != 4 )
					{
						Log.Error( $"Invalid elseif statement: {logic.Value}" );
						continue;
					}

					var variable1 = parts[1];
					var op = parts[2];
					var variable2 = parts[3];

					Statements.Add( new ElseIfStatement
					{
						Variable1 = variable1, Operator = ParseOperator( op ), Variable2 = variable2
					} );
				}
				else if ( logic.Value == "endif" )
				{
					Statements.Add( new EndIfStatement() );
				}
				else
				{
					Log.Error( $"Unknown logic statement: {logic.Value}" );
				}
			}
		}

		private ComparisonOperator ParseOperator( string op )
		{
			switch ( op )
			{
				case "==":
					return ComparisonOperator.Equal;
				case "!=":
					return ComparisonOperator.NotEqual;
				case ">":
					return ComparisonOperator.GreaterThan;
				case "<":
					return ComparisonOperator.LessThan;
				case ">=":
					return ComparisonOperator.GreaterThanOrEqual;
				case "<=":
					return ComparisonOperator.LessThanOrEqual;
				default:
					Log.Error( $"Unknown operator: {op}" );
					return ComparisonOperator.Equal;
			}
		}

		public LogicNode( DialogueLine line ) : base( line )
		{
		}
	}


	public class Choice
	{
		public string Text { get; set; }
		public int TargetIndex { get; set; }
		public string TargetLabel { get; set; }
	}

	public class DialogueLine
	{
		public DialogueParser Parser { get; set; }

		public int Index { get; set; }

		// public string Speaker { get; set; }
		// public string Text { get; set; }
		public BaseNode Node { get; set; }

		public DialogueLine()
		{
		}

		public DialogueLine( DialogueParser parser )
		{
			Parser = parser;
		}

		public DialogueLine( DialogueParser parser, BaseNode node )
		{
			Parser = parser;
			Node = node;
		}

		public string Print()
		{
			if ( Node is TextNode textNode )
			{
				return $"{textNode.Speaker}: {textNode.Body}";
			}
			else if ( Node is ChoiceNode choiceNode )
			{
				return $"{choiceNode.Speaker}: {choiceNode.Text}";
			}
			else if ( Node is LogicNode logicNode )
			{
				return $"Logic: {string.Join( ", ", logicNode.Logic )}";
			}
			else
			{
				return "Unknown node";
			}
		}
	}

	private string _text;
	private string[] _lines;
	private int _currentSymbolIndex;
	private int _currentLine;
	private int _currentIndent;
	private Dictionary<string, string> _meta = new();

	public Dictionary<string, object> Variables { get; set; } = new();

	private readonly string[] _comparisonOperators = { "==", "!=", ">", "<", ">=", "<=" };
	private readonly string[] _statements = { "if", "else", "elseif", "endif" };


	public void Load( string text )
	{
		_text = text;
		// _lines = _text.Split( '\n' );
		_lines = _text.Split( new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None );
		_currentSymbolIndex = 0;
		ParseMeta();
	}

	private void ParseMeta()
	{
		var meta = new Dictionary<string, string>();
		var i = 0;
		foreach ( var line in _lines )
		{
			Log.Info( $"Parsing meta: {line} ({line.Length})" );
			if ( line == "---" )
				break;

			var parts = line.Split( ':' );
			if ( parts.Length == 2 )
			{
				meta[parts[0].Trim()] = parts[1].Trim();
			}

			i++;
		}

		_meta = meta;
		_currentLine = i + 1;
	}

	public DialogueLine JumpTo( int index )
	{
		Log.Info( $"Jumping to line {index}" );
		_currentLine = index;
		return Next();
	}

	public DialogueLine Next()
	{
		_currentSymbolIndex = 0;

		var line = new DialogueLine( this );
		line.Index = _currentLine;

		// var indent = 0;


		/*while ( _currentSymbolIndex < _text.Length )
		{
			var symbol = _text[_currentSymbolIndex];

			// end of line
			if ( symbol == '\n' )
			{
				_currentSymbolIndex++;
				break;
			}

			// check for indent
			if ( !hasCheckedIndent )
			{
				if ( symbol == ' ' )
				{
					indent++;
					_currentSymbolIndex++;
					continue;
				}
				else
				{
					hasCheckedIndent = true;
				}
			}

			// check for choices
			if ( symbol == '-' && _text[_currentSymbolIndex + 1] == '>' )
			{
				line.Node = new ChoiceNode();
				line.Node.Indent = indent;
				_currentSymbolIndex += 2;

				// add first option
				var text = "";
				while ( _text[_currentSymbolIndex] != '\n' && _currentSymbolIndex < _text.Length )
				{
					text += _text[_currentSymbolIndex];
					_currentSymbolIndex++;
				}
				var choice = new Choice();
				choice.Text = text.Trim();
				((ChoiceNode)line.Node).Choices.Add( choice );

				// find next option



			}




		}*/

		var activeLine = _lines.ElementAtOrDefault( _currentLine );
		if ( activeLine == null )
		{
			Log.Info( $"End of dialogue reached" );
			return null;
		}

		Log.Info( $"Checking line {_currentLine}: {activeLine}" );

		// check for indent
		_currentIndent = GetIndent( activeLine );

		activeLine = activeLine.Substring( _currentIndent );

		// check for choices
		if ( GetString( activeLine, 0, 2 ) == "->" )
		{
			Log.Info( $"Checking for choices at line {_currentLine} with indent {_currentIndent}" );
			line.Node = new ChoiceNode( line );
			line.Node.Indent = _currentIndent;
			_currentSymbolIndex += 2;

			// add first option
			/*var text = "";
			while ( activeLine[_currentSymbolIndex] != '\n' && _currentSymbolIndex < activeLine.Length )
			{
				text += activeLine[_currentSymbolIndex];
				_currentSymbolIndex++;
			}*/
			var text = GetInterpolatedString( activeLine, _currentSymbolIndex,
				activeLine.Length - _currentSymbolIndex );
			_currentSymbolIndex += text.Length;

			var choice = new Choice();
			choice.Text = text.Trim();
			choice.TargetIndex = _currentLine + 1;
			((ChoiceNode)line.Node).Choices.Add( choice );

			// find the rest of the options
			/*var lineSearch = _currentLine + 1;
			while ( true )
			{
				var nextLine = _lines.ElementAtOrDefault( lineSearch );
				if ( nextLine == null )
					break;

				var nextIndent = GetIndent( nextLine );
				if ( nextIndent != _currentIndent )
					break;

				if ( GetString( nextLine, nextIndent, 2 ) != "->" )
				{
					break;
				}

				_currentSymbolIndex = nextIndent + 2;
				text = GetInterpolatedString( nextLine, _currentSymbolIndex, nextLine.Length - _currentSymbolIndex );
				_currentSymbolIndex += text.Length;

				choice = new Choice();
				choice.Text = text.Trim();
				choice.TargetIndex = lineSearch + 1;
				((ChoiceNode)line.Node).Choices.Add( choice );
			}*/

			// find next line with same indent
			var searchLine = _currentLine + 1;
			while ( true )
			{
				var nextLine = GetNextLineWithIndent( searchLine, _currentIndent );
				if ( nextLine == -1 )
					break;

				var nextLineText = _lines[nextLine];
				if ( GetString( nextLineText, _currentIndent, 2 ) != "->" )
					break;

				_currentSymbolIndex = _currentIndent + 2;
				text = GetInterpolatedString( nextLineText, _currentSymbolIndex,
					nextLineText.Length - _currentSymbolIndex );
				_currentSymbolIndex += text.Length;

				choice = new Choice();
				choice.Text = text.Trim();
				choice.TargetIndex = nextLine + 1;
				((ChoiceNode)line.Node).Choices.Add( choice );

				searchLine = nextLine + 1;
			}

			foreach ( var c in ((ChoiceNode)line.Node).Choices )
			{
				Log.Info( $"Parsed choice: {c.Text} -> {c.TargetIndex}" );
			}

			return line;
		}


		// check for logic
		if ( GetString( activeLine, 0, 2 ) == "<<" )
		{
			Log.Info( $"Checking for logic at line {_currentLine} with indent {_currentIndent}" );
			_currentSymbolIndex += 2;

			// find the end of the logic comparison
			var logic = "";
			while ( GetString( activeLine, _currentSymbolIndex, 2 ) != ">>" )
			{
				logic += activeLine[_currentSymbolIndex];
				_currentSymbolIndex++;
			}

			_currentSymbolIndex += 2;

			Log.Info( $"Found logic: {logic}" );

			line.Node = new LogicNode( line );
			((LogicNode)line.Node).Logic.Add( _currentLine, logic.Trim() );

			if ( logic.Trim().StartsWith( "if " ) )
			{
				Log.Info( $"Found if statement: {logic}" );

				var searchLine = _currentLine + 1;
				while ( true )
				{
					var nextLine = GetNextLineWithIndent( searchLine, _currentIndent );
					if ( nextLine == -1 )
						break;

					var nextLineText = _lines[nextLine];

					if ( GetString( nextLineText, _currentIndent, 2 ) == "<<" )
					{
						Log.Info( $"Found nested logic at line {nextLine}" );
						_currentSymbolIndex = _currentIndent + 2;
						var nestedLogic = "";
						while ( GetString( nextLineText, _currentSymbolIndex, 2 ) != ">>" )
						{
							nestedLogic += nextLineText[_currentSymbolIndex];
							_currentSymbolIndex++;
						}

						_currentSymbolIndex += 2;
						((LogicNode)line.Node).Logic.Add( nextLine, nestedLogic.Trim() );
					}
					else
					{
						break;
					}

					searchLine = nextLine + 1;
				}
			}
			else
			{
				Log.Error( $"Unknown logic statement: {logic}" );
			}

			foreach ( var l in ((LogicNode)line.Node).Logic )
			{
				Log.Info( $"Parsed logic: {l}" );
			}

			var result = ((LogicNode)line.Node).Run();

			JumpTo( result );

			return line;
		}

		line.Node = new TextNode( line );

		// check for speaker
		Log.Info( $"Checking for speaker at line {_currentLine} with indent {_currentIndent}" );
		var speaker = "";
		while ( activeLine[_currentSymbolIndex] != ':' && _currentSymbolIndex < activeLine.Length )
		{
			speaker += activeLine[_currentSymbolIndex];
			_currentSymbolIndex++;
		}

		Log.Info( $"Found speaker: {speaker}" );

		_currentSymbolIndex++; // skip ':'
		// line.Speaker = ParseVariables( speaker.Trim() );
		// Log.Info( $"Parsed speaker: {line.Speaker}" );
		line.Node.Speaker = ParseVariables( speaker.Trim() );

		// check for text
		Log.Info( $"Checking for text at line {_currentLine} with indent {_currentIndent}" );
		var linetext = "";
		while ( _currentSymbolIndex < activeLine.Length )
		{
			linetext += activeLine[_currentSymbolIndex];
			_currentSymbolIndex++;
		}

		// line.Text = ParseVariables( linetext.Trim() );
		// Log.Info( $"Parsed line {_currentLine}: {line.Speaker} - {line.Text}" );
		((TextNode)line.Node).Body = ParseVariables( linetext.Trim() );

		_currentLine++;
		return line;
	}

	private int GetIndent( string line )
	{
		var indent = 0;
		while ( line[indent] == ' ' )
		{
			indent++;
		}

		return indent;
	}

	private int GetNextLineWithIndent( int start, int indent )
	{
		for ( var i = start; i < _lines.Length; i++ )
		{
			if ( GetIndent( _lines[i] ) == indent )
				return i;

			// if we go back to a lower indent, we've gone too far
			if ( GetIndent( _lines[i] ) < indent )
				return -1;
		}

		return -1;
	}

	private string GetString( string line, int start, int length )
	{
		return new string( line.Skip( start ).Take( length ).ToArray() );
	}

	private string GetInterpolatedString( string line, int start, int length )
	{
		var str = new string( line.Skip( start ).Take( length ).ToArray() );
		return ParseVariables( str );
	}

	private string ParseVariables( string text )
	{
		foreach ( var variable in Variables )
		{
			text = text.Replace( "{$" + variable.Key + "}", variable.Value.ToString() );
		}

		// warn for missing variables
		if ( text.Contains( "{$" ) )
		{
			Log.Warning( $"Missing variables in text: {text}" );
		}

		return text;
	}

	[Button( "Test" )]
	public void Test()
	{
		Load( SampleDialogue );
		Variables["NpcName"] = "Bob";
		Variables["ItemName"] = "Sword";
		Variables["ItemPrice"] = 10;
		/*while ( true )
		{
			var line = Next();
			if ( line == null )
				break;
			Log.Info( $"[{line.Speaker}] {line.Text}" );
		}*/

		var line = Next();
		Log.Info( $"Result1: {line.Print()}" );

		var line2 = Next();
		Log.Info( $"Result2: {line2.Print()}" );

		var choice = ((ChoiceNode)line2.Node).Choices[0];
		Log.Info( $"Choice: {choice.Text} -> {choice.TargetIndex}" );

		var line3 = JumpTo( choice.TargetIndex );
		Log.Info( $"Result3: {line3.Print()}" );
	}
}