Editor/MovieMaker/Session/Playback.cs
using Sandbox.MovieMaker;

namespace Editor.MovieMaker;

#nullable enable
public sealed partial class Session
{
	private bool _isPlaying;
	private bool _isLooping = true;
	private float _timeScale = 1f;
	private bool _syncPlayback = true;

	private MovieTime? _lastPlayerPosition;
	private bool _applyNextFrame;
	private MovieTime _lastAppliedTime;

	public bool IsOpenInEditor => Editor.Session == this;

	public bool IsPlaying
	{
		get => IsEditorScene ? _isPlaying : Player.IsPlaying;
		set
		{
			if ( IsEditorScene ) _isPlaying = value;
			else Player.IsPlaying = value;
		}
	}

	public bool IsLooping
	{
		get => IsEditorScene ? _isLooping : Player.IsLooping;
		set
		{
			if ( IsEditorScene ) _isLooping = Cookies.IsLooping = value;
			else Player.IsLooping = value;
		}
	}

	public bool SyncPlayback
	{
		get => _syncPlayback;
		set => _syncPlayback = Cookies.SyncPlayback = value;
	}

	public float TimeScale
	{
		get => IsEditorScene ? _timeScale : Player.TimeScale;
		set
		{
			if ( IsEditorScene ) _timeScale = Cookies.TimeScale = value;
			else Player.TimeScale = value;
		}
	}

	public void ApplyFrame( MovieTime time )
	{
		_applyNextFrame = false;

		if ( IsOpenInEditor && SyncPlayback )
		{
			foreach ( var player in Player.Scene.GetAllComponents<MoviePlayer>() )
			{
				if ( player == Player ) continue;

				player.Position = time;
			}
		}

		ApplyFrameCore( time );
	}

	private void ApplyFrameCore( MovieTime time )
	{
		Parent?.ApplyFrameCore( SequenceTransform * time );

		foreach ( var view in TrackList.AllTracks )
		{
			view.ApplyFrame( time );
		}

		AdvanceAnimations( time - _lastAppliedTime );

		_lastAppliedTime = time;
	}

	public void RefreshNextFrame()
	{
		_applyNextFrame = true;
	}

	private void PlaybackFrame()
	{
		if ( IsPlaying && IsEditorScene )
		{
			var targetTime = PlayheadTime + MovieTime.FromSeconds( RealTime.Delta * TimeScale );

			// Handle looping / reaching end

			if ( !IsRecording )
			{
				if ( LoopTimeRange is { } loopRange )
				{
					if ( targetTime <= loopRange.Start || targetTime >= loopRange.End )
					{
						targetTime = loopRange.Start;
					}
				}
				else if ( targetTime >= Project.Duration && Project.Duration.IsPositive )
				{
					if ( IsLooping )
					{
						targetTime = MovieTime.Zero;
					}
					else
					{
						targetTime = Project.Duration;

						IsPlaying = false;
					}
				}
			}

			PlayheadTime = targetTime;
		}
		else if ( _lastPlayerPosition is { } lastPlayerPosition && lastPlayerPosition != Player.Position )
		{
			// We're setting the backing field here because we don't want to call ApplyFrame / set the Player position,
			// since we're reacting to the Player advancing time.

			_playheadTime = lastPlayerPosition;
			PlayheadChanged?.Invoke( PlayheadTime );
		}

		_lastPlayerPosition = Player.Position;
	}
}