Editor/MovieMaker/Timeline/TimelineTrack.cs
using System.Linq;
using Editor.MovieMaker.BlockDisplays;
using Sandbox.MovieMaker;

namespace Editor.MovieMaker;

#nullable enable

public partial class TimelineTrack : GraphicsItem
{
	// TODO: should this be in DisplayInfo?
	private static Dictionary<Type, Color> HandleColors { get; } = new()
	{
		{ typeof(Vector3), Theme.Blue },
		{ typeof(Rotation), Theme.Green },
		{ typeof(Color), Theme.Pink },
		{ typeof(float), Theme.Yellow },
	};

	public Timeline Timeline { get; }
	public Session Session { get; }
	public TrackView View { get; }

	private readonly record struct BlockItemKey( ITrackBlock Block, MovieTime? Offset = null ) : IEquatable<BlockItemKey>
	{
		public bool Equals( BlockItemKey other )
		{
			return ReferenceEqualityComparer.Instance.Equals( Block, other.Block ) && Offset == other.Offset;
		}

		public override int GetHashCode()
		{
			return HashCode.Combine( ReferenceEqualityComparer.Instance.GetHashCode( Block ), Offset );
		}
	}

	private readonly List<BlockItemKey> _visibleBlocks = new();
	private readonly SynchronizedList<BlockItemKey, BlockItem> _blockItems;

	public IReadOnlyList<BlockItem> BlockItems => _blockItems;

	public Color HandleColor { get; }

	public TimelineTrack( Timeline timeline, TrackView view )
	{
		Timeline = timeline;
		Session = timeline.Session;
		View = view;

		HoverEvents = true;
		ToolTip = view.Description;

		HandleColor = HandleColors.TryGetValue( view.Track.TargetType, out var color ) ? color : Color.Gray;

		_blockItems = new SynchronizedList<BlockItemKey, BlockItem>(
			AddBlockItem, RemoveBlockItem, UpdateBlockItem );

		View.Changed += View_Changed;
		View.ValueChanged += View_ValueChanged;
	}

	protected override void OnDestroy()
	{
		Session.EditMode?.ClearTimelineItems( this );

		View.Changed -= View_Changed;
		View.ValueChanged -= View_ValueChanged;
	}

	private void View_Changed( TrackView view )
	{
		UpdateItems();
	}

	private void View_ValueChanged( TrackView view )
	{
		UpdateItems();
	}

	internal void UpdateLayout()
	{
		PrepareGeometryChange();

		var position = View.Position;

		Position = new Vector2( 0, position );
		Size = new Vector2( 50000, Timeline.TrackHeight );

		UpdateItems();
	}

	internal void OnSelected()
	{
		View.InspectProperty();
	}

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

		if ( e.LeftMouseButton )
		{
			OnSelected();
		}
	}

	public void UpdateItems()
	{
		var previewOffset = Session.TrackList.PreviewOffset;

		_visibleBlocks.Clear();
		_visibleBlocks.AddRange( View.Blocks.Select( x => new BlockItemKey( x ) ) );
		_visibleBlocks.AddRange( View.PreviewBlocks.Select( x => new BlockItemKey( x, previewOffset ) ) );

		_blockItems.Update( _visibleBlocks );

		Session.EditMode?.UpdateTimelineItems( this );
	}

	private BlockItem AddBlockItem( BlockItemKey src ) => BlockItem.Create( this, src.Block, src.Offset ?? default );

	private void RemoveBlockItem( BlockItem dst ) => dst.Destroy();

	private bool UpdateBlockItem( BlockItemKey src, ref BlockItem dst )
	{
		if ( dst.Block.GetType() != src.Block.GetType() )
		{
			dst = AddBlockItem( src );
		}

		dst.Block = src.Block;
		dst.Offset = src.Offset ?? default;
		dst.Layout();

		return true;
	}
}