ChitChat/Components/DialogueSystemComponent.cs
using Sandbox;
using System;
using System.Collections.Generic;

namespace ChitChat;

[Title("Dialogue System")][Category("ChitChat")][Icon("forum")]
public sealed class DialogueSystemComponent : Component
{
	[Header("Input")]
	[Property][InputAction] public List<string> Inputs { get; set; } = ["attack1", "jump"];

	[Header("Data")]
	[Property][ResourceType(".chit")] public DialogueData DialogueData { get; set; }

	[Header("Components")]
	[Property] public DialoguePanelComponent UIComponent { get; set; }
	[Property][Description("Will automatically be added if not set.")][RequireComponent]
	public DialogueWriterComponent Writer { get; set; }
	[Property][Description("Will automatically be added if not set.")][RequireComponent]
	public DialogueAudioComponent Audio { get; set; }

	[Header("Settings")]
	[Property] private bool PlayOnStart { get; set; } = true;
	[Property][Description("How long it takes before another input event is registered.")] 
	private float InputDelay { get; set; } = 0.3f;

	[Header("Mouse")]
	[Property][Description("If the cursor should be visable during dialogue.")] 
	private bool MouseVisableInDialogue { get; set; } = true;
	[Property][Description("If the cursor should be visable after the dialogue has ended.")] 
	private bool MouseVisableAfterDialogueEnded { get; set; } = false;

	public event Action<DialogueData> OnDialogueStarted;
	public event Action<DialogueData> OnDialogueEnded;

	private DialogueActionBase _currentDialogueData;
	private DialogueInputState _currentInputState;

	private bool _isRunning = false;
	private int _currentDialogueIndex = -1;
	private float _inputTimer = 0.0f;
	
	private float _delayTimer = 0.0f;
	private float _inputDelay = 0.0f;

	protected override void OnStart()
	{
		base.OnStart();

		if (!UIComponent.IsValid())
		{
			Log.Error("No " + nameof(DialoguePanelComponent) + " on Dialogue System!");
			return;
		}

		if (!Writer.IsValid())
		{
			Writer = AddComponent<DialogueWriterComponent>();
		}

		if (!Audio.IsValid())
		{
			Audio = AddComponent<DialogueAudioComponent>();
		}

		Writer.SetUIComponent(UIComponent);
		Writer.onCharacterAdded += Audio.OnCharacterAdded;
		Writer.onWritingDone += Audio.StopSounds;

		if (PlayOnStart)
			StartDialogue();
	}

	protected override void OnDestroy()
	{
		Writer.onCharacterAdded -= Audio.OnCharacterAdded;
		Writer.onWritingDone -= Audio.StopSounds;
	}

	public void StartDialogue(DialogueData data)
	{
		DialogueData = data;
		StartDialogue();
	}
	
	public void StartDialogue()
	{
		if (DialogueData == null)
		{
			Log.Error("No Dialogue data on Dialogue System!");
			return;
		}

		UIComponent.Activate();

		if(UIComponent.DialoguePanel == null) return;

		UIComponent.DialoguePanel.onChoiceClicked = OnChoiceSelected;

		if(MouseVisableInDialogue)
			Mouse.Visibility = MouseVisibility.Visible;
		else
			Mouse.Visibility = MouseVisibility.Hidden;

		ResetDialogue();
		NextDialogue();

		_isRunning = true;
		OnDialogueStarted?.Invoke(DialogueData);
	}

	/// <summary>
	/// Continues the dialogue if there are more entries in the DialogueData.
	/// </summary>
	/// <param name="nextDialogue">If it should start after the last dialogue played.</param>
	public void ContinueDialogue(bool nextDialogue = true)
	{
		if(nextDialogue)
			_currentDialogueIndex++;

		StartDialogue();
	}

	public void StopDialogue()
	{
		UIComponent.Deactivate();
		Writer.StopWriting();
		
		Audio.StopSounds();

		if(MouseVisableAfterDialogueEnded)
			Mouse.Visibility = MouseVisibility.Visible;
		else
			Mouse.Visibility = MouseVisibility.Hidden;

		OnDialogueEnded?.Invoke(DialogueData);
	}

	public void ResetDialogue()
	{
		_currentDialogueIndex = -1;
		_delayTimer = 0;
		_inputTimer = 0;
		_inputDelay = 0;
		_currentInputState = DialogueInputState.None;
	}

	public void SetInputState(DialogueInputState state, float delay) 
	{
		_inputDelay = delay;
		_currentInputState = state;
	}

	public void SetChoices(List<Choice> choices)
	{
		UIComponent.SetChoices(choices);
	}

	public void OnEventAction(string eventName)
	{
		IEventActionListener.Post(x => x.OnEventAction(eventName));
	}

	protected override void OnUpdate()
	{
		if (_inputTimer < InputDelay)
			_inputTimer += Time.Delta;

		if (!_isRunning || _currentInputState == DialogueInputState.None)
			return;

		if (_inputTimer >= InputDelay)
		{
			if(_currentInputState == DialogueInputState.WaitForInput)
			{
				if (OnInput())
				{
					if (!Writer.IsWriting)
						NextDialogue();
					else
					{
						Writer.CompleteWriting();
						_inputTimer = 0;
					}
				}
			}
			else if(_currentInputState == DialogueInputState.WaitForSeconds)
			{
				if(_delayTimer >= _inputDelay)
				{
					NextDialogue();
				}
				else
					_delayTimer += Time.Delta;
			}
			else if(_currentInputState == DialogueInputState.WaitForWriteEffect && !Writer.IsWriting)
			{
				if (OnInput())
				{
					NextDialogue();
				}
			}
		}
	}

	private void NextDialogue()
	{
		_currentDialogueIndex++;
		_delayTimer = 0;
		_inputTimer = 0;
		_inputDelay = 0;
		_currentInputState = DialogueInputState.None;

		Audio.StopSounds();

		if (_currentDialogueIndex < DialogueData.DialogueDatas.Count)
		{
			if (_currentDialogueData != null)
				_currentDialogueData.OnExit();

			_currentDialogueData = DialogueData.DialogueDatas[_currentDialogueIndex];
			while (!_currentDialogueData.OnEnter(this, UIComponent, Writer, Audio))
			{
				_currentDialogueIndex++;
				if (_currentDialogueIndex < DialogueData.DialogueDatas.Count)
				{
					_currentDialogueData = DialogueData.DialogueDatas[_currentDialogueIndex];
				}
				else
				{
					StopDialogue();
					break;
				}
			}
		}
		else
		{
			StopDialogue();
		}
	}

	private void OnChoiceSelected(DialogueData data)
	{
		if (_inputTimer >= InputDelay)
		{
			if (data.IsValid())
				StartDialogue(data);
			else
				NextDialogue();
		}
	}

	private bool OnInput()
	{
		foreach (string action in Inputs)
		{
			if(Input.Released(action))
				return true;
		}

		return false;
	}
}