Weapons/ToolGun/Modes/Wire/WireTool.cs
[Icon( "🔗" )]
[Title( "#tool.name.wire" )]
[ClassName( "wire" )]
[Group( "#tool.group.tools" )]
public sealed class WireTool : BaseConstraintToolMode
{
	private GameObject _hoveredRoot;
	private SignalPortDescription[] _hoveredPorts = [];
	private SignalPortDescription _sourcePort;
	private int _selectedPortIndex;
	private (GameObject Root, int Stage, SignalPortDescription Source) _hoveredPortsKey;
	private RealTimeSince _sinceHoveredPortsRefresh;

	private SignalsOverlay.Entry _hoverEntry;
	private SignalPortDescription[] _hoverEntryPorts;
	private int _hoverEntryIndex = -1;

	private SignalsOverlay _overlay;

	public override string Description => Stage == 1 ? "#tool.hint.wire.stage1" : "#tool.hint.wire.stage0";
	public override string PrimaryAction => Stage == 1 ? "#tool.hint.wire.finish" : "#tool.hint.wire.source";
	public override string ReloadAction => "#tool.hint.wire.remove";
	public override bool UseSnapGrid => false;

	public override void OnControl()
	{
		if ( Input.Pressed( "attack2" ) )
			_sourcePort = null;

		var select = TraceSelect();
		UpdateHoveredSelection( select );
		SelectPortWithMouseWheel();

		var pressedPrimary = Input.Pressed( "attack1" );
		if ( pressedPrimary && SelectedPort is null )
		{
			Input.Clear( "attack1" );
		}
		else if ( pressedPrimary && Stage == 0 )
		{
			_sourcePort = SelectedPort;
		}
		else if ( pressedPrimary && !SignalSystem.AreCompatible( _sourcePort, SelectedPort ) )
		{
			Input.Clear( "attack1" );
		}

		base.OnControl();

		IsValidState = SelectedPort is not null && (Stage == 0 || _sourcePort is not null);

		UpdateOverlay();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		// The overlay hides itself once we stop calling Show.
		_sourcePort = null;
		_selectedPortIndex = 0;
		_hoverEntry = null;
		_hoverEntryPorts = null;
		_overlay = null;
	}

	private SignalPortDescription SelectedPort
	{
		get
		{
			if ( _hoveredPorts.Length == 0 ) return null;
			var port = _hoveredPorts[_selectedPortIndex.Clamp( 0, _hoveredPorts.Length - 1 )];
			return IsPortValid( port ) ? port : null;
		}
	}

	private static bool IsPortValid( SignalPortDescription port ) =>
		port?.Component.IsValid() == true && port.Component.GameObject.IsValid();

	private void UpdateHoveredSelection( SelectionPoint select )
	{
		// Port descriptions can outlive their component. Cancel the pending wire
		// before accessing the source or filtering compatible targets.
		if ( Stage != 0 && (!IsPortValid( _sourcePort ) || !Point1.IsValid()) )
		{
			Stage = 0;
			Point1 = default;
			Point2 = default;
			_sourcePort = null;
			_hoveredPortsKey = default;
			_hoveredPorts = [];
			_selectedPortIndex = 0;
		}

		var root = select.IsValid() ? select.GameObject.Root : null;
		var targetChanged = root != _hoveredRoot;
		_hoveredRoot = root;

		if ( Stage == 0 )
			_sourcePort = null;

		// Enumerating ports reflects over the whole contraption — only redo it when the
		// selection changed, plus a slow tick to catch components toggling on or off.
		var key = (root, Stage, _sourcePort);
		if ( key != _hoveredPortsKey || _sinceHoveredPortsRefresh > 0.2f )
		{
			_hoveredPortsKey = key;
			_sinceHoveredPortsRefresh = 0;

			var canTarget = root.IsValid() && (Stage == 0 || (_sourcePort is not null && root != _sourcePort.Component.GameObject.Root));
			_hoveredPorts = !canTarget ? [] : Stage == 0
				? SignalSystem.GetPorts( root ).ToArray()
				: SignalSystem.GetCompatiblePorts( root, _sourcePort ).ToArray();
		}

		if ( targetChanged || _selectedPortIndex >= _hoveredPorts.Length )
			_selectedPortIndex = 0;
	}

	private void SelectPortWithMouseWheel()
	{
		var wheel = Input.MouseWheel.y;
		if ( wheel.AlmostEqual( 0f ) ) return;

		if ( _hoveredPorts.Length > 1 )
		{
			var direction = wheel > 0f ? -1 : 1;
			_selectedPortIndex = (_selectedPortIndex + direction + _hoveredPorts.Length) % _hoveredPorts.Length;
		}

		// Inventory reads this later in the frame. Wire tool owns the wheel while choosing a port.
		Input.MouseWheel = default;
	}

	private void UpdateOverlay()
	{
		if ( !_overlay.IsValid() )
			_overlay = Scene.Get<SignalsOverlay>();
		if ( !_overlay.IsValid() ) return;
		if ( Player?.WantsHideHud == true ) return;

		// The overlay collects and draws the wires itself; we just keep it up and
		// hand it the hover card for the port being picked.
		if ( !_hoveredRoot.IsValid() || SelectedPort is null )
		{
			_hoverEntry = null;
			_hoverEntryPorts = null;
		}
		else if ( _hoveredPorts != _hoverEntryPorts || _selectedPortIndex != _hoverEntryIndex )
		{
			// This runs every frame — only rebuild the card when the selection changed.
			_hoverEntryPorts = _hoveredPorts;
			_hoverEntryIndex = _selectedPortIndex;

			var options = _hoveredPorts.Select( port => new SignalsOverlay.Option( port.Title )
			{
				Icon = port.Icon,
				Description = string.IsNullOrWhiteSpace( port.Description )
					? $"Signal {port.Kind} on {port.ComponentTitle}."
					: port.Description
			} ).ToArray();

			_hoverEntry = new SignalsOverlay.Entry( _hoveredRoot, SelectedPort.Icon )
			{
				Card = new SignalsOverlay.Card( SelectedPort.ComponentTitle )
				{
					Options = options,
					SelectedOption = _selectedPortIndex
				}
			};
		}

		_overlay.Show( _hoverEntry );
	}

	protected override IEnumerable<GameObject> FindConstraints( GameObject linked, GameObject target )
	{
		foreach ( var link in linked.GetComponentsInChildren<ManualLink>( true ) )
			if ( linked == target || link.Body?.Root == target )
				yield return link.GameObject;
	}

	protected override void CreateConstraint( SelectionPoint point1, SelectionPoint point2 )
	{
		var links = ManualLink.CreatePair( point1.GameObject, point2.GameObject );
		Track( links );

		var undo = Player.Undo.Create();
		undo.Name = "Link";
		undo.Add( links[0] );
	}

	protected override void OnConstraintSubmitted( SelectionPoint point1, SelectionPoint point2 )
	{
		SignalSystem.SetConnected( _sourcePort, SelectedPort, true );

		_sourcePort = null;
	}
}