Editor/MovieMaker/Modes/Motion/MotionEditMode.Recording.cs
using System.Collections.Immutable;
using Sandbox.MovieMaker;
using System.Globalization;
using System.Text.Json.Nodes;
using System.Linq;

namespace Editor.MovieMaker;

#nullable enable

partial class MotionEditMode
{
	private bool _stopPlayingAfterRecording;

	public override bool AllowRecording => true;

	private MovieClipRecorder? _recorder;
	private MovieTime _recordingStartTime;
	private MovieTime _recordingLastTime;

	private sealed class FilteredClip : IClip
	{
		private readonly ImmutableArray<ITrack> _tracks;
		private readonly MovieClipRecorder _recorder;
		private readonly ImmutableDictionary<Guid, IReferenceTrack> _referenceTracks;

		public FilteredClip( IEnumerable<ITrack> tracks, MovieClipRecorder recorder )
		{
			_tracks = [..tracks];
			_recorder = recorder;
			_referenceTracks = _tracks.OfType<IReferenceTrack>()
				.ToImmutableDictionary( x => x.Id, x => x );
		}

		public IEnumerable<ITrack> Tracks => _tracks;

		public MovieTime Duration => _recorder.Duration + 1d;

		public IReferenceTrack? GetTrack( Guid trackId ) => _referenceTracks.GetValueOrDefault( trackId );
	}

	protected override bool OnStartRecording()
	{
		var options = new RecorderOptions( Project.SampleRate );

		_recorder = new MovieClipRecorder( Session.Binder, options );
		_stopPlayingAfterRecording = !Session.IsPlaying;
		_recordingStartTime = Session.PlayheadTime;
		_recordingLastTime = _recordingStartTime;

		foreach ( var view in Session.TrackList.EditableTracks )
		{
			_recorder.Tracks.Add( (IProjectPropertyTrack)view.Track );
		}

		var playbackIgnoreTracks = Session.TrackList.AllTracks
			.Where( x => !x.IsLocked )
			.Select( x => x.Track );

		Session.Player.Clip = new FilteredClip( ((IClip)Session.Project).Tracks.Except( playbackIgnoreTracks ), _recorder );
		Session.IsPlaying = true;

		return true;
	}

	protected override void OnStopRecording()
	{
		if ( _recorder is not { } recorder ) return;

		var timeRange = new MovieTimeRange( 0d, recorder.Duration );

		if ( _stopPlayingAfterRecording )
		{
			Session.IsPlaying = false;
		}

		Session.Player.Clip = Session.Project;

		foreach ( var trackRecorder in recorder.Tracks )
		{
			if ( Session.TrackList.Find( (IProjectTrack)trackRecorder.Track ) is { } view )
			{
				view.ClearPreviewBlocks();
			}
		}

		var compiled = recorder.ToClip();

		var sourceClip = new ProjectSourceClip( Guid.NewGuid(), compiled, new JsonObject
		{
			{ "Date", DateTime.UtcNow.ToString( "o", CultureInfo.InvariantCulture ) },
			{ "IsEditor", Session.Player.Scene.IsEditor },
			{ "SceneSource", Json.ToNode( Session.Player.Scene.Source ) },
			{ "MoviePlayer", Json.ToNode( Session.Player.Id ) }
		} );

		Clipboard = new ClipboardData( new TimeSelection( timeRange, DefaultInterpolation ), compiled.Tracks
			.OfType<IPropertyTrack>()
			.Select( Project.GetTrack )
			.OfType<IProjectPropertyTrack>()
			.ToImmutableDictionary( x => x.Id, x => x.CreateSourceBlocks( sourceClip ) ) );

		Session.PlayheadTime = _recordingStartTime;

		if ( LoadChangesFromClipboard() )
		{
			DisplayAction( "radio_button_checked" );
		}
	}

	private void RecordingFrame()
	{
		if ( !Session.IsRecording ) return;

		var time = Session.PlayheadTime;
		var deltaTime = MovieTime.Max( time - _recordingLastTime, 0d );

		Session.TrackList.PreviewOffset = _recordingStartTime;

		if ( _recorder?.Advance( deltaTime ) is true )
		{
			foreach ( var trackRecorder in _recorder.Tracks )
			{
				var track = (IProjectPropertyTrack)trackRecorder.Track;

				if ( Session.TrackList.Find( track ) is not { } view ) continue;
				if ( !view.IsExpanded ) continue;

				var finishedBlocks = trackRecorder.FinishedBlocks;

				if ( trackRecorder.CurrentBlock is { } current )
				{
					view.SetPreviewBlocks( [], [..finishedBlocks, current] );
				}
				else
				{
					view.SetPreviewBlocks( [], finishedBlocks );
				}
			}
		}

		_recordingLastTime = time;
	}
}