// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.core.inventory.container;

import com.hypixel.hytale.event.IEvent;
import com.hypixel.hytale.protocol.ItemResourceType;
import java.util.Objects;
import java.util.Comparator;
import java.util.Arrays;
import com.hypixel.fastutil.shorts.Short2ObjectConcurrentHashMap;
import com.hypixel.hytale.function.consumer.ShortObjectConsumer;
import com.hypixel.hytale.server.core.inventory.transaction.MaterialTransaction;
import com.hypixel.hytale.server.core.inventory.transaction.ResourceTransaction;
import com.hypixel.hytale.server.core.inventory.transaction.TagTransaction;
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
import it.unimi.dsi.fastutil.ints.IntArrays;
import java.util.Collections;
import java.util.Collection;
import java.util.function.Predicate;
import java.util.Iterator;
import com.hypixel.hytale.server.core.inventory.transaction.MoveType;
import com.hypixel.hytale.server.core.inventory.transaction.ItemStackTransaction;
import com.hypixel.hytale.server.core.inventory.transaction.MoveTransaction;
import com.hypixel.hytale.server.core.inventory.transaction.TagSlotTransaction;
import com.hypixel.hytale.server.core.inventory.transaction.ResourceSlotTransaction;
import com.hypixel.hytale.server.core.inventory.ResourceQuantity;
import com.hypixel.hytale.server.core.inventory.transaction.MaterialSlotTransaction;
import com.hypixel.hytale.server.core.inventory.MaterialQuantity;
import com.hypixel.hytale.server.core.inventory.transaction.SlotTransaction;
import java.util.List;
import com.hypixel.hytale.server.core.inventory.transaction.ActionType;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import com.hypixel.hytale.server.core.inventory.transaction.ListTransaction;
import com.hypixel.hytale.server.core.inventory.transaction.ItemStackSlotTransaction;
import com.hypixel.hytale.server.core.inventory.transaction.Transaction;
import com.hypixel.hytale.event.EventPriority;
import com.hypixel.hytale.event.EventRegistration;
import java.util.function.Consumer;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import com.hypixel.hytale.protocol.ItemWithAllMetadata;
import java.util.Map;
import javax.annotation.Nonnull;
import com.hypixel.hytale.protocol.InventorySection;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.inventory.transaction.ClearTransaction;
import java.util.function.Function;
import java.util.function.Supplier;
import com.hypixel.hytale.server.core.inventory.container.filter.SlotFilter;
import com.hypixel.hytale.server.core.inventory.container.filter.FilterActionType;
import com.hypixel.hytale.server.core.inventory.container.filter.FilterType;
import com.hypixel.hytale.event.SyncEventBusRegistry;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.codec.lookup.CodecMapCodec;

public abstract class ItemContainer
{
    public static final CodecMapCodec<ItemContainer> CODEC;
    public static final boolean DEFAULT_ADD_ALL_OR_NOTHING = false;
    public static final boolean DEFAULT_REMOVE_ALL_OR_NOTHING = true;
    public static final boolean DEFAULT_FULL_STACKS = false;
    public static final boolean DEFAULT_EXACT_AMOUNT = true;
    public static final boolean DEFAULT_FILTER = true;
    protected static final HytaleLogger LOGGER;
    protected final SyncEventBusRegistry<Void, ItemContainerChangeEvent> externalChangeEventRegistry;
    protected final SyncEventBusRegistry<Void, ItemContainerChangeEvent> internalChangeEventRegistry;
    
    public ItemContainer() {
        this.externalChangeEventRegistry = new SyncEventBusRegistry<Void, ItemContainerChangeEvent>(ItemContainer.LOGGER, ItemContainerChangeEvent.class);
        this.internalChangeEventRegistry = new SyncEventBusRegistry<Void, ItemContainerChangeEvent>(ItemContainer.LOGGER, ItemContainerChangeEvent.class);
    }
    
    public abstract short getCapacity();
    
    public abstract void setGlobalFilter(final FilterType p0);
    
    public abstract void setSlotFilter(final FilterActionType p0, final short p1, final SlotFilter p2);
    
    public abstract ItemContainer clone();
    
    protected abstract <V> V readAction(final Supplier<V> p0);
    
    protected abstract <X, V> V readAction(final Function<X, V> p0, final X p1);
    
    protected abstract <V> V writeAction(final Supplier<V> p0);
    
    protected abstract <X, V> V writeAction(final Function<X, V> p0, final X p1);
    
    protected abstract ClearTransaction internal_clear();
    
    @Nullable
    protected abstract ItemStack internal_getSlot(final short p0);
    
    @Nullable
    protected abstract ItemStack internal_setSlot(final short p0, final ItemStack p1);
    
    @Nullable
    protected abstract ItemStack internal_removeSlot(final short p0);
    
    protected abstract boolean cantAddToSlot(final short p0, final ItemStack p1, final ItemStack p2);
    
    protected abstract boolean cantRemoveFromSlot(final short p0);
    
    protected abstract boolean cantDropFromSlot(final short p0);
    
    protected abstract boolean cantMoveToSlot(final ItemContainer p0, final short p1);
    
    @Nonnull
    public InventorySection toPacket() {
        final InventorySection packet = new InventorySection();
        packet.capacity = this.getCapacity();
        packet.items = this.toProtocolMap();
        return packet;
    }
    
    @Nonnull
    public Map<Integer, ItemWithAllMetadata> toProtocolMap() {
        final Map<Integer, ItemWithAllMetadata> map = new Int2ObjectOpenHashMap<ItemWithAllMetadata>();
        this.forEachWithMeta((slot, itemStack, _map) -> {
            if (ItemStack.isEmpty(itemStack) || !itemStack.isValid()) {
                return;
            }
            else {
                _map.put((int)slot, itemStack.toPacket());
                return;
            }
        }, map);
        return map;
    }
    
    public EventRegistration registerChangeEvent(@Nonnull final Consumer<ItemContainerChangeEvent> consumer) {
        return this.registerChangeEvent((short)0, consumer);
    }
    
    public EventRegistration registerChangeEvent(@Nonnull final EventPriority priority, @Nonnull final Consumer<ItemContainerChangeEvent> consumer) {
        return this.registerChangeEvent(priority.getValue(), consumer);
    }
    
    public EventRegistration registerChangeEvent(final short priority, @Nonnull final Consumer<ItemContainerChangeEvent> consumer) {
        return this.externalChangeEventRegistry.register(priority, null, consumer);
    }
    
    public ClearTransaction clear() {
        final ClearTransaction transaction = this.writeAction(this::internal_clear);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canAddItemStackToSlot(final short slot, @Nonnull final ItemStack itemStack, final boolean allOrNothing, final boolean filter) {
        validateSlotIndex(slot, this.getCapacity());
        return this.writeAction(() -> {
            final int quantityRemaining = itemStack.getQuantity();
            final ItemStack slotItemStack = this.internal_getSlot(slot);
            if (filter && this.cantAddToSlot(slot, itemStack, slotItemStack)) {
                return false;
            }
            else if (slotItemStack == null) {
                return true;
            }
            else if (!itemStack.isStackableWith(slotItemStack)) {
                return false;
            }
            else {
                final int quantity = slotItemStack.getQuantity();
                final int quantityAdjustment = Math.min(slotItemStack.getItem().getMaxStack() - quantity, quantityRemaining);
                final int newQuantityRemaining = quantityRemaining - quantityAdjustment;
                if (allOrNothing) {
                    return quantityRemaining <= 0;
                }
                else {
                    return quantityRemaining != newQuantityRemaining;
                }
            }
        });
    }
    
    @Nonnull
    public ItemStackSlotTransaction addItemStackToSlot(final short slot, @Nonnull final ItemStack itemStack) {
        return this.addItemStackToSlot(slot, itemStack, false, true);
    }
    
    @Nonnull
    public ItemStackSlotTransaction addItemStackToSlot(final short slot, @Nonnull final ItemStack itemStack, final boolean allOrNothing, final boolean filter) {
        final ItemStackSlotTransaction transaction = InternalContainerUtilItemStack.internal_addItemStackToSlot(this, slot, itemStack, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    @Nonnull
    public ItemStackSlotTransaction setItemStackForSlot(final short slot, final ItemStack itemStack) {
        return this.setItemStackForSlot(slot, itemStack, true);
    }
    
    @Nonnull
    public ItemStackSlotTransaction setItemStackForSlot(final short slot, final ItemStack itemStack, final boolean filter) {
        final ItemStackSlotTransaction transaction = InternalContainerUtilItemStack.internal_setItemStackForSlot(this, slot, itemStack, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    @Nullable
    public ItemStack getItemStack(final short slot) {
        validateSlotIndex(slot, this.getCapacity());
        return this.readAction(() -> this.internal_getSlot(slot));
    }
    
    @Nonnull
    public ItemStackSlotTransaction replaceItemStackInSlot(final short slot, final ItemStack itemStackToRemove, final ItemStack itemStack) {
        final ItemStackSlotTransaction transaction = this.internal_replaceItemStack(slot, itemStackToRemove, itemStack);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public ListTransaction<ItemStackSlotTransaction> replaceAll(final SlotReplacementFunction func) {
        return this.replaceAll(func, true);
    }
    
    private ListTransaction<ItemStackSlotTransaction> replaceAll(final SlotReplacementFunction func, final boolean ignoreEmpty) {
        final ListTransaction<ItemStackSlotTransaction> transaction = this.writeAction(() -> {
            final short capacity = this.getCapacity();
            final ObjectArrayList<ItemStackSlotTransaction> transactionsList = new ObjectArrayList<ItemStackSlotTransaction>(capacity);
            for (short slot = 0; slot < capacity; ++slot) {
                final ItemStack existing = this.internal_getSlot(slot);
                if (!ignoreEmpty || !ItemStack.isEmpty(existing)) {
                    final ItemStack replacement = func.replace(slot, existing);
                    this.internal_setSlot(slot, replacement);
                    transactionsList.add(new ItemStackSlotTransaction(true, ActionType.REPLACE, slot, existing, replacement, existing, true, false, false, false, replacement, replacement));
                }
            }
            return new ListTransaction(true, (List<Transaction>)transactionsList);
        });
        this.sendUpdate(transaction);
        return transaction;
    }
    
    protected ItemStackSlotTransaction internal_replaceItemStack(final short slot, @Nullable final ItemStack itemStackToRemove, final ItemStack itemStack) {
        validateSlotIndex(slot, this.getCapacity());
        return this.writeAction(() -> {
            final ItemStack slotItemStack = this.internal_getSlot(slot);
            if ((slotItemStack == null && itemStackToRemove != null) || (slotItemStack != null && itemStackToRemove == null) || (slotItemStack != null && !itemStackToRemove.isStackableWith(slotItemStack))) {
                return new ItemStackSlotTransaction(false, ActionType.REPLACE, slot, slotItemStack, slotItemStack, null, true, false, false, false, itemStack, itemStack);
            }
            else {
                this.internal_setSlot(slot, itemStack);
                return new ItemStackSlotTransaction(true, ActionType.REPLACE, slot, slotItemStack, itemStack, slotItemStack, true, false, false, false, itemStack, null);
            }
        });
    }
    
    @Nonnull
    public SlotTransaction removeItemStackFromSlot(final short slot) {
        return this.removeItemStackFromSlot(slot, true);
    }
    
    @Nonnull
    public SlotTransaction removeItemStackFromSlot(final short slot, final boolean filter) {
        final SlotTransaction transaction = InternalContainerUtilItemStack.internal_removeItemStackFromSlot(this, slot, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    @Nonnull
    public ItemStackSlotTransaction removeItemStackFromSlot(final short slot, final int quantityToRemove) {
        return this.removeItemStackFromSlot(slot, quantityToRemove, true, true);
    }
    
    @Nonnull
    public ItemStackSlotTransaction removeItemStackFromSlot(final short slot, final int quantityToRemove, final boolean allOrNothing, final boolean filter) {
        final ItemStackSlotTransaction transaction = InternalContainerUtilItemStack.internal_removeItemStackFromSlot(this, slot, quantityToRemove, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    @Deprecated
    public ItemStackSlotTransaction internal_removeItemStack(final short slot, final int quantityToRemove) {
        return InternalContainerUtilItemStack.internal_removeItemStackFromSlot(this, slot, quantityToRemove, true, true);
    }
    
    @Nonnull
    public ItemStackSlotTransaction removeItemStackFromSlot(final short slot, final ItemStack itemStackToRemove, final int quantityToRemove) {
        return this.removeItemStackFromSlot(slot, itemStackToRemove, quantityToRemove, true, true);
    }
    
    @Nonnull
    public ItemStackSlotTransaction removeItemStackFromSlot(final short slot, final ItemStack itemStackToRemove, final int quantityToRemove, final boolean allOrNothing, final boolean filter) {
        final ItemStackSlotTransaction transaction = InternalContainerUtilItemStack.internal_removeItemStackFromSlot(this, slot, itemStackToRemove, quantityToRemove, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    @Nonnull
    public MaterialSlotTransaction removeMaterialFromSlot(final short slot, @Nonnull final MaterialQuantity material) {
        return this.removeMaterialFromSlot(slot, material, true, true, true);
    }
    
    @Nonnull
    public MaterialSlotTransaction removeMaterialFromSlot(final short slot, @Nonnull final MaterialQuantity material, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        final MaterialSlotTransaction transaction = InternalContainerUtilMaterial.internal_removeMaterialFromSlot(this, slot, material, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    @Nonnull
    public ResourceSlotTransaction removeResourceFromSlot(final short slot, @Nonnull final ResourceQuantity resource) {
        return this.removeResourceFromSlot(slot, resource, true, true, true);
    }
    
    @Nonnull
    public ResourceSlotTransaction removeResourceFromSlot(final short slot, @Nonnull final ResourceQuantity resource, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        final ResourceSlotTransaction transaction = InternalContainerUtilResource.internal_removeResourceFromSlot(this, slot, resource, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    @Nonnull
    public TagSlotTransaction removeTagFromSlot(final short slot, final int tagIndex, final int quantity) {
        return this.removeTagFromSlot(slot, tagIndex, quantity, true, true);
    }
    
    @Nonnull
    public TagSlotTransaction removeTagFromSlot(final short slot, final int tagIndex, final int quantity, final boolean allOrNothing, final boolean filter) {
        final TagSlotTransaction transaction = InternalContainerUtilTag.internal_removeTagFromSlot(this, slot, tagIndex, quantity, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    @Nonnull
    public MoveTransaction<ItemStackTransaction> moveItemStackFromSlot(final short slot, @Nonnull final ItemContainer containerTo) {
        return this.moveItemStackFromSlot(slot, containerTo, true);
    }
    
    @Nonnull
    public MoveTransaction<ItemStackTransaction> moveItemStackFromSlot(final short slot, @Nonnull final ItemContainer containerTo, final boolean filter) {
        return this.moveItemStackFromSlot(slot, containerTo, false, filter);
    }
    
    @Nonnull
    public MoveTransaction<ItemStackTransaction> moveItemStackFromSlot(final short slot, @Nonnull final ItemContainer containerTo, final boolean allOrNothing, final boolean filter) {
        final MoveTransaction<ItemStackTransaction> transaction = this.internal_moveItemStackFromSlot(slot, containerTo, allOrNothing, filter);
        this.sendUpdate(transaction);
        containerTo.sendUpdate(transaction.toInverted(this));
        return transaction;
    }
    
    protected MoveTransaction<ItemStackTransaction> internal_moveItemStackFromSlot(final short slot, @Nonnull final ItemContainer containerTo, final boolean allOrNothing, final boolean filter) {
        validateSlotIndex(slot, this.getCapacity());
        return this.writeAction(() -> containerTo.writeAction(() -> {
            if (filter && this.cantRemoveFromSlot(slot)) {
                return null;
            }
            else {
                final ItemStack itemFrom = this.internal_removeSlot(slot);
                if (ItemStack.isEmpty(itemFrom)) {
                    final SlotTransaction slotTransaction = new SlotTransaction(false, ActionType.REMOVE, slot, null, null, null, false, false, filter);
                    return new MoveTransaction(false, slotTransaction, MoveType.MOVE_FROM_SELF, containerTo, ItemStackTransaction.FAILED_ADD);
                }
                else {
                    final SlotTransaction fromTransaction = new SlotTransaction(true, ActionType.REMOVE, slot, itemFrom, null, null, false, false, filter);
                    final ItemStackTransaction addTransaction = InternalContainerUtilItemStack.internal_addItemStack(containerTo, itemFrom, allOrNothing, false, filter);
                    final ItemStack remainder = addTransaction.getRemainder();
                    if (!ItemStack.isEmpty(remainder)) {
                        InternalContainerUtilItemStack.internal_addItemStackToSlot(this, slot, remainder, allOrNothing, false);
                    }
                    return new MoveTransaction(addTransaction.succeeded(), fromTransaction, MoveType.MOVE_FROM_SELF, containerTo, addTransaction);
                }
            }
        }));
    }
    
    @Nonnull
    public MoveTransaction<ItemStackTransaction> moveItemStackFromSlot(final short slot, final int quantity, @Nonnull final ItemContainer containerTo) {
        return this.moveItemStackFromSlot(slot, quantity, containerTo, false, true);
    }
    
    @Nonnull
    public MoveTransaction<ItemStackTransaction> moveItemStackFromSlot(final short slot, final int quantity, @Nonnull final ItemContainer containerTo, final boolean allOrNothing, final boolean filter) {
        final MoveTransaction<ItemStackTransaction> transaction = this.internal_moveItemStackFromSlot(slot, quantity, containerTo, allOrNothing, filter);
        this.sendUpdate(transaction);
        containerTo.sendUpdate(transaction.toInverted(this));
        return transaction;
    }
    
    protected MoveTransaction<ItemStackTransaction> internal_moveItemStackFromSlot(final short slot, final int quantity, @Nonnull final ItemContainer containerTo, final boolean allOrNothing, final boolean filter) {
        validateSlotIndex(slot, this.getCapacity());
        validateQuantity(quantity);
        return this.writeAction(() -> containerTo.writeAction(() -> {
            if (filter && this.cantRemoveFromSlot(slot)) {
                return null;
            }
            else if (filter && containerTo.cantMoveToSlot(this, slot)) {
                final ItemStack itemStack = this.internal_getSlot(slot);
                final SlotTransaction slotTransaction = new SlotTransaction(false, ActionType.REMOVE, slot, itemStack, itemStack, null, false, false, filter);
                return new MoveTransaction(false, slotTransaction, MoveType.MOVE_FROM_SELF, containerTo, ItemStackTransaction.FAILED_ADD);
            }
            else {
                final ItemStackSlotTransaction fromTransaction = this.internal_removeItemStack(slot, quantity);
                if (!fromTransaction.succeeded()) {
                    final SlotTransaction slotTransaction2 = new SlotTransaction(false, ActionType.REMOVE, slot, null, null, null, false, false, filter);
                    return new MoveTransaction(false, slotTransaction2, MoveType.MOVE_FROM_SELF, containerTo, ItemStackTransaction.FAILED_ADD);
                }
                else {
                    final ItemStack itemFrom = fromTransaction.getOutput();
                    if (ItemStack.isEmpty(itemFrom)) {
                        final SlotTransaction slotTransaction3 = new SlotTransaction(false, ActionType.REMOVE, slot, null, null, null, false, false, filter);
                        return new MoveTransaction(false, slotTransaction3, MoveType.MOVE_FROM_SELF, containerTo, ItemStackTransaction.FAILED_ADD);
                    }
                    else {
                        final ItemStackTransaction addTransaction = InternalContainerUtilItemStack.internal_addItemStack(containerTo, itemFrom, allOrNothing, false, filter);
                        final ItemStack remainder = addTransaction.getRemainder();
                        if (!ItemStack.isEmpty(remainder)) {
                            InternalContainerUtilItemStack.internal_addItemStackToSlot(this, slot, remainder, allOrNothing, false);
                        }
                        return new MoveTransaction(addTransaction.succeeded(), fromTransaction, MoveType.MOVE_FROM_SELF, containerTo, addTransaction);
                    }
                }
            }
        }));
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<ItemStackTransaction>> moveItemStackFromSlot(final short slot, final ItemContainer... containerTo) {
        return this.moveItemStackFromSlot(slot, false, true, containerTo);
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<ItemStackTransaction>> moveItemStackFromSlot(final short slot, final boolean allOrNothing, final boolean filter, @Nonnull final ItemContainer... containerTo) {
        final ListTransaction<MoveTransaction<ItemStackTransaction>> transaction = this.internal_moveItemStackFromSlot(slot, allOrNothing, filter, containerTo);
        this.sendUpdate(transaction);
        for (final MoveTransaction<ItemStackTransaction> moveItemStackTransaction : transaction.getList()) {
            moveItemStackTransaction.getOtherContainer().sendUpdate(moveItemStackTransaction.toInverted(this));
        }
        return transaction;
    }
    
    @Nonnull
    private ListTransaction<MoveTransaction<ItemStackTransaction>> internal_moveItemStackFromSlot(final short slot, final boolean allOrNothing, final boolean filter, @Nonnull final ItemContainer[] containerTo) {
        final List<MoveTransaction<ItemStackTransaction>> transactions = new ObjectArrayList<MoveTransaction<ItemStackTransaction>>();
        for (final ItemContainer itemContainer : containerTo) {
            final MoveTransaction<ItemStackTransaction> transaction = this.internal_moveItemStackFromSlot(slot, itemContainer, allOrNothing, filter);
            transactions.add(transaction);
            if (transaction.succeeded()) {
                final ItemStackTransaction addTransaction = transaction.getAddTransaction();
                if (ItemStack.isEmpty(addTransaction.getRemainder())) {
                    break;
                }
            }
        }
        return new ListTransaction<MoveTransaction<ItemStackTransaction>>(!transactions.isEmpty(), transactions);
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<ItemStackTransaction>> moveItemStackFromSlot(final short slot, final int quantity, final ItemContainer... containerTo) {
        return this.moveItemStackFromSlot(slot, quantity, false, true, containerTo);
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<ItemStackTransaction>> moveItemStackFromSlot(final short slot, final int quantity, final boolean allOrNothing, final boolean filter, @Nonnull final ItemContainer... containerTo) {
        final ListTransaction<MoveTransaction<ItemStackTransaction>> transaction = this.internal_moveItemStackFromSlot(slot, quantity, allOrNothing, filter, containerTo);
        this.sendUpdate(transaction);
        for (final MoveTransaction<ItemStackTransaction> moveItemStackTransaction : transaction.getList()) {
            moveItemStackTransaction.getOtherContainer().sendUpdate(moveItemStackTransaction.toInverted(this));
        }
        return transaction;
    }
    
    @Nonnull
    private ListTransaction<MoveTransaction<ItemStackTransaction>> internal_moveItemStackFromSlot(final short slot, final int quantity, final boolean allOrNothing, final boolean filter, @Nonnull final ItemContainer[] containerTo) {
        final List<MoveTransaction<ItemStackTransaction>> transactions = new ObjectArrayList<MoveTransaction<ItemStackTransaction>>();
        for (final ItemContainer itemContainer : containerTo) {
            final MoveTransaction<ItemStackTransaction> transaction = this.internal_moveItemStackFromSlot(slot, quantity, itemContainer, allOrNothing, filter);
            transactions.add(transaction);
            if (transaction.succeeded()) {
                final ItemStackTransaction addTransaction = transaction.getAddTransaction();
                if (ItemStack.isEmpty(addTransaction.getRemainder())) {
                    break;
                }
            }
        }
        return new ListTransaction<MoveTransaction<ItemStackTransaction>>(!transactions.isEmpty(), transactions);
    }
    
    @Nonnull
    public MoveTransaction<SlotTransaction> moveItemStackFromSlotToSlot(final short slot, final int quantity, @Nonnull final ItemContainer containerTo, final short slotTo) {
        return this.moveItemStackFromSlotToSlot(slot, quantity, containerTo, slotTo, true);
    }
    
    @Nonnull
    public MoveTransaction<SlotTransaction> moveItemStackFromSlotToSlot(final short slot, final int quantity, @Nonnull final ItemContainer containerTo, final short slotTo, final boolean filter) {
        final MoveTransaction<SlotTransaction> transaction = this.internal_moveItemStackFromSlot(slot, quantity, containerTo, slotTo, filter);
        this.sendUpdate(transaction);
        containerTo.sendUpdate(transaction.toInverted(this));
        return transaction;
    }
    
    protected MoveTransaction<SlotTransaction> internal_moveItemStackFromSlot(final short slot, final int quantity, @Nonnull final ItemContainer containerTo, final short slotTo, final boolean filter) {
        validateSlotIndex(slot, this.getCapacity());
        validateSlotIndex(slotTo, containerTo.getCapacity());
        validateQuantity(quantity);
        return this.writeAction(() -> containerTo.writeAction(() -> {
            if (filter && this.cantRemoveFromSlot(slot)) {
                final ItemStack itemStack = this.internal_getSlot(slot);
                final SlotTransaction slotTransaction = new SlotTransaction(false, ActionType.REMOVE, slot, itemStack, itemStack, null, false, false, filter);
                return new MoveTransaction(false, slotTransaction, MoveType.MOVE_FROM_SELF, containerTo, SlotTransaction.FAILED_ADD);
            }
            else if (filter && containerTo.cantMoveToSlot(this, slot)) {
                final ItemStack itemStack2 = this.internal_getSlot(slot);
                final SlotTransaction slotTransaction2 = new SlotTransaction(false, ActionType.REMOVE, slot, itemStack2, itemStack2, null, false, false, filter);
                return new MoveTransaction(false, slotTransaction2, MoveType.MOVE_FROM_SELF, containerTo, SlotTransaction.FAILED_ADD);
            }
            else {
                final ItemStackSlotTransaction fromTransaction = this.internal_removeItemStack(slot, quantity);
                if (!fromTransaction.succeeded()) {
                    return new MoveTransaction(false, fromTransaction, MoveType.MOVE_FROM_SELF, containerTo, SlotTransaction.FAILED_ADD);
                }
                else {
                    final ItemStack itemFrom = fromTransaction.getOutput();
                    if (ItemStack.isEmpty(itemFrom)) {
                        return new MoveTransaction(true, fromTransaction, MoveType.MOVE_FROM_SELF, containerTo, SlotTransaction.FAILED_ADD);
                    }
                    else {
                        final ItemStack itemTo = containerTo.getItemStack(slotTo);
                        if (filter && containerTo.cantAddToSlot(slotTo, itemFrom, itemTo)) {
                            this.internal_setSlot(slot, fromTransaction.getSlotBefore());
                            final SlotTransaction slotTransaction3 = new SlotTransaction(true, ActionType.REMOVE, slot, fromTransaction.getSlotBefore(), fromTransaction.getSlotAfter(), null, false, false, filter);
                            final SlotTransaction addTransaction = new SlotTransaction(false, ActionType.ADD, slotTo, itemTo, itemTo, null, false, false, filter);
                            return new MoveTransaction(false, slotTransaction3, MoveType.MOVE_FROM_SELF, containerTo, addTransaction);
                        }
                        else if (ItemStack.isEmpty(itemTo)) {
                            final ItemStackSlotTransaction addTransaction2 = InternalContainerUtilItemStack.internal_setItemStackForSlot(containerTo, slotTo, itemFrom, filter);
                            return new MoveTransaction(true, fromTransaction, MoveType.MOVE_FROM_SELF, containerTo, addTransaction2);
                        }
                        else if (!itemFrom.isStackableWith(itemTo)) {
                            if (ItemStack.isEmpty(fromTransaction.getSlotAfter())) {
                                if (filter && this.cantAddToSlot(slot, itemTo, itemFrom)) {
                                    this.internal_setSlot(slot, fromTransaction.getSlotBefore());
                                    final SlotTransaction slotTransaction4 = new SlotTransaction(true, ActionType.REMOVE, slot, fromTransaction.getSlotBefore(), fromTransaction.getSlotAfter(), null, false, false, filter);
                                    final SlotTransaction addTransaction3 = new SlotTransaction(false, ActionType.ADD, slotTo, itemTo, itemTo, null, false, false, filter);
                                    return new MoveTransaction(false, slotTransaction4, MoveType.MOVE_FROM_SELF, containerTo, addTransaction3);
                                }
                                else {
                                    this.internal_setSlot(slot, itemTo);
                                    containerTo.internal_setSlot(slotTo, itemFrom);
                                    final SlotTransaction from = new SlotTransaction(true, ActionType.REPLACE, slot, itemFrom, itemTo, null, false, false, filter);
                                    final SlotTransaction to = new SlotTransaction(true, ActionType.REPLACE, slotTo, itemTo, itemFrom, null, false, false, filter);
                                    return new MoveTransaction(true, from, MoveType.MOVE_FROM_SELF, containerTo, to);
                                }
                            }
                            else {
                                this.internal_setSlot(slot, fromTransaction.getSlotBefore());
                                final SlotTransaction slotTransaction5 = new SlotTransaction(true, ActionType.REMOVE, slot, fromTransaction.getSlotBefore(), fromTransaction.getSlotAfter(), null, false, false, filter);
                                final SlotTransaction addTransaction4 = new SlotTransaction(false, ActionType.ADD, slotTo, itemTo, itemTo, null, false, false, filter);
                                return new MoveTransaction(false, slotTransaction5, MoveType.MOVE_FROM_SELF, containerTo, addTransaction4);
                            }
                        }
                        else {
                            final int maxStack = itemFrom.getItem().getMaxStack();
                            final int newQuantity = itemFrom.getQuantity() + itemTo.getQuantity();
                            if (newQuantity <= maxStack) {
                                final ItemStackSlotTransaction addTransaction5 = InternalContainerUtilItemStack.internal_setItemStackForSlot(containerTo, slotTo, itemTo.withQuantity(newQuantity), filter);
                                return new MoveTransaction(true, fromTransaction, MoveType.MOVE_FROM_SELF, containerTo, addTransaction5);
                            }
                            else {
                                final ItemStackSlotTransaction addTransaction6 = InternalContainerUtilItemStack.internal_setItemStackForSlot(containerTo, slotTo, itemTo.withQuantity(maxStack), filter);
                                final int remainder = newQuantity - maxStack;
                                final int quantityLeft = ItemStack.isEmpty(fromTransaction.getSlotAfter()) ? 0 : fromTransaction.getSlotAfter().getQuantity();
                                this.internal_setSlot(slot, itemFrom.withQuantity(remainder + quantityLeft));
                                return new MoveTransaction(true, fromTransaction, MoveType.MOVE_FROM_SELF, containerTo, addTransaction6);
                            }
                        }
                    }
                }
            }
        }));
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<ItemStackTransaction>> moveAllItemStacksTo(final ItemContainer... containerTo) {
        return this.moveAllItemStacksTo((Predicate<ItemStack>)null, containerTo);
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<ItemStackTransaction>> moveAllItemStacksTo(final Predicate<ItemStack> itemPredicate, final ItemContainer... containerTo) {
        final ListTransaction<MoveTransaction<ItemStackTransaction>> transaction = this.internal_moveAllItemStacksTo(itemPredicate, containerTo);
        this.sendUpdate(transaction);
        for (final MoveTransaction<ItemStackTransaction> moveItemStackTransaction : transaction.getList()) {
            moveItemStackTransaction.getOtherContainer().sendUpdate(moveItemStackTransaction.toInverted(this));
        }
        return transaction;
    }
    
    @Nonnull
    protected ListTransaction<MoveTransaction<ItemStackTransaction>> internal_moveAllItemStacksTo(@Nullable final Predicate<ItemStack> itemPredicate, final ItemContainer[] containerTo) {
        return this.writeAction(() -> {
            final List<MoveTransaction<ItemStackTransaction>> transactions = new ObjectArrayList<MoveTransaction<ItemStackTransaction>>();
            for (short i = 0; i < this.getCapacity(); ++i) {
                if (!this.cantRemoveFromSlot(i)) {
                    final ItemStack checkedItem = this.internal_getSlot(i);
                    if (!ItemStack.isEmpty(checkedItem)) {
                        if (itemPredicate == null || itemPredicate.test(checkedItem)) {
                            transactions.addAll(this.moveItemStackFromSlot(i, containerTo).getList());
                        }
                    }
                }
            }
            return new ListTransaction(true, (List<Transaction>)transactions);
        });
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<ItemStackTransaction>> quickStackTo(@Nonnull final ItemContainer... containerTo) {
        return this.moveAllItemStacksTo(itemStack -> {
            final ItemContainer[] arr$ = containerTo;
            final int len$ = arr$.length;
            int i$ = 0;
            while (i$ < len$) {
                final ItemContainer itemContainer = arr$[i$];
                if (itemContainer.containsItemStacksStackableWith(itemStack)) {
                    return true;
                }
                else {
                    ++i$;
                }
            }
            return false;
        }, containerTo);
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<SlotTransaction>> combineItemStacksIntoSlot(@Nonnull final ItemContainer containerTo, final short slotTo) {
        final ListTransaction<MoveTransaction<SlotTransaction>> transaction = this.internal_combineItemStacksIntoSlot(containerTo, slotTo);
        this.sendUpdate(transaction);
        for (final MoveTransaction<SlotTransaction> moveSlotTransaction : transaction.getList()) {
            moveSlotTransaction.getOtherContainer().sendUpdate(moveSlotTransaction.toInverted(this));
        }
        return transaction;
    }
    
    @Nonnull
    protected ListTransaction<MoveTransaction<SlotTransaction>> internal_combineItemStacksIntoSlot(@Nonnull final ItemContainer containerTo, final short slotTo) {
        validateSlotIndex(slotTo, containerTo.getCapacity());
        return this.writeAction(() -> {
            final ItemStack itemStack = containerTo.internal_getSlot(slotTo);
            final Item item = itemStack.getItem();
            final int maxStack = item.getMaxStack();
            if (ItemStack.isEmpty(itemStack) || itemStack.getQuantity() >= maxStack) {
                return new ListTransaction(false, Collections.emptyList());
            }
            else {
                int count = 0;
                final int[] quantities = new int[this.getCapacity()];
                final int[] indexes = new int[this.getCapacity()];
                for (short i = 0; i < this.getCapacity(); ++i) {
                    if (!this.cantRemoveFromSlot(i)) {
                        final ItemStack itemFrom = this.internal_getSlot(i);
                        if (itemStack != itemFrom) {
                            if (!ItemStack.isEmpty(itemFrom)) {
                                if (!(!itemFrom.isStackableWith(itemStack))) {
                                    indexes[count] = i;
                                    quantities[count] = itemFrom.getQuantity();
                                    ++count;
                                }
                            }
                        }
                    }
                }
                IntArrays.quickSort(quantities, indexes, 0, count);
                int quantity = itemStack.getQuantity();
                final ObjectArrayList<MoveTransaction<SlotTransaction>> list = new ObjectArrayList<MoveTransaction<SlotTransaction>>();
                MoveTransaction<SlotTransaction> transaction;
                for (int ai = 0; ai < count && quantity < maxStack; quantity = (ItemStack.isEmpty(transaction.getAddTransaction().getSlotAfter()) ? 0 : transaction.getAddTransaction().getSlotAfter().getQuantity()), ++ai) {
                    final short j = (short)indexes[ai];
                    final ItemStack itemFrom2 = this.internal_getSlot(j);
                    transaction = this.internal_moveItemStackFromSlot(j, itemFrom2.getQuantity(), containerTo, slotTo, true);
                    list.add(transaction);
                }
                return new ListTransaction<MoveTransaction<SlotTransaction>>(true, (List<Transaction>)list);
            }
        });
    }
    
    @Nonnull
    public ListTransaction<MoveTransaction<SlotTransaction>> swapItems(final short srcPos, @Nonnull final ItemContainer containerTo, final short destPos, final short length) {
        final ListTransaction<MoveTransaction<SlotTransaction>> transaction = this.internal_swapItems(srcPos, containerTo, destPos, length);
        this.sendUpdate(transaction);
        for (final MoveTransaction<SlotTransaction> moveItemStackTransaction : transaction.getList()) {
            moveItemStackTransaction.getOtherContainer().sendUpdate(moveItemStackTransaction.toInverted(this));
        }
        return transaction;
    }
    
    @Nonnull
    protected ListTransaction<MoveTransaction<SlotTransaction>> internal_swapItems(final short srcPos, @Nonnull final ItemContainer containerTo, final short destPos, final short length) {
        if (srcPos < 0) {
            throw new IndexOutOfBoundsException("srcPos < 0");
        }
        if (srcPos + length > this.getCapacity()) {
            throw new IndexOutOfBoundsException("srcPos + length > capacity");
        }
        if (destPos < 0) {
            throw new IndexOutOfBoundsException("destPos < 0");
        }
        if (destPos + length > containerTo.getCapacity()) {
            throw new IndexOutOfBoundsException("destPos + length > dest.capacity");
        }
        return this.writeAction(() -> containerTo.writeAction(() -> {
            final ObjectArrayList<MoveTransaction<SlotTransaction>> list = new ObjectArrayList<MoveTransaction<SlotTransaction>>(length);
            for (short slot = 0; slot < length; ++slot) {
                list.add(this.internal_swapItems(containerTo, (short)(srcPos + slot), (short)(destPos + slot)));
            }
            return new ListTransaction<MoveTransaction<SlotTransaction>>(true, (List<Transaction>)list);
        }));
    }
    
    @Nonnull
    protected MoveTransaction<SlotTransaction> internal_swapItems(@Nonnull final ItemContainer containerTo, final short slotFrom, final short slotTo) {
        final ItemStack itemFrom = this.internal_removeSlot(slotFrom);
        final ItemStack itemTo = containerTo.internal_removeSlot(slotTo);
        if (itemTo != null && !itemTo.isEmpty()) {
            this.internal_setSlot(slotFrom, itemTo);
        }
        if (itemFrom != null && !itemFrom.isEmpty()) {
            containerTo.internal_setSlot(slotTo, itemFrom);
        }
        final SlotTransaction from = new SlotTransaction(true, ActionType.REPLACE, slotFrom, itemFrom, itemTo, null, false, false, false);
        final SlotTransaction to = new SlotTransaction(true, ActionType.REPLACE, slotTo, itemTo, itemFrom, null, false, false, false);
        return new MoveTransaction<SlotTransaction>(true, from, MoveType.MOVE_FROM_SELF, containerTo, to);
    }
    
    public boolean canAddItemStack(@Nonnull final ItemStack itemStack) {
        return this.canAddItemStack(itemStack, false, true);
    }
    
    public boolean canAddItemStack(@Nonnull final ItemStack itemStack, final boolean fullStacks, final boolean filter) {
        final Item item = itemStack.getItem();
        if (item == null) {
            throw new IllegalArgumentException(itemStack.getItemId() + " is an invalid item!");
        }
        final int itemMaxStack = item.getMaxStack();
        return this.readAction(() -> {
            int testQuantityRemaining = itemStack.getQuantity();
            if (!fullStacks) {
                testQuantityRemaining = InternalContainerUtilItemStack.testAddToExistingItemStacks(this, itemStack, itemMaxStack, testQuantityRemaining, filter);
            }
            final int testQuantityRemaining2 = InternalContainerUtilItemStack.testAddToEmptySlots(this, itemStack, itemMaxStack, testQuantityRemaining, filter);
            return testQuantityRemaining2 <= 0;
        });
    }
    
    @Nonnull
    public ItemStackTransaction addItemStack(@Nonnull final ItemStack itemStack) {
        return this.addItemStack(itemStack, false, false, true);
    }
    
    @Nonnull
    public ItemStackTransaction addItemStack(@Nonnull final ItemStack itemStack, final boolean allOrNothing, final boolean fullStacks, final boolean filter) {
        final ItemStackTransaction transaction = InternalContainerUtilItemStack.internal_addItemStack(this, itemStack, allOrNothing, fullStacks, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canAddItemStacks(final List<ItemStack> itemStacks) {
        return this.canAddItemStacks(itemStacks, false, true);
    }
    
    public boolean canAddItemStacks(@Nullable final List<ItemStack> itemStacks, final boolean fullStacks, final boolean filter) {
        if (itemStacks == null || itemStacks.isEmpty()) {
            return true;
        }
        final List<TempItemData> tempItemDataList = new ObjectArrayList<TempItemData>(itemStacks.size());
        for (final ItemStack itemStack : itemStacks) {
            final Item item = itemStack.getItem();
            if (item == null) {
                throw new IllegalArgumentException(itemStack.getItemId() + " is an invalid item!");
            }
            tempItemDataList.add(new TempItemData(itemStack, item));
        }
        return this.readAction(() -> {
            for (final TempItemData tempItemData : tempItemDataList) {
                final int itemMaxStack = tempItemData.item().getMaxStack();
                final ItemStack itemStack2 = tempItemData.itemStack();
                int testQuantityRemaining = itemStack2.getQuantity();
                if (!fullStacks) {
                    testQuantityRemaining = InternalContainerUtilItemStack.testAddToExistingItemStacks(this, itemStack2, itemMaxStack, testQuantityRemaining, filter);
                }
                final int testQuantityRemaining2 = InternalContainerUtilItemStack.testAddToEmptySlots(this, itemStack2, itemMaxStack, testQuantityRemaining, filter);
                if (testQuantityRemaining2 > 0) {
                    return false;
                }
            }
            return true;
        });
    }
    
    public ListTransaction<ItemStackTransaction> addItemStacks(final List<ItemStack> itemStacks) {
        return this.addItemStacks(itemStacks, false, false, true);
    }
    
    public ListTransaction<ItemStackTransaction> addItemStacks(@Nullable final List<ItemStack> itemStacks, final boolean allOrNothing, final boolean fullStacks, final boolean filter) {
        if (itemStacks == null || itemStacks.isEmpty()) {
            return ListTransaction.getEmptyTransaction(true);
        }
        final ListTransaction<ItemStackTransaction> transaction = InternalContainerUtilItemStack.internal_addItemStacks(this, itemStacks, allOrNothing, fullStacks, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public ListTransaction<ItemStackSlotTransaction> addItemStacksOrdered(final List<ItemStack> itemStacks) {
        return this.addItemStacksOrdered(itemStacks, false, true);
    }
    
    public ListTransaction<ItemStackSlotTransaction> addItemStacksOrdered(final short offset, final List<ItemStack> itemStacks) {
        return this.addItemStacksOrdered(offset, itemStacks, false, true);
    }
    
    public ListTransaction<ItemStackSlotTransaction> addItemStacksOrdered(final List<ItemStack> itemStacks, final boolean allOrNothing, final boolean filter) {
        return this.addItemStacksOrdered((short)0, itemStacks, allOrNothing, filter);
    }
    
    public ListTransaction<ItemStackSlotTransaction> addItemStacksOrdered(final short offset, @Nullable final List<ItemStack> itemStacks, final boolean allOrNothing, final boolean filter) {
        if (itemStacks == null || itemStacks.isEmpty()) {
            return ListTransaction.getEmptyTransaction(true);
        }
        final ListTransaction<ItemStackSlotTransaction> transaction = InternalContainerUtilItemStack.internal_addItemStacksOrdered(this, offset, itemStacks, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canRemoveItemStack(final ItemStack itemStack) {
        return this.canRemoveItemStack(itemStack, true, true);
    }
    
    public boolean canRemoveItemStack(@Nullable final ItemStack itemStack, final boolean exactAmount, final boolean filter) {
        return itemStack == null || this.readAction(() -> {
            final int testQuantityRemaining = InternalContainerUtilItemStack.testRemoveItemStackFromItems(this, itemStack, itemStack.getQuantity(), filter);
            if (testQuantityRemaining > 0) {
                return false;
            }
            else {
                return !exactAmount || testQuantityRemaining >= 0;
            }
        });
    }
    
    @Nonnull
    public ItemStackTransaction removeItemStack(@Nonnull final ItemStack itemStack) {
        return this.removeItemStack(itemStack, true, true);
    }
    
    @Nonnull
    public ItemStackTransaction removeItemStack(@Nonnull final ItemStack itemStack, final boolean allOrNothing, final boolean filter) {
        final ItemStackTransaction transaction = InternalContainerUtilItemStack.internal_removeItemStack(this, itemStack, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canRemoveItemStacks(final List<ItemStack> itemStacks) {
        return this.canRemoveItemStacks(itemStacks, true, true);
    }
    
    public boolean canRemoveItemStacks(@Nullable final List<ItemStack> itemStacks, final boolean exactAmount, final boolean filter) {
        return itemStacks == null || itemStacks.isEmpty() || this.readAction(() -> {
            for (final ItemStack itemStack : itemStacks) {
                final int testQuantityRemaining = InternalContainerUtilItemStack.testRemoveItemStackFromItems(this, itemStack, itemStack.getQuantity(), filter);
                if (testQuantityRemaining > 0) {
                    return false;
                }
                else if (exactAmount && testQuantityRemaining < 0) {
                    return false;
                }
                else {
                    continue;
                }
            }
            return true;
        });
    }
    
    public ListTransaction<ItemStackTransaction> removeItemStacks(final List<ItemStack> itemStacks) {
        return this.removeItemStacks(itemStacks, true, true);
    }
    
    public ListTransaction<ItemStackTransaction> removeItemStacks(@Nullable final List<ItemStack> itemStacks, final boolean allOrNothing, final boolean filter) {
        if (itemStacks == null || itemStacks.isEmpty()) {
            return ListTransaction.getEmptyTransaction(true);
        }
        final ListTransaction<ItemStackTransaction> transaction = InternalContainerUtilItemStack.internal_removeItemStacks(this, itemStacks, allOrNothing, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canRemoveTag(final int tagIndex, final int quantity) {
        return this.canRemoveTag(tagIndex, quantity, true, true);
    }
    
    public boolean canRemoveTag(final int tagIndex, final int quantity, final boolean exactAmount, final boolean filter) {
        return this.readAction(() -> {
            final int testQuantityRemaining = InternalContainerUtilTag.testRemoveTagFromItems(this, tagIndex, quantity, filter);
            if (testQuantityRemaining > 0) {
                return false;
            }
            else {
                return !exactAmount || testQuantityRemaining >= 0;
            }
        });
    }
    
    @Nonnull
    public TagTransaction removeTag(final int tagIndex, final int quantity) {
        return this.removeTag(tagIndex, quantity, true, true, true);
    }
    
    @Nonnull
    public TagTransaction removeTag(final int tagIndex, final int quantity, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        final TagTransaction transaction = InternalContainerUtilTag.internal_removeTag(this, tagIndex, quantity, allOrNothing, exactAmount, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canRemoveResource(final ResourceQuantity resource) {
        return this.canRemoveResource(resource, true, true);
    }
    
    public boolean canRemoveResource(@Nullable final ResourceQuantity resource, final boolean exactAmount, final boolean filter) {
        return resource == null || this.readAction(() -> {
            final int testQuantityRemaining = InternalContainerUtilResource.testRemoveResourceFromItems(this, resource, resource.getQuantity(), filter);
            if (testQuantityRemaining > 0) {
                return false;
            }
            else {
                return !exactAmount || testQuantityRemaining >= 0;
            }
        });
    }
    
    @Nonnull
    public ResourceTransaction removeResource(@Nonnull final ResourceQuantity resource) {
        return this.removeResource(resource, true, true, true);
    }
    
    @Nonnull
    public ResourceTransaction removeResource(@Nonnull final ResourceQuantity resource, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        final ResourceTransaction transaction = InternalContainerUtilResource.internal_removeResource(this, resource, allOrNothing, exactAmount, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canRemoveResources(final List<ResourceQuantity> resources) {
        return this.canRemoveResources(resources, true, true);
    }
    
    public boolean canRemoveResources(@Nullable final List<ResourceQuantity> resources, final boolean exactAmount, final boolean filter) {
        return resources == null || resources.isEmpty() || this.readAction(() -> {
            for (final ResourceQuantity resource : resources) {
                final int testQuantityRemaining = InternalContainerUtilResource.testRemoveResourceFromItems(this, resource, resource.getQuantity(), filter);
                if (testQuantityRemaining > 0) {
                    return false;
                }
                else if (exactAmount && testQuantityRemaining < 0) {
                    return false;
                }
                else {
                    continue;
                }
            }
            return true;
        });
    }
    
    public ListTransaction<ResourceTransaction> removeResources(final List<ResourceQuantity> resources) {
        return this.removeResources(resources, true, true, true);
    }
    
    public ListTransaction<ResourceTransaction> removeResources(@Nullable final List<ResourceQuantity> resources, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        if (resources == null || resources.isEmpty()) {
            return ListTransaction.getEmptyTransaction(true);
        }
        final ListTransaction<ResourceTransaction> transaction = InternalContainerUtilResource.internal_removeResources(this, resources, allOrNothing, exactAmount, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canRemoveMaterial(final MaterialQuantity material) {
        return this.canRemoveMaterial(material, true, true);
    }
    
    public boolean canRemoveMaterial(@Nullable final MaterialQuantity material, final boolean exactAmount, final boolean filter) {
        return material == null || this.readAction(() -> {
            final int testQuantityRemaining = InternalContainerUtilMaterial.testRemoveMaterialFromItems(this, material, material.getQuantity(), filter);
            if (testQuantityRemaining > 0) {
                return false;
            }
            else {
                return !exactAmount || testQuantityRemaining >= 0;
            }
        });
    }
    
    @Nonnull
    public MaterialTransaction removeMaterial(@Nonnull final MaterialQuantity material) {
        return this.removeMaterial(material, true, true, true);
    }
    
    @Nonnull
    public MaterialTransaction removeMaterial(@Nonnull final MaterialQuantity material, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        final MaterialTransaction transaction = InternalContainerUtilMaterial.internal_removeMaterial(this, material, allOrNothing, exactAmount, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean canRemoveMaterials(final List<MaterialQuantity> materials) {
        return this.canRemoveMaterials(materials, true, true);
    }
    
    public boolean canRemoveMaterials(@Nullable final List<MaterialQuantity> materials, final boolean exactAmount, final boolean filter) {
        return materials == null || materials.isEmpty() || this.readAction(() -> {
            for (final MaterialQuantity material : materials) {
                final int testQuantityRemaining = InternalContainerUtilMaterial.testRemoveMaterialFromItems(this, material, material.getQuantity(), filter);
                if (testQuantityRemaining > 0) {
                    return false;
                }
                else if (exactAmount && testQuantityRemaining < 0) {
                    return false;
                }
                else {
                    continue;
                }
            }
            return true;
        });
    }
    
    public List<TestRemoveItemSlotResult> getSlotMaterialsToRemove(@Nullable final List<MaterialQuantity> materials, final boolean exactAmount, final boolean filter) {
        final List<TestRemoveItemSlotResult> slotMaterials = new ObjectArrayList<TestRemoveItemSlotResult>();
        if (materials == null || materials.isEmpty()) {
            return slotMaterials;
        }
        return this.readAction(() -> {
            for (final MaterialQuantity material : materials) {
                final TestRemoveItemSlotResult testResult = InternalContainerUtilMaterial.getTestRemoveMaterialFromItems(this, material, material.getQuantity(), filter);
                if (testResult.quantityRemaining > 0) {
                    slotMaterials.clear();
                    return slotMaterials;
                }
                else if (exactAmount && testResult.quantityRemaining < 0) {
                    slotMaterials.clear();
                    return slotMaterials;
                }
                else {
                    slotMaterials.add(testResult);
                }
            }
            return slotMaterials;
        });
    }
    
    public ListTransaction<MaterialTransaction> removeMaterials(final List<MaterialQuantity> materials) {
        return this.removeMaterials(materials, true, true, true);
    }
    
    public ListTransaction<MaterialTransaction> removeMaterials(@Nullable final List<MaterialQuantity> materials, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        if (materials == null || materials.isEmpty()) {
            return ListTransaction.getEmptyTransaction(true);
        }
        final ListTransaction<MaterialTransaction> transaction = InternalContainerUtilMaterial.internal_removeMaterials(this, materials, allOrNothing, exactAmount, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public ListTransaction<MaterialSlotTransaction> removeMaterialsOrdered(final short offset, final List<MaterialQuantity> materials) {
        return this.removeMaterialsOrdered(offset, materials, true, true, true);
    }
    
    public ListTransaction<MaterialSlotTransaction> removeMaterialsOrdered(final List<MaterialQuantity> materials, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        return this.removeMaterialsOrdered((short)0, materials, allOrNothing, exactAmount, filter);
    }
    
    public ListTransaction<MaterialSlotTransaction> removeMaterialsOrdered(final short offset, @Nullable final List<MaterialQuantity> materials, final boolean allOrNothing, final boolean exactAmount, final boolean filter) {
        if (materials == null || materials.isEmpty()) {
            return ListTransaction.getEmptyTransaction(true);
        }
        if (offset + materials.size() > this.getCapacity()) {
            return ListTransaction.getEmptyTransaction(false);
        }
        final ListTransaction<MaterialSlotTransaction> transaction = InternalContainerUtilMaterial.internal_removeMaterialsOrdered(this, offset, materials, allOrNothing, exactAmount, filter);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    public boolean isEmpty() {
        return this.readAction(() -> {
            short i = 0;
            while (i < this.getCapacity()) {
                final ItemStack itemStack = this.internal_getSlot(i);
                if (itemStack != null && !itemStack.isEmpty()) {
                    return false;
                }
                else {
                    ++i;
                }
            }
            return true;
        });
    }
    
    public int countItemStacks(@Nonnull final Predicate<ItemStack> itemPredicate) {
        return this.readAction(() -> {
            int count = 0;
            for (short i = 0; i < this.getCapacity(); ++i) {
                final ItemStack itemStack = this.internal_getSlot(i);
                if (!ItemStack.isEmpty(itemStack)) {
                    if (!(!itemPredicate.test(itemStack))) {
                        count += itemStack.getQuantity();
                    }
                }
            }
            return count;
        });
    }
    
    public boolean containsItemStacksStackableWith(@Nonnull final ItemStack itemStack) {
        return this.readAction(() -> {
            short i = 0;
            while (i < this.getCapacity()) {
                final ItemStack checked = this.internal_getSlot(i);
                if (!ItemStack.isEmpty(checked) && itemStack.isStackableWith(checked)) {
                    return true;
                }
                else {
                    ++i;
                }
            }
            return false;
        });
    }
    
    public void forEach(@Nonnull final ShortObjectConsumer<ItemStack> action) {
        for (short i = 0; i < this.getCapacity(); ++i) {
            final ItemStack itemStack = this.getItemStack(i);
            if (!ItemStack.isEmpty(itemStack)) {
                action.accept(i, itemStack);
            }
        }
    }
    
    public <T> void forEachWithMeta(@Nonnull final Short2ObjectConcurrentHashMap.ShortBiObjConsumer<ItemStack, T> consumer, final T meta) {
        for (short i = 0; i < this.getCapacity(); ++i) {
            final ItemStack itemStack = this.getItemStack(i);
            if (!ItemStack.isEmpty(itemStack)) {
                consumer.accept(i, itemStack, meta);
            }
        }
    }
    
    @Nonnull
    public List<ItemStack> removeAllItemStacks() {
        final List<ItemStack> items = new ObjectArrayList<ItemStack>();
        final ListTransaction<SlotTransaction> transaction = this.writeAction(() -> {
            final List<SlotTransaction> transactions = new ObjectArrayList<SlotTransaction>();
            for (short i = 0; i < this.getCapacity(); ++i) {
                if (!this.cantRemoveFromSlot(i)) {
                    final ItemStack itemStack = this.internal_removeSlot(i);
                    if (!ItemStack.isEmpty(itemStack)) {
                        items.add(itemStack);
                        transactions.add(new SlotTransaction(true, ActionType.REMOVE, i, itemStack, null, itemStack, false, false, true));
                    }
                }
            }
            return new ListTransaction(true, (List<Transaction>)transactions);
        });
        this.sendUpdate(transaction);
        return items;
    }
    
    @Nonnull
    public List<ItemStack> dropAllItemStacks() {
        return this.dropAllItemStacks(true);
    }
    
    @Nonnull
    public List<ItemStack> dropAllItemStacks(final boolean filter) {
        final List<ItemStack> items = new ObjectArrayList<ItemStack>();
        final ListTransaction<SlotTransaction> transaction = this.writeAction(() -> {
            final List<SlotTransaction> transactions = new ObjectArrayList<SlotTransaction>();
            for (short i = 0; i < this.getCapacity(); ++i) {
                if (!filter || !this.cantDropFromSlot(i)) {
                    final ItemStack itemStack = this.internal_removeSlot(i);
                    if (!ItemStack.isEmpty(itemStack)) {
                        items.add(itemStack);
                        transactions.add(new SlotTransaction(true, ActionType.REMOVE, i, itemStack, null, itemStack, false, false, true));
                    }
                }
            }
            return new ListTransaction(true, (List<Transaction>)transactions);
        });
        this.sendUpdate(transaction);
        return items;
    }
    
    @Nonnull
    public ListTransaction<SlotTransaction> sortItems(@Nonnull final SortType sort) {
        final ListTransaction<SlotTransaction> transaction = this.internal_sortItems(sort);
        this.sendUpdate(transaction);
        return transaction;
    }
    
    protected ListTransaction<SlotTransaction> internal_sortItems(@Nonnull final SortType sort) {
        return this.writeAction(() -> {
            final ItemStack[] stacks = new ItemStack[this.getCapacity()];
            int stackOffset = 0;
            for (short i = 0; i < stacks.length; ++i) {
                if (!this.cantRemoveFromSlot(i)) {
                    final ItemStack slot = this.internal_getSlot(i);
                    if (slot != null) {
                        final Item item = slot.getItem();
                        final int maxStack = item.getMaxStack();
                        int slotQuantity = slot.getQuantity();
                        if (maxStack > 1) {
                            for (int j = 0; j < stackOffset && slotQuantity > 0; ++j) {
                                final ItemStack stack = stacks[j];
                                if (!(!slot.isStackableWith(stack))) {
                                    final int stackQuantity = stack.getQuantity();
                                    if (stackQuantity < maxStack) {
                                        final int adjust = Math.min(slotQuantity, maxStack - stackQuantity);
                                        slotQuantity -= adjust;
                                        stacks[j] = stack.withQuantity(stackQuantity + adjust);
                                    }
                                }
                            }
                        }
                        if (slotQuantity > 0) {
                            stackOffset++;
                            final Object o;
                            final int n;
                            o[n] = ((slotQuantity != slot.getQuantity()) ? slot.withQuantity(slotQuantity) : slot);
                        }
                    }
                }
            }
            Arrays.sort(stacks, sort.getComparator());
            final ObjectArrayList<SlotTransaction> transactions = new ObjectArrayList<SlotTransaction>(stacks.length);
            int stackOffset2 = 0;
            for (short k = 0; k < stacks.length; ++k) {
                if (!this.cantRemoveFromSlot(k)) {
                    final ItemStack existing = this.internal_getSlot(k);
                    final ItemStack replacement = stacks[stackOffset2];
                    if (!this.cantAddToSlot(k, replacement, existing)) {
                        ++stackOffset2;
                        if (existing != replacement) {
                            this.internal_setSlot(k, replacement);
                            transactions.add(new SlotTransaction(true, ActionType.REMOVE, k, existing, null, replacement, false, false, true));
                        }
                    }
                }
            }
            int l = stackOffset2;
            while (l < stacks.length) {
                if (stacks[l] != null) {
                    throw new IllegalStateException("Had leftover stacks that didn't get sorted!");
                }
                else {
                    ++l;
                }
            }
            return new ListTransaction<SlotTransaction>(true, (List<Transaction>)transactions);
        });
    }
    
    protected void sendUpdate(@Nonnull final Transaction transaction) {
        if (!transaction.succeeded()) {
            return;
        }
        final ItemContainerChangeEvent event = new ItemContainerChangeEvent(this, transaction);
        this.externalChangeEventRegistry.dispatchFor(null).dispatch(event);
        this.internalChangeEventRegistry.dispatchFor(null).dispatch(event);
    }
    
    public boolean containsContainer(final ItemContainer itemContainer) {
        return itemContainer == this;
    }
    
    public void doMigration(final Function<String, String> blockMigration) {
        Objects.requireNonNull(blockMigration);
        this.writeAction(_blockMigration -> {
            for (short i = 0; i < this.getCapacity(); ++i) {
                final ItemStack slot = this.internal_getSlot(i);
                if (!ItemStack.isEmpty(slot)) {
                    final String oldItemId = slot.getItemId();
                    final String newItemId = _blockMigration.apply(slot.getItemId());
                    if (!oldItemId.equals(newItemId)) {
                        this.internal_setSlot(i, new ItemStack(newItemId, slot.getQuantity(), slot.getMetadata()));
                    }
                }
            }
            return null;
        }, blockMigration);
    }
    
    @Nullable
    public static ItemResourceType getMatchingResourceType(@Nonnull final Item item, @Nonnull final String resourceId) {
        final ItemResourceType[] resourceTypes = item.getResourceTypes();
        if (resourceTypes == null) {
            return null;
        }
        for (final ItemResourceType resourceType : resourceTypes) {
            if (resourceId.equals(resourceType.id)) {
                return resourceType;
            }
        }
        return null;
    }
    
    public static void validateQuantity(final int quantity) {
        if (quantity < 0) {
            throw new IllegalArgumentException("Quantity is less than zero! " + quantity + " < 0");
        }
    }
    
    public static void validateSlotIndex(final short slot, final int capacity) {
        if (slot < 0) {
            throw new IllegalArgumentException("Slot is less than zero! " + slot + " < 0");
        }
        if (slot >= capacity) {
            throw new IllegalArgumentException("Slot is outside capacity! " + slot + " >= " + capacity);
        }
    }
    
    @Nonnull
    public static <T extends ItemContainer> T copy(@Nonnull final ItemContainer from, @Nonnull final T to, @Nullable final List<ItemStack> remainder) {
        from.forEach((slot, itemStack) -> {
            if (slot >= to.getCapacity()) {
                if (remainder != null) {
                    remainder.add(itemStack);
                }
                return;
            }
            else if (ItemStack.isEmpty(itemStack)) {
                return;
            }
            else {
                to.setItemStackForSlot(slot, itemStack);
                return;
            }
        });
        return to;
    }
    
    public static <T extends ItemContainer> T ensureContainerCapacity(@Nullable final T inputContainer, final short capacity, @Nonnull final Short2ObjectConcurrentHashMap.ShortFunction<T> newContainerSupplier, final List<ItemStack> remainder) {
        if (inputContainer == null) {
            return newContainerSupplier.apply(capacity);
        }
        if (inputContainer.getCapacity() == capacity) {
            return inputContainer;
        }
        return copy(inputContainer, newContainerSupplier.apply(capacity), remainder);
    }
    
    public static ItemContainer getNewContainer(final short capacity, @Nonnull final Short2ObjectConcurrentHashMap.ShortFunction<ItemContainer> supplier) {
        return (capacity > 0) ? supplier.apply(capacity) : EmptyItemContainer.INSTANCE;
    }
    
    static {
        CODEC = new CodecMapCodec<ItemContainer>(true);
        LOGGER = HytaleLogger.forEnclosingClass();
    }
    
    record TempItemData(ItemStack itemStack, Item item) {}
    
    record ItemContainerChangeEvent(ItemContainer container, Transaction transaction) implements IEvent<Void> {
        @Nonnull
        @Override
        public String toString() {
            return "ItemContainerChangeEvent{container=" + String.valueOf(this.container) + ", transaction=" + String.valueOf(this.transaction);
        }
    }
}
