Editor/MovieMaker/Modes/Motion/MotionEditMode.Changes.cs
using System.Collections.Immutable;
using System.Linq;
using Sandbox;
using Sandbox.MovieMaker;

namespace Editor.MovieMaker;

#nullable enable

partial class MotionEditMode
{
	private IMovieModification? _modification;
	private ToolBarGroup? _modificationControls;

	private RealTimeSince _lastActionTime;

	public override bool AllowTrackCreation => TimeSelection is not null;

	public IMovieModification? Modification
	{
		get => _modification;
	}

	public bool HasChanges => Modification is not null;

	public Color SelectionColor
	{
		get
		{
			var t = MathF.Pow( Math.Clamp( 1f - _lastActionTime, 0f, 1f ), 8f );
			var color = (HasChanges ? Theme.Yellow : Theme.Blue).WithAlpha( 0.25f );

			return Color.Lerp( color, Color.White.WithAlpha( 0.5f ), t );
		}
	}

	public string? LastActionIcon { get; private set; }

	public IMovieModification SetModification( Type type, TimeSelection selection )
	{
		if ( _modification?.GetType() == type )
		{
			TimeSelection = selection;
			return _modification;
		}

		_modification?.ClearPreview();
		_modificationControls?.Destroy();

		_modification = (IMovieModification)Activator.CreateInstance( type )!;
		_modification.Initialize( this );

		_modificationControls = ToolBar.AddGroup();

		_modification.AddControls( _modificationControls );

		var commitDisplay = new ToolBarItemDisplay( "Apply", "done", "Commit all pending track changes." );
		var cancelDisplay = new ToolBarItemDisplay( "Cancel", "clear", "Cancel all pending track changes." );

		var commit = _modificationControls.AddAction( commitDisplay, CommitChanges );
		var cancel = _modificationControls.AddAction( cancelDisplay, ClearChanges );

		commit.Background = Theme.Green;
		cancel.Background = Theme.Red;

		TimeSelection = selection;

		return _modification;
	}

	public T SetModification<T>( TimeSelection selection ) where T : IMovieModification =>
		(T)SetModification( typeof(T), selection );

	private void ClearChanges()
	{
		if ( Modification is null ) return;

		Modification?.ClearPreview();

		_modificationControls?.Destroy();
		_modificationControls = null;

		_modification = null;

		DisplayAction( "clear" );
		SelectionChanged();
	}

	private void CommitChanges()
	{
		if ( TimeSelection is not { } selection || Modification is not { } modification || !HasChanges ) return;

		using ( Session.History.Push( "Commit" ) )
		{
			modification.Commit( selection );
		}

		ClearChanges();
		DisplayAction( "approval" );

		Session.ClipModified();
	}

	private void Delete( bool shiftTime )
	{
		if ( TimeSelection is { } selection ) Delete( selection.PeakTimeRange, shiftTime );
	}

	private void Delete( MovieTimeRange timeRange, bool shiftTime )
	{
		var changed = false;

		using ( Session.History.Push( shiftTime ? "Remove Time" : "Clear Time" ) )
		{
			foreach ( var view in Session.TrackList.EditableTracks )
			{
				var track = (IProjectPropertyTrack)view.Track;

				if ( shiftTime )
				{
					changed |= track.Remove( timeRange ) && view.MarkValueChanged();
				}
				else
				{
					changed |= track.Clear( timeRange ) && view.MarkValueChanged();
				}
			}
		}

		if ( changed )
		{
			Session.ClipModified();
			DisplayAction( "delete" );
		}
	}

	protected override void OnBackspace()
	{
		Delete( true );
	}

	protected override void OnDelete()
	{
		Delete( false );
	}

	protected override void OnInsert()
	{
		if ( TimeSelection is not { } selection ) return;

		var changed = false;

		using ( Session.History.Push( "Insert" ) )
		{
			foreach ( var view in Session.TrackList.EditableTracks )
			{
				var track = (IProjectPropertyTrack)view.Track;

				changed |= track.Insert( selection.PeakTimeRange ) && view.MarkValueChanged();
			}
		}

		if ( changed )
		{
			DisplayAction( "keyboard_tab" );
		}
	}

	private static ClipboardData? Clipboard { get; set; }

	protected override void OnSelectAll()
	{
		TimeSelection = new TimeSelection( (MovieTime.Zero, Project.Duration), DefaultInterpolation );
	}

	protected override void OnCut()
	{
		OnCopy();
		Delete( false );

		DisplayAction( "content_cut" );
	}

	protected override void OnCopy()
	{
		if ( TimeSelection is not { } selection ) return;

		var timeRange = selection.TotalTimeRange;
		var offset = Session.PlayheadTime;
		var tracks = new Dictionary<Guid, IReadOnlyList<IProjectPropertyBlock>>();
		var slicedBlocks = new List<IProjectPropertyBlock>();

		foreach ( var view in Session.TrackList.EditableTracks )
		{
			var track = (IProjectPropertyTrack)view.Track;

			slicedBlocks.Clear();
			slicedBlocks.AddRange( track.Slice( timeRange ).Select( x => x.Shift( -offset ) ) );

			if ( slicedBlocks.Count > 0 )
			{
				tracks[track.Id] = slicedBlocks.ToImmutableList();
			}
		}

		if ( tracks.Count <= 0 ) return;

		Clipboard = new ClipboardData( selection - offset, tracks.ToImmutableDictionary() );

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

	protected override void OnPaste()
	{
		if ( LoadChangesFromClipboard() )
		{
			DisplayAction( "content_paste" );
		}
	}

	private bool LoadChangesFromClipboard()
	{
		if ( Clipboard is not { } clipboard ) return false;

		ClearChanges();

		var selection = clipboard.Selection + Session.PlayheadTime;
		var pasteTime = selection.TotalStart;

		TimeSelection = selection;

		SetModification<BlendModification>( selection )
			.SetFromClipboard( clipboard, pasteTime, Project );

		SelectionChanged();

		return true;
	}

	private MovieResource CreateSequence( MovieTimeRange timeRange )
	{
		var project = new MovieProject();
		var offset = -timeRange.Start;

		foreach ( var editable in Session.TrackList.EditableTracks )
		{
			if ( editable.Track is not IProjectPropertyTrack propertyTrack ) continue; // TODO
			if ( propertyTrack.Slice( timeRange ) is not { Count: > 0 } slice ) continue;

			var trackCopy = (IProjectPropertyTrack)project.GetOrAddTrack( editable.Track );

			trackCopy.SetBlocks( [.. slice.Select( x => x.Shift( offset ) )] );
		}

		Delete( timeRange, false );

		var resource = new MovieResource { EditorData = project.Serialize(), Compiled = project.Compile() };
		var track = Project.AddSequenceTrack( "Sequences" );

		track.AddBlock( timeRange, new MovieTransform( -offset ), resource );

		Session.TrackList.Update();

		return resource;
	}

	protected override void OnTrackStateChanged( TrackView view )
	{
		if ( view.Track is not IProjectPropertyTrack track ) return;
		if ( TimeSelection is not { } selection || Modification is not { } modification ) return;

		modification.UpdatePreview( selection, track );
	}

	protected override bool OnPreChange( TrackView view )
	{
		if ( TimeSelection is not { } selection ) return false;
		if ( view.Track is not IProjectPropertyTrack track ) return false;
		if ( view.Target is not ITrackProperty property ) return false;

		if ( Modification is not BlendModification blend )
		{
			Modification?.Commit( selection );

			blend = SetModification<BlendModification>( selection );
		}

		return blend.PreChange( track, property );
	}

	protected override bool OnPostChange( TrackView view )
	{
		if ( TimeSelection is not { } selection || Modification is not BlendModification blend ) return false;
		if ( view.Track is not IProjectPropertyTrack track ) return false;
		if ( view.Target is not ITrackProperty property ) return false;

		return blend.PostChange( track, property ) && blend.UpdatePreview( selection, track );
	}

	private bool _hasSelectionItems;

	private void SelectionChanged()
	{
		if ( TimeSelection is { } selection )
		{
			SourceTimeRange = Modification?.SourceTimeRange;

			Modification?.UpdatePreview( selection );

			if ( !_hasSelectionItems )
			{
				_hasSelectionItems = true;

				Timeline.Add( new TimeSelectionPeakItem( this ) );

				Timeline.Add( new TimeSelectionFadeItem( this, FadeKind.FadeIn ) );
				Timeline.Add( new TimeSelectionFadeItem( this, FadeKind.FadeOut ) );

				Timeline.Add( new TimeSelectionHandleItem( this ) );
				Timeline.Add( new TimeSelectionHandleItem( this ) );
				Timeline.Add( new TimeSelectionHandleItem( this ) );
				Timeline.Add( new TimeSelectionHandleItem( this ) );
			}

			UpdateSelectionItems( Timeline.VisibleRect );
		}
		else if ( _hasSelectionItems )
		{
			_hasSelectionItems = false;

			SourceTimeRange = null;

			foreach ( var item in Timeline.Items.OfType<TimeSelectionItem>().ToArray() )
			{
				item.Destroy();
			}
		}

		Session.RefreshNextFrame();
	}

	protected override void OnViewChanged( Rect viewRect )
	{
		UpdateSelectionItems( viewRect );
	}

	private void UpdateSelectionItems( Rect viewRect )
	{
		if ( TimeSelection is not { } selection ) return;

		foreach ( var item in Timeline.Items.OfType<TimeSelectionItem>() )
		{
			item.UpdatePosition( selection, viewRect );
		}
	}

	public void DisplayAction( string icon )
	{
		_lastActionTime = 0f;
		LastActionIcon = icon;

		UpdateSelectionItems( Timeline.VisibleRect );
	}

	protected override void OnFrame()
	{
		RecordingFrame();

		if ( _lastActionTime < 1f )
		{
			UpdateSelectionItems( Timeline.VisibleRect );
		}
	}
}