Editor/MovieMaker/Timeline/Timeline.Dragging.cs
using System.Collections.Immutable;
using Sandbox.MovieMaker;
using System.Linq;

namespace Editor.MovieMaker;

#nullable enable

public interface IMovieTrackItem
{
	ITrackBlock? Block { get; }
	MovieTimeRange TimeRange { get; }
}

public interface IMovieDraggable : IMovieTrackItem
{
	void StartDrag() { }
	void Drag( MovieTime delta );
	void EndDrag() { }
}

public enum BlockEdge
{
	Start,
	End
}

public interface IMovieResizable : IMovieTrackItem
{
	MovieTimeRange? FullTimeRange { get; }

	void StartResize( BlockEdge edge ) { }
	void Resize( BlockEdge edge, MovieTime delta );
	void EndResize() { }
}

partial class Timeline
{
	private readonly List<IMovieDraggable> _draggedItems = new();
	private readonly List<(IMovieResizable Item, BlockEdge Edge)> _resizedItems = new();
	private MovieTime _lastDragTime;
	private MovieTime _minDragTime;
	private MovieTime? _maxDragTime;
	private SnapOptions _dragSnapOptions;
	private IHistoryScope? _dragScope;

	public bool IsDragging => _draggedItems.Count > 0 || _resizedItems.Count > 0;

	public Rect GetSceneRect( GraphicsItem item, MovieTimeRange timeRange )
	{
		timeRange = timeRange.ClampStart( 0d );

		var min = Session.TimeToPixels( timeRange.Start );
		var max = Session.TimeToPixels( timeRange.End );

		return item.SceneRect with { Left = min, Right = max };
	}

	private BlockEdge? GetBlockEdge( Vector2 scenePos, GraphicsItem item )
	{
		var leftMax = Math.Min( item.SceneRect.Left + 8f, item.Center.x );
		var rightMin = Math.Max( item.SceneRect.Right - 8f, item.Center.x );

		if ( scenePos.x < leftMax )
		{
			return BlockEdge.Start;
		}

		if ( scenePos.x > rightMin )
		{
			return BlockEdge.End;
		}

		return null;
	}

	private void UpdateCursor( Vector2 scenePos, GraphicsItem item )
	{
		if ( item is IMovieDraggable )
		{
			item.Cursor = CursorShape.Finger;
		}

		if ( item is IMovieResizable && GetBlockEdge( scenePos, item ) is not null )
		{
			item.Cursor = CursorShape.SizeH;
		}
	}

	private bool StartDragging( Vector2 scenePos, GraphicsItem item )
	{
		_dragScope = null;

		_lastDragTime = Session.ScenePositionToTime( scenePos, new SnapOptions( SnapFlag.TrackBlock ) );

		_draggedItems.Clear();
		_resizedItems.Clear();

		if ( StartResizing( scenePos, item ) ) return true;

		if ( item is not IMovieDraggable )
		{
			return false;
		}

		if ( !item.Selected )
		{
			DeselectAll();
			item.Selected = true;
		}

		_draggedItems.AddRange( SelectedItems.OfType<IMovieDraggable>() );

		_dragSnapOptions = new SnapOptions(
			IgnoreBlocks: _draggedItems.Select( x => x.Block )
				.OfType<ITrackBlock>()
				.ToImmutableHashSet(),
			SnapOffsets: _draggedItems.Select( x => x.TimeRange.Start - _lastDragTime )
				.Concat( _draggedItems.Select( x => x.TimeRange.End - _lastDragTime ) )
				.Order().Distinct().ToArray() );

		_minDragTime = -_dragSnapOptions.SnapOffsets[0];
		_maxDragTime = null;

		foreach ( var dragged in _draggedItems )
		{
			dragged.StartDrag();
		}

		return _draggedItems.Count > 0;
	}

	private bool StartResizing( Vector2 scenePos, GraphicsItem item )
	{
		if ( item is not IMovieResizable resizable )
		{
			return false;
		}

		if ( GetBlockEdge( scenePos, item ) is not { } edge )
		{
			return false;
		}

		if ( !item.Selected )
		{
			DeselectAll();
			item.Selected = true;
		}

		_lastDragTime = edge == BlockEdge.Start ? resizable.TimeRange.Start : resizable.TimeRange.End;

		_resizedItems.AddRange( SelectedItems.OfType<IMovieResizable>()
			.Where( x => x.TimeRange.Start == _lastDragTime || x.TimeRange.End == _lastDragTime )
			.Select( x => (x, x.TimeRange.Start == _lastDragTime ? BlockEdge.Start : BlockEdge.End) ) );

		_dragSnapOptions = new SnapOptions(
			IgnoreBlocks: _resizedItems.Select( x => x.Item.Block )
				.OfType<ITrackBlock>()
				.ToImmutableHashSet() );

		var min = MovieTime.Zero;
		var max = MovieTime.MaxValue;

		foreach ( var resized in _resizedItems )
		{
			if ( resized.Item.FullTimeRange is { } fullRange )
			{
				min = MovieTime.Max( min, fullRange.Start );
				max = MovieTime.Min( max, fullRange.End );
			}

			if ( resized.Edge == BlockEdge.Start )
			{
				max = MovieTime.Min( max, resized.Item.TimeRange.End );
			}
			else
			{
				min = MovieTime.Max( min, resized.Item.TimeRange.Start );
			}

			resized.Item.StartResize( resized.Edge );
		}

		_minDragTime = min;
		_maxDragTime = max;

		return _resizedItems.Count > 0;
	}

	private void Drag( Vector2 scenePos )
	{
		var time = MovieTime.Max( _minDragTime, Session.ScenePositionToTime( scenePos, _dragSnapOptions ) );

		if ( _maxDragTime is { } maxTime )
		{
			time = MovieTime.Min( time, maxTime );
		}

		var delta = time - _lastDragTime;

		if ( delta.IsZero ) return;

		_lastDragTime = time;

		var isResizing = _resizedItems.Count > 0;

		_dragScope ??= Session.History.Push( isResizing ? "Resize Selection" : "Drag Selection" );

		if ( isResizing )
		{
			foreach ( var (item, edge) in _resizedItems )
			{
				item.Resize( edge, delta );
			}
		}
		else
		{
			foreach ( var item in _draggedItems )
			{
				item.Drag( delta );
			}
		}

		Session.EditMode?.DragItems( _draggedItems, delta );

		_dragScope?.PostChange();
	}

	private void StopDragging()
	{
		foreach ( var item in _draggedItems )
		{
			item.EndDrag();
		}

		foreach ( var (item, _) in _resizedItems )
		{
			item.EndResize();
		}

		_dragScope?.Dispose();
		_draggedItems.Clear();
		_resizedItems.Clear();

		Cursor = CursorShape.Arrow;
	}
}