Code/Systems/Player/ScriptPlayer.Labels.cs
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Sandbox;
using Sandbox.Audio;
using VNBase.UI;
using VNBase.Assets;
using Script = VNScript.Script;
using Sound = VNBase.Assets.Sound;

namespace VNBase;

public sealed partial class ScriptPlayer
{
	/// <summary>
	/// Current text segment index within the active label
	/// </summary>
	private int _currentTextIndex;
	
	private async void SetLabel( Script.Label label )
	{
		try
		{
			// Clean up any existing text effect before starting a new one
			SkipDialogueEffect();
			ActiveLabel = label;
			_currentTextIndex = 0; // Reset text index when setting new label
			
			if ( LoggingEnabled )
			{
				Log.Info( $"Loading Label {label.Name}" );
			}
			
			// Execute code blocks BEFORE processing dialogues
			// This ensures variables are set before dialogue tries to reference them
			if ( label.AfterLabel?.CodeBlocks is not null )
			{
				var environment = ActiveScript?.GetEnvironment() ?? _environment;
				
				foreach ( var codeBlock in label.AfterLabel.CodeBlocks )
				{
					try
					{
						codeBlock.Execute( environment );
					}
					catch ( Exception e )
					{
						Log.Error( $"Error executing code block in label {label.Name}: {e.Message}" );
					}
				}
			}
			
			State.Characters.Clear();
			label.Characters.ForEach( State.Characters.Add );
			
			foreach ( var sound in label.Assets.OfType<Sound>() )
			{
				State.Sounds.Add( sound );
				
				if ( sound is Music )
				{
					State.BackgroundMusic = MusicPlayer.Play( FileSystem.Mounted, sound.EventName );
					State.BackgroundMusic.TargetMixer = Mixer.FindMixerByName( "Music" );
				}
				else
				{
					if ( string.IsNullOrEmpty( sound.MixerName ) )
					{
						sound.Play();
					}
					else
					{
						sound.Play( sound.MixerName );
					}
				}
				
				if ( LoggingEnabled )
				{
					Log.Info( $"Played SoundAsset {sound} from label {label.Name}" );
				}
			}
			
			try
			{
				State.Background = label.Assets.OfType<Background>().SingleOrDefault()?.Path;
			}
			catch ( InvalidOperationException )
			{
				Log.Error( $"There can only be one {nameof(Background)} in label {label.Name}!" );
				State.Background = null;
			}
			
			if ( _currentTextIndex == 0 )
			{
				OnLabelSet?.Invoke( label );
			}
			
			// Display the current text segment
			await DisplayCurrentTextSegment();
		}
		catch ( Exception e )
		{
			Log.Error( e.Message );
		}
	}
	
	private async Task DisplayCurrentTextSegment()
	{
		if ( ActiveLabel is null || ActiveLabel.Dialogues.Count == 0 )
		{
			// No dialogues to display - go straight to after label logic
			if ( ActiveLabel?.AfterLabel is not null )
			{
				ExecuteAfterLabel();
			}
			
			return;
		}
		
		if ( _currentTextIndex >= ActiveLabel.Dialogues.Count )
		{
			Log.Error( $"Text index {_currentTextIndex} out of range for label {ActiveLabel.Name}" );
			return;
		}
		
		var activeDialogue = ActiveLabel.Dialogues[_currentTextIndex];
		State.SpeakingCharacter = activeDialogue.Speaker;
		
		// Use the same environment as code blocks
		var environment = ActiveScript?.GetEnvironment() ?? _environment;
		
		if ( Settings.TextEffectEnabled )
		{
			_cts = new CancellationTokenSource();
			
			try
			{
				var formattedText = activeDialogue.Text.Format( environment );
				await Settings.TextEffect.Play( formattedText, (int)Settings.TextEffectSpeed, UpdateDialogueText, _cts.Token );
				EndDialogue( activeDialogue, ActiveLabel );
			}
			catch ( OperationCanceledException )
			{
				EndDialogue( activeDialogue, ActiveLabel );
			}
		}
		else
		{
			// Skip the text effect entirely
			EndDialogue( activeDialogue, ActiveLabel );
		}
	}
	
	/// <summary>
	/// Advances to the next text segment in the current label, or executes AfterLabel if there are no more segments
	/// </summary>
	// ReSharper disable once MemberCanBePrivate.Global
	public void AdvanceText()
	{
		if ( ActiveLabel is null )
		{
			ExecuteAfterLabel();
			return;
		}
		
		_currentTextIndex++;
		OnTextAdvanced?.Invoke( _currentTextIndex );
		
		// If we have more text segments, display the next one
		if ( _currentTextIndex < ActiveLabel.Dialogues.Count )
		{
			_ = DisplayCurrentTextSegment();
		}
		else
		{
			// No more text segments
			ExecuteAfterLabel();
		}
	}
	
	private void ExecuteAfterLabel()
	{
		if ( ActiveScript is null || ActiveLabel is null )
		{
			Log.Error( $"Unable to execute the AfterLabel, there is either no active script or label!" );
			
			return;
		}
		
		var afterLabel = ActiveLabel.AfterLabel;
		
		if ( afterLabel is null )
		{
			return;
		}
		
		foreach ( var sound in State.Sounds.Where( sound => sound is not Music ).ToArray() )
		{
			sound.Stop();
			State.Sounds.Remove( sound );
		}
		
		// Do not let us continue if there is an empty input box.
		var hasInput = ActiveLabel.ActiveInput is not null;
		
		if ( hasInput && Hud is not null )
		{
			var input = Hud.GetSubPanel<TextInput>();
			
			if ( input is null )
			{
				return;
			}
			
			if ( string.IsNullOrWhiteSpace( input.Entry?.Text ) )
			{
				return;
			}
		}
		
		if ( afterLabel.IsLastLabel )
		{
			UnloadScript();
			return;
		}
		
		if ( !string.IsNullOrEmpty( afterLabel.ScriptPath ) )
		{
			LoadScript( afterLabel.ScriptPath );
			return;
		}
		
		if ( afterLabel.TargetLabel is null )
		{
			return;
		}
		
		if ( _activeDialogue is null )
		{
			Log.Error( "There is no active dialogue set, unable to switch active labels!" );
			return;
		}
		
		SetLabel( _activeDialogue.Labels[afterLabel.TargetLabel] );
	}
	
	private async void EndDialogue( Script.Dialogue dialogue, Script.Label label )
	{
		try
		{
			if ( ActiveScript is null || ActiveLabel is null )
			{
				return;
			}
			
			// Use the same environment as code blocks
			var environment = ActiveScript.GetEnvironment();
			
			// Check if this is the last dialogue in the label
			var isLastDialogue = _currentTextIndex >= ActiveLabel.Dialogues.Count - 1;
			
			// If we are in Automatic Mode and there are no choices, check if we should auto-advance
			if ( IsAutomaticMode && label.Choices.Count == 0 && !isLastDialogue )
			{
				try
				{
					await Task.DelaySeconds( Settings.AutoDelay );
					
					// Auto-advance to next text segment or after label
					AdvanceText();
					
					return;
				}
				catch ( OperationCanceledException )
				{
					State.IsDialogueFinished = false;
				}
			}
			
			var formattedText = dialogue.Text.Format( environment );
			if ( State.DialogueText != formattedText )
			{
				State.DialogueText = formattedText;
			}
			
			// Only set choices if this is the last dialogue
			if ( isLastDialogue )
			{
				State.Choices = ActiveLabel.Choices;
			}
			else
			{
				State.Choices = [];
			}
			
			State.IsDialogueFinished = true;
			AddToDialogueHistory( dialogue, label );
		}
		catch ( Exception e )
		{
			Log.Error( e.Message );
		}
	}
	
	private void UpdateDialogueText( string text )
	{
		State.DialogueText = text;
		State.IsDialogueFinished = false;
	}
}