using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Core;
using Core.Lifecycle;
using AppleHills.Core;
namespace UI.Tracking
{
///
/// Singleton manager that tracks off-screen targets and displays directional pins.
/// Uses ManagedBehaviour pattern and lazily accesses camera via QuickAccess (like AudioManager).
///
public class OffScreenTrackerManager : ManagedBehaviour
{
[Header("Configuration")]
[Tooltip("Prefab for the off-screen tracking pin")]
[SerializeField] private OffScreenPin pinPrefab;
[Tooltip("Pixel padding from screen edges (pins appear this many pixels from edge)")]
[SerializeField] private float screenPadding = 50f;
[Tooltip("Buffer zone in pixels to prevent spawn/despawn flickering (spawn is stricter than despawn)")]
[SerializeField] private float bufferZone = 100f;
[Tooltip("Time in seconds a target must be off-screen before pin appears")]
[SerializeField] private float spawnDebounceDelay = 0.3f;
[Tooltip("Time in seconds a target must be on-screen before pin disappears")]
[SerializeField] private float despawnDebounceDelay = 0.2f;
[Tooltip("Update interval in seconds for checking target visibility")]
[SerializeField] private float updateInterval = 0.1f;
// Singleton instance
private static OffScreenTrackerManager _instance;
public static OffScreenTrackerManager Instance => _instance;
// Tracking data
private Dictionary _trackedTargets = new Dictionary();
// Pin pooling
private List _inactivePins = new List();
// Coroutine tracking
private Coroutine _updateCoroutine;
// Auto-created canvas for pins
private Canvas _pinCanvas;
private RectTransform _pinContainer;
///
/// Nested class to track per-target state and timers
///
private class TargetTrackingData
{
public TrackableTarget Target;
public OffScreenPin ActivePin;
public float OffScreenTimer;
public float OnScreenTimer;
public bool IsCurrentlyOffScreen;
public TargetTrackingData(TrackableTarget target)
{
Target = target;
ActivePin = null;
OffScreenTimer = 0f;
OnScreenTimer = 0f;
IsCurrentlyOffScreen = false;
}
}
internal override void OnManagedAwake()
{
// Set singleton instance
_instance = this;
}
internal override void OnManagedStart()
{
// Validate configuration
if (pinPrefab == null)
{
Logging.Error("[OffScreenTrackerManager] Pin prefab not assigned!");
return;
}
// Create dedicated canvas for pins
CreatePinCanvas();
// Subscribe to scene load events from SceneManagerService (like InputManager does)
// This must happen in ManagedStart because SceneManagerService instance needs to be set first
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
Logging.Debug("[OffScreenTrackerManager] Subscribed to SceneLoadCompleted events");
}
// Initialize for current scene and start coroutine
InitializeForCurrentScene();
}
///
/// Called when any scene finishes loading. Refreshes camera and restarts coroutine.
///
private void OnSceneLoadCompleted(string sceneName)
{
Logging.Debug($"[OffScreenTrackerManager] Scene loaded: {sceneName}, reinitializing camera and coroutine");
InitializeForCurrentScene();
}
///
/// Initialize camera reference and start/restart tracking coroutine for current scene
///
private void InitializeForCurrentScene()
{
// Stop existing coroutine if running
if (_updateCoroutine != null)
{
StopCoroutine(_updateCoroutine);
_updateCoroutine = null;
Logging.Debug("[OffScreenTrackerManager] Stopped previous coroutine");
}
// Start the tracking coroutine (camera accessed lazily via QuickAccess)
_updateCoroutine = StartCoroutine(UpdateTrackingCoroutine());
Logging.Debug("[OffScreenTrackerManager] Started tracking coroutine");
}
internal override void OnManagedDestroy()
{
// Unsubscribe from SceneManagerService events (like InputManager does)
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
}
}
private void OnDestroy()
{
// Stop coroutine
if (_updateCoroutine != null)
{
StopCoroutine(_updateCoroutine);
}
// Clean up pooled pins
foreach (var pin in _inactivePins)
{
if (pin != null)
{
Destroy(pin.gameObject);
}
}
_inactivePins.Clear();
// Clean up active pins
foreach (var data in _trackedTargets.Values)
{
if (data.ActivePin != null)
{
Destroy(data.ActivePin.gameObject);
}
}
_trackedTargets.Clear();
// Clean up canvas
if (_pinCanvas != null)
{
Destroy(_pinCanvas.gameObject);
}
// Clear singleton
if (_instance == this)
{
_instance = null;
}
}
///
/// Create a dedicated canvas for pins with sort order 50
///
private void CreatePinCanvas()
{
// Create a new GameObject for the canvas
GameObject canvasObj = new GameObject("OffScreenPinCanvas");
canvasObj.transform.SetParent(transform, false);
// Add and configure Canvas
_pinCanvas = canvasObj.AddComponent