Editor/MovieMaker/Editor/TrackListWidget.cs
using Sandbox.MovieMaker;
using Sandbox.UI;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;

namespace Editor.MovieMaker;

#nullable enable

/// <summary>
/// Lists tracks in the current movie, and allows you to add or remove them.
/// </summary>
public sealed class TrackListWidget : Widget
{
	public Session Session { get; }

	private SceneEditorSession SceneEditorSession { get; }

	public IEnumerable<TrackWidget> RootTracks => _rootTracks;
	public IEnumerable<TrackWidget> Tracks => RootTracks.SelectMany( EnumerateDescendants );

	private static IEnumerable<TrackWidget> EnumerateDescendants( TrackWidget track ) =>
		[track, .. track.Children.SelectMany( EnumerateDescendants )];

	private TrackListView? _trackList;
	private readonly ImmutableArray<ProjectNavigationWidget> _projectNavWidgets;
	private readonly SynchronizedSet<TrackView, TrackWidget> _rootTracks;

	private readonly Widget _trackContainer;
	private readonly Widget _dragTarget;
	private Widget? _placeholder;

	public TrackListWidget( ListPanel parent, Session session )
		: base( parent )
	{
		Session = session;

		SceneEditorSession = SceneEditorSession.Resolve( Session.Player.Scene );
		SceneEditorSession.Selection.OnItemAdded += OnSelectionAdded;

		_trackContainer = new Widget( this )
		{
			Layout = Layout.Column(),
			FixedWidth = Width
		};

		_trackContainer.Layout.Margin = new Margin( 4f, 0f );
		_trackContainer.Layout.Spacing = 0f;

		var navWidgets = new List<ProjectNavigationWidget>();

		var parentSession = session;

		while ( parentSession is not null )
		{
			navWidgets.Add( new ProjectNavigationWidget( this, parentSession, parentSession == session ) );

			parentSession = parentSession.Parent;
		}

		navWidgets.Reverse();

		_projectNavWidgets = [..navWidgets];

		_dragTarget = new DragTargetWidget( this ) { FixedWidth = Width, Visible = false };

		_rootTracks = new SynchronizedSet<TrackView, TrackWidget>(
			AddRootTrack, RemoveRootTrack, UpdateChildTrack );

		Session.ViewChanged += Session_ViewChanged;

		Load( Session.TrackList );
	}

	protected override void OnResize()
	{
		_trackContainer.FixedWidth = Width;
	}

	private TrackWidget AddRootTrack( TrackView source ) => _trackContainer.Layout.Add( new TrackWidget( this, null, source ) );
	private void RemoveRootTrack( TrackWidget item ) => item.Destroy();
	private bool UpdateChildTrack( TrackView source, TrackWidget item ) => item.UpdateLayout();

	private void OnSelectionAdded( object item )
	{
		if ( Tracks.Any( x => x.IsFocused ) || Session.Editor.TimelinePanel?.Timeline.IsFocused is not true ) return;
		if ( item is not GameObject go ) return;
		if ( Tracks.FirstOrDefault( x => x.View.Target is ITrackReference<GameObject> { IsBound: true } target && target.Value == go ) is not { } track ) return;

		track.Focus( false );

		if ( Parent is ScrollArea scrollArea )
		{
			scrollArea.MakeVisible( track );
		}
	}

	protected override void OnPaint()
	{
		Paint.SetBrushAndPen( Theme.ControlBackground );
		Paint.DrawRect( LocalRect );
	}

	public override void OnDestroyed()
	{
		if ( _trackList is not null )
		{
			_trackList.Changed -= TrackList_Changed;
		}

		Session.ViewChanged -= Session_ViewChanged;
		SceneEditorSession.Selection.OnItemAdded -= OnSelectionAdded;
	}

	protected override void OnWheel( WheelEvent e )
	{
		Session.TrackListScrollPosition -= e.Delta / 5f;
		e.Accept();
	}

	private Vector2 _lastMouseScreenPos;

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );

		_lastMouseScreenPos = e.ScreenPosition;
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );

		var delta = e.ScreenPosition - _lastMouseScreenPos;

		if ( e.ButtonState == MouseButtons.Middle )
		{
			Session.TrackListScrollPosition -= delta.y;
			e.Accepted = true;
		}

		_lastMouseScreenPos = e.ScreenPosition;
	}

	private void Load( TrackListView trackList )
	{
		if ( _trackList == trackList ) return;

		if ( _trackList is not null )
		{
			_trackList.Changed -= TrackList_Changed;
		}

		_trackList = trackList;
		_trackList.Changed += TrackList_Changed;

		TrackList_Changed( trackList );
	}

	private void Session_ViewChanged()
	{
		_dragTarget.Position = 0f;
		_dragTarget.FixedSize = new Vector2( Width, 64f );

		if ( _rootTracks.Count == 0 )
		{
			_trackContainer.Position = 0f;
			_trackContainer.FixedSize = Size;
			return;
		}

		var tracksHeight = _rootTracks
			.Select( x => x.View.Position + x.View.Height + Timeline.RootTrackSpacing )
			.DefaultIfEmpty( 64f )
			.Max() - Timeline.RootTrackSpacing;

		var headerHeight = _projectNavWidgets.Sum( x => x.Height ) + Timeline.RootTrackSpacing;

		Session.TrackListHeaderHeight = headerHeight;

		_trackContainer.Position = new Vector2( 0f, Session.TrackListScrollOffset - Session.TrackListScrollPosition - headerHeight );
		_trackContainer.FixedWidth = Width;
		_trackContainer.FixedHeight = tracksHeight + headerHeight;
	}

	private void TrackList_Changed( TrackListView trackList )
	{
		_placeholder?.Destroy();
		_rootTracks.Update( trackList.RootTracks );

		_trackContainer.Layout.Clear( false );

		foreach ( var navWidget in _projectNavWidgets )
		{
			_trackContainer.Layout.Add( navWidget );
		}

		foreach ( var track in _rootTracks )
		{
			_trackContainer.Layout.AddSpacingCell( Timeline.RootTrackSpacing );
			_trackContainer.Layout.Add( track );
		}

		if ( _rootTracks.Count == 0 )
		{
			CreatePlaceholder();
		}

		Session_ViewChanged();
	}

	private void CreatePlaceholder()
	{
		var row = _trackContainer.Layout.AddRow();

		row.Margin = 32f;

		_placeholder = new Label( "Drag a <b>GameObject</b>, <b>Component</b>, <b>MovieResource</b> or <b>inspector property</b> here to create a track." )
		{
			Alignment = TextFlag.Center | TextFlag.WordWrap,
			WordWrap = true
		};

		row.Add( _placeholder );
	}

	internal static bool HasDraggedTracks( DragData data )
	{
		if ( data.OfType<GameObject>().Any() ) return true;
		if ( data.OfType<Component>().Any() ) return true;

		if ( data.OfType<SerializedProperty>().FirstOrDefault() is { } property )
		{
			if ( property.Parent.Targets?.FirstOrDefault() is Component parentComponent )
			{
				return true;
			}

			return false;
		}

		if ( data.Assets.FirstOrDefault( x => x.AssetPath?.EndsWith( ".movie" ) ?? false ) is { } assetData )
		{
			var assetTask = assetData.GetAssetAsync();

			if ( !assetTask.IsCompleted ) return false;
			if ( assetTask.Result?.LoadResource<MovieResource>() is not { } resource ) return false;

			return true;
		}

		return false;
	}

	private IEnumerable<IProjectTrack> CreateDraggedTracks( DragData data )
	{
		using var scope = Session.History.Push( "Create New Track(s)" );

		if ( data.OfType<GameObject>().ToArray() is { Length: > 0 } gos )
		{
			foreach ( var go in gos )
			{
				yield return Session.GetOrCreateTrack( go );
				yield return Session.GetOrCreateTrack( go, nameof( GameObject.Enabled ) );
				yield return Session.GetOrCreateTrack( go, nameof( GameObject.LocalPosition ) );
				yield return Session.GetOrCreateTrack( go, nameof( GameObject.LocalRotation ) );

				if ( go.GetComponent<PlayerController>() is { } controller )
				{
					yield return Session.GetOrCreateTrack( controller );
					yield return Session.GetOrCreateTrack( controller, nameof( PlayerController.EyeAngles ) );
					yield return Session.GetOrCreateTrack( controller, nameof( PlayerController.WishVelocity ) );
					yield return Session.GetOrCreateTrack( controller, nameof( PlayerController.IsSwimming ) );
					yield return Session.GetOrCreateTrack( controller, nameof( PlayerController.IsClimbing ) );
					yield return Session.GetOrCreateTrack( controller, nameof( PlayerController.IsDucking ) );
				}

				if ( go.GetComponent<Rigidbody>() is { } rigidBody )
				{
					yield return Session.GetOrCreateTrack( rigidBody );
					yield return Session.GetOrCreateTrack( rigidBody, nameof( Rigidbody.Velocity ) );
				}
			}

			yield break;
		}

		if ( data.OfType<Component>().FirstOrDefault() is { } component )
		{
			yield return Session.GetOrCreateTrack( component );

			if ( component is SkinnedModelRenderer skinnedRenderer )
			{
				if ( skinnedRenderer.Parameters.Graph is { } graph )
				{
					for ( var i = 0; i < graph.ParamCount; ++i )
					{
						var paramName = graph.GetParameterName( i );

						yield return Session.GetOrCreateTrack( component, $"{nameof( SkinnedModelRenderer.Parameters )}.{paramName}" );
					}
				}

				foreach ( var morphName in skinnedRenderer.Morphs.Names )
				{
					yield return Session.GetOrCreateTrack( component, $"{nameof( SkinnedModelRenderer.Morphs )}.{morphName}" );
				}
			}

			yield break;
		}

		if ( data.OfType<SerializedProperty>().FirstOrDefault() is { } property )
		{
			if ( property.Parent.Targets?.FirstOrDefault() is Component parentComponent )
			{
				yield return Session.GetOrCreateTrack( parentComponent, property.Name );
			}

			yield break;
		}
	}

	private static PropertyInfo? DragData_Current { get; } = typeof( DragData )
		.GetProperty( "Current", BindingFlags.Static | BindingFlags.NonPublic );

	private static DragData? CurrentDrag => (DragData?)DragData_Current?.GetValue( null );

	[EditorEvent.Frame]
	private void Frame()
	{
		_dragTarget.Visible = CurrentDrag is { } data && HasDraggedTracks( data );
	}

	internal void AddTracksFromDrag( DragData data )
	{
		CreateDraggedTracks( data ).ToImmutableArray();

		_dragTarget.Hide();

		Session.TrackList.Update();
		Session.ClipModified();
	}
}

file sealed class DragTargetWidget : Widget
{
	public bool HasDrag { get; private set; }

	public new TrackListWidget Parent { get; }

	public DragTargetWidget( TrackListWidget parent )
		: base( parent )
	{
		Parent = parent;

		AcceptDrops = true;

		Layout = Layout.Row();
		Layout.Margin = new Margin( 32f, 8f, 32f, 8f );
		Layout.Spacing = 8f;

		Layout.AddStretchCell();
		Layout.Add( new Icon( "playlist_add" )
		{
			Color = Theme.Green,
			PixelHeight = 32f,
			Alignment = TextFlag.RightCenter,
			FixedSize = 32f
		} );
		Layout.Add( new Label( "Drag here to create a new track." )
		{
			Color = Theme.Green,
			Alignment = TextFlag.LeftCenter | TextFlag.WordWrap,
			WordWrap = true
		} );
		Layout.AddStretchCell();
	}

	protected override void OnPaint()
	{
		var background = Theme.WidgetBackground;

		Paint.ClearPen();
		Paint.SetBrushLinear( LocalRect.BottomLeft, LocalRect.BottomLeft - new Vector2( 0f, 8f ), background.WithAlpha( 0f ), background );
		Paint.DrawRect( LocalRect );

		if ( HasDrag )
		{
			Paint.SetBrush( Theme.ControlBackground );
		}
		else
		{
			Paint.ClearBrush();
		}

		Paint.SetPen( Theme.Green, 2f, PenStyle.Dash );
		Paint.DrawRect( LocalRect.Shrink( 8f ), 4f );
	}

	public override void OnDragHover( DragEvent ev )
	{
		HasDrag = true;

		ev.Action = TrackListWidget.HasDraggedTracks( ev.Data )
			? DropAction.Link
			: DropAction.Ignore;
	}

	public override void OnDragLeave()
	{
		HasDrag = false;
	}

	public override void OnDragDrop( DragEvent ev )
	{
		Parent.AddTracksFromDrag( ev.Data );

		HasDrag = false;
	}
}

file sealed class Icon : Widget
{
	public TextFlag Alignment { get; set; } = TextFlag.Center;
	public Color Color { get; set; } = Theme.TextControl;
	public string IconName { get; set; }
	public float PixelHeight { get; set; } = 16f;

	public Icon( string name )
	{
		IconName = name;
	}

	protected override void OnPaint()
	{
		Paint.SetPen( Color );
		Paint.DrawIcon( LocalRect, IconName, PixelHeight, Alignment );
	}
}