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

namespace Marionette.Tools;

/// <summary>
/// The BonesObject tab - header fields (Model / Asset Path / Rig Asset Path / Animation Speed)
/// over a real tree of the rig's bones, matching the reference video: pelvis's chain and a
/// separate root_IK utility chain (aim_matrix/foot_IK_target/hand_IK_attach) both as top-level
/// roots, each bone shown with its actual children rather than a flat list.
/// </summary>
internal sealed class RigBonesPanel : Widget
{
	private readonly RigViewport _viewport;
	private TreeView _tree;
	private ControlSheet _header;
	private Editor.Label _assetPathValue;

	public Action Edited { get; set; }

	private RigAnimDocument _anim;
	private Asset _asset;

	public RigBonesPanel( Widget parent, RigViewport viewport ) : base( parent )
	{
		Name = "BonesObject";
		WindowTitle = "BonesObject";
		SetWindowIcon( "polyline" );

		_viewport = viewport;

		Layout = Layout.Column();

		Layout.Add( RigHelpBox.Create( this,
			"Which model this clip poses, where the clip file lives, and a live tree of every " +
			"bone in the model.",
			new[]
			{
				RigHelpBox.S( "Model",
					"The skinned model this clip is authored against. Set it here (or drag a " +
					"model in) to load it into the viewport - nothing will show up there until " +
					"this is set." ),

				RigHelpBox.S( "Asset Path",
					"Read-only - shows where this .riganim file lives on disk once it's been " +
					"saved." ),

				RigHelpBox.S( "Rig Asset Path",
					"Points at a separate Control Rig (.ctrlrig) asset holding this model's " +
					"IK/Limit constraints - its own file rather than living inside this clip, " +
					"because several clips can share one rig. Drag a .ctrlrig asset onto this " +
					"field, or click the folder icon to browse for one. Optional - only needed if " +
					"you're using the Constraints tab. Don't have one yet? Right-click in the " +
					"Assets panel -> New -> Control Rig." ),

				RigHelpBox.S( "Animation Speed",
					"Frames per second this clip plays at, used by the Timeline's transport here " +
					"and by RigAnimPlayerComponent when it plays this clip back in-game." ),

				RigHelpBox.S( "Frame Count",
					"How long the clip is. The Timeline shows exactly this many frames - if it " +
					"looks too short to fit what you're animating, this is the number to raise, " +
					"not the timeline's zoom. At 30 fps, 30 frames is one second." ),

				RigHelpBox.S( "Bone tree",
					"Every bone in Model, arranged the way it's actually rigged. Click a bone here " +
					"to select it, same as clicking its dot in the viewport - either way brings up " +
					"its rotate ring so you can pose it." ),
			} ) );

		// Its own row, outside the ControlSheet entirely - AddLayout( Layout.Row() ) rendered as
		// a plain, unstyled label with no value visible next to it (confirmed by report: it read
		// as missing altogether, not just differently styled from the other rows). A persistent
		// widget built once here, with only its text updated on rebuild, sidesteps that.
		var assetPathRow = new Widget( this ) { Layout = Layout.Row() };
		assetPathRow.Layout.Margin = new Sandbox.UI.Margin( 8, 4 );
		assetPathRow.Layout.Spacing = 8;
		assetPathRow.Layout.Add( new Editor.Label( "Asset Path" ) { FixedWidth = 110 } );
		_assetPathValue = assetPathRow.Layout.Add( new Editor.Label( "" ) { Enabled = false }, 1 );
		Layout.Add( assetPathRow );

		_header = new ControlSheet();
		Layout.Add( _header );

		_tree = new TreeView( this );
		_tree.OnSelectionChanged = objs =>
		{
			if ( objs?.FirstOrDefault() is string bone )
				_viewport.Select( bone );
		};
		Layout.Add( _tree, 1 );

		Rebuild();
	}

	public void SetAsset( Asset asset, RigAnimDocument anim )
	{
		_asset = asset;
		_anim = anim;
		RebuildHeader();
		Rebuild();
	}

	private void RebuildHeader()
	{
		_header.Clear( true );

		_assetPathValue.Text = _asset is null ? "(not saved yet - Ctrl+S)" : $"{_asset.Name}   {_asset.Path}";

		if ( _anim is null )
			return;

		var serialized = EditorTypeLibrary.GetSerializedObject( _anim );
		serialized.OnPropertyChanged += _ =>
		{
			Rebuild();
			Edited?.Invoke();
		};

		if ( serialized.TryGetProperty( nameof( RigAnimDocument.SourceModel ), out var model ) )
			_header.AddRow( model );

		if ( serialized.TryGetProperty( nameof( RigAnimDocument.RigAsset ), out var rigAsset ) )
			_header.AddRow( rigAsset );

		if ( serialized.TryGetProperty( nameof( RigAnimDocument.AnimationSpeed ), out var speed ) )
			_header.AddRow( speed );

		// HOW LONG THE CLIP IS. Missing from this sheet until now, which meant FrameCount was
		// stuck at its default of 30 forever - the timeline drew the whole clip correctly and the
		// clip simply could not be made longer. It reads as the timeline being truncated, or as
		// scrolling being broken, because those are the explanations that occur to you when the
		// real one isn't on screen anywhere.
		if ( serialized.TryGetProperty( nameof( RigAnimDocument.FrameCount ), out var frameCount ) )
			_header.AddRow( frameCount );

		// Reference props - static models to pose against. Sits with the source model rather than
		// in its own tab because it answers the same question: what is in this viewport.
		if ( serialized.TryGetProperty( nameof( RigAnimDocument.ReferenceProps ), out var props ) )
			_header.AddRow( props );
	}

	public void Rebuild()
	{
		_tree.Clear();

		foreach ( var root in _viewport.RootBoneNames() )
		{
			var node = _tree.AddItem( new BoneTreeNode( root, _viewport ) );
			_tree.Open( node );
		}
	}

	private sealed class BoneTreeNode : TreeNode<string>
	{
		private readonly RigViewport _viewport;

		public BoneTreeNode( string bone, RigViewport viewport ) : base( bone )
		{
			_viewport = viewport;
		}

		public override void OnPaint( VirtualWidget item )
		{
			PaintSelection( item );

			Paint.SetPen( Theme.Blue );
			Paint.DrawIcon( item.Rect, "fiber_manual_record", 12, TextFlag.LeftCenter );

			Paint.SetPen( Theme.Text );
			Paint.DrawText( item.Rect.Shrink( 20, 0, 0, 0 ), Value, TextFlag.LeftCenter );
		}

		protected override void BuildChildren()
		{
			Clear();

			AddItems( _viewport.ChildBoneNames( Value ).Select( c => new BoneTreeNode( c, _viewport ) ) );
		}
	}
}