context: using System;
  using System.Collections.Generic;
  using System.Collections.ObjectModel;
  using System.Linq;
  using System.Threading;
  using System.Threading.Tasks;
  
  using log4net;
  
  using ACE.Common;
  using ACE.Database;
  using ACE.Database.Models.Shard;
  using ACE.Entity;
  using ACE.Entity.Enum;
  using ACE.Entity.Enum.Properties;
  using ACE.Server.Entity;
  using ACE.Server.Network.Enum;
  using ACE.Server.Network.GameEvent.Events;
  using ACE.Server.Network.GameMessages;
  using ACE.Server.Network.GameMessages.Messages;
  using ACE.Server.WorldObjects;
  
  using Biota = ACE.Entity.Models.Biota;
  
  namespace ACE.Server.Managers
  {
      public static class PlayerManager
      {
          private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
  
          private static readonly ReaderWriterLockSlim playersLock = new ReaderWriterLockSlim();
          private static readonly Dictionary<uint, Player> onlinePlayers = new Dictionary<uint, Player>();
          private static readonly Dictionary<uint, OfflinePlayer> offlinePlayers = new Dictionary<uint, OfflinePlayer>();
  
          /// <summary>
          /// OfflinePlayers will be saved to the database every 1 hour
          /// </summary>
          private static readonly TimeSpan databaseSaveInterval = TimeSpan.FromHours(1);
  
          private static DateTime lastDatabaseSave = DateTime.MinValue;
  
          /// <summary>
          /// This will load all the players from the database into the OfflinePlayers dictionary. It should be called before WorldManager is initialized.
          /// </summary>
          public static void Initialize()
          {
              var results = DatabaseManager.Shard.BaseDatabase.GetAllPlayerBiotasInParallel();
  
              Parallel.ForEach(results, ConfigManager.Config.Server.Threading.DatabaseParallelOptions, result =>
              {
                  var offlinePlayer = new OfflinePlayer(result);
  
                  lock (offlinePlayers)
                      offlinePlayers[offlinePlayer.Guid.Full] = offlinePlayer;
              });
          }
  
          private static readonly LinkedList<Player> playersPendingLogoff = new LinkedList<Player>();
  
          public static void AddPlayerToLogoffQueue(Player player)
          {
              if (!playersPendingLogoff.Contains(player))
                  playersPendingLogoff.AddLast(player);
          }
  
          public static void Tick()
          {
              // Database Save
              if (lastDatabaseSave + databaseSaveInterval <= DateTime.UtcNow)
                  SaveOfflinePlayersWithChanges();
  
              var currentUnixTime = Time.GetUnixTime();
  
              while (playersPendingLogoff.Count > 0)
              {
                  var first = playersPendingLogoff.First.Value;
  
                  if (first.LogoffTimestamp <= currentUnixTime)
                  {
                      playersPendingLogoff.RemoveFirst();
                      first.LogOut_Inner();
                      first.Session.logOffRequestTime = DateTime.UtcNow;
                  }
                  else
                  {
                      break;
                  }
              }
          }
  
          /// <summary>
          /// This will save any player in the OfflinePlayers dictionary that has ChangesDetected. The biotas are saved in parallel.
          /// </summary>
          public static void SaveOfflinePlayersWithChanges()
          {
              lastDatabaseSave = DateTime.UtcNow;
  
              var biotas = new Collection<(Biota biota, ReaderWriterLockSlim rwLock)>();
  
              playersLock.EnterReadLock();
              try
              {
                  foreach (var player in offlinePlayers.Values)
                  {
                      if (player.ChangesDetected)
                      {
                          player.SaveBiotaToDatabase(false);
                          biotas.Add((player.Biota, player.BiotaDatabaseLock));
                      }
                  }
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
  
              DatabaseManager.Shard.SaveBiotasInParallel(biotas, result => { });
          }
          
  
          /// <summary>
          /// This would be used when a new player is created after the server has started.
          /// When a new Player is created, they're created in an offline state, and then set to online shortly after as the login sequence continues.
          /// </summary>
          public static void AddOfflinePlayer(Player player)
          {
              playersLock.EnterWriteLock();
              try
              {
                  var offlinePlayer = new OfflinePlayer(player.Biota);
                  offlinePlayers[offlinePlayer.Guid.Full] = offlinePlayer;
              }
              finally
              {
                  playersLock.ExitWriteLock();
              }
          }
  
          /// <summary>
          /// This will return null if the player wasn't found.
          /// </summary>
          public static OfflinePlayer GetOfflinePlayer(ObjectGuid guid)
          {
              return GetOfflinePlayer(guid.Full);
          }
  
          /// <summary>
          /// This will return null if the player wasn't found.
          /// </summary>
          public static OfflinePlayer GetOfflinePlayer(uint guid)
          {
              playersLock.EnterReadLock();
              try
              {
                  if (offlinePlayers.TryGetValue(guid, out var value))
                      return value;
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
  
              return null;
          }
  
          /// <summary>
          /// This will return null of the name was not found.
          /// </summary>
          public static OfflinePlayer GetOfflinePlayer(string name)
          {
              var admin = "+" + name;
  
              playersLock.EnterReadLock();
              try
              {
                  var offlinePlayer = offlinePlayers.Values.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || p.Name.Equals(admin, StringComparison.OrdinalIgnoreCase));
  
                  if (offlinePlayer != null)
                      return offlinePlayer;
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
  
              return null;
          }
  
          public static List<IPlayer> GetAllPlayers()
          {
              var offlinePlayers = GetAllOffline();
              var onlinePlayers = GetAllOnline();
  
              var allPlayers = new List<IPlayer>();
  
              allPlayers.AddRange(offlinePlayers);
              allPlayers.AddRange(onlinePlayers);
  
              return allPlayers;
          }
  
          public static int GetOfflineCount()
          {
              playersLock.EnterReadLock();
              try
              {
                  return offlinePlayers.Count;
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
          }
  
          public static List<OfflinePlayer> GetAllOffline()
          {
              var results = new List<OfflinePlayer>();
  
              playersLock.EnterReadLock();
              try
              {
                  foreach (var player in offlinePlayers.Values)
                      results.Add(player);
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
  
              return results;
          }
  
          public static int GetOnlineCount()
          {
              playersLock.EnterReadLock();
              try
              {
                  return onlinePlayers.Count;
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
          }
  
          /// <summary>
          /// This will return null if the player wasn't found.
          /// </summary>
          public static Player GetOnlinePlayer(ObjectGuid guid)
          {
              return GetOnlinePlayer(guid.Full);
          }
  
          /// <summary>
          /// This will return null if the player wasn't found.
          /// </summary>
          public static Player GetOnlinePlayer(uint guid)
          {
              playersLock.EnterReadLock();
              try
              {
                  if (onlinePlayers.TryGetValue(guid, out var value))
                      return value;
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
  
              return null;
          }
  
          /// <summary>
          /// This will return null of the name was not found.
          /// </summary>
          public static Player GetOnlinePlayer(string name)
          {
              var admin = "+" + name;
  
              playersLock.EnterReadLock();
              try
              {
                  var onlinePlayer = onlinePlayers.Values.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || p.Name.Equals(admin, StringComparison.OrdinalIgnoreCase));
  
                  if (onlinePlayer != null)
                      return onlinePlayer;
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
  
              return null;
          }
  
          public static List<Player> GetAllOnline()
          {
              var results = new List<Player>();
  
              playersLock.EnterReadLock();
              try
              {
                  foreach (var player in onlinePlayers.Values)
                      results.Add(player);
              }
              finally
              {
                  playersLock.ExitReadLock();
              }
  
              return results;
          }
  
  
          /// <summary>
          /// This will return true if the player was successfully added.
          /// It will return false if the player was not found in the OfflinePlayers dictionary (which should never happen), or player already exists in the OnlinePlayers dictionary (which should never happen).
          /// This will always be preceded by a call to GetOfflinePlayer()
          /// </summary>
          public static bool SwitchPlayerFromOfflineToOnline(Player player)
          {
              playersLock.EnterWriteLock();
              try
              {
                  if (!offlinePlayers.Remove(player.Guid.Full, out var offlinePlayer))
                      return false; // This should never happen
  
                  if (offlinePlayer.ChangesDetected)
                      player.ChangesDetected = true;
  
                  player.Allegiance = offlinePlayer.Allegiance;
                  player.AllegianceNode = offlinePlayer.AllegianceNode;
  
                  if (!onlinePlayers.TryAdd(player.Guid.Full, player))
                      return false;
              }
              finally
              {
                  playersLock.ExitWriteLock();
              }
  
              AllegianceManager.LoadPlayer(player);
  
              player.SendFriendStatusUpdates();
  
              return true;
          }
  
          /// <summary>
          /// This will return true if the player was successfully added.
          /// It will return false if the player was not found in the OnlinePlayers dictionary (which should never happen), or player already exists in the OfflinePlayers dictionary (which should never happen).
          /// </summary>
          public static bool SwitchPlayerFromOnlineToOffline(Player player)
          {
              playersLock.EnterWriteLock();
              try
              {
  
gt:                 if (!onlinePlayers.Remove(player.Guid.Full, out _))