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

namespace Marionette.Tools;

/// <summary>
/// The Constraints tab - IK and Limit constraints on the RigDocument (.ctrlrig), matching the
/// reference video's "[IK] IK 0" / "[Limit] limitrot_hand" entries: Name/TargetBone/Weight/
/// Enabled plus type-specific fields, an On/Off toggle, reorder, and Delete.
///
/// Constraints live on the rig, not the clip, so editing here affects every .riganim authored
/// against this .ctrlrig - the same reason RigDocument is its own asset in the first place.
///
/// This tab is full CRUD - add/edit/reorder/enable/delete both constraint types, all of it saved
/// to the .ctrlrig - and both types now actually solve, in RigConstraintSolver:
///
///   - Limit clamps a bone's parent-space rotation, both while dragging and while scrubbing.
///   - IK is a closed-form two-bone solve (law of cosines), run when the constraint's Target Bone
///     is dragged. Chains longer than two bones would need FABRIK or CCD; the arm/leg case has an
///     exact answer and doesn't benefit from iterating.
///
/// Both act at author time and bake into ordinary FK keyframes - see RigConstraintSolver for why
/// that trade was made over live per-frame constraint evaluation.
/// </summary>
internal sealed class RigConstraintsPanel : Widget
{
	private RigDocument _rig;
	private readonly Widget _list;

	public Action Edited { get; set; }

	public RigConstraintsPanel( Widget parent ) : base( parent )
	{
		Name = "Constraints";
		WindowTitle = "Constraints";
		SetWindowIcon( "link" );

		Layout = Layout.Column();

		Layout.Add( RigHelpBox.Create( this,
			"A list of IK and Limit constraints, saved to this clip's Control Rig (.ctrlrig) asset " +
			"rather than the clip itself - every clip that points at the same rig shares them.",
			new[]
			{
				RigHelpBox.S( "IK Constraint",
					"Solves a chain of bones (starting at Target Bone, going up Chain Length " +
					"parents) to reach toward a target, bending through Pole Direction, instead of " +
					"you rotating each joint by hand. Use when an arm, leg, or tail needs to reach " +
					"naturally." ),

				RigHelpBox.S( "Limit Constraint",
					"Clamps a bone's rotation to stay between Min Angles and Max Angles. Use when " +
					"a joint shouldn't be able to bend past a certain point (an elbow that " +
					"shouldn't bend backward, for example)." ),

				RigHelpBox.S( "How to use it",
					"Click Add Constraint, pick a type, type the bone's exact name into Target " +
					"Bone, fill in its fields, then use the On/Off toggle, the down-arrow to " +
					"reorder, or Delete on the row under each one." ),

				RigHelpBox.S( "IK: drag the end, not the chain",
					"An IK constraint's Target Bone is the END of the chain - the hand, not the " +
					"shoulder. Drag that bone and the two bones above it rotate to follow. Switch " +
					"the toolbar to Move mode for IK; positioning the hand is the whole point. " +
					"Pole Direction decides which way the elbow or knee points." ),

				RigHelpBox.S( "Constraints bake into keyframes",
					"Posing through a constraint writes ordinary keyframes for every bone it " +
					"moved, so clips play back identically everywhere with no solver involved. " +
					"The trade: changing a constraint later does not retroactively change poses " +
					"already authored - re-drag the bone to solve it again." ),
			} ) );

		var bar = Layout.AddRow();
		bar.Margin = 4;
		bar.Add( new Editor.Label( "Constraints" ), 1 );
		bar.Add( new Button( "Add Constraint", "add" ) { Clicked = OpenAddMenu } );

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

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

		Rebuild();
	}

	public void SetRig( RigDocument rig )
	{
		_rig = rig;
		Rebuild();
	}

	private void OpenAddMenu()
	{
		if ( _rig is null )
			return;

		var menu = new Menu( this );
		menu.AddOption( "IK Constraint", "swap_calls", () => Add( new IkConstraint { Name = UniqueName( "IK" ) } ) );
		menu.AddOption( "Limit Constraint", "block", () => Add( new LimitConstraint { Name = UniqueName( "Limit" ) } ) );
		menu.OpenAtCursor();
	}

	private string UniqueName( string prefix )
	{
		var index = 0;
		var existing = _rig.IkConstraints.Select( c => c.Name ).Concat( _rig.LimitConstraints.Select( c => c.Name ) ).ToHashSet();

		while ( existing.Contains( $"{prefix} {index}" ) )
			index++;

		return $"{prefix} {index}";
	}

	private void Add( IkConstraint ik )
	{
		_rig.IkConstraints.Add( ik );
		Rebuild();
		Edited?.Invoke();
	}

	private void Add( LimitConstraint limit )
	{
		_rig.LimitConstraints.Add( limit );
		Rebuild();
		Edited?.Invoke();
	}

	public void Rebuild()
	{
		_list.Layout.Clear( true );

		if ( _rig is null )
		{
			_list.Layout.Add( new Editor.Label( "Open a Rig Animation asset with a Rig Asset Path set to edit its constraints." ) { Enabled = false } );
			return;
		}

		foreach ( var ik in _rig.IkConstraints.ToList() )
			_list.Layout.Add( BuildIk( ik ) );

		foreach ( var limit in _rig.LimitConstraints.ToList() )
			_list.Layout.Add( BuildLimit( limit ) );

		if ( _rig.IkConstraints.Count == 0 && _rig.LimitConstraints.Count == 0 )
			_list.Layout.Add( new Editor.Label( "No constraints yet - Add Constraint above" ) { Enabled = false } );
	}

	private Widget Section( string badge, Color badgeColor, string name, out Layout body )
	{
		var panel = new Widget( _list ) { Layout = Layout.Column() };
		panel.Layout.Margin = 6;
		panel.Layout.Spacing = 4;

		var header = panel.Layout.AddRow();
		header.Spacing = 6;

		var chip = new Editor.Label( badge ) { Color = badgeColor };
		chip.SetStyles( "font-weight: bold;" );
		header.Add( chip );
		header.Add( new Editor.Label( name ) { Enabled = false }, 1 );

		body = panel.Layout.AddColumn();

		return panel;
	}

	private Widget BuildIk( IkConstraint ik )
	{
		var panel = Section( "[IK]", Theme.Blue, ik.Name, out var body );

		var serialized = EditorTypeLibrary.GetSerializedObject( ik );
		serialized.OnPropertyChanged += _ => Edited?.Invoke();

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

		body.Add( ToggleRow(
			() => ik.Enabled, v => { ik.Enabled = v; Edited?.Invoke(); },
			() => Reorder( _rig.IkConstraints, ik ),
			() =>
			{
				_rig.IkConstraints.Remove( ik );
				Rebuild();
				Edited?.Invoke();
			},
			resetHandle: () => { ik.PoleDirection = Vector3.Up; Rebuild(); Edited?.Invoke(); } ) );

		return panel;
	}

	private Widget BuildLimit( LimitConstraint limit )
	{
		var panel = Section( "[Limit]", Theme.Yellow, limit.Name, out var body );

		var serialized = EditorTypeLibrary.GetSerializedObject( limit );
		serialized.OnPropertyChanged += _ => Edited?.Invoke();

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

		body.Add( ToggleRow(
			() => limit.Enabled, v => { limit.Enabled = v; Edited?.Invoke(); },
			() => Reorder( _rig.LimitConstraints, limit ),
			() =>
			{
				_rig.LimitConstraints.Remove( limit );
				Rebuild();
				Edited?.Invoke();
			} ) );

		return panel;
	}

	private Widget ToggleRow( Func<bool> getEnabled, Action<bool> setEnabled, Action reorder, Action delete, Action resetHandle = null )
	{
		var row = new Widget { Layout = Layout.Row() };
		row.Layout.Spacing = 4;

		var toggle = new Button( getEnabled() ? "On" : "Off", getEnabled() ? "toggle_on" : "toggle_off" );
		toggle.Clicked = () =>
		{
			setEnabled( !getEnabled() );
			toggle.Text = getEnabled() ? "On" : "Off";
			toggle.Icon = getEnabled() ? "toggle_on" : "toggle_off";
		};
		row.Layout.Add( toggle );

		row.Layout.Add( new Button( "", "swap_vert" ) { Clicked = () => { reorder(); Rebuild(); }, ToolTip = "Move down" } );

		if ( resetHandle is not null )
			row.Layout.Add( new Button( "Reset Handle", "restart_alt" ) { Clicked = resetHandle } );

		row.Layout.AddStretchCell();
		row.Layout.Add( new Button( "Delete", "delete" ) { Clicked = delete } );

		return row;
	}

	private static void Reorder<T>( System.Collections.Generic.List<T> list, T item )
	{
		var index = list.IndexOf( item );
		if ( index < 0 || index >= list.Count - 1 )
			return;

		(list[index], list[index + 1]) = (list[index + 1], list[index]);
	}
}