content: package emu.grasscutter.game.player;
  
  import dev.morphia.annotations.Entity;
  import dev.morphia.annotations.Transient;
  import emu.grasscutter.data.GameData;
  import emu.grasscutter.data.excels.CodexAnimalData;
  import emu.grasscutter.game.entity.EntityMonster;
  import emu.grasscutter.game.entity.GameEntity;
  import emu.grasscutter.game.inventory.GameItem;
  import emu.grasscutter.server.packet.send.PacketCodexDataUpdateNotify;
  import lombok.Getter;
  import lombok.val;
  
  import java.util.*;
  
  @Entity
  public class PlayerCodex {
      @Transient private Player player;
  
      //itemId is not codexId!
      @Getter private Set<Integer> unlockedWeapon;
      @Getter private Map<Integer, Integer> unlockedAnimal;
      @Getter private Set<Integer> unlockedMaterial;
      @Getter private Set<Integer> unlockedBook;
      @Getter private Set<Integer> unlockedTip;
      @Getter private Set<Integer> unlockedView;
      @Getter private Set<Integer> unlockedReliquary;
      @Getter private Set<Integer> unlockedReliquarySuitCodex;
  
      public PlayerCodex() {
          this.unlockedWeapon = new HashSet<>();
          this.unlockedAnimal = new HashMap<>();
          this.unlockedMaterial = new HashSet<>();
          this.unlockedBook = new HashSet<>();
          this.unlockedTip = new HashSet<>();
          this.unlockedView = new HashSet<>();
          this.unlockedReliquary = new HashSet<>();
          this.unlockedReliquarySuitCodex = new HashSet<>();
      }
  
      public PlayerCodex(Player player) {
          this();
          this.player = player;
      }
  
      public void setPlayer(Player player) {
          this.player = player;
          this.fixReliquaries();
      }
  
      public void checkAddedItem(GameItem item) {
          val itemData = item.getItemData();
          val itemId = item.getItemId();
          switch (itemData.getItemType()) {
              case ITEM_WEAPON -> {
                  Optional.ofNullable(GameData.getCodexWeaponDataIdMap().get(itemId))
                      .ifPresent(codexData -> {
                          if (this.getUnlockedWeapon().add(itemId)) {
                              this.player.save();
                              this.player.sendPacket(new PacketCodexDataUpdateNotify(2, codexData.getId()));
                          }
                      });
              }
              case ITEM_MATERIAL -> {
                  switch (itemData.getMaterialType()) {
                      // Is this check even needed?
                      case MATERIAL_FOOD, MATERIAL_WIDGET, MATERIAL_EXCHANGE, MATERIAL_AVATAR_MATERIAL, MATERIAL_NOTICE_ADD_HP -> {
                          Optional.ofNullable(GameData.getCodexMaterialDataIdMap().get(itemId))
                              .ifPresent(codexData -> {
                                  if (this.getUnlockedMaterial().add(itemId)) {
                                      this.player.save();
                                      this.player.sendPacket(new PacketCodexDataUpdateNotify(4, codexData.getId()));
                                  }
                              });
                      }
                      default -> {}
                  }
              }
              case ITEM_RELIQUARY -> {
                  val reliquaryId = (itemId/10) * 10;  // Normalize to 0-substat form
                  if (this.getUnlockedReliquary().add(reliquaryId))
                      checkUnlockedSuits(reliquaryId);
              }
              default -> {}
          }
      }
  
      public void checkAnimal(GameEntity target, CodexAnimalData.CountType countType) {
          if (target instanceof EntityMonster) {
              val monsterId = ((EntityMonster) target).getMonsterData().getId();
              val codexAnimal = GameData.getCodexAnimalDataMap().get(monsterId);
              if (codexAnimal == null) return;
  
              val animalCountType = codexAnimal.getCountType();
              if (animalCountType != countType && animalCountType != null) return;
  
              this.getUnlockedAnimal().merge(monsterId, 1, (i, j) -> i + 1);
  
              player.save();
              this.player.sendPacket(new PacketCodexDataUpdateNotify(3, monsterId));
          }
      }
  
      public void checkUnlockedSuits(int reliquaryId) {
          GameData.getCodexReliquaryArrayList().stream()
              .filter(x -> !this.getUnlockedReliquarySuitCodex().contains(x.getId()))
              .filter(x -> x.containsId(reliquaryId))
              .filter(x -> this.getUnlockedReliquary().containsAll(x.getIds()))
              .forEach(x -> {
                  int id = x.getId();
                  this.getUnlockedReliquarySuitCodex().add(id);
                  this.player.save();
                  this.player.sendPacket(new PacketCodexDataUpdateNotify(8, id));
              });
      }
  
      @Deprecated  // Maybe remove this if we ever stop caring about older dbs
      private void fixReliquaries() {
          // Migrate older database entries which were using non-canonical forms of itemIds
          val newReliquaries = new HashSet<Integer>();
          this.unlockedReliquary.forEach(i -> newReliquaries.add((i/10)*10));
          this.unlockedReliquary = newReliquaries;
  
          GameData.getCodexReliquaryArrayList().stream()
              .filter(x -> !this.getUnlockedReliquarySuitCodex().contains(x.getId()))
              .filter(x -> this.getUnlockedReliquary().containsAll(x.getIds()))
              .forEach(x -> this.getUnlockedReliquarySuitCodex().add(x.getId()));
          this.player.save();
      }
  }
  