context: #region using directives
  
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Threading;
  using System.Threading.Tasks;
  using PoGo.NecroBot.Logic.PoGoUtils;
  using PoGo.NecroBot.Logic.State;
  using PoGo.NecroBot.Logic.Utils;
  using PokemonGo.RocketAPI;
  using POGOProtos.Data;
  using POGOProtos.Data.Player;
  using POGOProtos.Enums;
  using POGOProtos.Inventory;
  using POGOProtos.Inventory.Item;
  using POGOProtos.Networking.Responses;
  using POGOProtos.Settings.Master;
  
  #endregion
  
  namespace PoGo.NecroBot.Logic
  {
     
      public class Inventory
      {
          private readonly Client _client;
          private readonly ILogicSettings _logicSettings;
          private GetPlayerResponse _player = null;
          private int _level = 0;
          private DownloadItemTemplatesResponse _templates;
          private IEnumerable<PokemonSettings> _pokemonSettings;
  
          private readonly List<ItemId> _revives = new List<ItemId> { ItemId.ItemRevive, ItemId.ItemMaxRevive };
          private GetInventoryResponse _cachedInventory;
          private DateTime _lastRefresh;
  
          public Inventory(Client client, ILogicSettings logicSettings)
          {
              _client = client;
              _logicSettings = logicSettings;
          }
  
          private readonly List<ItemId> _pokeballs = new List<ItemId>
          {
              ItemId.ItemPokeBall,
              ItemId.ItemGreatBall,
              ItemId.ItemUltraBall,
              ItemId.ItemMasterBall
          };
  
          private readonly List<ItemId> _potions = new List<ItemId>
          {
              ItemId.ItemPotion,
              ItemId.ItemSuperPotion,
              ItemId.ItemHyperPotion,
              ItemId.ItemMaxPotion
          };
  
          public async Task DeletePokemonFromInvById(ulong id)
          {
              var inventory = await GetCachedInventory();
              var pokemon =
                  inventory.InventoryDelta.InventoryItems.FirstOrDefault(
                      i => i.InventoryItemData.PokemonData != null && i.InventoryItemData.PokemonData.Id == id);
              if (pokemon != null)
                  inventory.InventoryDelta.InventoryItems.Remove(pokemon);
          }
  
          public async Task<LevelUpRewardsResponse> GetLevelUpRewards(Inventory inv )
          {
              return await GetLevelUpRewards(inv.GetPlayerStats().Result.FirstOrDefault().Level);
          }
  
          private async Task<GetInventoryResponse> GetCachedInventory()
          {
              if (_player==null) GetPlayerData();
              var now = DateTime.UtcNow;
  
              if (_cachedInventory!=null && _lastRefresh.AddSeconds(30).Ticks > now.Ticks)
                  return _cachedInventory;
  
              return await RefreshCachedInventory();
          }
          
  
          public async Task<IEnumerable<PokemonData>> GetDuplicatePokemonToTransfer( 
                  IEnumerable<PokemonId> pokemonsNotToTransfer, IEnumerable<PokemonId> pokemonsToEvolve,
                  bool keepPokemonsThatCanEvolve = false, bool prioritizeIVoverCp = false
               )
          {
              var myPokemon = await GetPokemons();
  
              var myPokemonList = myPokemon.ToList();
  
              var pokemonToTransfer = myPokemonList.Where(p => !pokemonsNotToTransfer.Contains(p.PokemonId) && p.DeployedFortId == string.Empty && p.Favorite == 0).ToList();
  
              try
              {
                  pokemonToTransfer =
                      pokemonToTransfer.Where(
                          p =>
                          {
                              var pokemonTransferFilter = GetPokemonTransferFilter(p.PokemonId);
  
                              bool ret = true;
                              pokemonTransferFilter.Moves.ForEach(moveset =>
                              {
                                  ret = !pokemonTransferFilter.MovesOperator.BoolFunc(
                                      moveset.Intersect(new[] { p.Move1, p.Move2 }).Any(),
                                      pokemonTransferFilter.KeepMinOperator.BoolFunc(
                                          p.Cp >= pokemonTransferFilter.KeepMinCp,
                                          PokemonInfo.CalculatePokemonPerfection(p) >= pokemonTransferFilter.KeepMinIvPercentage,
                                          pokemonTransferFilter.KeepMinOperator.ReverseBoolFunc(
                                              pokemonTransferFilter.KeepMinOperator.InverseBool(pokemonTransferFilter.UseKeepMinLvl),
                                              PokemonInfo.GetLevel(p) >= pokemonTransferFilter.KeepMinLvl)));
                              });
                              return ret;
                          }).ToList();
              }
              catch (Exception e)
              {
                  throw e;
              }
              
              var myPokemonSettings = await GetPokemonSettings();
              var pokemonSettings = myPokemonSettings.ToList();
  
              var myPokemonFamilies = await GetPokemonFamilies();
              var pokemonFamilies = myPokemonFamilies.ToArray();
              
              var results = new List<PokemonData>();
  
              foreach (var pokemonGroupToTransfer in pokemonToTransfer.GroupBy(p => p.PokemonId).ToList())
              {
                  var amountToKeepInStorage = Math.Max(GetPokemonTransferFilter(pokemonGroupToTransfer.Key).KeepMinDuplicatePokemon, 0);
  
                  var settings = pokemonSettings.Single(x => x.PokemonId == pokemonGroupToTransfer.Key);
                  var familyCandy = pokemonFamilies.Single(x => settings.FamilyId == x.FamilyId);
  
                  int? modFromEvolve = null;
  
                  if (keepPokemonsThatCanEvolve &&
                      pokemonsToEvolve.Contains(pokemonGroupToTransfer.Key) &&
                      settings.CandyToEvolve > 0 &&
                      settings.EvolutionIds.Count != 0)
                  {
                      var possibleCountToEvolve = familyCandy.Candy_ / settings.CandyToEvolve;
                      amountToKeepInStorage = Math.Max(amountToKeepInStorage, possibleCountToEvolve);
  
                      //remain candy
                      modFromEvolve = familyCandy.Candy_%settings.CandyToEvolve;
                  }
  
                  var inStorage = myPokemonList.Count(data => data.PokemonId == pokemonGroupToTransfer.Key);
  
                  var weakPokemonCount = pokemonGroupToTransfer.Count();
  
                  var needToRemove = inStorage - amountToKeepInStorage;
                  if (needToRemove <= 0)
                      continue;
  
                  var canBeRemoved = Math.Min(needToRemove, weakPokemonCount);
  
                  if (canBeRemoved <= 0)
                      continue;
  
                  //Lets calc new canBeRemoved pokemons according to transferring some of them and use it as future candy to keepPokemonsThatCanEvolve
                  if (modFromEvolve.HasValue)
                  {
                      // its an solution in fixed numbers of equations with two variables 
                      // (N = X + Y, X + C >= Y * E) -> (X >= (N * E - C / 1 + E)
                      // where N - current canBeRemoved,  X - new canBeRemoved, Y - possible to keep more, E - CandyToEvolve, C - modFromEvolve(remain candy) ) )
                      canBeRemoved = (settings.CandyToEvolve * canBeRemoved - modFromEvolve.Value) / (1 + settings.CandyToEvolve) + 
                                      Math.Sign((settings.CandyToEvolve * canBeRemoved - modFromEvolve.Value) % (1 + settings.CandyToEvolve));
                  }
  
                  var skipCount = weakPokemonCount - canBeRemoved;
                  
                  if (prioritizeIVoverCp)
                  {
                      results.AddRange(pokemonGroupToTransfer
                          .OrderByDescending(PokemonInfo.CalculatePokemonPerfection)
                          .ThenByDescending(n => n.Cp)
                          .Skip(skipCount));
                  }
                  else
                  {
                      results.AddRange(pokemonGroupToTransfer
                          .OrderByDescending(x => x.Cp)
                          .ThenByDescending(PokemonInfo.CalculatePokemonPerfection)
                          .Skip(skipCount));
                  }
              }
  
              #region For testing
              /*
              results.ForEach(data =>
              {
                  var bestPokemonOfType = (_logicSettings.PrioritizeIvOverCp
                   ? myPokemonList.Where(x => x.PokemonId == data.PokemonId)
                  .OrderByDescending(x => x.Cp)
                  .FirstOrDefault()
                   : myPokemonList.Where(x => x.PokemonId == data.PokemonId)
                  .OrderByDescending(PokemonInfo.CalculatePokemonPerfection)
                  .FirstOrDefault()) ?? data;
  
                  var perfection = PokemonInfo.CalculatePokemonPerfection(data);
                  var cp = data.Cp;
  
                  var bestPerfection = PokemonInfo.CalculatePokemonPerfection(bestPokemonOfType);
                  var bestCp = bestPokemonOfType.Cp;
              });
              */
              #endregion
  
              return results;
          }
  
          public async Task<IEnumerable<EggIncubator>> GetEggIncubators()
          {
              var inventory = await GetCachedInventory();
              return
                  inventory.InventoryDelta.InventoryItems
                      .Where(x => x.InventoryItemData.EggIncubators != null)
                      .SelectMany(i => i.InventoryItemData.EggIncubators.EggIncubator)
                      .Where(i => i != null);
          }
  
          public async Task<IEnumerable<PokemonData>> GetEggs()
          {
              var inventory = await GetCachedInventory();
              return
                  inventory.InventoryDelta.InventoryItems.Select(i => i.InventoryItemData?.PokemonData)
  
gt:                     .Where(p => p != null && p.IsEgg);