UI/Common/FocusInput.cs
namespace sGBA;

public sealed class FocusInput
{
	public const float DefaultStickDeadzone = 0.5f;
	public const float MouseWakeThreshold = 5f;
	public const float NavStickDeadzone = 0.1f;
	public const float DefaultRepeatDelay = 0.4f;
	public const float DefaultRepeatRate = 0.08f;

	public readonly struct StickEdges
	{
		public bool Up { get; init; }
		public bool Down { get; init; }
		public bool Left { get; init; }
		public bool Right { get; init; }

		public bool Any => Up || Down || Left || Right;
	}

	public readonly struct NavTriggers
	{
		public bool Up { get; init; }
		public bool Down { get; init; }
		public bool Left { get; init; }
		public bool Right { get; init; }

		public bool Any => Up || Down || Left || Right;
	}

	public bool UseGamepad { get; private set; }

	private bool _edgeUp, _edgeDown, _edgeLeft, _edgeRight;
	private bool _navUpHeld, _navDownHeld, _navLeftHeld, _navRightHeld;
	private float _upHold, _upNext;
	private float _downHold, _downNext;
	private float _leftHold, _leftNext;
	private float _rightHold, _rightNext;

	public void Begin( bool useGamepad )
	{
		UseGamepad = useGamepad;
		ResetTransientState();
		Mouse.Visibility = useGamepad ? MouseVisibility.Hidden : MouseVisibility.Visible;
	}

	public void End()
	{
		var wasGamepad = UseGamepad;
		ResetTransientState();
		Mouse.Visibility = wasGamepad ? MouseVisibility.Hidden : MouseVisibility.Visible;
	}

	public StickEdges Tick( bool extraGamepadInput = false )
	{
		var sx = Input.GetAnalog( InputAnalog.LeftStickX );
		var sy = Input.GetAnalog( InputAnalog.LeftStickY );

		var up = sy < -DefaultStickDeadzone;
		var down = sy > DefaultStickDeadzone;
		var left = sx < -DefaultStickDeadzone;
		var right = sx > DefaultStickDeadzone;

		var edges = new StickEdges
		{
			Up = up && !_edgeUp,
			Down = down && !_edgeDown,
			Left = left && !_edgeLeft,
			Right = right && !_edgeRight,
		};

		_edgeUp = up; _edgeDown = down; _edgeLeft = left; _edgeRight = right;

		UpdateInputMode( extraGamepadInput || edges.Any );
		return edges;
	}

	public NavTriggers TickRepeating(
		string upAction = "GBA_Up",
		string downAction = "GBA_Down",
		string leftAction = "GBA_Left",
		string rightAction = "GBA_Right",
		float repeatDelay = DefaultRepeatDelay,
		float repeatRate = DefaultRepeatRate )
	{
		var sx = Input.GetAnalog( InputAnalog.LeftStickX );
		var sy = Input.GetAnalog( InputAnalog.LeftStickY );

		var up = sy < -NavStickDeadzone;
		var down = sy > NavStickDeadzone;
		var left = sx < -NavStickDeadzone;
		var right = sx > NavStickDeadzone;

		var upEdge = up && !_navUpHeld;
		var downEdge = down && !_navDownHeld;
		var leftEdge = left && !_navLeftHeld;
		var rightEdge = right && !_navRightHeld;

		_navUpHeld = up; _navDownHeld = down; _navLeftHeld = left; _navRightHeld = right;

		var triggers = new NavTriggers
		{
			Up = ResolveRepeat( upAction, upEdge, up, ref _upHold, ref _upNext, repeatDelay, repeatRate ),
			Down = ResolveRepeat( downAction, downEdge, down, ref _downHold, ref _downNext, repeatDelay, repeatRate ),
			Left = ResolveRepeat( leftAction, leftEdge, left, ref _leftHold, ref _leftNext, repeatDelay, repeatRate ),
			Right = ResolveRepeat( rightAction, rightEdge, right, ref _rightHold, ref _rightNext, repeatDelay, repeatRate ),
		};

		UpdateInputMode( triggers.Any );
		return triggers;
	}

	public void ForceGamepadMode()
	{
		if ( UseGamepad ) return;
		UseGamepad = true;
		Mouse.Visibility = MouseVisibility.Hidden;
	}

	public void ForceMouseMode()
	{
		if ( !UseGamepad ) return;
		UseGamepad = false;
		Mouse.Visibility = MouseVisibility.Visible;
		ResetTransientState();
	}

	private void UpdateInputMode( bool gamepadInputDetected )
	{
		if ( gamepadInputDetected && !UseGamepad )
		{
			UseGamepad = true;
			Mouse.Visibility = MouseVisibility.Hidden;
		}
		else if ( UseGamepad && Mouse.Delta.Length > MouseWakeThreshold )
		{
			UseGamepad = false;
			Mouse.Visibility = MouseVisibility.Visible;
		}
	}

	private void ResetTransientState()
	{
		_edgeUp = _edgeDown = _edgeLeft = _edgeRight = false;
		_navUpHeld = _navDownHeld = _navLeftHeld = _navRightHeld = false;
		_upHold = _upNext = 0f;
		_downHold = _downNext = 0f;
		_leftHold = _leftNext = 0f;
		_rightHold = _rightNext = 0f;
	}

	private static bool ResolveRepeat( string action, bool stickEdge, bool stickHeld,
		ref float holdTime, ref float nextRepeat, float repeatDelay, float repeatRate )
	{
		if ( Input.Pressed( action ) || stickEdge )
		{
			holdTime = 0f;
			nextRepeat = repeatDelay;
			return true;
		}

		if ( Input.Down( action ) || stickHeld )
		{
			holdTime += Time.Delta;
			if ( holdTime >= nextRepeat )
			{
				nextRepeat += repeatRate;
				return true;
			}
			return false;
		}

		holdTime = 0f;
		return false;
	}
}