Editor/RigControlEditor/RigInspectorPanel.cs
using Editor;
using Sandbox;
using Marionette;
using System;

namespace Marionette.Tools;

/// <summary>
/// The Inspector tab - the selected bone's transform as numbers you can type into.
///
/// WHY THIS EXISTS: the bone gizmo's rings are world-axis aligned, so there is no handle that
/// means "raise the arm". Posing was therefore all feel and no measure - you dragged until it
/// looked right, and nothing could tell you what you had done or ask you to do it again. That
/// also made the tutorial impossible to write: "swing it forward and up" was the best available
/// instruction, because there was no value to name.
///
/// Numbers fix both ends. You can dial a pose in precisely, read back what a drag produced, and
/// copy a value from one side of a rig to the other.
///
/// THE NUMBERS ARE PARENT-SPACE, which is the same space keyframes are stored in and the same
/// space the viewport's corner readout shows. So a bone's rotation reads 0,0,0 at its rest pose
/// rather than at some arbitrary world orientation, and what you see here is literally what gets
/// written to the clip. World-space numbers would have been meaningless for a shoulder.
///
/// THERE IS NO SYNC LAYER HERE, DELIBERATELY. The first version of this panel kept a snapshot
/// object, pushed live values into it on every refresh, wrote them back on every change, and
/// carried two guard flags to stop those two directions fighting. Every bug the panel had came
/// from that: most visibly, typing 63 into a field produced 6, because the write bounced back
/// through a same-frame bone read - which returns the PRE-WRITE value, since overrides only fold
/// in on the scene tick - and overwrote the keystroke.
///
/// BoneTransform's properties now read and write the bone directly, so the sheet is editing the
/// pose rather than editing a copy that something else has to reconcile. That is how the engine's
/// own inspectors work, and it deletes the entire class of bug rather than guarding against it.
/// </summary>
internal sealed class RigInspectorPanel : Widget
{
	private readonly RigViewport _viewport;

	private readonly ControlSheet _sheet;
	private readonly Editor.Label _boneLabel;
	private readonly BoneTransform _values;

	/// <summary>Raised after a field edit has already been applied - the window turns this into an
	/// undo step, matching how every other panel here reports itself.</summary>
	public Action Edited { get; set; }

	/// <summary>The bone the sheet was last built for. Rebuilding on every refresh would steal
	/// focus out of the field you're typing into, so it only happens when the subject changes.</summary>
	private string _builtFor;

	public RigInspectorPanel( Widget parent, RigViewport viewport ) : base( parent )
	{
		Name = "Inspector";
		WindowTitle = "Inspector";
		SetWindowIcon( "tune" );

		_viewport = viewport;

		_values = new BoneTransform
		{
			Viewport = viewport,
			Edited = () => Edited?.Invoke()
		};

		Layout = Layout.Column();

		Layout.Add( RigHelpBox.Create( this,
			"The selected bone's pose as numbers, in the same parent-space form the clip stores.",
			new[]
			{
				RigHelpBox.S( "Rotation",
					"Pitch, yaw and roll relative to the parent bone. All zero means the bone is " +
					"sitting exactly in its rest pose. This is the one you want for almost all " +
					"posing - joints rotate, they don't slide." ),

				RigHelpBox.S( "Position",
					"Offset from the parent bone. Changing this pulls the joint away from its " +
					"parent and stretches the skin, so leave it alone unless you're placing a root " +
					"or an IK target." ),
			} ) );

		var header = new Widget( this ) { Layout = Layout.Row() };
		header.Layout.Margin = new Sandbox.UI.Margin( 8, 4 );
		header.Layout.Spacing = 8;
		header.Layout.Add( new Editor.Label( "Bone" ) { FixedWidth = 110 } );
		_boneLabel = header.Layout.Add( new Editor.Label( "" ), 1 );
		Layout.Add( header );

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

		Layout.AddStretchCell();

		Refresh();
	}

	/// <summary>
	/// Points the sheet at whichever bone is selected.
	///
	/// This no longer pushes any values - the properties read the bone themselves - so it only has
	/// work to do when the SUBJECT changes. Calling it on every frame of a drag is harmless and
	/// costs nothing.
	/// </summary>
	public void Refresh()
	{
		var bone = _viewport?.SelectedBone;

		if ( string.IsNullOrEmpty( bone ) )
		{
			_boneLabel.Text = "(nothing selected)";

			if ( _builtFor is not null )
			{
				_sheet.Clear( true );
				_values.Bone = null;
				_builtFor = null;
			}

			return;
		}

		_boneLabel.Text = bone;

		if ( _builtFor == bone )
			return;

		_values.Bone = bone;
		_builtFor = bone;

		_sheet.Clear( true );

		var serialized = EditorTypeLibrary.GetSerializedObject( _values );

		if ( serialized.TryGetProperty( nameof( BoneTransform.Rotation ), out var rotation ) )
			_sheet.AddRow( rotation );

		if ( serialized.TryGetProperty( nameof( BoneTransform.Position ), out var position ) )
			_sheet.AddRow( position );
	}
}

/// <summary>
/// The selected bone's pose, as live properties rather than a snapshot.
///
/// Reading a property reads the bone; writing one poses the bone. Nothing is cached, so there is
/// nothing to keep in step and no direction for the two to fight in - a drag changes what the
/// fields report simply because the fields report the bone, and typing changes the bone because
/// that is what the setter does.
///
/// Top-level and internal, matching every other type here, rather than nested and private: the
/// type library indexes the assembly's types, and a private nested class is the kind of thing it
/// may reasonably decline to surface.
/// </summary>
internal sealed class BoneTransform
{
	public RigViewport Viewport { get; set; }
	public string Bone { get; set; }
	public Action Edited { get; set; }

	/// <summary>The bone's current parent-space pose, or an identity-ish default when there isn't
	/// one - the sheet is built before a selection exists on first open.</summary>
	private Transform Current =>
		Viewport is not null && !string.IsNullOrEmpty( Bone )
			&& Viewport.TryGetLocalTransform( Bone, out var local )
				? local
				: Transform.Zero;

	[Property]
	public Angles Rotation
	{
		get => Current.Rotation.Angles();
		set
		{
			var current = Current;

			Write( new Transform( current.Position, value.ToRotation(), current.Scale ) );
		}
	}

	[Property]
	public Vector3 Position
	{
		get => Current.Position;
		set
		{
			var current = Current;

			Write( new Transform( value, current.Rotation, current.Scale ) );
		}
	}

	/// <summary>Scale is carried through untouched rather than exposed - nothing in this tool
	/// poses by scaling a bone, and a scale field is a good way to break a rig by accident.</summary>
	private void Write( Transform local )
	{
		if ( Viewport is null || string.IsNullOrEmpty( Bone ) )
			return;

		Viewport.SetLocalTransform( Bone, local );

		Edited?.Invoke();
	}
}