Editor/EffigyEditor/EffigyViewport.BodyDrag.cs
using Editor;
using Effigy;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Marionette.EditorTools;

/// <summary>
/// Dragging the bodies an open Transform is holding — move them with the mouse.
///
/// THE SAME BARGAIN AS THE FACE ARROW, one level up. Transform already moves bodies; what it asked
/// for was three numbers typed into a panel, and typing 0.4 into Z to find out whether 0.4 was right
/// is not how anybody places a part. Press Transform, drag the handle, watch the part go.
///
/// THREE ARROWS HERE, NOT ONE. The face handle is deliberately single-axis because a face has a
/// direction of its own and the other two arrows would be inert (see EffigyViewport.FaceDrag). A
/// body has no such direction: every axis moves it, so every axis is honest, and Gizmo.Control.Position
/// is exactly the handle for that.
///
/// ONLY WHILE A TRANSFORM'S DIALOG IS OPEN, for the reason that keeps this parametric: a body does
/// not move because it was shoved, it moves because a FEATURE says it does. The drag writes the open
/// feature's Translate — typing, with the mouse — and nothing is appended to the tree behind anyone's
/// back. The handle lives exactly as long as that dialog.
///
/// THE VIEWPORT RESOLVES THE DRAG AND STOPS THERE. Which parameter the displacement lands in is a
/// question about the feature, so it lives with the dialog that owns it.
/// </summary>
internal sealed partial class EffigyViewport
{
	/// <summary>
	/// Set by the dialog of a feature that moves whole bodies, for as long as that dialog is open.
	/// The gate that keeps this parametric.
	///
	/// TURNING IT ON STARTS AT MOVE, whatever the last Transform was left on. Pressing Transform is
	/// what promises arrows; opening one onto a scale nub because the previous dialog ended there
	/// would be a handle nobody asked for.
	/// </summary>
	public bool BodyDragEnabled
	{
		get => _bodyDragEnabled;
		set
		{
			if ( value && !_bodyDragEnabled )
				_bodyDragMode = BodyDragMode.Move;

			_bodyDragEnabled = value;
		}
	}

	private bool _bodyDragEnabled;

	/// <summary>What the body handle does. One handle at a time: arrows, rings and a scale nub on one
	/// origin fight for the same pixels, and a click that lands on the wrong one is a part that jumps.</summary>
	public enum BodyDragMode { Move, Rotate, Scale }

	private BodyDragMode _bodyDragMode = BodyDragMode.Move;

	/// <summary>
	/// Switch the body handle, or do nothing when there is no body handle — W/E/R call this beside
	/// <see cref="SetBoneDragMode"/>, so the three keys mean one thing whichever handle is up.
	///
	/// REFUSED MID-DRAG. Swapping the control under a held button leaves the old one believing it is
	/// still grabbed, and the new one takes the next frame's mouse as the start of a drag.
	/// </summary>
	public void SetBodyDragMode( BodyDragMode mode )
	{
		if ( !BodyDragEnabled || _draggingBody )
			return;

		_bodyDragMode = mode;
	}

	/// <summary>
	/// Raised once when a drag starts, before anything has moved, with where the handle was.
	///
	/// THE ANCHOR IS PASSED because rotate and scale pivot about it, and the consumer — which turns
	/// a drag into Translate — has to keep that point still while the rest turns or grows round it.
	/// </summary>
	public Action<Vec3> BodyDragBegan { get; set; }

	/// <summary>
	/// Raised every frame the handle moves: the displacement accumulated since the drag started,
	/// not the per-frame delta.
	///
	/// The total rather than something to integrate, because the consumer sets a parameter from it
	/// and a parameter is a value, not an increment.
	/// </summary>
	public Action<Vec3> BodyDragMoved { get; set; }

	/// <summary>
	/// Raised every frame the rings turn: the rotation since the grab as an axis and an angle in
	/// degrees, world-aligned, about the handle.
	///
	/// AXIS AND DEGREES because that is what a Transform stores; the viewport takes the quaternion
	/// apart so the dialog is handed the feature's own vocabulary rather than one to unpick.
	/// </summary>
	public Action<Vec3, float> BodyRotateDragged { get; set; }

	/// <summary>Raised every frame the scale nub moves: the uniform factor since the grab, about the
	/// handle, already clamped away from zero.</summary>
	public Action<float> BodyScaleDragged { get; set; }

	/// <summary>
	/// Whether anything is wired to what the handle does NOW. A handle nobody listens to is a hitbox
	/// over the model that eats clicks and does nothing, so without a consumer there is no handle.
	/// </summary>
	private bool HasBodyDragConsumer() => _bodyDragMode switch
	{
		BodyDragMode.Rotate => BodyRotateDragged is not null,
		BodyDragMode.Scale => BodyScaleDragged is not null,
		_ => BodyDragMoved is not null,
	};

	/// <summary>Raised when the button comes up.</summary>
	public Action BodyDragEnded { get; set; }

	/// <summary>True while the handle is being dragged. Idle picking stands down for the duration,
	/// or the click that ends the drag would also re-pick whatever is under the cursor.</summary>
	public bool IsDraggingBody => _draggingBody;

	private bool _draggingBody;
	private Vector3 _bodyDragAnchor;
	private Vector3 _bodyDragDelta;

	/// <summary>
	/// The handle for the bodies the open feature is holding, and the drag it reports.
	///
	/// Stood down while a sketch, the sculpt brush, the paint brush or the bone tool is armed: each
	/// of those has a click of its own, and a set of arrows floating over the model while one of them
	/// is live is an invitation to a click that will not do what it looks like.
	/// </summary>
	private void BodyDragFrame()
	{
		if ( !BodyDragEnabled || !HasBodyDragConsumer() )
		{
			EndBodyDrag();
			return;
		}

		if ( (IsSketching || IsSculpting || IsPainting || IsMaterialBrushing || IsNoting || BoneToolActive) && !_draggingBody )
		{
			EndBodyDrag();
			return;
		}

		// MID-DRAG THE BODIES ARE TRAVELLING. The rebuild moves them by exactly the displacement the
		// handle has reported, so anchor plus displacement is where they are — reading their live
		// centre back would add that movement a second time and the arrows would run away from the
		// cursor.
		//
		// Rotate and scale keep the anchor still — that is what the pivot is for — so their handle
		// stays where it was grabbed.
		if ( _draggingBody )
		{
			DrawBodyDragHandle( _bodyDragMode == BodyDragMode.Move ? _bodyDragAnchor + _bodyDragDelta : _bodyDragAnchor );
			return;
		}

		if ( !TryBodyDragHandle( out var centre ) )
		{
			EndBodyDrag();
			return;
		}

		DrawBodyDragHandle( centre );
	}

	/// <summary>
	/// The handle for the current mode, and the drag it reports.
	///
	/// EVERY CONTROL RETURNS FALSE on a frame the value did not move, so a still frame mid-drag is not
	/// the end of the drag — the mouse button is what says that (RigViewport.cs:2337 makes the same
	/// distinction for the same reason).
	///
	/// THE THREE DISAGREE ABOUT WHAT THEY REPORT. Position hands back a per-frame delta, so it is
	/// accumulated; Rotate and Scale hand back the total since the grab, so they are assigned.
	/// Accumulating those would compound every frame's rotation onto the last.
	/// </summary>
	private void DrawBodyDragHandle( Vector3 origin )
	{
		using var scope = Gizmo.Scope( "body-drag", new Transform( origin ) );

		Gizmo.Hitbox.DepthBias = 0.01f;

		switch ( _bodyDragMode )
		{
			case BodyDragMode.Rotate:
			{
				if ( !Gizmo.Control.Rotate( "body-rotate", Rotation.Identity, out var rotation ) )
					break;

				BeginBodyDrag( origin );

				// w is the cosine of half the angle, the vector part the axis times its sine.
				var w = rotation.w.Clamp( -1f, 1f );
				var sin = MathF.Sqrt( MathF.Max( 0f, 1f - w * w ) );

				// No rotation has no axis to name; the grab has happened, nothing has turned yet.
				if ( sin < 1e-6f )
					return;

				var axis = new Vec3( rotation.x, rotation.y, rotation.z ) / sin;
				var degrees = 2f * MathF.Acos( w ).RadianToDegree();

				BodyRotateDragged?.Invoke( axis, degrees );
				return;
			}

			case BodyDragMode.Scale:
			{
				if ( !Gizmo.Control.Scale( "body-scale", 1f, out var scale ) )
					break;

				BeginBodyDrag( origin );

				// Clamped as the rig's scale handle is (RigViewport.cs:2472): a factor reaching zero
				// makes the Transform refuse to rebuild, which reads as the drag breaking the model.
				BodyScaleDragged?.Invoke( scale.Clamp( 0.01f, 100f ) );
				return;
			}

			default:
			{
				if ( !Gizmo.Control.Position( "body-move", Vector3.Zero, out var delta, Rotation.Identity ) )
					break;

				BeginBodyDrag( origin );

				_bodyDragDelta += delta;

				BodyDragMoved?.Invoke( new Vec3( _bodyDragDelta.x, _bodyDragDelta.y, _bodyDragDelta.z ) );
				return;
			}
		}

		// Held through a frame that simply did not move.
		if ( _draggingBody && Gizmo.IsLeftMouseDown )
			return;

		EndBodyDrag();
	}

	private void BeginBodyDrag( Vector3 origin )
	{
		if ( _draggingBody )
			return;

		_draggingBody = true;
		_bodyDragAnchor = origin;
		_bodyDragDelta = Vector3.Zero;
		BodyDragBegan?.Invoke( new Vec3( origin.x, origin.y, origin.z ) );
	}

	private void EndBodyDrag()
	{
		if ( !_draggingBody )
			return;

		_draggingBody = false;
		_bodyDragDelta = Vector3.Zero;
		BodyDragEnded?.Invoke();
	}

	/// <summary>
	/// Where the handle sits: the centre of the bounds of the bodies the feature will move.
	///
	/// THE FEATURE'S SET, NOT THE IDLE ONE — SelectedBodyIds is what the DIALOG is holding, and the
	/// handle answers the dialog. EMPTY MEANS EVERY BODY, because that is what BodySelectionParam
	/// means when nothing has been picked, and a Transform opened on nothing still moves the whole
	/// model; a handle that refused to appear until a body was picked would be lying about that.
	///
	/// BOUNDS CENTRE RATHER THAN A VERTEX AVERAGE, so a dense end of a part does not drag the arrows
	/// off into it. Where a translate handle sits does not change what the drag does — it only has to
	/// be somewhere the user reads as "the thing I am moving".
	/// </summary>
	private bool TryBodyDragHandle( out Vector3 centre )
	{
		centre = Vector3.Zero;

		var wanted = SelectedBodyIds;
		var all = wanted is null || wanted.Count == 0;

		var min = Vec3.Zero;
		var max = Vec3.Zero;
		var found = false;

		foreach ( var body in _displayBodies )
		{
			if ( body?.Mesh is not { } mesh || !body.Visible )
				continue;

			if ( !all && (body.Id is not { } id || !wanted.Contains( id )) )
				continue;

			// Cached per mesh - see TryMeshBounds. This runs every frame something is selected,
			// and it used to walk every vertex of every selected body to do it.
			if ( !TryMeshBounds( mesh, out var bodyMin, out var bodyMax ) )
				continue;

			if ( !found )
			{
				min = bodyMin;
				max = bodyMax;
				found = true;
				continue;
			}

			min = new Vec3( MathF.Min( min.x, bodyMin.x ), MathF.Min( min.y, bodyMin.y ), MathF.Min( min.z, bodyMin.z ) );
			max = new Vec3( MathF.Max( max.x, bodyMax.x ), MathF.Max( max.y, bodyMax.y ), MathF.Max( max.z, bodyMax.z ) );
		}

		if ( !found )
			return false;

		var mid = (min + max) * 0.5f;

		centre = new Vector3( mid.x, mid.y, mid.z );

		return true;
	}
}