Editor/RigControlEditor/RigEventProperties.cs
using Editor;
using Marionette;
using Sandbox;
using System;
using System.Linq;

namespace Marionette.Tools;

/// <summary>
/// The AnimEvents tab - the event list up top ("Add Event" alongside it), the selected event's
/// fields below via a ControlSheet built off its SerializedObject (the same machinery the
/// Inspector uses for any [Property]-decorated type, so Name/StartFrame/EndFrame/Side/Model/
/// AttachBone/FollowBone/Offset all show up with the right widget per type for free).
/// </summary>
internal sealed class RigEventProperties : Widget
{
	private RigAnimDocument _asset;
	private RigEvent _selected;

	private ListView _list;
	private Widget _detail;

	public Action Edited { get; set; }
	public Action<RigEvent> Selected { get; set; }

	public RigEventProperties( Widget parent ) : base( parent )
	{
		Name = "AnimEvents";
		WindowTitle = "AnimEvents";
		SetWindowIcon( "bolt" );

		Layout = Layout.Column();

		Layout.Add( RigHelpBox.Create( this,
			"The list of frame-ranged events on this clip. Each one spawns a prop model, attaches " +
			"it to a bone for a span of frames, then removes it - the \"burger appears in hand_L " +
			"from frame 0 to 270\" idea.",
			new[]
			{
				RigHelpBox.S( "Fields",
					"Name - just a label, for your own reference.\n" +
					"Start / End Frame - the event is active only while the playhead is inside " +
					"this range.\n" +
					"Model - the prop to spawn (drag one in, or use the folder icon).\n" +
					"Attach Bone - the exact bone name to parent it to (e.g. hand_L).\n" +
					"Follow Bone - on: re-parents to the bone every frame, for something held. " +
					"off: snaps in place once at Start Frame and stays there, for something set " +
					"down.\n" +
					"Offset - position/rotation/scale relative to the bone.\n" +
					"Side - Client spawns locally only (cheap, cosmetic). Server spawns on the " +
					"host, for anything that should be networked or hittable by other players." ),

				RigHelpBox.S( "How to use it",
					"Add Event creates one; select it in the list to edit its fields below; Delete " +
					"Event removes whichever one is selected. Nothing here plays back " +
					"automatically in-game - RigEventPlayerComponent, on the model in your actual " +
					"scene, reads this list and does the spawning at runtime." ),

				RigHelpBox.S( "Morph Events (below)",
					"Same start/end-frame idea, but drives a morph target's value instead of " +
					"spawning a prop - for facial expressions or similar." ),
			} ) );

		var bar = Layout.AddRow();
		bar.Margin = 4;
		bar.Spacing = 4;
		bar.Add( new Editor.Label( "Events" ), 1 );
		bar.Add( new Button( "Add Event", "add" ) { Clicked = AddEvent } );

		_list = Layout.Add( new ListView( this ) { ItemSize = new Vector2( -1, 28 ) } );
		_list.ItemPaint = PaintRow;
		_list.ItemSelected = o => Select( o as RigEvent );
		_list.FixedHeight = 140;

		var scroll = Layout.Add( new ScrollArea( this ), 1 );
		scroll.VerticalScrollbarMode = ScrollbarMode.Auto;
		scroll.HorizontalScrollbarMode = ScrollbarMode.Off;

		_detail = new Widget( this ) { Layout = Layout.Column() };
		_detail.Layout.Margin = 8;
		_detail.Layout.Spacing = 4;
		scroll.Canvas = _detail;

		Rebuild();
	}

	public void SetAsset( RigAnimDocument asset )
	{
		_asset = asset;
		_selected = null;
		Rebuild();
	}

	public void Select( RigEvent evt )
	{
		_selected = evt;
		Selected?.Invoke( evt );
		RebuildDetail();
	}

	private void AddEvent()
	{
		if ( _asset is null )
			return;

		var evt = new RigEvent { Name = "event", StartFrame = 0, EndFrame = Math.Min( 10, Math.Max( 1, _asset.FrameCount - 1 ) ) };
		_asset.Events.Add( evt );

		Rebuild();
		Select( evt );
		Edited?.Invoke();
	}

	private void DeleteEvent( RigEvent evt )
	{
		if ( _asset is null || !_asset.Events.Remove( evt ) )
			return;

		if ( _selected == evt )
			Select( null );

		Rebuild();
		Edited?.Invoke();
	}

	public void Rebuild()
	{
		_list.SetItems( _asset?.Events.Cast<object>().ToList() ?? new() );
		RebuildDetail();
	}

	private void PaintRow( VirtualWidget vw )
	{
		if ( vw.Object is not RigEvent evt )
			return;

		var rect = vw.Rect;

		if ( Paint.HasSelected || Paint.HasMouseOver )
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.Primary.WithAlpha( Paint.HasSelected ? 0.5f : 0.2f ) );
			Paint.DrawRect( rect, 3 );
		}

		Paint.ClearBrush();
		Paint.SetPen( Paint.HasSelected ? Color.White : Theme.Text );
		Paint.DrawText( new Rect( rect.Left + 8, rect.Top, rect.Width * 0.5f, rect.Height ), evt.Name, TextFlag.LeftCenter );

		Paint.SetPen( Theme.Text.WithAlpha( 0.6f ) );
		Paint.DrawText( new Rect( rect.Left, rect.Top, rect.Width - 12, rect.Height ),
			$"{evt.StartFrame}-{evt.EndFrame}  ·  {evt.Side}", TextFlag.RightCenter );
	}

	private void RebuildDetail()
	{
		_detail.Layout.Clear( true );

		if ( _asset is null )
		{
			_detail.Layout.Add( new Editor.Label( "Open a Rig Events asset to edit its events." ) { Enabled = false } );
			return;
		}

		if ( _selected is null )
		{
			_detail.Layout.Add( new Editor.Label( "Select an event above to edit it." ) { Enabled = false } );
		}
		else
		{
			var serialized = EditorTypeLibrary.GetSerializedObject( _selected );

			serialized.OnPropertyChanged += _ => Edited?.Invoke();

			var sheet = new ControlSheet();
			sheet.AddObject( serialized );

			_detail.Layout.Add( sheet );

			var deleteRow = _detail.Layout.AddRow();
			deleteRow.AddStretchCell();
			deleteRow.Add( new Button( "Delete Event", "delete" )
			{
				Clicked = () => DeleteEvent( _selected )
			} );
		}

		BuildMorphEvents();
	}

	/// <summary>Morph triggers (the "eat morph" row in the reference video) - simple frame-ranged
	/// entries independent of the bone events above, so they're always shown rather than gated on
	/// a selection.</summary>
	private void BuildMorphEvents()
	{
		_detail.Layout.AddSpacingCell( 12 );

		var header = _detail.Layout.AddRow();
		header.Add( new Editor.Label( "Morph Events" ) { Enabled = false }, 1 );
		header.Add( new Button( "add", "add" ) { Clicked = AddMorphEvent } );

		foreach ( var morph in _asset.MorphEvents )
			_detail.Layout.Add( MorphRow( morph ) );
	}

	private Widget MorphRow( MorphEvent morph )
	{
		var row = new Widget( _detail ) { Layout = Layout.Column() };
		row.Layout.Margin = 4;

		var title = row.Layout.AddRow();
		title.Add( new Editor.Label( string.IsNullOrWhiteSpace( morph.Name ) ? "morph" : morph.Name ), 1 );
		title.Add( new Button( "delete", "delete" )
		{
			Clicked = () =>
			{
				_asset.MorphEvents.Remove( morph );
				Rebuild();
				Edited?.Invoke();
			}
		} );

		var sheet = new ControlSheet();
		var serialized = EditorTypeLibrary.GetSerializedObject( morph );
		serialized.OnPropertyChanged += _ => Edited?.Invoke();
		sheet.AddObject( serialized );
		row.Layout.Add( sheet );

		return row;
	}

	private void AddMorphEvent()
	{
		if ( _asset is null )
			return;

		_asset.MorphEvents.Add( new MorphEvent { Name = "morph", StartFrame = 0, EndFrame = Math.Min( 10, Math.Max( 1, _asset.FrameCount - 1 ) ) } );

		Rebuild();
		Edited?.Invoke();
	}
}