public class ColoredPipe extends Pipe {

    public ColoredPipe(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager, ItemDistributorService itemDistributor) {
        super(ductType, blockLoc, world, chunk, settingsInv, globalDuctManager, itemDistributor);
    }

    @Override
    public Material getBreakParticleData() {
        return ((ColoredPipeType) getDuctType()).getColoringMaterial();
    }

} public class CraftingPipe extends Pipe {

    private ItemData[] recipeItems;
    private Recipe recipe;
    private TPDirection outputDir;
    private List<ItemStack> cachedItems;

    public CraftingPipe(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager, ItemDistributorService itemDistributor) {
        super(ductType, blockLoc, world, chunk, settingsInv, globalDuctManager, itemDistributor);
        recipeItems = new ItemData[9];
        outputDir = null;
        cachedItems = new ArrayList<>();
    }

    @Override
    public void tick(boolean bigTick, TransportPipes transportPipes, DuctManager ductManager) {
        super.tick(bigTick, transportPipes, ductManager);
        if (bigTick) {
            performCrafting((PipeManager) ductManager, transportPipes);
        }
    }

    @Override
    protected Map<TPDirection, Integer> calculateItemDistribution(PipeItem pipeItem, TPDirection movingDir, List<TPDirection> dirs, TransportPipes transportPipes) {
        ItemStack overflow = addCachedItem(pipeItem.getItem(), transportPipes);
        if (overflow != null) {
            transportPipes.runTaskSync(() -> getWorld().dropItem(getBlockLoc().toLocation(getWorld()), overflow));
        }
        return null;
    }

    public void performCrafting(PipeManager pipeManager, TransportPipes transportPipes) {
        if (outputDir != null && recipe != null) {
            ItemStack resultItem = recipe.getResult();

            List<RecipeChoice> ingredients = new ArrayList<>();
            if (recipe instanceof ShapelessRecipe) {
                ingredients.addAll(((ShapelessRecipe) recipe).getChoiceList());
            } else if (recipe instanceof ShapedRecipe) {
                Map<Character, Integer> charCounts = new HashMap<>();
                for (String row : ((ShapedRecipe) recipe).getShape()) {
                    for (char c : row.toCharArray()) {
                        charCounts.put(c, charCounts.getOrDefault(c, 0) + 1);
                    }
                }
                for (Character c : charCounts.keySet()) {
                    RecipeChoice ingredientChoice = ((ShapedRecipe) recipe).getChoiceMap().get(c);
                    if (ingredientChoice != null) {
                        ingredients.addAll(Collections.nCopies(charCounts.get(c), ingredientChoice));
                    }
                }
            }

            List<ItemStack> cachedItems = new ArrayList<>();
            for (ItemStack cachedItem : this.cachedItems) {
                cachedItems.add(cachedItem.clone());
            }

            //iterate needed ingredients
            Iterator<RecipeChoice> neededIngredientsIt = ingredients.iterator();
            while (neededIngredientsIt.hasNext()) {
                RecipeChoice neededIngredient = neededIngredientsIt.next();

                //iterate cached items
                for (int i = 0; i < cachedItems.size(); i++) {
                    if (neededIngredient.test(cachedItems.get(i))) {
                        if (cachedItems.get(i).getAmount() > 1) {
                            cachedItems.get(i).setAmount(cachedItems.get(i).getAmount() - 1);
                        } else {
                            cachedItems.remove(i);
                        }
                        neededIngredientsIt.remove();
                        break;
                    }
                }

            }

            if (ingredients.isEmpty()) {
                // update real cachedItems list
                this.cachedItems.clear();
                this.cachedItems.addAll(cachedItems);

                transportPipes.runTaskSync(() -> {
                    settingsInv.save(null);
                    settingsInv.populate();
                });

                // output result item
                PipeItem pipeItem = new PipeItem(resultItem.clone(), getWorld(), getBlockLoc(), outputDir);
                pipeItem.getRelativeLocation().set(0.5d, 0.5d, 0.5d);
                pipeItem.resetOldRelativeLocation();
                pipeManager.spawnPipeItem(pipeItem);
                pipeManager.putPipeItemInPipe(pipeItem);

            }
        }
    }

    public int spaceForItem(ItemData data) {
        int space = 0;

        for (int i = 0; i < 9; i++) {
            if (i >= cachedItems.size()) {
                space += data.toItemStack().getMaxStackSize();
            } else {
                ItemStack item = cachedItems.get(i);
                if (item.isSimilar(data.toItemStack()) && item.getAmount() < item.getMaxStackSize()) {
                    space += item.getMaxStackSize() - item.getAmount();
                }
            }
        }

        return space;
    }

    public ItemData[] getRecipeItems() {
        return recipeItems;
    }

    public TPDirection getOutputDir() {
        return outputDir;
    }

    public void setOutputDir(TPDirection outputDir) {
        this.outputDir = outputDir;
    }

    public Recipe getRecipe() {
        return recipe;
    }

    public void setRecipe(Recipe recipe) {
        this.recipe = recipe;
    }

    public List<ItemStack> getCachedItems() {
        return cachedItems;
    }

    public ItemStack addCachedItem(ItemStack item, TransportPipes transportPipes) {
        for (ItemStack cachedItem : cachedItems) {
            if (cachedItem.isSimilar(item)) {
                int cachedItemAmount = cachedItem.getAmount();
                cachedItem.setAmount(Math.min(cachedItem.getMaxStackSize(), cachedItemAmount + item.getAmount()));
                int overflow = cachedItemAmount + item.getAmount() - cachedItem.getMaxStackSize();
                if (overflow > 0) {
                    item.setAmount(overflow);
                } else {
                    item = null;
                    break;
                }
            }
        }
        if (cachedItems.size() < 9 && item != null) {
            cachedItems.add(item);
            item = null;
        }

        transportPipes.runTaskSync(() -> {
            settingsInv.save(null);
            settingsInv.populate();
        });
        return item;
    }

    public void updateOutputDirection(boolean cycle) {
        TPDirection oldOutputDirection = getOutputDir();
        Set<TPDirection> connections = getAllConnections();
        if (connections.isEmpty()) {
            outputDir = null;
        } else if (cycle || outputDir == null || !connections.contains(outputDir)) {
            do {
                if (outputDir == null) {
                    outputDir = TPDirection.NORTH;
                } else {
                    outputDir = outputDir.next();
                }
            } while (!connections.contains(outputDir));
        }
        if (oldOutputDirection != outputDir) {
            settingsInv.populate();
        }
    }

    @Override
    public void notifyConnectionChange() {
        super.notifyConnectionChange();
        updateOutputDirection(false);
    }

    @Override
    public Material getBreakParticleData() {
        return Material.CRAFTING_TABLE;
    }

    @Override
    public void saveToNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.saveToNBTTag(compoundTag, itemService);

        compoundTag.putInt("outputDir", outputDir != null ? outputDir.ordinal() : -1);

        ListTag<StringTag> recipeItemsListTag = new ListTag<>(StringTag.class);
        for (int i = 0; i < 9; i++) {
            ItemData itemData = recipeItems[i];
            if (itemData == null) {
                recipeItemsListTag.add(new StringTag(null));
            } else {
                recipeItemsListTag.addString(itemService.serializeItemStack(itemData.toItemStack()));
            }
        }
        compoundTag.put("recipeItems", recipeItemsListTag);

        ListTag<StringTag> cachedItemsListTag = new ListTag<>(StringTag.class);
        for (int i = 0; i < cachedItems.size(); i++) {
            ItemStack itemStack = cachedItems.get(i);
            cachedItemsListTag.addString(itemService.serializeItemStack(itemStack));
        }
        compoundTag.put("cachedItems", cachedItemsListTag);

    }

    @Override
    public void loadFromNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.loadFromNBTTag(compoundTag, itemService);

        outputDir = compoundTag.getInt("outputDir") != -1 ? TPDirection.values()[compoundTag.getInt("outputDir")] : null;

        ListTag<StringTag> recipeItemsListTag = (ListTag<StringTag>) compoundTag.getListTag("recipeItems");
        for (int i = 0; i < 9; i++) {
            if (i >= recipeItemsListTag.size()) {
                recipeItems[i] = null;
                continue;
            }
            ItemStack deserialized = itemService.deserializeItemStack(recipeItemsListTag.get(i).getValue());
            recipeItems[i] = deserialized != null ? new ItemData(deserialized) : null;
        }

        cachedItems.clear();
        ListTag<StringTag> cachedItemsListTag = (ListTag<StringTag>) compoundTag.getListTag("cachedItems");
        for (int i = 0; i < cachedItemsListTag.size(); i++) {
            ItemStack deserialized = itemService.deserializeItemStack(cachedItemsListTag.get(i).getValue());
            if (deserialized != null)
                cachedItems.add(deserialized);
        }

        settingsInv.populate();
        settingsInv.save(null);
    }

    @Override
    public List<ItemStack> destroyed(TransportPipes transportPipes, DuctManager ductManager, Player destroyer) {
        List<ItemStack> items = super.destroyed(transportPipes, ductManager, destroyer);
        for (int i = 0; i < 9; i++) {
            ItemData id = recipeItems[i];
            if (id != null) {
                items.add(id.toItemStack().clone());
            }
        }
        for (ItemStack cachedItem : cachedItems) {
            items.add(cachedItem.clone());
        }
        return items;
    }
} public class ExtractionPipe extends Pipe {

    private TPDirection extractDirection;
    private ExtractCondition extractCondition;
    private ExtractAmount extractAmount;
    private ItemFilter itemFilter;

    public ExtractionPipe(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager, ItemDistributorService itemDistributor) {
        super(ductType, blockLoc, world, chunk, settingsInv, globalDuctManager, itemDistributor);
        this.extractDirection = null;
        this.extractCondition = ExtractCondition.NEEDS_REDSTONE;
        this.extractAmount = ExtractAmount.EXTRACT_1;
        this.itemFilter = new ItemFilter();
    }

    @Override
    public void syncBigTick(DuctManager ductManager) {
        super.syncBigTick(ductManager);

        PipeManager pipeManager = (PipeManager) ductManager;

        if (extractDirection == null || extractCondition == ExtractCondition.NEVER_EXTRACT) {
            return;
        }

        //extract item
        TransportPipesContainer container = pipeManager.getContainerAtLoc(getWorld(), getBlockLoc().getNeighbor(extractDirection));
        if (container != null) {
            if (extractCondition == ExtractCondition.NEEDS_REDSTONE) {
                Block block = getBlockLoc().toBlock(getWorld());
                if (!block.isBlockIndirectlyPowered() && !block.isBlockPowered()) {
                    return;
                }
            }
            ItemStack item = container.extractItem(extractDirection, extractAmount.getAmount(), itemFilter);
            if (item != null) {
                PipeItem pipeItem = new PipeItem(item, getWorld(), getBlockLoc(), extractDirection.getOpposite());
                pipeManager.spawnPipeItem(pipeItem);
                pipeManager.putPipeItemInPipe(pipeItem);
            }
        }

    }

    public void updateExtractDirection(boolean cycle) {
        TPDirection oldExtractDirection = getExtractDirection();
        Map<TPDirection, TransportPipesContainer> containerConnections = getContainerConnections();
        if (containerConnections.isEmpty()) {
            extractDirection = null;
        } else if (cycle || extractDirection == null || !containerConnections.containsKey(extractDirection)) {
            do {
                if (extractDirection == null) {
                    extractDirection = TPDirection.NORTH;
                } else {
                    extractDirection = extractDirection.next();
                }
            } while (!containerConnections.containsKey(extractDirection));
        }
        if (oldExtractDirection != getExtractDirection()) {
            globalDuctManager.updateDuctInRenderSystems(this, true);
            settingsInv.populate();
        }
    }

    public TPDirection getExtractDirection() {
        return extractDirection;
    }

    public void setExtractDirection(TPDirection extractDirection) {
        this.extractDirection = extractDirection;
    }

    public ExtractCondition getExtractCondition() {
        return extractCondition;
    }

    public void setExtractCondition(ExtractCondition extractCondition) {
        this.extractCondition = extractCondition;
    }

    public ExtractAmount getExtractAmount() {
        return extractAmount;
    }

    public void setExtractAmount(ExtractAmount extractAmount) {
        this.extractAmount = extractAmount;
    }

    public ItemFilter getItemFilter() {
        return itemFilter;
    }

    public void setItemFilter(ItemFilter itemFilter) {
        this.itemFilter = itemFilter;
    }

    @Override
    public Material getBreakParticleData() {
        return Material.OAK_PLANKS;
    }

    @Override
    public List<ItemStack> destroyed(TransportPipes transportPipes, DuctManager ductManager, Player destroyer) {
        List<ItemStack> drop = super.destroyed(transportPipes, ductManager, destroyer);
        drop.addAll(itemFilter.getAsItemStacks());
        return drop;
    }

    @Override
    public void notifyConnectionChange() {
        super.notifyConnectionChange();
        updateExtractDirection(false);
    }

    @Override
    public void saveToNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.saveToNBTTag(compoundTag, itemService);

        compoundTag.putInt("extractDir", extractDirection != null ? extractDirection.ordinal() : -1);
        compoundTag.putInt("extractCondition", extractCondition.ordinal());
        compoundTag.putInt("extractAmount", extractAmount.ordinal());
        CompoundTag itemFilterTag = new CompoundTag();
        itemFilter.saveToNBTTag(itemFilterTag, itemService);
        compoundTag.put("itemFilter", itemFilterTag);

    }

    @Override
    public void loadFromNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.loadFromNBTTag(compoundTag, itemService);

        extractDirection = compoundTag.getInt("extractDir") != -1 ? TPDirection.values()[compoundTag.getInt("extractDir")] : null;
        extractCondition = ExtractCondition.values()[compoundTag.getInt("extractCondition")];
        extractAmount = ExtractAmount.values()[compoundTag.getInt("extractAmount")];
        itemFilter = new ItemFilter();
        itemFilter.loadFromNBTTag(compoundTag.getCompoundTag("itemFilter"), itemService);

        settingsInv.populate();
    }
} public class GoldenPipe extends Pipe {

    private ItemFilter[] itemFilters;

    public GoldenPipe(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager, ItemDistributorService itemDistributor) {
        super(ductType, blockLoc, world, chunk, settingsInv, globalDuctManager, itemDistributor);
        itemFilters = new ItemFilter[Color.values().length];
        for (int i = 0; i < Color.values().length; i++) {
            itemFilters[i] = new ItemFilter();
        }
    }

    public ItemFilter getItemFilter(Color gpc) {
        return itemFilters[gpc.ordinal()];
    }

    public void setItemFilter(Color gpc, ItemFilter itemFilter) {
        itemFilters[gpc.ordinal()] = itemFilter;
    }

    @Override
    protected Map<TPDirection, Integer> calculateItemDistribution(PipeItem pipeItem, TPDirection movingDir, List<TPDirection> dirs, TransportPipes transportPipes) {
        Map<TPDirection, Integer> absWeights = new HashMap<>();
        for (TPDirection dir : dirs) {
            int amount = getItemFilter(Color.getByDir(dir)).applyFilter(pipeItem.getItem());
            absWeights.put(dir, amount);
        }
        return itemDistributor.splitPipeItem(pipeItem.getItem(), absWeights, this);
    }

    @Override
    public Material getBreakParticleData() {
        return Material.GOLD_BLOCK;
    }

    @Override
    public List<ItemStack> destroyed(TransportPipes transportPipes, DuctManager ductManager, Player destroyer) {
        List<ItemStack> drop = super.destroyed(transportPipes, ductManager, destroyer);
        for (Color gpc : Color.values()) {
            drop.addAll(getItemFilter(gpc).getAsItemStacks());
        }
        return drop;
    }

    @Override
    public void saveToNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.saveToNBTTag(compoundTag, itemService);

        ListTag<CompoundTag> itemFiltersTag = new ListTag<>(CompoundTag.class);
        for (Color color : Color.values()) {
            CompoundTag filterCompoundTag = new CompoundTag();
            ItemFilter itemFilter = getItemFilter(color);
            itemFilter.saveToNBTTag(filterCompoundTag, itemService);
            itemFiltersTag.add(filterCompoundTag);
        }
        compoundTag.put("itemFilters", itemFiltersTag);

    }

    @Override
    public void loadFromNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.loadFromNBTTag(compoundTag, itemService);

        ListTag<CompoundTag> itemFiltersTag = (ListTag<CompoundTag>) compoundTag.getListTag("itemFilters");
        for (Color color : Color.values()) {
            ItemFilter itemFilter = new ItemFilter();
            itemFilter.loadFromNBTTag(itemFiltersTag.get(color.ordinal()), itemService);
            itemFilters[color.ordinal()] = itemFilter;
        }

        settingsInv.populate();
    }

    public enum Color {

        BLUE(Material.BLUE_WOOL, Material.BLUE_STAINED_GLASS_PANE, LangConf.Key.COLORS_BLUE.get(), TPDirection.EAST),
        YELLOW(Material.YELLOW_WOOL, Material.YELLOW_STAINED_GLASS_PANE, LangConf.Key.COLORS_YELLOW.get(), TPDirection.WEST),
        RED(Material.RED_WOOL, Material.RED_STAINED_GLASS_PANE, LangConf.Key.COLORS_RED.get(), TPDirection.SOUTH),
        WHITE(Material.WHITE_WOOL, Material.WHITE_STAINED_GLASS_PANE, LangConf.Key.COLORS_WHITE.get(), TPDirection.NORTH),
        GREEN(Material.LIME_WOOL, Material.LIME_STAINED_GLASS_PANE, LangConf.Key.COLORS_GREEN.get(), TPDirection.UP),
        BLACK(Material.BLACK_WOOL, Material.BLACK_STAINED_GLASS_PANE, LangConf.Key.COLORS_BLACK.get(), TPDirection.DOWN);

        private Material woolMaterial;
        private Material glassPaneMaterial;
        private String displayName;
        private TPDirection direction;

        Color(Material woolMaterial, Material glassPaneMaterial, String displayName, TPDirection direction) {
            this.woolMaterial = woolMaterial;
            this.glassPaneMaterial = glassPaneMaterial;
            this.displayName = displayName;
            this.direction = direction;
        }

        public Material getWoolMaterial() {
            return woolMaterial;
        }

        public Material getGlassPaneMaterial() {
            return glassPaneMaterial;
        }

        public String getDisplayName() {
            return displayName;
        }

        public TPDirection getDirection() {
            return direction;
        }

        public static Color getByDir(TPDirection dir) {
            for (Color c : values()) {
                if (c.direction.equals(dir)) {
                    return c;
                }
            }
            return null;
        }
    }

} public class IcePipe extends Pipe {

    public IcePipe(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager, ItemDistributorService itemDistributor) {
        super(ductType, blockLoc, world, chunk, settingsInv, globalDuctManager, itemDistributor);
    }

    @Override
    double getPipeItemSpeed() {
        return super.getPipeItemSpeed() * 4;
    }

    @Override
    public Material getBreakParticleData() {
        return Material.ICE;
    }

} public class IronPipe extends Pipe {

    private TPDirection currentOutputDirection;

    public IronPipe(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager, ItemDistributorService itemDistributor) {
        super(ductType, blockLoc, world, chunk, settingsInv, globalDuctManager, itemDistributor);
        currentOutputDirection = TPDirection.UP;
    }

    @Override
    public void notifyClick(Player p, boolean shift) {
        super.notifyClick(p, shift);
        cycleOutputDirection();
        p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1f, 1f);
    }

    public TPDirection getCurrentOutputDirection() {
        return currentOutputDirection;
    }

    public void setCurrentOutputDirection(TPDirection currentOutputDirection) {
        this.currentOutputDirection = currentOutputDirection;
    }

    @Override
    protected Map<TPDirection, Integer> calculateItemDistribution(PipeItem pipeItem, TPDirection movingDir, List<TPDirection> dirs, TransportPipes transportPipes) {
        Map<TPDirection, Integer> absWeights = new HashMap<>();
        if (dirs.contains(getCurrentOutputDirection())) {
            absWeights.put(getCurrentOutputDirection(), 1);
        }
        return itemDistributor.splitPipeItem(pipeItem.getItem(), absWeights, this);
    }

    private void cycleOutputDirection() {
        Set<TPDirection> allConns = getAllConnections();
        if (allConns.isEmpty()) {
            return;
        }

        TPDirection oldOutputDirection = currentOutputDirection;
        int dirId;
        do {
            dirId = currentOutputDirection.ordinal();
            dirId++;
            dirId %= TPDirection.values().length;
            currentOutputDirection = TPDirection.values()[dirId];
        } while (!allConns.contains(currentOutputDirection));

        if (oldOutputDirection != currentOutputDirection) {
            globalDuctManager.updateDuctInRenderSystems(this, true);
        }

    }

    @Override
    public Material getBreakParticleData() {
        return Material.IRON_BLOCK;
    }

    @Override
    public void notifyConnectionChange() {
        super.notifyConnectionChange();
        Set<TPDirection> allConns = getAllConnections();
        if (!allConns.isEmpty() && !allConns.contains(currentOutputDirection)) {
            cycleOutputDirection();
        }
    }

    @Override
    public void saveToNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.saveToNBTTag(compoundTag, itemService);

        compoundTag.putInt("outputDir", currentOutputDirection.ordinal());

    }

    @Override
    public void loadFromNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.loadFromNBTTag(compoundTag, itemService);

        currentOutputDirection = TPDirection.values()[compoundTag.getInt("outputDir")];

    }
} public class Pipe extends Duct {

    /**
     * THREAD-SAFE
     * contains all the items that are inside this pipe and should be updated
     */
    private final List<PipeItem> items;
    /**
     * THREAD-SAFE
     * contains all the items that are just put inside this pipe and should be updated and put into the items list the next tick
     */
    private final List<PipeItem> futureItems;

    /**
     * THREAD-SAFE
     * contains all the items that could not be put into the next pipe or container because it is inside an unloaded chunk.
     * As the next pipe / container gets loaded again, these items get put into it one by one.<p /><p />
     * This means that all of the pipeItems inside this list have got a blockLocation which differs from this pipe's blockLocation.
     * The blockLocation of one of these pipeItems may be pointing on a container block or on a different pipe.
     */
    private final List<PipeItem> unloadedItems;

    ItemDistributorService itemDistributor;
    private Map<TPDirection, TransportPipesContainer> connectedContainers;

    public Pipe(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager, ItemDistributorService itemDistributor) {
        super(ductType, blockLoc, world, chunk, settingsInv, globalDuctManager);
        this.items = Collections.synchronizedList(new ArrayList<>());
        this.futureItems = Collections.synchronizedList(new ArrayList<>());
        this.unloadedItems = Collections.synchronizedList(new ArrayList<>());
        this.itemDistributor = itemDistributor;

        this.connectedContainers = Collections.synchronizedMap(new HashMap<>());
    }

    public Map<TPDirection, TransportPipesContainer> getContainerConnections() {
        return connectedContainers;
    }

    @Override
    public Set<TPDirection> getAllConnections() {
        Set<TPDirection> allConnections = super.getAllConnections();
        allConnections.addAll(getContainerConnections().keySet());
        return allConnections;
    }

    public List<PipeItem> getItems() {
        return items;
    }

    public List<PipeItem> getFutureItems() {
        return futureItems;
    }

    public List<PipeItem> getUnloadedItems() {
        return unloadedItems;
    }

    public void putPipeItem(PipeItem pipeItem) {
        futureItems.add(pipeItem);
    }

    double getPipeItemSpeed() {
        return 0.125d;
    }

    @Override
    public void tick(boolean bigTick, TransportPipes transportPipes, DuctManager ductManager) {
        super.tick(bigTick, transportPipes, ductManager);

        // activate futureItems
        synchronized (getFutureItems()) {
            Iterator<PipeItem> futureItemsIt = getFutureItems().iterator();
            while (futureItemsIt.hasNext()) {
                PipeItem futureItem = futureItemsIt.next();
                getItems().add(futureItem);
                futureItemsIt.remove();
            }
        }

        // extract items from unloaded list and put into next pipe
        if (bigTick) {
            synchronized (getUnloadedItems()) {
                if (!getUnloadedItems().isEmpty()) {
                    PipeItem unloadedItem = getUnloadedItems().get(getUnloadedItems().size() - 1);
                    Duct newPipe = globalDuctManager.getDuctAtLoc(getWorld(), unloadedItem.getBlockLoc());
                    if (newPipe instanceof Pipe && newPipe.isInLoadedChunk()) {
                        ((Pipe) newPipe).getItems().add(unloadedItem);
                        getUnloadedItems().remove(unloadedItem);
                    }
                }
            }
        }

    }

    @Override
    public void postTick(boolean bigTick, TransportPipes transportPipes, DuctManager ductManager, GeneralConf generalConf) {
        super.postTick(bigTick, transportPipes, ductManager, generalConf);

        PipeManager pipeManager = (PipeManager) ductManager;
        if (items.size() > generalConf.getMaxItemsPerPipe()) {
            transportPipes.runTaskAsync(() -> {
                globalDuctManager.unregisterDuct(this);
                globalDuctManager.unregisterDuctInRenderSystem(this, true);
                globalDuctManager.updateNeighborDuctsConnections(this);
                globalDuctManager.updateNeighborDuctsInRenderSystems(this, true);
                globalDuctManager.playDuctDestroyActions(this, null);
            }, 0L);
            return;
        }

        List<PipeItem> copiedItems;
        synchronized (items) {
            copiedItems = new ArrayList<>(items);
        }

        for (int i = copiedItems.size() - 1; i >= 0; i--) {
            PipeItem pipeItem = copiedItems.get(i);

            long factor = (long) (getPipeItemSpeed() * RelativeLocation.PRECISION);
            pipeItem.getRelativeLocation().add(pipeItem.getMovingDir().getX() * factor, pipeItem.getMovingDir().getY() * factor, pipeItem.getMovingDir().getZ() * factor);
            pipeManager.updatePipeItemPosition(pipeItem);
            pipeItem.resetOldRelativeLocation();

            if (pipeItem.getRelativeLocation().isEquals(0.5d, 0.5d, 0.5d)) {

                //arrival at middle

                //calculate possible moving directions
                List<TPDirection> possibleMovingDirs = new ArrayList<>(getAllConnections());

                Map<TPDirection, Integer> distribution = calculateItemDistribution(pipeItem, pipeItem.getMovingDir(), possibleMovingDirs, transportPipes);

                if (distribution == null || distribution.isEmpty()) {
                    items.remove(pipeItem);
                    pipeManager.despawnPipeItem(pipeItem);
                    if (distribution != null) {
                        //drop item
                        transportPipes.runTaskSync(() -> {
                            pipeItem.getWorld().dropItem(pipeItem.getBlockLoc().toLocation(pipeItem.getWorld()), pipeItem.getItem());
                        });
                    }
                    continue;
                }

                ItemStack itemStack = pipeItem.getItem().clone();

                PipeItem tempPipeItem = null;
                for (TPDirection dir : distribution.keySet()) {
                    int amount = distribution.get(dir);
                    if (tempPipeItem == null) {
                        tempPipeItem = pipeItem;
                    } else {
                        tempPipeItem = new PipeItem(itemStack.clone(), getWorld(), getBlockLoc(), dir);
                    }
                    tempPipeItem.getItem().setAmount(amount);
                    tempPipeItem.setMovingDir(dir);
                    tempPipeItem.getRelativeLocation().set(0.5d, 0.5d, 0.5d);
                    tempPipeItem.resetOldRelativeLocation();
                    if (!items.contains(tempPipeItem)) {
                        items.add(tempPipeItem);
                        pipeManager.spawnPipeItem(tempPipeItem);
                    }
                }
            } else if (pipeItem.getRelativeLocation().isXEquals(0d) || pipeItem.getRelativeLocation().isYEquals(0d) || pipeItem.getRelativeLocation().isZEquals(0d)
                    || pipeItem.getRelativeLocation().isXEquals(1d) || pipeItem.getRelativeLocation().isYEquals(1d) || pipeItem.getRelativeLocation().isZEquals(1d)) {
                //arrival at end of pipe

                Duct duct = getDuctConnections().get(pipeItem.getMovingDir());
                TransportPipesContainer transportPipesContainer = getContainerConnections().get(pipeItem.getMovingDir());

                if (duct instanceof Pipe) {

                    Pipe pipe = (Pipe) duct;

                    //make pipe item ready for next pipe
                    pipeItem.setBlockLoc(pipe.getBlockLoc());
                    pipeItem.getRelativeLocation().switchValues();
                    pipeItem.resetOldRelativeLocation();

                    //remove from current pipe and add to new one
                    items.remove(pipeItem);
                    if (pipe.isInLoadedChunk()) {
                        pipe.putPipeItem(pipeItem);
                    } else {
                        unloadedItems.add(pipeItem);
                    }
                } else {
                    items.remove(pipeItem);
                    pipeManager.despawnPipeItem(pipeItem);

                    if (transportPipesContainer != null) {

                        pipeItem.setBlockLoc(getBlockLoc().getNeighbor(pipeItem.getMovingDir()));
                        pipeItem.getRelativeLocation().switchValues();
                        pipeItem.resetOldRelativeLocation();

                        transportPipes.runTaskSync(() -> {
                            if (transportPipesContainer.isInLoadedChunk()) {

                                ItemStack overflow = transportPipesContainer.insertItem(pipeItem.getMovingDir(), pipeItem.getItem());
                                if (overflow != null) {
                                    getWorld().dropItem(getBlockLoc().toLocation(getWorld()), overflow);
                                }
                            } else {
                                unloadedItems.add(pipeItem);
                            }
                        });
                    } else {
                        //drop item
                        transportPipes.runTaskSync(() -> {
                            pipeItem.getWorld().dropItem(pipeItem.getBlockLoc().getNeighbor(pipeItem.getMovingDir()).toLocation(pipeItem.getWorld()), pipeItem.getItem());
                        });
                    }
                }
            }

        }

    }

    @Override
    public void syncBigTick(DuctManager ductManager) {
        super.syncBigTick(ductManager);

        PipeManager pipeManager = (PipeManager) ductManager;

        // put one of the unloaded items into the container block it belongs to or drop it if there is no longer a container
        synchronized (getUnloadedItems()) {
            if (!getUnloadedItems().isEmpty()) {
                PipeItem unloadedItem = getUnloadedItems().get(getUnloadedItems().size() - 1);
                TransportPipesContainer newContainer = pipeManager.getContainerAtLoc(getWorld(), unloadedItem.getBlockLoc());
                if (newContainer != null && newContainer.isInLoadedChunk()) {
                    ItemStack overflow = newContainer.insertItem(unloadedItem.getMovingDir(), unloadedItem.getItem());
                    getUnloadedItems().remove(unloadedItem);
                    if (overflow != null) {
                        getWorld().dropItem(getBlockLoc().toLocation(getWorld()), overflow);
                    }
                } else if (newContainer == null && !(globalDuctManager.getDuctAtLoc(getWorld(), unloadedItem.getBlockLoc()) instanceof Pipe)) {
                    //nothing there
                    getWorld().dropItem(getBlockLoc().toLocation(getWorld()), unloadedItem.getItem());
                    getUnloadedItems().remove(unloadedItem);
                }
            }
        }

    }

    /**
     * can be overridden to calculate how a pipeItem which arrives at the middle of the pipe should be split and in which directions theses parts should go.
     * Return null to fully remove the pipeItem and return an empty map to fully remove the item and additionally drop it in the world.
     */
    protected Map<TPDirection, Integer> calculateItemDistribution(PipeItem pipeItem, TPDirection movingDir, List<TPDirection> dirs, TransportPipes transportPipes) {
        Map<TPDirection, Integer> absWeights = new HashMap<>();
        dirs.stream().filter(dir -> !dir.equals(movingDir.getOpposite())).forEach(dir -> absWeights.put(dir, 1));
        return itemDistributor.splitPipeItem(pipeItem.getItem(), absWeights, this);
    }

    @Override
    public List<ItemStack> destroyed(TransportPipes transportPipes, DuctManager ductManager, Player destroyer) {
        List<ItemStack> dropItems = super.destroyed(transportPipes, ductManager, destroyer);

        synchronized (items) {
            items.forEach(pipeItem -> {
                ((PipeManager) ductManager).despawnPipeItem(pipeItem);
                dropItems.add(pipeItem.getItem());
            });
            items.clear();
        }
        synchronized (futureItems) {
            futureItems.forEach(pipeItem -> {
                ((PipeManager) ductManager).despawnPipeItem(pipeItem);
                dropItems.add(pipeItem.getItem());
            });
            futureItems.clear();
        }
        synchronized (unloadedItems) {
            unloadedItems.forEach(pipeItem -> {
                ((PipeManager) ductManager).despawnPipeItem(pipeItem);
                dropItems.add(pipeItem.getItem());
            });
            unloadedItems.clear();
        }

        return dropItems;
    }

    @Override
    public PipeType getDuctType() {
        return (PipeType) super.getDuctType();
    }

    @Override
    public void saveToNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.saveToNBTTag(compoundTag, itemService);

        List<PipeItem> accumulatedItems = new ArrayList<>();
        accumulatedItems.addAll(getItems());
        accumulatedItems.addAll(getFutureItems());
        List<PipeItem> unloadedItems = new ArrayList<>(getUnloadedItems());

        ListTag<CompoundTag> accumulatedItemsListTag = new ListTag<>(CompoundTag.class);
        ListTag<CompoundTag> unloadedItemsListTag = new ListTag<>(CompoundTag.class);

        for (PipeItem accumulatedItem : accumulatedItems) {
            CompoundTag itemTag = new CompoundTag();
            accumulatedItem.saveToNBTTag(itemTag, itemService);
            accumulatedItemsListTag.add(itemTag);
        }

        for (PipeItem unloadedItem : unloadedItems) {
            CompoundTag itemTag = new CompoundTag();
            unloadedItem.saveToNBTTag(itemTag, itemService);
            unloadedItemsListTag.add(itemTag);
        }

        compoundTag.put("pipeItems", accumulatedItemsListTag);
        compoundTag.put("unloadedPipeItems", unloadedItemsListTag);
    }

    @Override
    public void loadFromNBTTag(CompoundTag compoundTag, ItemService itemService) {
        super.loadFromNBTTag(compoundTag, itemService);

        ListTag<CompoundTag> accumulatedItemsListTag = (ListTag<CompoundTag>) compoundTag.getListTag("pipeItems");
        ListTag<CompoundTag> unloadedItemsListTag = (ListTag<CompoundTag>) compoundTag.getListTag("unloadedPipeItems");

        for (CompoundTag itemTag : accumulatedItemsListTag) {
            PipeItem pipeItem = new PipeItem();
            pipeItem.loadFromNBTTag(itemTag, getWorld(), itemService);
            getItems().add(pipeItem);
        }

        for (CompoundTag itemTag : unloadedItemsListTag) {
            PipeItem pipeItem = new PipeItem();
            pipeItem.loadFromNBTTag(itemTag, getWorld(), itemService);
            getUnloadedItems().add(pipeItem);
        }

    }
} public class VoidPipe extends Pipe {

    public VoidPipe(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager, ItemDistributorService itemDistributor) {
        super(ductType, blockLoc, world, chunk, settingsInv, globalDuctManager, itemDistributor);
    }

    @Override
    protected Map<TPDirection, Integer> calculateItemDistribution(PipeItem pipeItem, TPDirection movingDir, List<TPDirection> dirs, TransportPipes transportPipes) {
        return null;
    }

    @Override
    public Material getBreakParticleData() {
        return Material.OBSIDIAN;
    }

} public class ItemDistributorService {

    @Inject
    private DuctRegister ductRegister;
    @Inject
    private GlobalDuctManager globalDuctManager;

    private Map<Pipe, Integer> pipeItemDistributionCounter;

    public ItemDistributorService() {
        this.pipeItemDistributionCounter = new HashMap<>();
    }

    /**
     * calculates the greatest common dividor of all abs weights and divides every weight by it.
     **/
    private void reduceAbsWeights(Map<TPDirection, Integer> absWeights) {
        BigInteger gcd = null;
        for (TPDirection dir : absWeights.keySet()) {
            if (absWeights.get(dir) == 0) {
                continue;
            }
            if (gcd == null) {
                gcd = BigInteger.valueOf(absWeights.get(dir));
            } else {
                gcd = BigInteger.valueOf(absWeights.get(dir)).gcd(gcd);
            }
        }
        for (TPDirection dir : absWeights.keySet()) {
            if (absWeights.get(dir) == 0) {
                continue;
            }
            absWeights.put(dir, absWeights.get(dir) / gcd.intValue());
        }
    }

    /**
     * this method maps the possible directions with their free space
     */
    private Map<TPDirection, Integer> calculateFreeSpaceForAllDirections(ItemStack item, Collection<TPDirection> dirs, Pipe pipe) {
        Map<TPDirection, Integer> freeSpaceMap = new HashMap<>();

        Map<BlockLocation, TransportPipesContainer> containerMap = ((PipeManager) (DuctManager<? extends Duct>) ductRegister.baseDuctTypeOf("pipe").getDuctManager()).getContainers(pipe.getWorld());
        Map<BlockLocation, Duct> ductMap = globalDuctManager.getDucts(pipe.getWorld());

        for (TPDirection dir : dirs) {
            freeSpaceMap.put(dir, Integer.MAX_VALUE);
            BlockLocation bl = pipe.getBlockLoc().getNeighbor(dir);
            if (containerMap != null && containerMap.containsKey(bl)) {
                // container at location
                TransportPipesContainer container = containerMap.get(bl);
                int freeSpace = container.spaceForItem(dir, item);
                freeSpaceMap.put(dir, freeSpace);
            } else if (ductMap != null && ductMap.containsKey(bl) && ductMap.get(bl) instanceof CraftingPipe) {
                // crafting pipe at location
                CraftingPipe cp = (CraftingPipe) ductMap.get(bl);
                int freeSpace = cp.spaceForItem(new ItemData(item));
                freeSpaceMap.put(dir, freeSpace);
            }
        }

        return freeSpaceMap;
    }

    public Map<TPDirection, Integer> splitPipeItem(ItemStack item, Map<TPDirection, Integer> absWeights, Pipe pipe) {
        Map<TPDirection, Integer> splitMap = new HashMap<>();
        Map<TPDirection, Integer> freeSpaceMap = calculateFreeSpaceForAllDirections(item, absWeights.keySet(), pipe);
        reduceAbsWeights(absWeights);

        List<TPDirection> weightsDirectionList = new ArrayList<>();
        for (TPDirection dir : absWeights.keySet()) {
            int absWeight = absWeights.get(dir);
            for (int i = 1; i <= absWeight; i++) {
                // make sure the free space of the nearby container block does not get ignored
                if (freeSpaceMap.get(dir) >= i) {
                    weightsDirectionList.add(dir);
                }
            }
        }

        if (weightsDirectionList.isEmpty()) {
            // return empty map so the item will be dropped
            // if null would be returned, no item will be dropped
            return splitMap;
        }

        int distributionCounter = pipeItemDistributionCounter.getOrDefault(pipe, 0);

        for (int i = 0; i < item.getAmount(); i++) {
            distributionCounter %= weightsDirectionList.size();

            TPDirection outputDir = weightsDirectionList.get(distributionCounter);
            if (splitMap.containsKey(outputDir)) {
                splitMap.put(outputDir, splitMap.get(outputDir) + 1);
            } else {
                splitMap.put(outputDir, 1);
            }

            distributionCounter++;
        }

        pipeItemDistributionCounter.put(pipe, distributionCounter);

        return splitMap;

    }

} public class DuctType {

    private BaseDuctType<? extends Duct> baseDuctType;
    private String name;
    private String displayName;
    private Set<DuctType> connectables;
    private Recipe ductRecipe;
    private String craftingPermission;

    public DuctType(BaseDuctType<? extends Duct> baseDuctType, String name, String displayName, String craftingPermission) {
        this.baseDuctType = baseDuctType;
        this.name = name;
        this.displayName = displayName;
        this.craftingPermission = craftingPermission;
        this.connectables = new HashSet<>();
    }

    public DuctType connectTo(String... ductTypeNames) {
        for (String name : ductTypeNames) {
            connectables.add(getBaseDuctType().ductTypeOf(name));
        }
        return this;
    }

    public DuctType connectToAll() {
        connectables.addAll(getBaseDuctType().ductTypes());
        return this;
    }

    public DuctType connectToClasses(Class<? extends DuctType> clazz) {
        for (DuctType dt : getBaseDuctType().ductTypes()) {
            if (clazz.isAssignableFrom(dt.getClass())) {
                connectables.add(dt);
            }
        }
        return this;
    }

    public DuctType disconnectFrom(String... ductTypeNames) {
        for (String name : ductTypeNames) {
            connectables.remove(getBaseDuctType().ductTypeOf(name));
        }
        return this;
    }

    public DuctType disconnectFromClasses(Class<? extends DuctType> clazz) {
        for (DuctType dt : getBaseDuctType().ductTypes()) {
            if (clazz.isAssignableFrom(dt.getClass())) {
                connectables.remove(dt);
            }
        }
        return this;
    }

    public BaseDuctType<? extends Duct> getBaseDuctType() {
        return baseDuctType;
    }

    public String getName() {
        return name;
    }

    public String getDisplayName() {
        return displayName;
    }

    public boolean is(String name) {
        return this.name.equalsIgnoreCase(name);
    }

    public String getFormattedTypeName() {
        return displayName;
    }

    public Set<DuctType> getConnectables() {
        return connectables;
    }

    public Recipe getDuctRecipe() {
        return ductRecipe;
    }

    public void setDuctRecipe(Recipe ductRecipe) {
        this.ductRecipe = ductRecipe;
        Bukkit.addRecipe(ductRecipe);
    }

    public boolean hasPlayerCraftingPermission(Player p) {
        return craftingPermission == null || p.hasPermission(craftingPermission);
    }

    public boolean connectsTo(DuctType otherDuctType) {
        return baseDuctType.equals(otherDuctType.baseDuctType) && connectables.contains(otherDuctType) && otherDuctType.connectables.contains(this);
    }

    @Override
    public String toString() {
        return "DuctType: " + name + "\n" +
                "Connectables: " + connectables.stream().map(dt -> dt.name).collect(Collectors.joining(", "));
    }

} public class ColoredPipeType extends PipeType {

    private Material coloringMaterial;

    public ColoredPipeType(BaseDuctType<Pipe> baseDuctType, String name, String displayName, Material coloringMaterial, String craftingPermission) {
        super(baseDuctType, name, displayName, craftingPermission);
        this.coloringMaterial = coloringMaterial;
    }

    public Material getColoringMaterial() {
        return coloringMaterial;
    }
} public class CraftingPipeSettingsInventory extends DuctSettingsInventory {

    @Override
    public void create() {
        inv = Bukkit.createInventory(null, 6 * 9, LangConf.Key.DUCT_INVENTORY_TITLE.get(duct.getDuctType().getFormattedTypeName()));
    }

    @Override
    public void closeForAllPlayers(TransportPipes transportPipes) {
        save(null);
        super.closeForAllPlayers(transportPipes);
    }

    @Override
    public void populate() {
        CraftingPipe pipe = (CraftingPipe) duct;
        TPDirection outputDir = pipe.getOutputDir();
        List<ItemStack> cachedItems = pipe.getCachedItems();

        ItemStack glassPane = itemService.createWildcardItem(Material.GRAY_STAINED_GLASS_PANE);
        for (int i = 0; i < 9; i++) {
            inv.setItem(i, glassPane);
        }

        ItemStack outputDirectionItem = itemService.changeDisplayNameAndLoreConfig(new ItemStack(Material.TRIPWIRE_HOOK), LangConf.Key.DUCT_INVENTORY_CRAFTINGPIPE_OUTPUTDIRECTION.getLines(outputDir != null ? outputDir.getDisplayName() : LangConf.Key.DIRECTIONS_NONE.get()));
        inv.setItem(8, outputDirectionItem);

        for (int col = 0; col < 9; col += 4) {
            for (int row = 1; row < 4; row++) {
                inv.setItem(row * 9 + col, glassPane);
            }
        }

        for (int i = 0; i < 9; i++) {
            ItemData id = pipe.getRecipeItems()[i];
            if (id != null) {
                inv.setItem(9 + 1 + (i / 3) * 9 + i % 3, id.toItemStack().clone());
            } else {
                inv.setItem(9 + 1 + (i / 3) * 9 + i % 3, null);
            }
        }

        for (int i = 0; i < 9; i++) {
            if (i != 4) {
                inv.setItem(9 + 5 + (i / 3) * 9 + i % 3, glassPane);
            }
        }

        for (int i = 0; i < 9; i++) {
            inv.setItem(4 * 9 + i, glassPane);
        }

        ItemStack retrieveItemsItem = itemService.changeDisplayNameAndLoreConfig(itemService.createHeadItem("5ca62fac-d094-4346-8361-e1dfdd970607", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzQzNzM0NmQ4YmRhNzhkNTI1ZDE5ZjU0MGE5NWU0ZTc5ZGFlZGE3OTVjYmM1YTEzMjU2MjM2MzEyY2YifX19", null), LangConf.Key.DUCT_INVENTORY_CRAFTINGPIPE_RETRIEVECACHEDITEMS.getLines());
        inv.setItem(4 * 9 + 8, retrieveItemsItem);

        for (int i = 0; i < 9; i++) {
            inv.setItem(5 * 9 + i, cachedItems.size() > i ? cachedItems.get(i) : null);
        }

        updateResultWithDelay();

    }

    private Recipe getCraftingRecipe(ItemStack[] items) {

        int col_min = -1;
        int col_max = 0;
        for (int col = 0; col < 3; col++) {
            boolean item = false;
            for (int row = 0; row < 3; row++) {
                item |= items[row * 3 + col] != null;
            }
            if (item && col_min == -1) {
                col_min = col;
            }
            if (item) {
                col_max = col;
            }
        }
        int row_min = -1;
        int row_max = 0;
        for (int row = 0; row < 3; row++) {
            boolean item = false;
            for (int col = 0; col < 3; col++) {
                item |= items[row * 3 + col] != null;
            }
            if (item && row_min == -1) {
                row_min = row;
            }
            if (item) {
                row_max = row;
            }
        }
        int in_rows = row_min == -1 ? 0 : row_max - row_min + 1;
        int in_cols = col_min == -1 ? 0 : col_max - col_min + 1;

        if (in_rows == 0 || in_cols == 0) {
            return null;
        }

        Map<Character, ItemStack> in_map = new HashMap<>();
        String[] in_shape = new String[in_rows];
        char current_char = 'a';
        for (int row = row_min; row <= row_max; row++) {
            StringBuilder sb = new StringBuilder();
            for (int col = col_min; col <= col_max; col++) {
                if (items[row * 3 + col] == null) {
                    sb.append(" ");
                } else {
                    char shape_char = '\0';
                    for (Character tempChar : in_map.keySet()) {
                        if (in_map.get(tempChar).equals(items[row * 3 + col])) {
                            shape_char = tempChar;
                        }
                    }
                    if (shape_char == '\0') {
                        shape_char = current_char++;
                        in_map.put(shape_char, items[row * 3 + col]);
                    }
                    sb.append(shape_char);
                }
            }
            in_shape[row - row_min] = sb.toString();
        }

        Iterator<Recipe> recipeIt = Bukkit.recipeIterator();
        recipe_loop:
        while (recipeIt.hasNext()) {
            Recipe recipe = recipeIt.next();
            if (recipe instanceof ShapedRecipe) {
                ShapedRecipe shapedRecipe = (ShapedRecipe) recipe;

                String[] shape = shapedRecipe.getShape();
                int rows = shape.length;
                int cols = shape[0].length();

                if (in_rows == rows && in_cols == cols) {
                    for (int row = 0; row < rows; row++) {
                        for (int col = 0; col < cols; col++) {
                            char recipeChar = shape[row].charAt(col);
                            char inChar = in_shape[row].charAt(col);
                            if (shapedRecipe.getChoiceMap().get(recipeChar) == null && in_map.get(inChar) == null) {
                                continue;
                            }
                            if (shapedRecipe.getChoiceMap().get(recipeChar) == null || in_map.get(inChar) == null) {
                                continue recipe_loop;
                            }
                            if(shapedRecipe.getChoiceMap().get(recipeChar).test(in_map.get(inChar))) {
                                continue;
                            }
                            continue recipe_loop;
                        }
                    }
                    return shapedRecipe;
                }
            } else if (recipe instanceof ShapelessRecipe) {
                ShapelessRecipe shapelessRecipe = (ShapelessRecipe) recipe;
                List<ItemStack> givenItems = new ArrayList<>(Arrays.asList(items));
                givenItems.removeIf(Objects::isNull);
                for (RecipeChoice ingredientChoice : shapelessRecipe.getChoiceList()) {
                    if (!givenItems.removeIf(ingredientChoice::test)) {
                        continue recipe_loop;
                    }
                }
                if (givenItems.isEmpty()) {
                    return shapelessRecipe;
                }
            }
        }
        return null;
    }

    @Override
    protected boolean click(Player p, int rawSlot, ClickType ct) {
        updateResultWithDelay();

        CraftingPipe pipe = (CraftingPipe) duct;

        // clicked change output direction
        if (rawSlot == 8) {
            save(p);
            pipe.updateOutputDirection(true);
            return true;
        }

        // retrieve cached items
        if (rawSlot == 4 * 9 + 8) {
            List<ItemStack> cachedItems = new ArrayList<>(pipe.getCachedItems());
            pipe.getCachedItems().clear();
            transportPipes.runTaskSync(() -> {
                Map<Integer, ItemStack> overflow = p.getInventory().addItem(cachedItems.toArray(new ItemStack[0]));
                for (ItemStack overflowItem : overflow.values()) {
                    p.getWorld().dropItem(p.getLocation(), overflowItem);
                }
            });

            save(p);
            populate();

            return true;
        }

        // clicked on recipe items
        if (slotInRecipeGrid(rawSlot)) {
            return false;
        }

        return rawSlot < inv.getSize();
    }

    @Override
    protected boolean drag(Player p, Set<Integer> rawSlots, DragType dragType) {
        updateResultWithDelay();
        for (Integer i : rawSlots) {
            if (!(i >= inv.getSize() || slotInRecipeGrid(i))) {
                return true;
            }
        }
        return false;
    }

    private boolean slotInRecipeGrid(int rawSlot) {
        if (rawSlot == 9 + 1 || rawSlot == 9 + 2 || rawSlot == 9 + 3) {
            return true;
        }
        if (rawSlot == 2 * 9 + 1 || rawSlot == 2 * 9 + 2 || rawSlot == 2 * 9 + 3) {
            return true;
        }
        if (rawSlot == 3 * 9 + 1 || rawSlot == 3 * 9 + 2 || rawSlot == 3 * 9 + 3) {
            return true;
        }
        return false;
    }

    @Override
    protected boolean collect_to_cursor(Player p, ItemStack cursor, int rawSlot) {
        return cursor != null;
    }

    private Recipe calculateRecipe() {
        ItemStack[] items = new ItemStack[9];
        for (int row = 0; row < 3; row++) {
            for (int col = 0; col < 3; col++) {
                if (inv.getItem(9 + 1 + row * 9 + col) != null) {
                    items[row * 3 + col] = inv.getItem(9 + 1 + row * 9 + col);
                } else {
                    items[row * 3 + col] = null;
                }
            }
        }
        return getCraftingRecipe(items);
    }

    private void updateResultWithDelay() {
        Bukkit.getScheduler().runTask(transportPipes, () -> {
            Recipe recipe = calculateRecipe();
            inv.setItem(24, recipe != null ? recipe.getResult() : null);
        });
    }

    @Override
    public void save(Player p) {

        CraftingPipe pipe = (CraftingPipe) duct;

        for (int i = 0; i < 9; i++) {
            ItemStack is = inv.getItem(9 + 1 + (i / 3) * 9 + i % 3);
            if (is != null && is.getType() != Material.AIR) {
                if (is.getAmount() > 1) {
                    ItemStack drop = is.clone();
                    drop.setAmount(is.getAmount() - 1);
                    if (p != null) {
                        p.getWorld().dropItem(p.getLocation(), drop);
                    } else {
                        duct.getWorld().dropItem(duct.getBlockLoc().toLocation(duct.getWorld()), drop);
                    }
                    is.setAmount(1);
                }
                pipe.getRecipeItems()[i] = new ItemData(is);
            } else {
                pipe.getRecipeItems()[i] = null;
            }
        }

        pipe.setRecipe(calculateRecipe());

    }
} public class ExtractionPipeSettingsInventory extends DuctSettingsInventory {

    private int scrollValue;

    public ExtractionPipeSettingsInventory() {
        scrollValue = 0;
    }

    @Override
    public void create() {
        inv = Bukkit.createInventory(null, 3 * 9, LangConf.Key.DUCT_INVENTORY_TITLE.get(duct.getDuctType().getFormattedTypeName()));
    }

    @Override
    public void closeForAllPlayers(TransportPipes transportPipes) {
        save(null);
        super.closeForAllPlayers(transportPipes);
    }

    @Override
    public void populate() {
        ExtractionPipe pipe = (ExtractionPipe) duct;
        TPDirection extractDir = pipe.getExtractDirection();

        ItemStack extractDirection = itemService.changeDisplayNameAndLoreConfig(new ItemStack(Material.TRIPWIRE_HOOK), LangConf.Key.DUCT_INVENTORY_EXTRACTIONPIPE_EXTRACTDIRECTION.getLines(extractDir != null ? extractDir.getDisplayName() : LangConf.Key.DIRECTIONS_NONE.get()));
        ItemStack extractCondition = itemService.changeDisplayNameAndLoreConfig(pipe.getExtractCondition().getDisplayItem(), LangConf.Key.DUCT_INVENTORY_EXTRACTIONPIPE_EXTRACTCONDITION.getLines(pipe.getExtractCondition().getDisplayName()));
        ItemStack extractAmount = itemService.changeDisplayNameAndLoreConfig(pipe.getExtractAmount().getDisplayItem(), LangConf.Key.DUCT_INVENTORY_EXTRACTIONPIPE_EXTRACTAMOUNT.getLines(pipe.getExtractAmount().getDisplayName()));

        ItemStack wool = itemService.changeDisplayNameAndLoreConfig(new ItemStack(Material.WHITE_WOOL), LangConf.Key.DUCT_INVENTORY_EXTRACTIONPIPE_FILTERTITLE.get(), LangConf.Key.DUCT_INVENTORY_FILTER_MODE_AND_STRICTNESS.getLines(pipe.getItemFilter().getFilterMode().getDisplayName(), pipe.getItemFilter().getFilterStrictness().getDisplayName()));
        ItemStack scrollLeft = itemService.changeDisplayName(itemService.createHeadItem("69b9a08d-4e89-4878-8be8-551caeacbf2a", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvM2ViZjkwNzQ5NGE5MzVlOTU1YmZjYWRhYjgxYmVhZmI5MGZiOWJlNDljNzAyNmJhOTdkNzk4ZDVmMWEyMyJ9fX0=", null), LangConf.Key.DUCT_INVENTORY_LEFTARROW.get());
        ItemStack scrollRight = itemService.changeDisplayName(itemService.createHeadItem("15f49744-9b61-46af-b1c3-71c6261a0d0e", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWI2ZjFhMjViNmJjMTk5OTQ2NDcyYWVkYjM3MDUyMjU4NGZmNmY0ZTgzMjIxZTU5NDZiZDJlNDFiNWNhMTNiIn19fQ==", null), LangConf.Key.DUCT_INVENTORY_RIGHTARROW.get());

        // basic settings
        for (int i = 0; i < 18; i++) {
            if (i == 2) {
                inv.setItem(i, extractDirection);
            } else if (i == 4) {
                inv.setItem(i, extractAmount);
            } else if (i == 6) {
                inv.setItem(i, extractCondition);
            } else {
                inv.setItem(i, itemService.createWildcardItem(Material.GRAY_STAINED_GLASS_PANE));
            }
        }

        inv.setItem(18, wool);

        // filtering stuff
        if (pipe.getItemFilter().getFilterMode() == FilterMode.BLOCK_ALL) {
            for (int i = 1; i < 9; i++) {
                inv.setItem(18 + i, itemService.createBarrierItem());
            }
        } else {
            inv.setItem(18 + 1, scrollLeft);
            inv.setItem(18 + 8, scrollRight);

            ItemData[] items = pipe.getItemFilter().getFilterItems();
            int indexWithScrollValue = scrollValue;
            for (int i = 2; i < 8; i++) {
                if (items[indexWithScrollValue] != null) {
                    inv.setItem(18 + i, items[indexWithScrollValue].toItemStack());
                } else {
                    inv.setItem(18 + i, null);
                }
                indexWithScrollValue++;
            }
        }
    }

    @Override
    protected boolean click(Player p, int rawSlot, ClickType ct) {
        ExtractionPipe pipe = (ExtractionPipe) duct;

        // clicked change extract direction
        if (rawSlot == 2) {
            save(p);
            pipe.updateExtractDirection(true);
            return true;
        }

        // clicked change extract amount
        if (rawSlot == 4) {
            save(p);
            pipe.setExtractAmount(pipe.getExtractAmount().next());
            populate();
            return true;
        }

        // clicked change extract condition
        if (rawSlot == 6) {
            save(p);
            pipe.setExtractCondition(pipe.getExtractCondition().next());
            populate();
            return true;
        }

        // clicked filtering mode wool
        if (rawSlot == 18) {
            if (ct == ClickType.LEFT || ct == ClickType.SHIFT_LEFT) {
                pipe.getItemFilter().setFilterMode(pipe.getItemFilter().getFilterMode().next());
            } else if (ct == ClickType.RIGHT || ct == ClickType.SHIFT_RIGHT) {
                pipe.getItemFilter().setFilterStrictness(pipe.getItemFilter().getFilterStrictness().next());
            }
            save(p);
            populate();
            return true;
        }

        // clicked scroll left
        if (rawSlot == 19) {
            save(p);
            if (scrollValue > 0) {
                scrollValue--;
            }
            populate();
            return true;
        }

        // clicked scroll right
        if (rawSlot == 26) {
            save(p);
            if (scrollValue < ItemFilter.MAX_ITEMS_PER_ROW - 6) {
                scrollValue++;
            }
            populate();
            return true;
        }

        return false;
    }

    @Override
    public void save(Player p) {
        ExtractionPipe pipe = (ExtractionPipe) duct;

        ItemData[] items = pipe.getItemFilter().getFilterItems();
        for (int i = 2; i < 8; i++) {
            ItemStack itemStack = inv.getItem(18 + i);
            if (itemService.isItemWildcardOrBarrier(itemStack)) {
                return;
            }
            if (itemStack != null && itemStack.getAmount() > 1) {
                ItemStack drop = itemStack.clone();
                drop.setAmount(itemStack.getAmount() - 1);
                p.getWorld().dropItem(p.getLocation(), drop);
                itemStack.setAmount(1);
            }
            items[scrollValue + i - 2] = itemStack != null ? new ItemData(itemStack) : null;
        }

    }
} public class GoldenPipeSettingsInventory extends DuctSettingsInventory {

    private Map<GoldenPipe.Color, Integer> scrollValues;

    @Override
    public void create() {
        scrollValues = new HashMap<>();
        inv = Bukkit.createInventory(null, 6 * 9, LangConf.Key.DUCT_INVENTORY_TITLE.get(duct.getDuctType().getFormattedTypeName()));
    }

    @Override
    public void closeForAllPlayers(TransportPipes transportPipes) {
        save(null);
        super.closeForAllPlayers(transportPipes);
    }

    @Override
    public void populate() {
        GoldenPipe pipe = (GoldenPipe) duct;
        Set<TPDirection> allConns = pipe.getAllConnections();
        for (int line = 0; line < 6; line++) {
            GoldenPipe.Color gpc = GoldenPipe.Color.values()[line];
            int scrollValue = scrollValues.getOrDefault(gpc, 0);

            FilterMode filterMode = pipe.getItemFilter(gpc).getFilterMode();
            FilterStrictness filterStrictness = pipe.getItemFilter(gpc).getFilterStrictness();
            ItemStack wool = itemService.changeDisplayNameAndLoreConfig(new ItemStack(gpc.getWoolMaterial()), LangConf.Key.DUCT_INVENTORY_GOLDENPIPE_FILTERTITLE.get(gpc.getDisplayName()), LangConf.Key.DUCT_INVENTORY_FILTER_MODE_AND_STRICTNESS.getLines(filterMode.getDisplayName(), filterStrictness.getDisplayName()));
            ItemStack glassPane = itemService.createWildcardItem(gpc.getGlassPaneMaterial());
            ItemStack barrier = itemService.createBarrierItem();
            ItemStack scrollLeft = itemService.changeDisplayName(itemService.createHeadItem("69b9a08d-4e89-4878-8be8-551caeacbf2a", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvM2ViZjkwNzQ5NGE5MzVlOTU1YmZjYWRhYjgxYmVhZmI5MGZiOWJlNDljNzAyNmJhOTdkNzk4ZDVmMWEyMyJ9fX0=", null), LangConf.Key.DUCT_INVENTORY_LEFTARROW.get());
            ItemStack scrollRight = itemService.changeDisplayName(itemService.createHeadItem("15f49744-9b61-46af-b1c3-71c6261a0d0e", "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWI2ZjFhMjViNmJjMTk5OTQ2NDcyYWVkYjM3MDUyMjU4NGZmNmY0ZTgzMjIxZTU5NDZiZDJlNDFiNWNhMTNiIn19fQ==", null), LangConf.Key.DUCT_INVENTORY_RIGHTARROW.get());

            inv.setItem(line * 9, wool);

            if (filterMode == FilterMode.BLOCK_ALL) {
                for (int i = 1; i < 9; i++) {
                    inv.setItem(line * 9 + i, barrier);
                }
            } else if (!allConns.contains(gpc.getDirection())) {
                for (int i = 1; i < 9; i++) {
                    inv.setItem(line * 9 + i, glassPane);
                }
            } else {
                inv.setItem(line * 9 + 1, scrollLeft);
                inv.setItem(line * 9 + 8, scrollRight);

                ItemData[] filterItems = pipe.getItemFilter(gpc).getFilterItems();
                int indexWithScrollValue = scrollValue;
                for (int i = 2; i < 8; i++) {
                    if (filterItems[indexWithScrollValue] != null) {
                        inv.setItem(line * 9 + i, filterItems[indexWithScrollValue].toItemStack());
                    } else {
                        inv.setItem(line * 9 + i, null);
                    }
                    indexWithScrollValue++;
                }
            }
        }
    }

    @Override
    protected boolean click(Player p, int rawSlot, ClickType ct) {
        GoldenPipe pipe = (GoldenPipe) duct;

        //clicked wool
        if (rawSlot >= 0 && rawSlot < inv.getSize() && rawSlot % 9 == 0) {

            int line = rawSlot / 9;
            GoldenPipe.Color gpc = GoldenPipe.Color.values()[line];
            if (ct == ClickType.LEFT || ct == ClickType.SHIFT_LEFT) {
                pipe.getItemFilter(gpc).setFilterMode(pipe.getItemFilter(gpc).getFilterMode().next());
            } else if (ct == ClickType.RIGHT || ct == ClickType.SHIFT_RIGHT) {
                pipe.getItemFilter(gpc).setFilterStrictness(pipe.getItemFilter(gpc).getFilterStrictness().next());
            }

            save(p);
            populate();

            return true;
        }

        //clicked scroll left
        if (rawSlot >= 0 && rawSlot < inv.getSize() && rawSlot % 9 == 1) {

            save(p);

            int line = rawSlot / 9;
            GoldenPipe.Color gpc = GoldenPipe.Color.values()[line];
            int scrollValue = scrollValues.getOrDefault(gpc, 0);
            if (scrollValue > 0) {
                scrollValue--;
            }
            scrollValues.put(gpc, scrollValue);

            populate();

            return true;
        }

        //clicked scroll right
        if (rawSlot >= 0 && rawSlot < inv.getSize() && rawSlot % 9 == 8) {

            save(p);

            int line = rawSlot / 9;
            GoldenPipe.Color gpc = GoldenPipe.Color.values()[line];
            int scrollValue = scrollValues.getOrDefault(gpc, 0);
            if (scrollValue < ItemFilter.MAX_ITEMS_PER_ROW - 6) {
                scrollValue++;
            }
            scrollValues.put(gpc, scrollValue);

            populate();

            return true;
        }

        return false;
    }

    @Override
    public void save(Player p) {

        GoldenPipe pipe = (GoldenPipe) duct;

        line_loop:
        for (int line = 0; line < 6; line++) {
            GoldenPipe.Color gpc = GoldenPipe.Color.values()[line];
            ItemData[] filterItems = pipe.getItemFilter(gpc).getFilterItems();
            int scrollValue = scrollValues.getOrDefault(gpc, 0);
            for (int i = 2; i < 8; i++) {
                ItemStack is = inv.getItem(line * 9 + i);
                //make sure the glass pane and barriers won't be saved
                if (!itemService.isItemWildcardOrBarrier(is)) {
                    if (is != null && is.getAmount() > 1) {
                        ItemStack drop = is.clone();
                        drop.setAmount(is.getAmount() - 1);
                        if (p != null) {
                            p.getWorld().dropItem(p.getLocation(), drop);
                        } else {
                            duct.getWorld().dropItem(duct.getBlockLoc().toLocation(duct.getWorld()), drop);
                        }
                        is.setAmount(1);
                    }
                    filterItems[scrollValue + i - 2] = is != null ? new ItemData(is) : null;
                } else {
                    continue line_loop;
                }
            }
        }

    }
} public class BlockLocation implements Comparable<BlockLocation> {

    private int x;
    private int y;
    private int z;

    public BlockLocation(Location loc) {
        this(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
    }

    public BlockLocation(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int z) {
        this.z = z;
    }

    public BlockLocation getNeighbor(TPDirection direction) {
        return new BlockLocation(x + direction.getVector().getBlockX(), y + direction.getVector().getBlockY(), z + direction.getVector().getBlockZ());
    }

    public Location toLocation(World world){
        return new Location(world, x, y, z);
    }

    public Block toBlock(World world){
        return toLocation(world).getBlock();
    }

    @Override
    public int compareTo(@NotNull BlockLocation o) {
        if (z < o.z) {
            return -1;
        } else if (z > o.z) {
            return 1;
        } else {
            if (y < o.y) {
                return -1;
            } else if (y > o.y) {
                return 1;
            } else {
                if (x < o.x) {
                    return -1;
                } else if (x > o.x) {
                    return 1;
                } else {
                    return 0;
                }
            }
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BlockLocation blockLoc = (BlockLocation) o;
        return x == blockLoc.x &&
                y == blockLoc.y &&
                z == blockLoc.z;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y, z);
    }

    @Override
    public String toString() {
        return x + ", " + y + ", " + z;
    }

    public static BlockLocation fromString(String s) {
        if(s == null) {
            return null;
        }
        String[] split = s.split(", ");
        if(split.length == 3) {
            try {
                int x = Integer.parseInt(split[0]);
                int y = Integer.parseInt(split[1]);
                int z = Integer.parseInt(split[2]);
                return new BlockLocation(x, y, z);
            } catch (NumberFormatException e){

            }
        }
        return null;
    }

}
import org.bukkit.Chunk;
import org.bukkit.World;
import javax.inject.Inject;
import de.robotricker.transportpipes.duct.pipe.ColoredPipe;
import de.robotricker.transportpipes.duct.pipe.CraftingPipe;
import de.robotricker.transportpipes.duct.pipe.ExtractionPipe;
import de.robotricker.transportpipes.duct.pipe.GoldenPipe;
import de.robotricker.transportpipes.duct.pipe.IcePipe;
import de.robotricker.transportpipes.duct.pipe.IronPipe;
import de.robotricker.transportpipes.duct.pipe.Pipe;
import de.robotricker.transportpipes.duct.pipe.VoidPipe;
import de.robotricker.transportpipes.duct.pipe.filter.ItemDistributorService;
import de.robotricker.transportpipes.duct.types.DuctType;
import de.robotricker.transportpipes.duct.types.pipetype.ColoredPipeType;
import de.robotricker.transportpipes.inventory.CraftingPipeSettingsInventory;
import de.robotricker.transportpipes.inventory.ExtractionPipeSettingsInventory;
import de.robotricker.transportpipes.inventory.GoldenPipeSettingsInventory;
import de.robotricker.transportpipes.location.BlockLocation;
package de.robotricker.transportpipes.duct.factory;




public class PipeFactory extends DuctFactory<Pipe> {

    @Inject
    private ItemDistributorService itemDistributor;

    @Override
    public Pipe createDuct(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk) {
        Pipe pipe = null;
        if (ductType instanceof ColoredPipeType) {
            pipe = new ColoredPipe(ductType, blockLoc, world, chunk, null, globalDuctManager, itemDistributor);
        } else if (ductType.is("Golden")) {
            GoldenPipeSettingsInventory inv = transportPipes.getInjector().newInstance(GoldenPipeSettingsInventory.class);