content: package com.infinityraider.agricraft.capability;
  
  import com.google.common.collect.Lists;
  import com.google.common.collect.Maps;
  import com.infinityraider.agricraft.AgriCraft;
  import com.infinityraider.agricraft.api.v1.AgriApi;
  import com.infinityraider.agricraft.api.v1.content.items.IAgriSeedBagItem;
  import com.infinityraider.agricraft.api.v1.genetics.IAgriGenome;
  import com.infinityraider.agricraft.api.v1.plant.IAgriPlant;
  import com.infinityraider.agricraft.capability.CapabilitySeedBagContents.Impl;
  import com.infinityraider.agricraft.content.core.ItemDynamicAgriSeed;
  import com.infinityraider.agricraft.impl.v1.plant.NoPlant;
  import com.infinityraider.agricraft.reference.AgriNBT;
  import com.infinityraider.agricraft.reference.Names;
  import com.infinityraider.infinitylib.capability.IInfSerializableCapabilityImplementation;
  import net.minecraft.nbt.CompoundTag;
  import net.minecraft.nbt.ListTag;
  import net.minecraft.resources.ResourceLocation;
  import net.minecraft.world.item.ItemStack;
  import net.minecraftforge.common.capabilities.Capability;
  import net.minecraftforge.common.capabilities.CapabilityManager;
  import net.minecraftforge.common.capabilities.CapabilityToken;
  
  import javax.annotation.Nonnull;
  import java.util.Comparator;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  
  import static com.infinityraider.agricraft.content.tools.ItemSeedBag.*;
  
  public class CapabilitySeedBagContents implements IInfSerializableCapabilityImplementation<ItemStack, Impl> {
      private static final CapabilitySeedBagContents INSTANCE = new CapabilitySeedBagContents();
  
      public static CapabilitySeedBagContents getInstance() {
          return INSTANCE;
      }
  
      public static ResourceLocation KEY = new ResourceLocation(AgriCraft.instance.getModId().toLowerCase(), Names.Items.SEED_BAG);
  
      public static final Capability<CapabilitySeedBagContents.Impl> CAPABILITY = CapabilityManager.get(new CapabilityToken<>(){});
  
      private CapabilitySeedBagContents() {}
  
      @Override
      public Class<Impl> getCapabilityClass() {
          return Impl.class;
      }
  
      @Override
      public Capability<Impl> getCapability() {
          return CAPABILITY;
      }
  
      @Override
      public boolean shouldApplyCapability(ItemStack stack) {
          return stack.getItem() instanceof IAgriSeedBagItem;
      }
  
      @Override
      public Impl createNewValue(ItemStack stack) {
          return new Impl((IAgriSeedBagItem) stack.getItem());
      }
  
      @Override
      public ResourceLocation getCapabilityKey() {
          return KEY;
      }
  
      @Override
      public Class<ItemStack> getCarrierClass() {
          return ItemStack.class;
      }
  
      public static class Impl implements IAgriSeedBagItem.Contents, Serializable<Impl> {
          private final IAgriSeedBagItem seedBag;
  
          private IAgriPlant plant;
  
          private final List<Entry> contents;
          private int count;
  
          private final Map<Integer, Comparator<Entry>> sorters;
          private int sorterIndex;
          private Comparator<Entry> subSorter;
  
          private ItemStack firstStack;
          private ItemStack lastStack;
  
          private Impl(IAgriSeedBagItem seedBag) {
              this.seedBag = seedBag;
              this.plant = NoPlant.getInstance();
              this.contents = Lists.newArrayList();
              this.count = 0;
              this.sorters = Maps.newHashMap();
              this.setSorterIndex(0);
              this.firstStack = ItemStack.EMPTY;
              this.lastStack = ItemStack.EMPTY;
          }
  
          public IAgriSeedBagItem getSeedBag() {
              return this.seedBag;
          }
  
          @Override
          public IAgriPlant getPlant() {
              return this.plant;
          }
  
          @Override
          public int getCount() {
              return this.count;
          }
  
          @Override
          public boolean isFull() {
              return this.getCount() >= this.getCapacity();
          }
  
          @Override
          public Sorter getSorter() {
              return this.getSeedBag().getSorter(this.getSorterIndex());
          }
  
          @Override
          public int getSorterIndex() {
              return this.sorterIndex;
          }
  
          @Override
          public void setSorterIndex(int index) {
              this.sorterIndex = index;
              this.subSorter = this.sorters.computeIfAbsent(index, value -> (a, b) -> this.getSorter().compare(a.getGenome(), b.getGenome()));
              this.sort();
          }
  
          protected void sort() {
              if(this.contents.size() > 0) {
                  this.contents.sort(this.subSorter);
                  this.firstStack = this.contents.get(0).initializeStack();
                  this.firstStack = this.contents.get(this.contents.size() - 1).initializeStack();
              }
          }
  
          @Override
          public int getSlots() {
              return 2;
          }
  
          @Nonnull
          @Override
          public ItemStack getStackInSlot(int slot) {
              if (slot == 0) {
                  return this.firstStack;
              }
              if (slot == 1) {
                  return this.lastStack;
              }
              return ItemStack.EMPTY;
          }
  
          @Nonnull
          @Override
          public ItemStack insertItem(int slot, @Nonnull ItemStack stack, boolean simulate) {
              if (this.isFull()) {
                  return stack;
              }
              if (this.isItemValid(slot, stack)) {
                  return ((ItemDynamicAgriSeed) stack.getItem()).getGenome(stack).map(genome -> {
                      boolean flag = true;
                      int amount = Math.min(this.getCapacity() - this.getCount(), stack.getCount());
                      if (amount <= 0) {
                          return stack;
                      }
                      for (Entry entry : this.contents) {
                          if (entry.matches(genome)) {
                              flag = false;
                              if (!simulate) {
                                  entry.add(amount);
                                  this.count += amount;
                              }
                              break;
                          }
                      }
                      if (flag && !simulate) {
                          if (!this.plant.isPlant()) {
                              this.plant = ((ItemDynamicAgriSeed) stack.getItem()).getPlant(stack);
                          }
                          this.contents.add(new Entry(genome, amount));
                          this.count += amount;
                          this.sort();
                      }
                      if (amount >= stack.getCount()) {
                          return ItemStack.EMPTY;
                      } else {
                          ItemStack result = stack.copy();
                          result.setCount(result.getCount() - amount);
                          return result;
                      }
                  }).orElse(stack);
              }
              return stack;
          }
  
          @Nonnull
          @Override
          public ItemStack extractItem(int slot, int amount, boolean simulate) {
              if(this.contents.size() >= 1) {
                  if(slot == 0) {
                      return this.extractFirstSeed(amount, simulate);
                  }
                  if(slot == 1) {
                      return this.extractLastSeed(amount, simulate);
                  }
              }
              return ItemStack.EMPTY;
          }
  
  
          @Nonnull
          @Override
          public ItemStack extractFirstSeed(int amount, boolean simulate) {
              if(this.firstStack.isEmpty()) {
                  return ItemStack.EMPTY;
              }
              ItemStack out = this.firstStack.copy();
              if (amount >= this.contents.get(0).getAmount()) {
                  // More seeds were requested than there actually are
                  Entry entry = simulate ? this.contents.get(0) : this.contents.remove(0);
                  out.setCount(entry.getAmount());
                  if (!simulate) {
                      this.count -= out.getCount();
                      if (this.contents.size() > 0) {
                          this.firstStack = this.contents.get(0).initializeStack();
                      } else {
                          this.firstStack = ItemStack.EMPTY;
                          this.lastStack = ItemStack.EMPTY;
                          this.plant = NoPlant.getInstance();
                      }
                  }
              } else {
                  out.setCount(amount);
                  if (!simulate) {
                      this.contents.get(0).extract(amount);
                      this.count -= out.getCount();
                  }
              }
              return out;
          }
  
          @Nonnull
          @Override
          public ItemStack extractLastSeed(int amount, boolean simulate) {
              if(this.lastStack.isEmpty()) {
                  return ItemStack.EMPTY;
              }
              ItemStack out = this.lastStack.copy();
              if (amount >= this.contents.get(this.contents.size() - 1).getAmount()) {
                  // More seeds were requested than there actually are
                  Entry entry = simulate ? this.contents.get(this.contents.size() - 1) : this.contents.remove(this.contents.size() - 1);
                  out.setCount(entry.getAmount());
                  if (!simulate) {
                      this.count -= out.getCount();
                      if (this.contents.size() > 0) {
                          this.lastStack = this.contents.get(this.contents.size() - 1).initializeStack();
                      } else {
                          this.firstStack = ItemStack.EMPTY;
                          this.lastStack = ItemStack.EMPTY;
                          this.plant = NoPlant.getInstance();
                      }
                  }
              } else {
                  out.setCount(amount);
                  if (!simulate) {
                      this.contents.get(this.contents.size() - 1).extract(amount);
                      this.count -= out.getCount();
                  }
              }
              return out;
          }
  
          @Override
          public int getCapacity() {
              return AgriCraft.instance.getConfig().seedBagCapacity();
          }
  
          @Override
          public int getSlotLimit(int slot) {
              return 64;
          }
  
          @Override
          public boolean isItemValid(int slot, @Nonnull ItemStack stack) {
              if(stack.getItem() instanceof ItemDynamicAgriSeed) {
                  ItemDynamicAgriSeed seed = (ItemDynamicAgriSeed) stack.getItem();
                  IAgriPlant stackPlant = seed.getPlant(stack);
                  if(stackPlant.isPlant()) {
                      return (!this.getPlant().isPlant()) || this.getPlant() == stackPlant;
                  }
              }
              return false;
          }
  
          @Override
          public void copyDataFrom(Impl from) {
              this.plant = from.plant;
              this.count = from.count;
              this.sorters.clear();
              this.sorters.putAll(from.sorters);
              this.sorterIndex = from.sorterIndex;
              this.subSorter = from.subSorter;
              this.firstStack = from.firstStack.copy();
              this.lastStack = from.lastStack.copy();
          }
  
          @Override
          public CompoundTag serializeNBT() {
              CompoundTag tag = new CompoundTag();
              // Write plant
              tag.putString(AgriNBT.PLANT, this.getPlant().getId());
              // Write contents
              ListTag entryTags = new ListTag();
              this.contents.forEach(entry -> entryTags.add(entry.writeToTag()));
              tag.put(AgriNBT.ENTRIES, entryTags);
              // Write sorter
              tag.putInt(AgriNBT.KEY, this.getSorterIndex());
              // Return the tag
              return tag;
          }
  
          @Override
          public void deserializeNBT(CompoundTag tag) {
              this.contents.clear();
              this.count = 0;
              this.firstStack = ItemStack.EMPTY;
              this.lastStack = ItemStack.EMPTY;
              this.plant = tag.contains(AgriNBT.PLANT)
                      ? AgriApi.getPlantRegistry().get(tag.getString(AgriNBT.PLANT)).orElse(NoPlant.getInstance())
                      : NoPlant.getInstance();
              if (this.getPlant().isPlant()) {
                  if (tag.contains(AgriNBT.ENTRIES)) {
                      ListTag entryTags = tag.getList(AgriNBT.ENTRIES, 10);
                      entryTags.stream().filter(entryTag -> entryTag instanceof CompoundTag)
                              .map(entryTag -> (CompoundTag) entryTag).
                              forEach(entryTag -> Entry.readFromTag(entryTag).ifPresent(entry -> {
                                  this.contents.add(entry);
                                  this.count += entry.getAmount();
                              }));
                      if (this.count > 0) {
                          this.firstStack = this.contents.get(0).initializeStack();
                          this.lastStack = this.contents.get(this.contents.size() - 1).initializeStack();
                      }
                  } else {
                      this.plant = NoPlant.getInstance();
                  }
              }
              this.setSorterIndex(tag.contains(AgriNBT.KEY) ? tag.getInt(AgriNBT.KEY) : 0);
          }
  
          private static class Entry {
              private final IAgriGenome genome;
              private int amount;
  
              protected Entry(IAgriGenome genome, int amount) {
                  this.genome = genome;
                  this.amount = amount;
              }
  
              public int getAmount() {
                  return this.amount;
              }
  
              public IAgriGenome getGenome() {
                  return this.genome;
              }
  
              public ItemStack initializeStack() {
                  return this.genome.toSeedStack();
              }
  
              public void add(int amount) {
                  this.amount += amount;
              }
  
              public void extract(int amount) {
                  this.amount -= amount;
                  this.amount = Math.max(this.amount, 0);
              }
  
              public boolean matches(IAgriGenome genome) {
                  return this.getGenome().equals(genome);
              }
  
              public CompoundTag writeToTag() {
                  CompoundTag tag = new CompoundTag();
                  CompoundTag genomeTag = new CompoundTag();
                  this.getGenome().writeToNBT(genomeTag);
                  tag.put(AgriNBT.GENOME, genomeTag);
                  tag.putInt(AgriNBT.ENTRIES, this.getAmount());
                  return tag;
              }
  
              public static Optional<Entry> readFromTag(CompoundTag tag) {
                  if(!tag.contains(AgriNBT.GENOME) || !tag.contains(AgriNBT.ENTRIES)) {
                      return Optional.empty();
                  }
                  IAgriGenome genome = AgriApi.getAgriGenomeBuilder(NoPlant.getInstance()).build();
                  if(!genome.readFromNBT(tag.getCompound(AgriNBT.GENOME))) {
                      return Optional.empty();
                  }
                  int count = tag.getInt(AgriNBT.ENTRIES);
                  return Optional.of(new Entry(genome, count));
              }
          }
      }
  }
  