Code/Internals/IProducer.cs
using Sandbox.Reactivity.Internals.Runtimes;

namespace Sandbox.Reactivity.Internals;

/// <summary>
/// A reactive object that produces a value and can be depended upon by a <see cref="IReaction" />.
/// </summary>
internal interface IProducer : IReactiveObject
{
	/// <summary>
	/// The list of <see cref="IReaction" />s that have added this producer as a dependency.
	/// </summary>
	List<IReaction> Reactions { get; }

	/// <summary>
	/// The <see cref="Runtime.Version" /> at which this producer's value last changed. When a producer updates its
	/// write version, it will increment the global version.
	/// </summary>
	uint WriteVersion { get; }

	/// <summary>
	/// The producer's raw current value without any reactivity tracking.
	/// </summary>
	object? NonReactiveValue { get; set; }

	/// <summary>
	/// Adds a reaction to notify when this producer's value changes.
	/// </summary>
	/// <param name="reaction">The reaction to add.</param>
	void AddReaction(IReaction reaction);

	/// <summary>
	/// Removes a reaction from being notified when this producer's value changes.
	/// </summary>
	/// <param name="reaction">The reaction to remove.</param>
	void RemoveReaction(IReaction reaction);
}

/// <inheritdoc />
/// <typeparam name="T">The type of this producer's value.</typeparam>
internal interface IProducer<out T> : IProducer
{
	/// <summary>
	/// The producer's current value.
	/// </summary>
	T Value { get; }
}

/// <inheritdoc />
/// <remarks>This producer can be modified directly by users to cause reactions.</remarks>
internal interface IWritableProducer<in T> : IProducer
{
	/// <summary>
	/// Sets this producer's current value.
	/// </summary>
	T Value { set; }
}

internal static class ProducerExtensions
{
	extension(IProducer producer)
	{
		/// <summary>
		/// Adds this producer as a dependency to any current reaction being tracked.
		/// </summary>
		internal void TrackRead()
		{
			if (!Reactive.Runtime.IsUntracking && Reactive.Runtime.CurrentReaction is { } reaction)
			{
				producer.AddReaction(reaction);
				reaction.AddDependency(producer);
			}
		}

		/// <summary>
		/// Calls <see cref="IReaction.OnDependencyChanged" /> on any reactions that this producer is a dependency of.
		/// Reactions can schedule themselves to run and/or also propagate reactivity to their children if they are
		/// producers themselves.
		/// </summary>
		/// <param name="state">
		/// The state that should be assigned to reactions. This should be <see cref="ReactionState.Stale" />
		/// if you're propagating due to a direct change. Producers that change their value based on a reaction should
		/// propagate <see cref="ReactionState.PossiblyStale" /> to allow for its value to be evaluated only when
		/// needed.
		/// </param>
		internal void PropagateReactivity(ReactionState state = ReactionState.Stale)
		{
			foreach (var reaction in producer.Reactions)
			{
				// reactions will check if the new state is applicable (i.e. they're not less stale than the new state)
				reaction.OnDependencyChanged(state);
			}
		}
	}
}