Transition.cs
using System;

namespace Sandbox.States;

public sealed class Transition : IComparable<Transition>, IValid
{
	private float? _minDelay;
	private float? _maxDelay;

	private Func<bool>? _condition;
	private string? _message;

	/// <summary>
	/// The state machine containing this transition.
	/// </summary>
	public StateMachineComponent StateMachine => Source.StateMachine;

	/// <summary>
	/// Unique ID of this transition in the <see cref="StateMachineComponent"/>.
	/// </summary>
	public int Id { get; }

	/// <summary>
	/// The state this transition originates from.
	/// </summary>
	public State Source { get; }

	/// <summary>
	/// The destination of this transition.
	/// </summary>
	public State Target { get; }

	/// <summary>
	/// Does this transition still belong to a state.
	/// </summary>
	public bool IsValid { get; internal set; }

	/// <summary>
	/// This transition has either a <see cref="Condition"/> or <see cref="Message"/>.
	/// </summary>
	public bool IsConditional => Condition is not null || Message is not null;

	/// <summary>
	/// This transition has either a min or max delay.
	/// </summary>
	public bool HasDelay => MinDelay is not null || MaxDelay is not null;

	public RealTimeSince LastTransitioned { get; internal set; }

	internal Transition( int id, State source, State target )
	{
		Source = source;
		Target = target;
		Id = id;
	}

	/// <summary>
	/// Optional delay before this transition can be taken. If <see cref="MaxDelay"/> is also provided,
	/// but without a <see cref="Condition"/>, then a uniformly random delay is selected between min and max.
	/// </summary>
	public float? MinDelay
	{
		get => _minDelay;
		set
		{
			_minDelay = value;
			Source.InvalidateTransitions();
		}
	}

	/// <summary>
	/// Optional delay until this transition can no longer be taken. If <see cref="MinDelay"/> is also provided,
	/// but without a <see cref="Condition"/>, then a uniformly random delay is selected between min and max.
	/// </summary>
	public float? MaxDelay
	{
		get => _maxDelay;
		set
		{
			_maxDelay = value;
			Source.InvalidateTransitions();
		}
	}

	public (float Min, float Max) DelayRange
	{
		get
		{
			var min = Math.Max( 0f, MinDelay ?? 0f );
			var max = Math.Max( MaxDelay ?? (IsConditional ? float.PositiveInfinity : min), min );

			return (min, max);
		}
	}

	/// <summary>
	/// Optional message string that will trigger this condition.
	/// Messages are sent with <see cref="StateMachineComponent.SendMessage"/>.
	/// </summary>
	public string? Message
	{
		get => _message;
		set
		{
			_message = value;
			Source.InvalidateTransitions();
		}
	}

	/// <summary>
	/// Optional condition to evaluate. If provided, the transition will be taken
	/// as soon as the condition evaluates to true, given we are between <see cref="MinDelay"/>
	/// and <see cref="MaxDelay"/>.
	/// </summary>
	public Func<bool>? Condition
	{
		get => _condition;
		set
		{
			_condition = value;
			Source.InvalidateTransitions();
		}
	}

	/// <summary>
	/// Action performed when this transition is taken.
	/// </summary>
	public Action? OnTransition { get; set; }

	public void Remove()
	{
		if ( !IsValid ) return;
		StateMachine.RemoveTransition( this );
	}

	public int CompareTo( Transition? other )
	{
		if ( other is null ) return 1;

		var minDelayCompare = (MinDelay ?? float.PositiveInfinity).CompareTo( other.MinDelay ?? float.PositiveInfinity );
		if ( minDelayCompare != 0 ) return minDelayCompare;

		var maxDelayCompare = (MaxDelay ?? float.PositiveInfinity).CompareTo( other.MaxDelay ?? float.PositiveInfinity );
		if ( maxDelayCompare != 0 ) return maxDelayCompare;

		var conditionCompare = (Condition is null).CompareTo( other.Condition is null );
		if ( conditionCompare != 0 ) return conditionCompare;

		var messageCompare = (Message is null).CompareTo( other.Message is null );
		if ( messageCompare != 0 ) return messageCompare;

		return Target.Id.CompareTo( other.Target.Id );
	}

	internal record Model( int Id, int SourceId, int TargetId, float? Delay, float? MinDelay, float? MaxDelay, string? Message, Func<bool>? Condition, Action? OnTransition );

	internal Model Serialize()
	{
		return new Model( Id, Source.Id, Target.Id, null, MinDelay, MaxDelay, Message, Condition, OnTransition );
	}

	internal void Deserialize( Model model )
	{
		if ( model.Delay is not null )
		{
			MinDelay = model.Delay;
			MaxDelay = null;
		}
		else
		{
			MinDelay = model.MinDelay;
			MaxDelay = model.MaxDelay;
		}

		Message = model.Message;
		Condition = model.Condition;
		OnTransition = model.OnTransition;
	}

	public void CopyFrom( Transition other )
	{
		Deserialize( other.Serialize() );
	}
	public override string ToString()
	{
		return $"{{ Id = {Id}, Source = {Source}, Target = {Target} }}";
	}
}