Editor/MovieMaker/Modes/Motion/TimeSelectionItem.cs
using System.Linq;
using Sandbox.MovieMaker;

namespace Editor.MovieMaker;

#nullable enable

partial class MotionEditMode
{
	private abstract class TimeSelectionItem : GraphicsItem
	{
		/// <summary>
		/// Capture time selection before being dragged so we can revert etc.
		/// </summary>
		protected TimeSelection? OriginalSelection { get; private set; }
		protected IModificationOptions? OriginalModificationOptions { get; private set; }

		public MotionEditMode EditMode { get; }

		protected TimeSelectionItem( MotionEditMode editMode )
		{
			EditMode = editMode;
		}

		public abstract void UpdatePosition( TimeSelection value, Rect viewRect );

		protected override void OnMousePressed( GraphicsMouseEvent e )
		{
			base.OnMousePressed( e );

			OriginalSelection = EditMode.TimeSelection;
			OriginalModificationOptions = EditMode.Modification?.Options;
		}

		protected override void OnMouseReleased( GraphicsMouseEvent e )
		{
			base.OnMouseReleased( e );

			OriginalSelection = null;
		}
	}

	/// <summary>
	/// Inner region of the timeline selection. Dragging it moves the whole selection left / right.
	/// </summary>
	private sealed class TimeSelectionPeakItem : TimeSelectionItem
	{
		public TimeSelectionPeakItem( MotionEditMode editMode )
			: base( editMode )
		{
			ZIndex = 10000;

			Movable = true;
			Cursor = CursorShape.Finger;
		}

		protected override void OnMoved()
		{
			if ( OriginalSelection is not { } selection ) return;

			var origTime = selection.PeakStart;
			var snapOptions = new SnapOptions( SnapFlag.Selection | SnapFlag.PasteBlock,
				SnapOffsets: [selection.TotalStart - origTime, selection.PeakEnd - origTime, selection.TotalEnd - origTime] );
			var startTime = EditMode.Session.ScenePositionToTime( Position, snapOptions );

			startTime = MovieTime.Max( selection.FadeIn.Duration, startTime );

			if ( OriginalModificationOptions is ITranslatableOptions translatable )
			{
				EditMode.Modification!.Options = translatable.WithOffset( startTime + translatable.Offset - selection.PeakStart );
			}

			EditMode.TimeSelection = selection with { PeakTimeRange = (startTime, startTime + selection.PeakTimeRange.Duration) };
		}

		public override void UpdatePosition( TimeSelection value, Rect viewRect )
		{
			PrepareGeometryChange();

			var timeRange = value.PeakTimeRange;

			Position = new Vector2( EditMode.Session.TimeToPixels( timeRange.Start ), viewRect.Top );
			Size = new Vector2( EditMode.Session.TimeToPixels( timeRange.Duration ), viewRect.Height );

			Update();
		}

		protected override void OnPaint()
		{
			var color = EditMode.SelectionColor;

			Paint.Antialiasing = true;

			Paint.SetBrush( color );
			Paint.SetPen( Color.White.WithAlpha( 0.5f ), 0.5f );
			Paint.DrawRect( LocalRect.Grow( 0f, 16f ) );

			if ( EditMode.LastActionIcon is { } icon && EditMode._lastActionTime < 1f )
			{
				var t = 1f - EditMode._lastActionTime;

				Paint.SetPen( Color.White.WithAlpha( t * t * t * t ) );
				Paint.DrawIcon( LocalRect.Grow( 32f, 0f ), icon, 32f );
			}
		}
	}

	private enum FadeKind
	{
		FadeIn,
		FadeOut
	}

	/// <summary>
	/// Fade in / out region of the timeline selection. Dragging it moves the fade left / right.
	/// If the selection has a zero-width peak (it fades out right after fading in), then
	/// you can move the whole selection by starting a drag in the direction of the other
	/// fade item.
	/// </summary>
	private sealed class TimeSelectionFadeItem : TimeSelectionItem
	{
		private bool? _moveWholeSelection;

		public FadeKind Kind { get; }

		public MovieTimeRange? TimeRange
		{
			get => Kind == FadeKind.FadeIn
				? EditMode.TimeSelection?.FadeInTimeRange
				: EditMode.TimeSelection?.FadeOutTimeRange;
		}

		public InterpolationMode? Interpolation
		{
			get => Kind == FadeKind.FadeIn
				? EditMode.TimeSelection?.FadeIn.Interpolation
				: EditMode.TimeSelection?.FadeOut.Interpolation;

			set
			{
				if ( EditMode.TimeSelection is not { } selection ) return;
				if ( value is not { } mode ) return;

				EditMode.TimeSelection = Kind == FadeKind.FadeIn
					? selection with { FadeIn = selection.FadeIn with { Interpolation = mode } }
					: selection with { FadeOut = selection.FadeOut with { Interpolation = mode } };
			}
		}

		public TimeSelectionFadeItem( MotionEditMode editMode, FadeKind kind )
			: base( editMode )
		{
			Kind = kind;

			ZIndex = 10001;

			HandlePosition = kind == FadeKind.FadeIn ? new Vector2( 1f, 0f ) : new Vector2( 0f, 0f );

			Movable = true;
			HoverEvents = true;
			Focusable = true;

			Cursor = CursorShape.Finger;
		}

		protected override void OnMousePressed( GraphicsMouseEvent e )
		{
			base.OnMousePressed( e );

			if ( OriginalSelection is not { } value ) return;

			_moveWholeSelection = value.PeakTimeRange.IsEmpty ? null : false;
		}

		public override void UpdatePosition( TimeSelection value, Rect viewRect )
		{
			PrepareGeometryChange();

			if ( TimeRange is not { } timeRange )
			{
				Position = new Vector2( -50000f, 0f );
			}
			else
			{
				Position = new Vector2( EditMode.Session.TimeToPixels( Kind == FadeKind.FadeIn ? timeRange.End : timeRange.Start ), viewRect.Top );
				Size = new Vector2( EditMode.Session.TimeToPixels( timeRange.Duration ), viewRect.Height );
			}

			Update();
		}

		protected override void OnMoved()
		{
			if ( OriginalSelection is not { } selection ) return;

			var snapOptions = _moveWholeSelection is true
				? new SnapOptions( SnapFlag.Selection,
					SnapOffsets: [-selection.FadeIn.Duration, selection.FadeOut.Duration] )
				: new SnapOptions( Kind == FadeKind.FadeIn ? SnapFlag.SelectionStart : SnapFlag.SelectionEnd,
					SnapOffsets: [Kind == FadeKind.FadeIn ? -selection.FadeIn.Duration : selection.FadeOut.Duration] );

			var time = EditMode.Session.ScenePositionToTime( Position, snapOptions );

			if ( time != selection.PeakStart )
			{
				_moveWholeSelection ??= Kind == FadeKind.FadeIn
					? time > selection.PeakStart
					: time < selection.PeakStart;
			}

			if ( _moveWholeSelection is true )
			{
				time = MovieTime.Max( selection.FadeIn.Duration, time );

				if ( OriginalModificationOptions is ITranslatableOptions translatable )
				{
					EditMode.Modification!.Options = translatable.WithOffset( time + translatable.Offset - selection.PeakStart );
				}

				EditMode.TimeSelection = selection.WithTimes(
					totalStart: time - selection.FadeIn.Duration, peakStart: time,
					peakEnd: time, totalEnd: time + selection.FadeOut.Duration );
			}
			else if ( Kind == FadeKind.FadeIn )
			{
				time = time.Clamp( (selection.FadeIn.Duration, selection.PeakEnd) );

				EditMode.TimeSelection = selection.WithTimes( totalStart: time - selection.FadeIn.Duration, peakStart: time );
			}
			else
			{
				time = MovieTime.Max( selection.PeakStart, time );

				EditMode.TimeSelection = selection.WithTimes( peakEnd: time, totalEnd: time + selection.FadeOut.Duration );
			}
		}

		protected override void OnPaint()
		{
			if ( Width < 1f || Interpolation is not { } interpolation ) return;

			var color = EditMode.SelectionColor;

			if ( Hovered ) color = color.Lighten( 0.1f );

			var fadeColor = color.WithAlpha( 0.02f );
			var scrubBarHeight = EditMode.Timeline.ScrubBarTop.Height;

			var (x0, x1) = Kind == FadeKind.FadeIn
				? (0f, Width)
				: (Width, 0f);

			Paint.Antialiasing = true;

			Paint.SetBrushLinear( new Vector2( x0, 0f ), new Vector2( x1, 0f ), fadeColor, color );
			Paint.SetPen( color.WithAlpha( 0.5f ), 0.5f );
			PaintExtensions.PaintMirroredCurve( t => interpolation.Apply( t ), LocalRect, scrubBarHeight, Kind == FadeKind.FadeOut );
		}
	}

	/// <summary>
	/// One of the boundaries between the 3 parts of the selection (fade in / peak / fade out). Drag it to move
	/// just that boundary. If sections of the selection are zero-width (no peak / no fade in / no fade out),
	/// then we work out which handle you wanted to drag based on the direction the drag starts. You can't move
	/// these handles past one another.
	/// </summary>
	private sealed class TimeSelectionHandleItem : TimeSelectionItem
	{
		private enum Index
		{
			TotalStart,
			PeakStart,
			PeakEnd,
			TotalEnd
		}

		private Index _minIndex;
		private Index _maxIndex;

		public TimeSelectionHandleItem( MotionEditMode editMode )
			: base( editMode )
		{
			Movable = true;
			HoverEvents = true;

			Cursor = CursorShape.SizeH;
			HandlePosition = new Vector2( 0.5f, 0f );

			ZIndex = 10002;
		}

		protected override void OnMousePressed( GraphicsMouseEvent e )
		{
			base.OnMousePressed( e );

			(_minIndex, _maxIndex) = GetIndexRange();
		}

		protected override void OnMoved()
		{
			var ignore = (SnapFlag)((int)SnapFlag.SelectionTotalStart << (int)_minIndex);
			var time = EditMode.Session.ScenePositionToTime( Position, ignore );

			if ( OriginalSelection is not { } value ) return;

			var originTime = GetTime( value, _minIndex );

			// If it's ambiguous which handle we are, pick a side
			// based on which direction we're dragged

			if ( time < originTime ) _maxIndex = _minIndex;
			if ( time > originTime ) _minIndex = _maxIndex;

			// Limit dragging to neighbouring control points

			var minTime = GetTime( value, _minIndex - 1 );
			var maxTime = GetTime( value, _maxIndex + 1 );

			EditMode.TimeSelection = SetTime( value, _minIndex, time.Clamp( (minTime, maxTime) ) );
		}

		private static MovieTime GetTime( TimeSelection value, Index index ) => index switch
		{
			Index.TotalStart => value.TotalStart,
			Index.PeakStart => value.PeakStart,
			Index.PeakEnd => value.PeakEnd,
			Index.TotalEnd => value.TotalEnd,
			< 0 => MovieTime.Zero,
			> Index.TotalEnd => MovieTime.MaxValue
		};

		private static TimeSelection SetTime( TimeSelection value, Index index, MovieTime time ) => index switch
		{
			Index.TotalStart => value.WithTimes( totalStart: time ),
			Index.PeakStart => value.WithTimes( peakStart: time ),
			Index.PeakEnd => value.WithTimes( peakEnd: time ),
			Index.TotalEnd => value.WithTimes( totalEnd: time ),
			_ => value
		};

		public override void UpdatePosition( TimeSelection value, Rect viewRect )
		{
			PrepareGeometryChange();

			var time = GetTime( value, GetIndexRange().Min );

			Position = new Vector2( EditMode.Session.TimeToPixels( time ), viewRect.Top );
			Size = new Vector2( 8f, viewRect.Height );
		}

		/// <summary>
		/// Get the possible range of handles this could be, if the time selection has
		/// overlapping control points.
		/// </summary>
		private (Index Min, Index Max) GetIndexRange()
		{
			if ( EditMode.TimeSelection is not { } value ) return default;

			var handles = EditMode.Timeline.Items.OfType<TimeSelectionHandleItem>()
				.OrderBy( x => x.Position.x );

			var index = Index.TotalStart + handles.TakeWhile( x => x != this ).Count();

			var minIndex = index;
			var maxIndex = index;

			var time = GetTime( value, index );

			while ( minIndex > Index.TotalStart && GetTime( value, minIndex - 1 ) == time )
			{
				minIndex -= 1;
			}

			while ( maxIndex < Index.TotalEnd && GetTime( value, maxIndex + 1 ) == time )
			{
				maxIndex += 1;
			}

			return (minIndex, maxIndex);
		}
	}
}