Editor/TransitionDelay.cs
using Editor.NodeEditor;
using System;

namespace Sandbox.States.Editor;

public record TransitionDelay( TransitionItem Item ) : ILabelSource
{
	protected StateMachineView View => Item.Source.View;
	public Transition Transition => Item.Transition!;

	public string Title => "Timing";

	public string? Description => Transition.HasDelay ? FormatDelayLong() : null;

	public string? Icon => Transition.HasDelay ? "alarm" : null;
	public string? Text => Transition.HasDelay ? FormatDelayShort() : null;

	public void BuildAddContextMenu( global::Editor.Menu menu )
	{
		if ( Transition.MinDelay is null )
		{
			BuildAddMinDelayMenu( menu );
		}

		if ( Transition.MaxDelay is null )
		{
			BuildAddMaxDelayMenu( menu );
		}
	}

	public void BuildModifyContextMenu( global::Editor.Menu menu )
	{
		if ( Transition.MinDelay is not null )
		{
			BuildModifyMinDelayMenu( menu );
		}

		if ( Transition.MaxDelay is not null )
		{
			BuildModifyMaxDelayMenu( menu );
		}
	}

	public bool IsValid => Transition.HasDelay;

	private string FormatDelayShort()
	{
		var (min, max) = Transition.DelayRange;

		if ( min <= 0f && float.IsPositiveInfinity( max ) )
		{
			return "N/A";
		}

		return min >= max ? $"={FormatDuration( min )}"
			: float.IsPositiveInfinity( max ) ? $">{FormatDuration( min )}"
			: min <= 0f ? $"<{FormatDuration( max )}"
			: $"{FormatDuration( min )} - {FormatDuration( max )}";
	}

	private string FormatDelayLong()
	{
		var (min, max) = Transition.DelayRange;
		var hasCondition = Transition.IsConditional;

		if ( min <= 0f && float.IsPositiveInfinity( max ) )
		{
			return "Can be taken at any time.";
		}

		return min >= max
			? $"Only taken after exactly <b>{FormatDuration( min )}</b>."
			: hasCondition
				? float.IsPositiveInfinity( max )
					? $"Taken as soon as a condition is met after at least <b>{FormatDuration( min )}</b>."
					: min <= 0f
						? $"Taken as soon as a condition is met before at most <b>{FormatDuration( max )}</b>."
						: $"Taken as soon as a condition is met after between <b>{FormatDuration( min )}</b> and <b>{FormatDuration( max )}</b>."
				: $"Taken at a random time between <b>{FormatDuration( min )}</b> and <b>{FormatDuration( max )}</b>.";
	}

	private static string FormatDuration( float seconds )
	{
		if ( float.IsPositiveInfinity( seconds ) )
		{
			return "\u221e";
		}

		if ( seconds < 0.001f )
		{
			return "0s";
		}

		var timeSpan = TimeSpan.FromSeconds( seconds );
		var result = "";

		if ( timeSpan.Hours > 0 )
		{
			result += $"{timeSpan.Hours}h";
		}

		if ( timeSpan.Minutes > 0 )
		{
			result += $"{timeSpan.Minutes}m";
		}

		if ( timeSpan.Seconds > 0 )
		{
			result += $"{timeSpan.Seconds}s";
		}

		if ( timeSpan.Milliseconds > 0 )
		{
			result += $"{timeSpan.Milliseconds}ms";
		}

		return result;
	}

	private void BuildAddMinDelayMenu( global::Editor.Menu menu )
	{
		menu.AddMenu( "Add Minimum Time", "hourglass_top" ).AddLineEdit( "Seconds", value: "1", autoFocus: true, onSubmit:
			delayStr =>
			{
				if ( !float.TryParse( delayStr, out var seconds ) || seconds <= 0f )
				{
					return;
				}

				View.LogEdit( "Transition Delay Added" );

				Transition.MinDelay = seconds;
				Item.ForceUpdate();
			} );
	}

	private void BuildModifyMinDelayMenu( global::Editor.Menu menu )
	{
		var minDelay = Transition.MinDelay!.Value;

		menu.AddHeading( "Minimum Time" );
		menu.AddLineEdit( "Seconds", value: minDelay.ToString( "R" ), autoFocus: false, onSubmit:
			delayStr =>
			{
				if ( !float.TryParse( delayStr, out var seconds ) ) return;

				View.LogEdit( "Transition Delay Changed" );

				Transition.MinDelay = seconds > 0f ? seconds : null;
				Item.ForceUpdate();
			} );

		menu.AddOption( "Clear", "clear", action: () =>
		{
			View.LogEdit( "Transition Delay Removed" );

			Transition.MinDelay = null;
			Item.ForceUpdate();
		} );
	}

	private void BuildAddMaxDelayMenu( global::Editor.Menu menu )
	{
		menu.AddMenu( "Add Maximum Time", "hourglass_bottom" ).AddLineEdit( "Seconds", value: (Transition.MinDelay ?? 1f).ToString( "R" ), autoFocus: true, onSubmit:
			delayStr =>
			{
				if ( !float.TryParse( delayStr, out var seconds ) || seconds < (Transition.MinDelay ?? 0f) ) return;

				View.LogEdit( "Transition Delay Added" );

				Transition.MaxDelay = seconds;
				Item.ForceUpdate();
			} );
	}

	private void BuildModifyMaxDelayMenu( global::Editor.Menu menu )
	{
		var maxDelay = Transition.MaxDelay!.Value;

		menu.AddHeading( "Maximum Time" );
		menu.AddLineEdit( "Seconds", value: maxDelay.ToString( "R" ), autoFocus: false, onSubmit:
			delayStr =>
			{
				if ( !float.TryParse( delayStr, out var seconds ) ) return;

				View.LogEdit( "Transition Delay Changed" );

				Transition.MaxDelay = seconds < Transition.MinDelay ? null : seconds;
				Item.ForceUpdate();
			} );

		menu.AddOption( "Clear", "clear", action: () =>
		{
			View.LogEdit( "Transition Delay Removed" );

			Transition.MaxDelay = null;
			Item.ForceUpdate();
		} );
	}

	public void Delete()
	{
		Transition.MinDelay = null;
		Transition.MaxDelay = null;
	}

	public void DoubleClick()
	{
		var menu = new global::Editor.Menu { DeleteOnClose = true };

		BuildModifyContextMenu( menu );

		menu.OpenAtCursor( true );
	}
}