Editor/WeaponImporter/Tool/UI/Preview/WeaponViewport.Picking.cs
using Editor;
using Sandbox;
using WeaponImporter.Core.Hands;

namespace WeaponImporter.Tool.UI;

/// <summary>Clicks in the preview: attachment markers, arms (per-action release) and the weapon surface.</summary>
public sealed partial class WeaponViewport
{
	private const float MarkerRadiusPx = 14f;
	private const float ArmRadiusPx = 12f;

	private readonly Dictionary<int, Side?> _boneSide = new();

	/// <summary>Right/left from the bone name suffix (hand_R, arm_lower_L, finger_index_0_R...).</summary>
	private Side? SideOf( int bone )
	{
		if ( _boneSide.TryGetValue( bone, out var side ) )
			return side;
		var name = _body.IsValid() && _body.Model is { } m ? m.GetBoneName( bone ).ToLowerInvariant() : "";
		side = name.EndsWith( "_r" ) ? Side.Right : name.EndsWith( "_l" ) ? Side.Left : null;
		_boneSide[bone] = side;
		return side;
	}

	private bool ToScreen( Vector3 world, out Vector2 screen )
	{
		screen = Camera.PointToScreenPixels( world, out var behind );
		return !behind;
	}

	/// <summary>The muzzle or eject marker under a viewport pixel (None when neither).</summary>
	public PickTarget HitMarker( Vector2 local )
	{
		var setup = _c.Setup;
		if ( setup is null || !Camera.IsValid() || !_weaponObject.IsValid() || FirstPerson )
			return PickTarget.None;
		var weapon = WeaponWorld;
		var best = PickTarget.None;
		var bestDistance = MarkerRadiusPx;
		foreach ( var (target, point) in new[] { (PickTarget.Muzzle, setup.Muzzle), (PickTarget.Eject, setup.Eject) } )
		{
			if ( point is null || !ToScreen( weapon.PointToWorld( Core.Setup.V.Of( point.Canonical ).ToEngine() ), out var screen ) )
				continue;
			var d = (screen - local).Length;
			if ( d < bestDistance )
			{
				bestDistance = d;
				best = target;
			}
		}
		return best;
	}

	/// <summary>Which character arm (upper arm, forearm, hand or fingers) is under a viewport pixel.</summary>
	public Side? HitArm( Vector2 local )
	{
		if ( !Camera.IsValid() || !_body.IsValid() || !_body.SceneModel.IsValid() || FirstPerson )
			return null;
		Side? best = null;
		var bestDistance = ArmRadiusPx;
		foreach ( var (bone, parent) in _armBones )
		{
			if ( SideOf( bone ) is not { } side )
				continue;
			if ( !ToScreen( _body.SceneModel.GetBoneWorldTransform( parent ).Position, out var a ) || !ToScreen( _body.SceneModel.GetBoneWorldTransform( bone ).Position, out var b ) )
				continue;
			var d = DistanceToSegment( local, a, b );
			if ( d < bestDistance )
			{
				bestDistance = d;
				best = side;
			}
		}
		return best;
	}

	private static float DistanceToSegment( Vector2 p, Vector2 a, Vector2 b )
	{
		var ab = b - a;
		var lengthSq = ab.x * ab.x + ab.y * ab.y;
		var t = lengthSq < 1e-4f ? 0f : Math.Clamp( ((p.x - a.x) * ab.x + (p.y - a.y) * ab.y) / lengthSq, 0f, 1f );
		return (p - (a + ab * t)).Length;
	}

	/// <summary>Where a marker is on screen (gate and tests), or null.</summary>
	public Vector2? MarkerScreenPosition( PickTarget target )
	{
		var point = target == PickTarget.Muzzle ? _c.Setup?.Muzzle : target == PickTarget.Eject ? _c.Setup?.Eject : null;
		if ( point is null || !Camera.IsValid() || !_weaponObject.IsValid() )
			return null;
		return ToScreen( WeaponWorld.PointToWorld( Core.Setup.V.Of( point.Canonical ).ToEngine() ), out var s ) ? s : null;
	}

	/// <summary>Middle of a forearm on screen (gate and tests), or null.</summary>
	public Vector2? ArmScreenPosition( Side side )
	{
		if ( !Camera.IsValid() || !_body.IsValid() || !_body.SceneModel.IsValid() )
			return null;
		foreach ( var (bone, parent) in _armBones )
		{
			if ( SideOf( bone ) != side || !_body.Model.GetBoneName( bone ).StartsWith( "hand", StringComparison.OrdinalIgnoreCase ) )
				continue;
			var mid = (_body.SceneModel.GetBoneWorldTransform( parent ).Position + _body.SceneModel.GetBoneWorldTransform( bone ).Position) * 0.5f;
			return ToScreen( mid, out var s ) ? s : null;
		}
		return null;
	}

	public int FirstPersonPieceCount => _fpPieces.Count;

	/// <summary>The hand whose grip contact marker is under a viewport pixel (drag to move the grip).</summary>
	public Side? HitGripContact( Vector2 local )
	{
		var setup = _c.Setup;
		if ( setup is null || !_c.ShowContacts || !Camera.IsValid() || !_weaponObject.IsValid() || OwnArmsView )
			return null;
		var weapon = WeaponWorld;
		Side? best = null;
		var bestDistance = MarkerRadiusPx;
		foreach ( var (side, grip) in new[] { (Side.Right, setup.Primary), (Side.Left, setup.UseSupportHand ? setup.Support : null) } )
		{
			if ( grip is null || !ToScreen( weapon.PointToWorld( Core.Setup.V.Of( grip.Contact ).ToEngine() ), out var screen ) )
				continue;
			var d = (screen - local).Length;
			if ( d < bestDistance )
			{
				bestDistance = d;
				best = side;
			}
		}
		return best;
	}

	/// <summary>Hand grip being dragged across the weapon, or null.</summary>
	private Side? _dragGrip;

	/// <summary>Starts dragging a grip contact when the press lands on one.</summary>
	private bool BeginGripDrag( Vector2 local )
	{
		if ( _c.Pick != PickTarget.None || HitGripContact( local ) is not { } side )
			return false;
		_dragGrip = side;
		Cursor = CursorShape.ClosedHand;
		_c.SetStatus( $"Moving the {(side == Side.Right ? "right" : "left")} grip: drag across the weapon.", Theme.Blue );
		return true;
	}

	private void DragGrip( Vector2 local )
	{
		if ( _dragGrip is not { } side )
			return;
		if ( RaycastWeapon( local ) is { } hit )
			_c.DragGrip( side, hit.Point, hit.Normal );
	}

	private void EndGripDrag()
	{
		if ( _dragGrip is null )
			return;
		_dragGrip = null;
		Cursor = CursorShape.Arrow;
		_c.EndDragGrip();
	}

	/// <summary>A plain left click (no drag) in the preview.</summary>
	private void Click( Vector2 local )
	{
		var pick = _c.Pick;
		if ( pick is PickTarget.Muzzle or PickTarget.Eject )
		{
			// Another marker selects that one; the weapon surface moves the selected one; anything else deselects.
			var marker = HitMarker( local );
			if ( marker != PickTarget.None && marker != pick )
			{
				_c.BeginPick( marker );
				return;
			}
			if ( RaycastWeapon( local ) is { } hit )
				_c.CompletePick( hit.Point, hit.Normal );
			else
			{
				_c.BeginPick( PickTarget.None );
				_c.SetStatus( "Marker deselected.", Theme.TextLight );
			}
			return;
		}
		if ( pick != PickTarget.None )
		{
			if ( RaycastWeapon( local ) is { } hit )
				_c.CompletePick( hit.Point, hit.Normal );
			else
				_c.SetStatus( "That missed the weapon. " + ImporterController.PickHint( pick ), Theme.Yellow );
			return;
		}

		var clicked = HitMarker( local );
		if ( clicked != PickTarget.None )
		{
			_c.BeginPick( clicked );
			return;
		}
		if ( HitArm( local ) is { } side )
			_c.ToggleHand( _c.Role, side );
	}
}