using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
namespace Input
{
public enum InputMode
{
Game,
UI,
GameAndUI,
InputDisabled
}
///
/// Handles input events and dispatches them to the appropriate ITouchInputConsumer.
/// Supports tap and hold/drag logic, with interactable delegation and debug logging.
///
public class InputManager : ManagedBehaviour
{
private const string UiActions = "UI";
private const string GameActions = "PlayerTouch";
private static InputManager _instance;
// Override consumer stack - using a list to support multiple overrides that can be removed in LIFO order
private readonly List _overrideConsumers = new List();
// Track which consumer is handling the current hold operation
private ITouchInputConsumer _activeHoldConsumer;
///
/// Singleton instance of the InputManager. No longer creates an instance if one doesn't exist.
///
public static InputManager Instance => _instance;
// Settings reference
private IInteractionSettings _interactionSettings;
private PlayerInput playerInput;
private InputAction tapMoveAction;
private InputAction holdMoveAction;
private InputAction positionAction;
private ITouchInputConsumer defaultConsumer;
private bool isHoldActive;
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
public override int ManagedAwakePriority => 25; // Input infrastructure
internal override void OnManagedAwake()
{
// Set instance immediately (early initialization)
_instance = this;
// Load verbosity settings early
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings().inputLogVerbosity;
// Initialize settings reference early (GameManager sets these up in its Awake)
_interactionSettings = GameManager.GetSettingsObject();
// Set up PlayerInput component and actions - critical for input to work
playerInput = GetComponent();
if (playerInput == null)
{
Debug.LogError("[InputManager] InputManager requires a PlayerInput component attached to the same GameObject.");
return;
}
tapMoveAction = playerInput.actions.FindAction("TapMove", false);
holdMoveAction = playerInput.actions.FindAction("HoldMove", false);
positionAction = playerInput.actions.FindAction("TouchPosition", false);
if (tapMoveAction != null)
tapMoveAction.performed += OnTapMovePerformed;
if (holdMoveAction != null)
{
holdMoveAction.performed += OnHoldMoveStarted;
holdMoveAction.canceled += OnHoldMoveCanceled;
}
// Initialize input mode for current scene
SwitchInputOnSceneLoaded(SceneManager.GetActiveScene().name);
}
internal override void OnManagedStart()
{
// Subscribe to scene load events from SceneManagerService
// This must happen in ManagedStart because SceneManagerService instance needs to be set first
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
}
}
///
/// Called when any scene finishes loading. Restores input to GameAndUI mode.
///
private void OnSceneLoadCompleted(string sceneName)
{
LogDebugMessage($"Scene loaded: {sceneName}, restoring input mode");
SwitchInputOnSceneLoaded(sceneName);
}
protected override void OnDestroy()
{
// Unsubscribe from SceneManagerService events
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
}
base.OnDestroy();
// Input action cleanup happens automatically
}
private void SwitchInputOnSceneLoaded(string sceneName)
{
if (sceneName.ToLower().Contains("mainmenu"))
{
SetInputMode(InputMode.GameAndUI);
}
else
{
SetInputMode(InputMode.GameAndUI);
}
}
public void SetInputMode(InputMode inputMode)
{
switch (inputMode)
{
case InputMode.UI:
playerInput.actions.FindActionMap(UiActions).Enable();
playerInput.actions.FindActionMap(GameActions).Disable();
break;
case InputMode.Game:
playerInput.actions.FindActionMap(UiActions).Disable();
playerInput.actions.FindActionMap(GameActions).Enable();
break;
case InputMode.GameAndUI:
playerInput.actions.FindActionMap(UiActions).Enable();
playerInput.actions.FindActionMap(GameActions).Enable();
break;
case InputMode.InputDisabled:
playerInput.actions.FindActionMap(UiActions).Disable();
playerInput.actions.FindActionMap(GameActions).Disable();
break;
}
}
void OnDisable()
{
if (tapMoveAction != null)
tapMoveAction.performed -= OnTapMovePerformed;
if (holdMoveAction != null)
{
holdMoveAction.performed -= OnHoldMoveStarted;
holdMoveAction.canceled -= OnHoldMoveCanceled;
}
}
///
/// Sets the default ITouchInputConsumer to receive input events.
///
public void SetDefaultConsumer(ITouchInputConsumer consumer)
{
defaultConsumer = consumer;
}
///
/// Handles tap input, delegates to interactable if present, otherwise to default consumer.
///
private void OnTapMovePerformed(InputAction.CallbackContext ctx)
{
Vector2 screenPos = positionAction.ReadValue();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
LogDebugMessage($"TapMove performed at {worldPos2D}");
// First try to delegate to an override consumer if available
if (TryDelegateToOverrideConsumer(screenPos, worldPos2D))
{
LogDebugMessage("Tap delegated to override consumer");
return;
}
// Then try to delegate to any ITouchInputConsumer (UI or world interactable)
if (!TryDelegateToAnyInputConsumer(screenPos, worldPos2D))
{
LogDebugMessage("No input consumer found, forwarding tap to default consumer");
defaultConsumer?.OnTap(worldPos2D);
}
else
{
LogDebugMessage("Tap delegated to input consumer");
}
}
///
/// Handles the start of a hold input.
///
private void OnHoldMoveStarted(InputAction.CallbackContext ctx)
{
isHoldActive = true;
Vector2 screenPos = positionAction.ReadValue();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
LogDebugMessage($"HoldMove started at {worldPos2D}");
// First check for override consumers
if (_overrideConsumers.Count > 0)
{
_activeHoldConsumer = _overrideConsumers[_overrideConsumers.Count - 1];
LogDebugMessage($"Hold delegated to override consumer: {_activeHoldConsumer}");
_activeHoldConsumer.OnHoldStart(worldPos2D);
return;
}
// If no override consumers, use default consumer
_activeHoldConsumer = defaultConsumer;
defaultConsumer?.OnHoldStart(worldPos2D);
}
///
/// Handles the end of a hold input.
///
private void OnHoldMoveCanceled(InputAction.CallbackContext ctx)
{
if (!isHoldActive) return;
isHoldActive = false;
Vector2 screenPos = positionAction.ReadValue();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
LogDebugMessage($"HoldMove canceled at {worldPos2D}");
// Notify the active hold consumer that the hold has ended
_activeHoldConsumer?.OnHoldEnd(worldPos2D);
_activeHoldConsumer = null;
}
///
/// Continuously updates hold move input while active.
///
void Update()
{
if (isHoldActive && holdMoveAction != null && holdMoveAction.phase == InputActionPhase.Performed)
{
Vector2 screenPos = positionAction.ReadValue();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
// LogDebugMessage($"HoldMove update at {worldPos2D}");
// Send hold move updates to the active hold consumer
_activeHoldConsumer?.OnHoldMove(worldPos2D);
}
}
///
/// Attempts to delegate a tap to any ITouchInputConsumer at the given position.
/// Checks both UI elements and world interactables.
///
private bool TryDelegateToAnyInputConsumer(Vector2 screenPos, Vector2 worldPos)
{
// First check if we hit a UI element implementing ITouchInputConsumer
if (TryDelegateToUIInputConsumer(screenPos))
{
return true;
}
// If no UI element with ITouchInputConsumer, try world interactables
return TryDelegateToInteractable(worldPos);
}
///
/// Attempts to delegate a tap to a UI element implementing ITouchInputConsumer.
///
private bool TryDelegateToUIInputConsumer(Vector2 screenPos)
{
// Check for UI elements under the pointer
var eventData = new PointerEventData(EventSystem.current)
{
position = screenPos
};
var results = new System.Collections.Generic.List();
EventSystem.current.RaycastAll(eventData, results);
foreach (var result in results)
{
// Try to get ITouchInputConsumer component
var consumer = result.gameObject.GetComponent();
if (consumer == null)
{
consumer = result.gameObject.GetComponentInParent();
}
if (consumer != null)
{
LogDebugMessage($"Delegating tap to UI consumer at {screenPos} (GameObject: {result.gameObject.name})");
consumer.OnTap(screenPos);
return true;
}
}
return false;
}
///
/// Attempts to delegate a tap to an interactable at the given world position.
/// Traces on the "Interactable" channel and logs detailed info.
///
private bool TryDelegateToInteractable(Vector2 worldPos)
{
// First try with the interaction layer mask if available
if (_interactionSettings != null)
{
LayerMask mask = _interactionSettings.InteractableLayerMask;
Collider2D hitWithMask = Physics2D.OverlapPoint(worldPos, mask);
if (hitWithMask != null)
{
var consumer = hitWithMask.GetComponent();
if (consumer == null)
{
consumer = hitWithMask.GetComponentInParent();
}
if (consumer != null)
{
LogDebugMessage($"Delegating tap to consumer at {worldPos} (GameObject: {hitWithMask.gameObject.name})");
consumer.OnTap(worldPos);
return true;
}
}
}
// If no hit with the mask or no settings available, try with all colliders
Collider2D hit = Physics2D.OverlapPoint(worldPos);
if (hit != null)
{
var consumer = hit.GetComponent();
if (consumer != null)
{
LogDebugMessage($"Delegating tap to consumer at {worldPos} (GameObject: {hit.gameObject.name})");
consumer.OnTap(worldPos);
return true;
}
LogDebugMessage($"Collider2D hit at {worldPos} (GameObject: {hit.gameObject.name}), but no ITouchInputConsumer found.");
}
else
{
LogDebugMessage($"No Collider2D found at {worldPos} for interactable delegation.");
}
return false;
}
///
/// Registers an override consumer to receive input events.
/// The most recently registered consumer will receive input first.
///
public void RegisterOverrideConsumer(ITouchInputConsumer consumer)
{
if (consumer == null || _overrideConsumers.Contains(consumer))
return;
_overrideConsumers.Add(consumer);
LogDebugMessage($"Override consumer registered: {consumer}");
}
///
/// Unregisters an override consumer, removing it from the input event delegation.
///
public void UnregisterOverrideConsumer(ITouchInputConsumer consumer)
{
if (consumer == null || !_overrideConsumers.Contains(consumer))
return;
// If this is the active hold consumer, reset it
if (_activeHoldConsumer == consumer)
{
_activeHoldConsumer = null;
}
_overrideConsumers.Remove(consumer);
LogDebugMessage($"Override consumer unregistered: {consumer}");
}
///
/// Clears all registered override consumers.
///
public void ClearOverrideConsumers()
{
_activeHoldConsumer = null;
_overrideConsumers.Clear();
LogDebugMessage("All override consumers cleared.");
}
///
/// Attempts to delegate a tap to the topmost registered override consumer, if any.
///
private bool TryDelegateToOverrideConsumer(Vector2 screenPos, Vector2 worldPos)
{
if (_overrideConsumers.Count == 0)
return false;
// Get the topmost override consumer (last registered)
var consumer = _overrideConsumers[_overrideConsumers.Count - 1];
LogDebugMessage($"Delegating tap to override consumer at {worldPos} (GameObject: {consumer})");
consumer.OnTap(worldPos);
return true;
}
private void LogDebugMessage(string message)
{
if (_logVerbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[InputManager] {message}");
}
}
}
}