Editor/Sprite/SpriteEditor/Timeline/FrameButton.cs
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SpriteTools.SpriteEditor.Timeline;

public class FrameButton : Widget
{
	MainWindow MainWindow;
	Timeline Timeline;
	public int FrameIndex;
	Pixmap Pixmap;

	public bool IsCurrentFrame => MainWindow.CurrentFrameIndex == FrameIndex;

	Drag dragData;
	bool draggingAbove = false;
	bool draggingBelow = false;
	int draggingLoopPoint = 0;
	bool draggingLoopPointStart = false;
	bool draggingLoopPointEnd = false;

	public static float FrameSize = 64f;
	public static List<FrameButton> Selected = new();
	static int lastSelectedIndex = 0;

	public FrameButton ( Timeline timeline, MainWindow window, int index ) : base( null )
	{
		Timeline = timeline;
		MainWindow = window;
		FrameIndex = index;
		Cursor = CursorShape.Finger;

		Layout = Layout.Row();
		Layout.Margin = 4;

		MinimumSize = new Vector2( FrameSize, FrameSize + 16f );
		MaximumSize = new Vector2( FrameSize, FrameSize + 16f );
		HorizontalSizeMode = SizeMode.Ignore;
		VerticalSizeMode = SizeMode.Ignore;

		// Get the texture for the frame
		var frame = MainWindow.SelectedAnimation.Frames[FrameIndex];
		var rect = frame.SpriteSheetRect;
		Pixmap = PixmapCache.Get( frame.FilePath, rect );

		StatusTip = $"Frame {FrameIndex + 1} - {frame.FilePath}";
		if ( frame.SpriteSheetRect.Width != 0 && frame.SpriteSheetRect.Height != 0 )
		{
			StatusTip += " - (" + frame.SpriteSheetRect + ")";
		}

		IsDraggable = true;
		AcceptDrops = true;
		MainWindow.OnTextureUpdate += Update;
	}

	public override void OnDestroyed ()
	{
		base.OnDestroyed();

		MainWindow.OnTextureUpdate -= Update;
		Selected.Remove( this );
	}

	protected override void OnContextMenu ( ContextMenuEvent e )
	{
		base.OnContextMenu( e );

		var m = new Menu( this );

		m.AddOption( "Add Broadcast Message", "wifi_tethering", AddEventPopup );
		var optionClear = m.AddOption( "Clear Broadcast Messages", "portable_wifi_off", ClearBroadcastEvents );
		optionClear.Enabled = MainWindow.SelectedAnimation.Frames[FrameIndex].Events.Count > 0;
		m.AddOption( "Duplicate", "content_copy", Duplicate );
		m.AddOption( "Delete", "delete", Delete );

		m.OpenAtCursor( false );
	}

	protected override void OnPaint ()
	{
		bool isSelected = Selected.Contains( this );
		var anim = MainWindow.SelectedAnimation;

		MinimumSize = new Vector2( FrameSize, FrameSize + 16f );
		MaximumSize = new Vector2( FrameSize, FrameSize + 16f );

		Paint.SetBrushAndPen( Theme.ControlBackground.Lighten( isSelected ? 2f : ( IsUnderMouse ? 1f : 0.5f ) ) );
		Paint.DrawRect( LocalRect );

		Paint.SetBrushAndPen( Theme.ControlBackground );
		Paint.DrawRect( new Rect( LocalRect.TopLeft.WithY( 16f ), LocalRect.BottomRight + Vector2.Down * 16f ).Shrink( 4 ) );
		if ( IsCurrentFrame )
		{
			Paint.SetBrushAndPen( Theme.Highlight.WithAlpha( 0.5f ) );
			Paint.DrawRect( new Rect( LocalRect.TopLeft, LocalRect.BottomRight.WithY( 16f ) ) );
		}

		Paint.SetPen( Theme.Text );
		var rect = new Rect( LocalRect.TopLeft, LocalRect.BottomRight.WithY( 16f ) );
		Paint.DrawText( rect, ( FrameIndex + 1 ).ToString(), TextFlag.Center );

		if ( dragData?.IsValid ?? false )
		{
			Paint.SetBrushAndPen( Theme.WindowBackground.WithAlpha( 0.5f ) );
			Paint.DrawRect( LocalRect );
		}

		//Log.Info( MainWindow.SelectedAnimation.Frames[FrameIndex] );

		var pixRect = new Rect( LocalRect.TopLeft + Vector2.Up * 16f, LocalRect.BottomRight - Vector2.Up * 16f ).Shrink( 4 );
		var aspectRatio = Pixmap.Width / (float)Pixmap.Height;
		if ( aspectRatio > 1f )
		{
			pixRect.Height = pixRect.Height / aspectRatio;
			pixRect.Top -= ( pixRect.Height - pixRect.Width ) / 2f;
			pixRect.Bottom += ( pixRect.Height - pixRect.Width ) / 2f;
		}
		else
		{
			pixRect.Width = pixRect.Width / ( (float)Pixmap.Height / Pixmap.Width );
			pixRect.Left -= ( pixRect.Width - pixRect.Height ) / 2f;
			pixRect.Right -= ( pixRect.Width - pixRect.Height ) / 2f;
		}
		Paint.Draw( pixRect, Pixmap );

		if ( anim.Frames[FrameIndex].Events.Count > 0 )
		{
			var tagRect = new Rect( LocalRect.BottomLeft + Vector2.Down * 20f, new Vector2( Width, 20f ) ).Shrink( 4 );
			Paint.SetBrushAndPen( Theme.Yellow.WithAlpha( 0.5f ) );
			Paint.DrawRect( tagRect );

			string events = string.Join( ", ", anim.Frames[FrameIndex].Events );

			tagRect.Position -= Vector2.Up;
			Paint.SetFont( "Inter", 7, 1000, false );
			Paint.SetPen( Theme.WindowBackground.WithAlpha( 0.4f ) );
			tagRect.Position += 1;
			Paint.DrawText( tagRect, events, TextFlag.Center );
			tagRect.Position -= 1;
			Paint.SetPen( Theme.Text );
			Paint.DrawText( tagRect, events, TextFlag.Center );
		}

		base.OnPaint();

		if ( anim.LoopMode != SpriteResource.LoopMode.None || draggingLoopPointStart || draggingLoopPointEnd )
		{
			var headerSize = 16f;
			var headerWidth = 4f;
			var isStart = anim.GetLoopStart() == FrameIndex;
			var isEnd = anim.GetLoopEnd() == FrameIndex;
			if ( isStart || draggingLoopPointStart )
			{
				var alpha = ( !( ( dragData?.IsValid ?? false ) && dragData.Data.Text == "loop-start" ) || draggingLoopPointStart ) ? 0.5f : 0.25f;
				Paint.SetPen( Theme.Yellow.WithAlpha( alpha ), headerWidth * 2f );
				Paint.DrawLine( LocalRect.TopLeft, LocalRect.BottomLeft );
				Paint.ClearPen();
				Paint.SetBrush( Theme.Yellow.WithAlpha( alpha ) );
				Paint.DrawPolygon( [LocalRect.TopLeft + new Vector2( headerWidth, 0 ), LocalRect.TopLeft + new Vector2( headerWidth, headerSize ), LocalRect.TopLeft + new Vector2( headerWidth + headerSize, headerSize / 2f )] );
			}
			if ( isEnd || draggingLoopPointEnd )
			{
				var alpha = ( !( ( dragData?.IsValid ?? false ) && dragData.Data.Text == "loop-end" ) || draggingLoopPointEnd ) ? 0.5f : 0.25f;
				Paint.SetPen( Theme.Yellow.WithAlpha( alpha ), headerWidth * 2f );
				Paint.DrawLine( LocalRect.TopRight, LocalRect.BottomRight );
				Paint.ClearPen();
				Paint.SetBrush( Theme.Yellow.WithAlpha( alpha ) );
				Paint.DrawPolygon( [LocalRect.TopRight - new Vector2( headerWidth, 0 ), LocalRect.TopRight + new Vector2( -headerWidth, headerSize ), LocalRect.TopRight + new Vector2( -( headerWidth + headerSize ), headerSize / 2f )] );
			}
		}

		if ( draggingAbove )
		{
			Paint.SetPen( Theme.Highlight, 2f, PenStyle.Dot );
			Paint.DrawLine( LocalRect.TopLeft, LocalRect.BottomLeft );
		}
		else if ( draggingBelow )
		{
			Paint.SetPen( Theme.Highlight, 2f, PenStyle.Dot );
			Paint.DrawLine( LocalRect.TopRight, LocalRect.BottomRight );
		}
	}

	protected override void OnDragStart ()
	{
		base.OnDragStart();

		dragData = new Drag( this );

		if ( draggingLoopPoint != 0 )
		{
			dragData.Data.Text = ( draggingLoopPoint == 1 ) ? "loop-start" : "loop-end";
		}
		else
		{
			dragData.Data.Object = this;
		}

		dragData.Execute();
	}

	public override void OnDragHover ( DragEvent ev )
	{
		base.OnDragHover( ev );

		if ( !TryDragOperation( ev, out var dragDelta ) )
		{
			draggingAbove = false;
			draggingBelow = false;
			draggingLoopPointStart = false;
			draggingLoopPointEnd = false;

			if ( ev.Data.Text == "loop-start" )
			{
				draggingLoopPointStart = true;
			}
			else if ( ev.Data.Text == "loop-end" )
			{
				draggingLoopPointEnd = true;
			}

			return;
		}

		draggingAbove = dragDelta > 0;
		draggingBelow = dragDelta < 0;
	}

	public override void OnDragDrop ( DragEvent ev )
	{
		base.OnDragDrop( ev );
		ResetDrag();

		if ( !TryDragOperation( ev, out var delta ) )
		{
			if ( ev.Data.Text == "loop-start" && FrameIndex <= MainWindow.SelectedAnimation.LoopEnd )
			{
				MainWindow.PushUndo( $"Change {MainWindow.SelectedAnimation.Name} Loop Start" );
				MainWindow.SelectedAnimation.LoopStart = FrameIndex;
				MainWindow.PushRedo();
			}
			else if ( ev.Data.Text == "loop-end" && FrameIndex >= MainWindow.SelectedAnimation.LoopStart )
			{
				MainWindow.PushUndo( $"Change {MainWindow.SelectedAnimation.Name} Loop End" );
				MainWindow.SelectedAnimation.LoopEnd = FrameIndex;
				MainWindow.PushRedo();
			}

			return;
		}

		Move( delta );
		Timeline.UpdateFrameList();
	}

	public override void OnDragLeave ()
	{
		base.OnDragLeave();
		ResetDrag();
	}

	void ResetDrag ()
	{
		draggingAbove = false;
		draggingBelow = false;
		draggingLoopPointStart = false;
		draggingLoopPointEnd = false;
	}

	bool TryDragOperation ( DragEvent ev, out int delta )
	{
		delta = 0;
		var draggingButton = ev.Data.OfType<FrameButton>().FirstOrDefault();
		if ( draggingButton is null ) return false;
		var otherIndex = draggingButton?.FrameIndex ?? -1;

		if ( otherIndex < 0 || MainWindow.SelectedAnimation == null || FrameIndex == otherIndex )
		{
			return false;
		}

		if ( FrameIndex == -1 || otherIndex == -1 )
		{
			return false;
		}

		delta = otherIndex - FrameIndex;
		return true;
	}

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

		var anim = MainWindow.SelectedAnimation;
		if ( anim.GetLoopStart() == FrameIndex && e.LocalPosition.x < 12 )
		{
			draggingLoopPoint = 1;
		}
		else if ( anim.GetLoopEnd() == FrameIndex && e.LocalPosition.x > Width - 12 )
		{
			draggingLoopPoint = 2;
		}
		else
		{
			draggingLoopPoint = 0;
		}
	}

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

		if ( !MainWindow.Playing )
			MainWindow.CurrentFrameIndex = FrameIndex;

		bool has = Selected.Contains( this );
		bool shifting = e.HasShift && Selected.Count > 0;

		if ( !e.HasCtrl && !e.HasShift )
		{
			Selected.Clear();
		}
		else if ( shifting )
		{
			Selected.Clear();
			int start = Math.Min( lastSelectedIndex, FrameIndex );
			int end = Math.Min( Math.Max( lastSelectedIndex, FrameIndex ), Timeline.Buttons.Count - 1 );

			for ( int i = start; i <= end; i++ )
			{
				Selected.Add( Timeline.Buttons[i] );
			}
		}

		if ( !shifting )
		{
			if ( has )
				Selected.Remove( this );
			else
				Selected.Add( this );

			lastSelectedIndex = FrameIndex;
		}
	}

	void AddEventPopup ()
	{
		var popup = new PopupWidget( MainWindow );
		popup.Layout = Layout.Column();
		popup.Layout.Margin = 16;
		popup.Layout.Spacing = 8;

		popup.Layout.Add( new Label( $"What would you like to name the event?" ) );

		var entry = new LineEdit( popup );
		var button = new Button.Primary( "Create" );

		button.MouseClick = () =>
		{
			AddBroadcastEvent( entry.Text, FrameIndex );
			popup.Visible = false;
		};

		entry.ReturnPressed += button.MouseClick;

		popup.Layout.Add( entry );

		var bottomBar = popup.Layout.AddRow();
		bottomBar.AddStretchCell();
		bottomBar.Add( button );

		popup.Position = Editor.Application.CursorPosition;
		popup.Visible = true;

		entry.Focus();
	}

	void AddBroadcastEvent ( string name, int frame )
	{
		if ( !MainWindow.SelectedAnimation.Frames[FrameIndex].Events.Contains( name ) )
		{
			MainWindow.PushUndo( $"Add Broadcast Event {name}" );
			MainWindow.SelectedAnimation.Frames[FrameIndex].Events.Add( name );
			MainWindow.PushRedo();

			MainWindow.OnAnimationChanges?.Invoke();
		}
	}

	void ClearBroadcastEvents ()
	{
		MainWindow.PushUndo( $"Clear Broadcast Events" );
		MainWindow.SelectedAnimation.Frames[FrameIndex].Events.Clear();
		MainWindow.PushRedo();
		MainWindow.OnAnimationChanges?.Invoke();
	}

	void Move ( int delta )
	{
		MainWindow.PushUndo( $"Re-Order {MainWindow.SelectedAnimation.Name} Frames" );

		var index = FrameIndex;
		var movingIndex = index + delta;
		var frame = MainWindow.SelectedAnimation.Frames[movingIndex];

		MainWindow.SelectedAnimation.Frames.RemoveAt( movingIndex );
		MainWindow.SelectedAnimation.Frames.Insert( index, frame );

		foreach ( var attachment in MainWindow.SelectedAnimation.Attachments )
		{
			if ( attachment.Points.Count == 0 ) continue;

			var maxIndex = Math.Max( index, movingIndex );
			if ( maxIndex >= attachment.Points.Count )
			{
				for ( int i = attachment.Points.Count; i <= maxIndex; i++ )
				{
					attachment.Points.Add( attachment.Points.Last() );
				}
			}

			var point = attachment.Points[movingIndex];
			attachment.Points.RemoveAt( movingIndex );
			attachment.Points.Insert( index, point );
		}

		MainWindow.PushRedo();
	}

	void Duplicate ()
	{
		MainWindow.PushUndo( $"Duplicate {MainWindow.SelectedAnimation.Name} Frame" );

		MainWindow.SelectedAnimation.Frames.Insert( FrameIndex, MainWindow.SelectedAnimation.Frames[FrameIndex].Copy() );

		foreach ( var attachment in MainWindow.SelectedAnimation.Attachments )
		{
			if ( attachment.Points.Count == 0 ) continue;

			if ( FrameIndex < attachment.Points.Count )
			{
				attachment.Points.Insert( FrameIndex, attachment.Points[FrameIndex] );
			}
		}

		Timeline.UpdateFrameList();

		MainWindow.PushRedo();
	}

	void Delete ()
	{
		if ( Selected.Count > 1 )
		{
			DeleteSelected();
			return;
		}

		MainWindow.PushUndo( $"Delete {MainWindow.SelectedAnimation.Name} Frame" );

		MainWindow.SelectedAnimation.Frames.RemoveAt( FrameIndex );

		foreach ( var attachment in MainWindow.SelectedAnimation.Attachments )
		{
			if ( attachment.Points.Count == 0 ) continue;

			if ( FrameIndex < attachment.Points.Count )
			{
				attachment.Points.RemoveAt( FrameIndex );
			}
		}

		Timeline.UpdateFrameList();

		MainWindow.PushRedo();
	}

	void DeleteSelected ()
	{
		MainWindow.PushUndo( $"Delete {Selected.Count} Frames from {MainWindow.SelectedAnimation.Name}" );

		Selected = Selected.OrderBy( x => x.FrameIndex ).ToList();
		for ( int i = Selected.Count - 1; i >= 0; i-- )
		{
			var button = Selected[i];
			MainWindow.SelectedAnimation.Frames.RemoveAt( button.FrameIndex );

			foreach ( var attachment in MainWindow.SelectedAnimation.Attachments )
			{
				if ( attachment.Points.Count == 0 ) continue;

				if ( button.FrameIndex < attachment.Points.Count )
				{
					attachment.Points.RemoveAt( button.FrameIndex );
				}
			}
		}

		Timeline.UpdateFrameList();

		MainWindow.PushRedo();
	}
}