namespace Fusion.Addons.InterestManagement
{
	using System.Collections.Generic;
	using Unity.Profiling;
	using UnityEngine;
	/// 
	/// Interest manager for players.
	/// Provides reference to active PlayerInterestView for owner player.
	/// Also provides reference to observed player which can be different from owner player.
	/// There is always one unique instance of PlayerInterestManager per player.
	/// 
	[DefaultExecutionOrder(PlayerInterestManager.EXECUTION_ORDER)]
	public class PlayerInterestManager : NetworkBehaviour
	{
		// CONSTANTS
		public const int EXECUTION_ORDER = InterestProvider.EXECUTION_ORDER + 1000;
		// PUBLIC MEMBERS
		/// 
		/// Owner player, equals to object input authority.
		/// 
		public PlayerRef Player { get => _player; }
		/// 
		/// Observed player. Can be used to spectate another player and get updates for objects in his interest.
		/// 
		public PlayerRef ObservedPlayer { get => _observedPlayer.IsRealPlayer == true ? _observedPlayer : _player; set => _observedPlayer = value; }
		/// 
		/// Reference to active interest view. A player can have multiple views spawned and switch between them.
		/// Only the active view is used to provide player interest, others are ignored.
		/// 
		public PlayerInterestView InterestView { get => _interestView; set => _interestView = value; }
		/// 
		/// Last tick in which player interest was updated.
		/// 
		public Tick LastUpdateTick { get => _lastUpdateTick; }
		/// 
		/// Last known interest cell count.
		/// The value comes from regular update or drawing gizmos.
		/// At runtime the value for server/host is always 0, the interest is not processed.
		/// 
		public int InterestCellCount { get => _interestCellCount; }
		/// 
		/// List of interest cells from Gizmo preview.
		/// This is used for a editor preview of objects in player interest on state authority.
		/// 
		public HashSet GizmoInterestCells { get => _gizmoCells; }
		// PRIVATE MEMBERS
		[SerializeField]
		[InlineHelp][Tooltip("Reference to view that provides interest for the player.")]
		private PlayerInterestView _interestView;
		private PlayerRef           _player;
		private PlayerRef           _observedPlayer;
		private Tick                _lastUpdateTick;
		private bool                _isUpdateRequested;
		private int                 _interestCellCount;
		private PlayerInterestView  _interestViewBackup;
		private InterestProviderSet _interestProviders = new InterestProviderSet();
		private static HashSet        _gizmoCells              = new HashSet();
		private static List _objectsInPlayerInterest = new List();
		private static ProfilerMarker      _updateInterestsMarker   = new ProfilerMarker($"{nameof(PlayerInterestManager)}.{nameof(UpdatePlayerInterests)}");
		// PUBLIC METHODS
		/// 
		/// Returns PlayerInterestView for observed player.
		/// 
		/// PlayerInterestView instance.
		public bool TryGetObservedPlayerView(out PlayerInterestView observedPlayerView)
		{
			GlobalInterestManager globalInterestManager = Runner.GetGlobalInterestManager();
			if (globalInterestManager.TryGetPlayerView(ObservedPlayer, out PlayerInterestView playerView) == true && playerView != null)
			{
				observedPlayerView = playerView;
				return true;
			}
			observedPlayerView = default;
			return false;
		}
		/// 
		/// Request update of player interest. This allows for priority update after an important action so the player receives data as soon as possible.
		/// 
		public void RequestInterestUpdate()
		{
			_isUpdateRequested = true;
		}
		/// 
		/// Checks input authority and other properties and immediately propagates changes.
		/// 
		public void RefreshPlayerProperties()
		{
			UpdatePlayerProperties();
		}
		// PlayerInterestManager INTERFACE
		/// 
		/// Can be used to defer evaluation of interest.
		/// 
		public virtual bool CanUpdatePlayerInterest(bool isUpdateRequested) => isUpdateRequested == true || Runner.IsForward == true;
		/// 
		/// Called before the player interest is updated.
		/// 
		protected virtual void BeforePlayerInterestUpdate() {}
		/// 
		/// Called after the player interest is updated.
		/// 
		protected virtual void AfterPlayerInterestUpdate(bool success, PlayerInterestView playerView) {}
		/// 
		/// Called on the end of Awake().
		/// 
		protected virtual void OnAwake() {}
		/// 
		/// Called on the end of Spawned(), after updating player properties and registering to global interest manager.
		/// 
		protected virtual void OnSpawned() {}
		/// 
		/// Called on the start of Despawned(), before unregistering from global interest manager and resetting state.
		/// 
		protected virtual void OnDespawned(NetworkRunner runner, bool hasState) {}
		/// 
		/// Called on the end of FixedUpdateNetwork(), after updating player properties and resolving player interest.
		/// 
		protected virtual void OnFixedUpdateNetwork() {}
		/// 
		/// Called on the end of Render(), after updating player properties.
		/// 
		protected virtual void OnRender() {}
		// NetworkBehaviour INTERFACE
		public override sealed void Spawned()
		{
			_player = Object.InputAuthority;
			if (_player.IsRealPlayer == true)
			{
				GlobalInterestManager globalInterestManager = Runner.GetGlobalInterestManager();
				globalInterestManager.RegisterPlayerManager(_player, this);
			}
			UpdatePlayerProperties();
			OnSpawned();
		}
		public override sealed void Despawned(NetworkRunner runner, bool hasState)
		{
			OnDespawned(runner, hasState);
			Runner.GetGlobalInterestManager().UnregisterPlayerManager(_player, this);
			_interestView = _interestViewBackup;
			_player            = default;
			_observedPlayer    = default;
			_lastUpdateTick    = default;
			_isUpdateRequested = default;
			_interestCellCount = default;
			_interestProviders.Clear();
		}
		public override sealed void FixedUpdateNetwork()
		{
			UpdatePlayerProperties();
			UpdatePlayerInterests();
			OnFixedUpdateNetwork();
		}
		public override sealed void Render()
		{
			UpdatePlayerProperties();
			OnRender();
		}
		// MonoBehaviour INTERFACE
		private void Awake()
		{
			if (_interestView == null)
			{
				_interestView = GetComponent();
			}
			_interestViewBackup = _interestView;
			OnAwake();
		}
		// PRIVATE METHODS
		private void UpdatePlayerProperties()
		{
			PlayerRef oldPlayer = _player;
			PlayerRef newPlayer = Object.InputAuthority;
			if (oldPlayer != newPlayer)
			{
				GlobalInterestManager globalInterestManager = Runner.GetGlobalInterestManager();
				globalInterestManager.UnregisterPlayerManager(oldPlayer, this);
				_player = newPlayer;
				if (newPlayer.IsRealPlayer == true)
				{
					globalInterestManager.RegisterPlayerManager(newPlayer, this);
				}
			}
			if (_interestView == null)
			{
				// Replace Unity null object by true null before updating player interests.
				// This allows using ReferenceEquals() instead of == operator during evaluations.
				_interestView = null;
			}
		}
		private void UpdatePlayerInterests()
		{
			NetworkRunner runner = Runner;
			if (IsInterestManagementActive(runner) == false)
				return;
			if (_player.IsRealPlayer == false)
				return;
			if (_player == runner.LocalPlayer)
				return;
			if (CanUpdatePlayerInterest(_isUpdateRequested) == false)
				return;
			_isUpdateRequested = false;
			_updateInterestsMarker.Begin();
			BeforePlayerInterestUpdate();
			bool               success    = false;
			PlayerInterestView playerView = null;
			HashSet playerCells = NetworkRunnerExtensions.GetAreaOfInterestCells(runner, _player);
			if (playerCells != null)
			{
				GlobalInterestManager globalInterestManager = Runner.GetGlobalInterestManager();
				if (globalInterestManager.GetProvidersForPlayer(ObservedPlayer, _interestProviders, out PlayerInterestView playerInterestView) == true)
				{
					GetInterestCells(_interestProviders, playerInterestView, playerCells);
					success    = true;
					playerView = playerInterestView;
					_lastUpdateTick = Runner.Tick;
				}
				_interestCellCount = playerCells.Count;
			}
			AfterPlayerInterestUpdate(success, playerView);
			_updateInterestsMarker.End();
		}
		private static void GetInterestCells(InterestProviderSet interestProviders, PlayerInterestView playerView, HashSet cells)
		{
			cells.Clear();
			interestProviders.Sort();
			List interestProviderValues = interestProviders.ReadOnlyValues;
			for (int i = Mathf.Max(0, interestProviders.GetLastOverrideProviderIndex()), count = interestProviderValues.Count; i < count; ++i)
			{
				IInterestProvider interestProvider = interestProviderValues[i];
				interestProvider.GetCellsForPlayer(playerView, cells);
			}
		}
		private static bool IsInterestManagementActive(NetworkRunner runner)
		{
			if (ReferenceEquals(runner, null) == true)
				return false;
			return runner.IsServer == true && runner.GameMode != GameMode.Shared && runner.Config.Simulation.ReplicationFeatures == NetworkProjectConfig.ReplicationFeatures.SchedulingAndInterestManagement;
		}
		private void DrawGizmos()
		{
			if (Application.isPlaying == true)
			{
				NetworkRunner runner = Runner;
				if (IsInterestManagementActive(runner) == true)
				{
					HashSet            interestCells     = default;
					List interestProviders = default;
					if (runner.LocalPlayer != _player)
					{
						interestCells = NetworkRunnerExtensions.GetAreaOfInterestCells(runner, _player);
					}
					GlobalInterestManager globalInterestManager = runner.GetGlobalInterestManager();
					if (globalInterestManager.GetProvidersForPlayer(ObservedPlayer, _interestProviders, out PlayerInterestView playerView) == true)
					{
						interestProviders = _interestProviders.ReadOnlyValues;
						if (runner.LocalPlayer == _player)
						{
							GetInterestCells(_interestProviders, playerView, _gizmoCells);
							interestCells = _gizmoCells;
						}
					}
					if (interestCells != null)
					{
						_interestCellCount = interestCells.Count;
						if (InterestUtility.DrawInterestCells == true)
						{
							InterestUtility.DrawCells(interestCells);
						}
						if (InterestUtility.DrawPlayerInterest == true)
						{
							if (NetworkRunnerExtensions.GetObjectsInPlayerInterest(Runner, Player, _objectsInPlayerInterest, interestCells) == true)
							{
								InterestUtility.DrawObjectsInPlayerInterest(_objectsInPlayerInterest);
							}
						}
					}
					if (interestProviders != null && InterestUtility.DrawPlayerInterest == true)
					{
						for (int i = Mathf.Max(0, _interestProviders.GetLastOverrideProviderIndex()), count = interestProviders.Count; i < count; ++i)
						{
							IInterestProvider interestProvider = interestProviders[i];
							interestProvider.DrawGizmosForPlayer(playerView);
						}
					}
				}
			}
			else
			{
				if (_interestView != null)
				{
					_interestView.SetPlayerInfo(_interestView.Transform, _interestView.Transform);
					_interestProviders.Clear();
					AddInterestProviderRecursive(_interestProviders, _interestView);
					_interestProviders.Sort();
					GetInterestCells(_interestProviders, _interestView, _gizmoCells);
					InterestUtility.DrawCells(_gizmoCells);
					_interestCellCount = _gizmoCells.Count;
				}
			}
			static void AddInterestProviderRecursive(InterestProviderSet interestProviders, InterestProvider interestProvider)
			{
				if (interestProvider == null)
					return;
				if (interestProviders.Add(interestProvider) == true)
				{
					if (interestProvider.ChildProviders != null)
					{
						for (int i = 0, count = interestProvider.ChildProviders.Count; i < count; ++i)
						{
							AddInterestProviderRecursive(interestProviders, interestProvider.ChildProviders[i]);
						}
					}
				}
			}
		}
#if UNITY_EDITOR
		[UnityEditor.DrawGizmo(UnityEditor.GizmoType.Active)]
		private static void DrawGizmo(PlayerInterestManager playerInterestManager, UnityEditor.GizmoType gizmoType)
		{
			if ((gizmoType & UnityEditor.GizmoType.Active) != UnityEditor.GizmoType.Active)
				return;
			playerInterestManager.DrawGizmos();
		}
#endif
	}
}