public interface TransportPipesContainer {

    /**
     * should give back the extracted item with the given amount if possible. If there are not enough items left to extract, just give back whats there
     */
    ItemStack extractItem(TPDirection extractDirection, int amount, ItemFilter itemFilter);

    /**
     * should insert the given item into this container block and gives back the item with the amount that did not fit inside or null if all fit inside.
     */
    ItemStack insertItem(TPDirection insertDirection, ItemStack insertion);

    /**
     * does not insert the given item but instead gives back the amount of items of the given type that would fit in this container at the current state.
     * This method is called asynchronously
     */
    int spaceForItem(TPDirection insertDirection, ItemStack insertion);

    boolean isInLoadedChunk();

} public abstract class Duct {

    protected GlobalDuctManager globalDuctManager;
    protected DuctSettingsInventory settingsInv;
    private DuctType ductType;
    private BlockLocation blockLoc;
    private World world;
    private Chunk chunk;
    private Map<TPDirection, Duct> connectedDucts;
    private BlockData obfuscatedWith;

    public Duct(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk, DuctSettingsInventory settingsInv, GlobalDuctManager globalDuctManager) {
        this.ductType = ductType;
        this.blockLoc = blockLoc;
        this.world = world;
        this.chunk = chunk;
        this.connectedDucts = Collections.synchronizedMap(new HashMap<>());
        this.settingsInv = settingsInv;
        this.globalDuctManager = globalDuctManager;
    }

    public void initSettingsInv(TransportPipes transportPipes) {
        if (settingsInv != null) {
            Bukkit.getPluginManager().registerEvents(settingsInv, transportPipes);
            settingsInv.setDuct(this);
            settingsInv.create();
        }
    }

    public DuctSettingsInventory getSettingsInv() {
        return settingsInv;
    }

    public void notifyClick(Player p, boolean shift) {
        if (settingsInv != null)
            settingsInv.openInv(p);
    }

    public DuctType getDuctType() {
        return ductType;
    }

    public BlockLocation getBlockLoc() {
        return blockLoc;
    }

    public World getWorld() {
        return world;
    }

    public boolean isInLoadedChunk() {
        return chunk.isLoaded();
    }

    public void notifyConnectionChange() {
        if (settingsInv != null) {
            settingsInv.populate();
        }
    }

    public BlockData obfuscatedWith() {
        return obfuscatedWith;
    }

    public void obfuscatedWith(BlockData obfuscatedWith) {
        this.obfuscatedWith = obfuscatedWith;
    }

    public void tick(boolean bigTick, TransportPipes transportPipes, DuctManager ductManager) {

    }

    public void postTick(boolean bigTick, TransportPipes transportPipes, DuctManager ductManager, GeneralConf generalConf) {

    }

    public void syncBigTick(DuctManager ductManager) {

    }

    public Map<TPDirection, Duct> getDuctConnections() {
        return connectedDucts;
    }

    public Set<TPDirection> getAllConnections() {
        return new HashSet<>(getDuctConnections().keySet());
    }

    public Material getBreakParticleData() {
        return null;
    }

    /**
     * just for the purpose of dropping inside items or other baseDuctType specific stuff
     */
    public List<ItemStack> destroyed(TransportPipes transportPipes, DuctManager ductManager, Player destroyer) {
        List<ItemStack> dropItems = new ArrayList<>();
        if (destroyer == null || destroyer.getGameMode() != GameMode.CREATIVE) {
            dropItems.add(getDuctType().getBaseDuctType().getItemManager().getClonedItem(getDuctType()));
        }

        if (settingsInv != null) {
            settingsInv.closeForAllPlayers(transportPipes);
        }

        //break particles
        if (destroyer != null && getBreakParticleData() != null) {
            transportPipes.runTaskSync(() -> destroyer.getWorld().spawnParticle(Particle.ITEM_CRACK, getBlockLoc().getX() + 0.5f, getBlockLoc().getY() + 0.5f, getBlockLoc().getZ() + 0.5f, 30, 0.25f, 0.25f, 0.25f, 0.05f, new ItemStack(getBreakParticleData())));
        }

        return dropItems;
    }

    public void saveToNBTTag(CompoundTag compoundTag, ItemService itemService) {
        if (obfuscatedWith != null) {
            compoundTag.putString("obfuscatedWith", obfuscatedWith.getAsString());
        }
    }

    public void loadFromNBTTag(CompoundTag compoundTag, ItemService itemService) {
        if (compoundTag.containsKey("obfuscatedWith")) {
            obfuscatedWith = Bukkit.createBlockData(compoundTag.getString("obfuscatedWith"));
            // replace barrier block with real obfuscation block
            if (getBlockLoc().toBlock(getWorld()).getType() == Material.BARRIER) {
                getBlockLoc().toBlock(getWorld()).setBlockData(obfuscatedWith);
            }
        }
    }

} public class DuctRegister {

    private List<BaseDuctType<? extends Duct>> baseDuctTypes;

    private TransportPipes plugin;
    private GeneralConf generalConf;

    @Inject
    public DuctRegister(TransportPipes plugin, GeneralConf generalConf) {
        this.plugin = plugin;
        this.generalConf = generalConf;
        this.baseDuctTypes = new ArrayList<>();
    }

    public <T extends Duct> BaseDuctType<T> registerBaseDuctType(String name, Class<? extends DuctManager<T>> ductManagerClass, Class<? extends DuctFactory<T>> ductFactoryClass, Class<? extends ItemManager<T>> itemManagerClass) {
        if (baseDuctTypes.stream().anyMatch(bdt -> bdt.getName().equalsIgnoreCase(name))) {
            throw new IllegalArgumentException("BaseDuctType '" + name + "' already exists");
        }
        BaseDuctType<T> baseDuctType = new BaseDuctType<>(name, plugin.getInjector().newInstance(ductManagerClass), plugin.getInjector().newInstance(ductFactoryClass), plugin.getInjector().newInstance(itemManagerClass));
        this.baseDuctTypes.add(baseDuctType);
        baseDuctType.getDuctManager().registerDuctTypes();
        baseDuctType.getItemManager().registerItems();

        if (generalConf.isCraftingEnabled()) {
            baseDuctType.getDuctManager().registerRecipes();
        }

        return baseDuctType;
    }

    public List<BaseDuctType<? extends Duct>> baseDuctTypes() {
        return baseDuctTypes;
    }

    public <T extends Duct> BaseDuctType<T> baseDuctTypeOf(String displayName) {
        return (BaseDuctType<T>) baseDuctTypes().stream().filter(bdt -> bdt.getName().equalsIgnoreCase(displayName)).findAny().orElse(null);
    }

    public void saveDuctTypeToNBTTag(DuctType ductType, CompoundTag ductTag) {
        ductTag.putString("baseDuctType", ductType.getBaseDuctType().getName());
        ductTag.putString("ductType", ductType.getName());
    }

    public void saveBlockLocToNBTTag(BlockLocation blockLoc, CompoundTag ductTag) {
        ductTag.putString("blockLoc", blockLoc.toString());
    }

    public DuctType loadDuctTypeFromNBTTag(CompoundTag ductTag) {
        BaseDuctType bdt = baseDuctTypeOf(ductTag.getString("baseDuctType"));
        if (bdt == null) {
            return null;
        }
        return bdt.ductTypeOf(ductTag.getString("ductType"));
    }

    public BlockLocation loadBlockLocFromNBTTag(CompoundTag ductTag) {
        return BlockLocation.fromString(ductTag.getString("blockLoc"));
    }

} public abstract class DuctManager<T extends Duct> {

    protected TransportPipes transportPipes;
    protected DuctRegister ductRegister;
    protected GlobalDuctManager globalDuctManager;
    protected ProtocolService protocolService;
    protected ItemService itemService;

    @Inject
    public DuctManager(TransportPipes transportPipes, DuctRegister ductRegister, GlobalDuctManager globalDuctManager, ProtocolService protocolService, ItemService itemService) {
        this.transportPipes = transportPipes;
        this.ductRegister = ductRegister;
        this.globalDuctManager = globalDuctManager;
        this.protocolService = protocolService;
        this.itemService = itemService;
    }

    public abstract void registerDuctTypes();

    public abstract void registerRecipes();

    public abstract void tick();

    /**
     * called inside the bukkit thread whenever a duct comes into visible range
     */
    public void notifyDuctShown(Duct duct, Player p) {
        if (globalDuctManager.getPlayerDucts(p).add(duct)) {
            protocolService.sendASD(p, duct.getBlockLoc(), globalDuctManager.getPlayerRenderSystem(p, duct.getDuctType().getBaseDuctType()).getASDForDuct(duct));
        }
    }

    /**
     * called inside the bukkit thread whenever a duct gets outside of the visible range
     */
    public void notifyDuctHidden(Duct duct, Player p) {
        if (globalDuctManager.getPlayerDucts(p).remove(duct)) {
            protocolService.removeASD(p, globalDuctManager.getPlayerRenderSystem(p, duct.getDuctType().getBaseDuctType()).getASDForDuct(duct));
        }
    }

    public void updateNonDuctConnections(Duct duct) {

    }

} public class GlobalDuctManager {

    protected TransportPipes transportPipes;
    protected ProtocolService protocolService;
    protected DuctRegister ductRegister;
    protected PlayerSettingsService playerSettingsService;

    /**
     * ThreadSafe
     **/
    private Map<World, Map<BlockLocation, Duct>> ducts;
    /**
     * ThreadSafe
     **/
    private Map<Player, Set<Duct>> playerDucts;

    @Inject
    public GlobalDuctManager(TransportPipes transportPipes, ProtocolService protocolService, DuctRegister ductRegister, PlayerSettingsService playerSettingsService) {
        this.transportPipes = transportPipes;
        this.protocolService = protocolService;
        this.ductRegister = ductRegister;
        this.playerSettingsService = playerSettingsService;
        this.ducts = Collections.synchronizedMap(new HashMap<>());
        this.playerDucts = Collections.synchronizedMap(new HashMap<>());
    }

    public Map<World, Map<BlockLocation, Duct>> getDucts() {
        return ducts;
    }

    public Map<BlockLocation, Duct> getDucts(World world) {
        return ducts.computeIfAbsent(world, v -> Collections.synchronizedMap(new TreeMap<>()));
    }

    public Duct getDuctAtLoc(World world, BlockLocation blockLoc) {
        Map<BlockLocation, Duct> ductMap = getDucts(world);
        return ductMap.get(blockLoc);
    }

    public Duct getDuctAtLoc(Location location) {
        return getDuctAtLoc(location.getWorld(), new BlockLocation(location));
    }

    public Set<Duct> getPlayerDucts(Player player) {
        return playerDucts.computeIfAbsent(player, p -> Collections.synchronizedSet(new HashSet<>()));
    }

    public RenderSystem getPlayerRenderSystem(Player player, BaseDuctType<? extends Duct> baseDuctType) {
        PlayerSettingsConf conf = playerSettingsService.getOrCreateSettingsConf(player);
        return conf.getRenderSystem(baseDuctType);
    }

    public Duct createDuctObject(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk) {
        return ductType.getBaseDuctType().getDuctFactory().createDuct(ductType, blockLoc, world, chunk);
    }

    public void registerDuct(Duct duct) {
        getDucts(duct.getWorld()).put(duct.getBlockLoc(), duct);
    }

    public void unregisterDuct(Duct duct) {
        getDucts(duct.getWorld()).remove(duct.getBlockLoc());
    }

    public void registerDuctInRenderSystems(Duct duct, boolean updateForPlayers) {
        RenderSystem renderSystem;
        for (int i = 0; i < 2; i++) {
            renderSystem = i == 0 ? duct.getDuctType().getBaseDuctType().getVanillaRenderSystem() : duct.getDuctType().getBaseDuctType().getModelledRenderSystem();
            if (renderSystem != null) {

                renderSystem.createDuctASD(duct, duct.getAllConnections());

                if (updateForPlayers) {
                    for (Player p : WorldUtils.getPlayerList(duct.getWorld())) {
                        PlayerSettingsConf conf = playerSettingsService.getOrCreateSettingsConf(p);
                        if (duct.getBlockLoc().toLocation(p.getWorld()).distance(p.getLocation()) <= conf.getRenderDistance()) {
                            if (conf.getRenderSystem(duct.getDuctType().getBaseDuctType()).equals(renderSystem)) {
                                getPlayerDucts(p).add(duct);
                                protocolService.sendASD(p, duct.getBlockLoc(), renderSystem.getASDForDuct(duct));
                            }
                        }
                    }
                }
            }
        }
    }

    public void updateDuctInRenderSystems(Duct duct, boolean updateForPlayers) {
        RenderSystem renderSystem;
        for (int i = 0; i < 2; i++) {
            renderSystem = i == 0 ? duct.getDuctType().getBaseDuctType().getVanillaRenderSystem() : duct.getDuctType().getBaseDuctType().getModelledRenderSystem();
            if (renderSystem != null) {

                List<ArmorStandData> removeASD = new ArrayList<>();
                List<ArmorStandData> addASD = new ArrayList<>();
                renderSystem.updateDuctASD(duct, duct.getAllConnections(), removeASD, addASD);

                if (updateForPlayers) {
                    for (Player p : WorldUtils.getPlayerList(duct.getWorld())) {
                        PlayerSettingsConf conf = playerSettingsService.getOrCreateSettingsConf(p);
                        if (conf.getRenderSystem(duct.getDuctType().getBaseDuctType()).equals(renderSystem)) {
                            if (getPlayerDucts(p).contains(duct)) {
                                protocolService.removeASD(p, removeASD);
                                protocolService.sendASD(p, duct.getBlockLoc(), addASD);
                            }
                        }
                    }
                }
            }
        }
    }

    public void unregisterDuctInRenderSystem(Duct duct, boolean updateForPlayers) {
        RenderSystem renderSystem;
        for (int i = 0; i < 2; i++) {
            renderSystem = i == 0 ? duct.getDuctType().getBaseDuctType().getVanillaRenderSystem() : duct.getDuctType().getBaseDuctType().getModelledRenderSystem();
            if (renderSystem != null) {
                if (updateForPlayers) {
                    for (Player p : WorldUtils.getPlayerList(duct.getWorld())) {
                        PlayerSettingsConf conf = playerSettingsService.getOrCreateSettingsConf(p);
                        if (conf.getRenderSystem(duct.getDuctType().getBaseDuctType()).equals(renderSystem)) {
                            if (getPlayerDucts(p).remove(duct)) {
                                protocolService.removeASD(p, renderSystem.getASDForDuct(duct));
                            }
                        }
                    }
                }

                renderSystem.destroyDuctASD(duct);

            }
        }
    }

    public void playDuctDestroyActions(Duct duct, Player destroyer) {
        List<ItemStack> dropItems = duct.destroyed(transportPipes, duct.getDuctType().getBaseDuctType().getDuctManager(), destroyer);
        transportPipes.runTaskSync(() -> {
            for (ItemStack is : dropItems) {
                duct.getWorld().dropItem(duct.getBlockLoc().toLocation(duct.getWorld()), is);
            }
        });
    }

    /**
     * recalculates the duct connections (in case of pipes, updates the container connections, too) of this duct and saves them.
     * Also notifies the duct about this change
     */
    public void updateDuctConnections(Duct duct) {
        //update duct connections
        duct.getDuctConnections().clear();
        for (TPDirection tpDir : TPDirection.values()) {
            Duct neighborDuct = getDuctAtLoc(duct.getWorld(), duct.getBlockLoc().getNeighbor(tpDir));
            if (neighborDuct != null && duct.getDuctType().connectsTo(neighborDuct.getDuctType())) {
                duct.getDuctConnections().put(tpDir, neighborDuct);
            }
        }
        //update baseDuctType specific other connections
        duct.getDuctType().getBaseDuctType().getDuctManager().updateNonDuctConnections(duct);
        //notify connections change
        duct.notifyConnectionChange();
    }

    public void updateNeighborDuctsConnections(Duct duct) {
        for (TPDirection ductConn : duct.getDuctConnections().keySet()) {
            updateDuctConnections(duct.getDuctConnections().get(ductConn));
        }
    }

    public void updateNeighborDuctsInRenderSystems(Duct duct, boolean updateForPlayers) {
        for (TPDirection ductConn : duct.getDuctConnections().keySet()) {
            updateDuctInRenderSystems(duct.getDuctConnections().get(ductConn), updateForPlayers);
        }
    }

    public void tick() {
        for (BaseDuctType<? extends Duct> baseDuctType : ductRegister.baseDuctTypes()) {
            baseDuctType.getDuctManager().tick();
        }
    }

} public class PipeManager extends DuctManager<Pipe> {

    private static final long BIG_TICK_COUNT = 10;

    private PlayerSettingsService playerSettingsService;
    private GeneralConf generalConf;

    /**
     * ThreadSafe
     **/
    private Map<World, Map<BlockLocation, TransportPipesContainer>> containers;

    /**
     * THREAD-SAFE
     */
    private Map<Player, Set<PipeItem>> playerItems;

    private ShapedRecipe wrenchRecipe;

    private long tickCounter;

    @Inject
    public PipeManager(TransportPipes transportPipes, DuctRegister ductRegister, GlobalDuctManager globalDuctManager, ProtocolService protocolService, ItemService itemService, PlayerSettingsService playerSettingsService, GeneralConf generalConf) {
        super(transportPipes, ductRegister, globalDuctManager, protocolService, itemService);
        this.playerSettingsService = playerSettingsService;
        this.generalConf = generalConf;
        playerItems = Collections.synchronizedMap(new HashMap<>());
        containers = Collections.synchronizedMap(new HashMap<>());
        tickCounter = 0;
    }

    public Map<World, Map<BlockLocation, TransportPipesContainer>> getContainers() {
        return containers;
    }

    public Map<BlockLocation, TransportPipesContainer> getContainers(World world) {
        return containers.computeIfAbsent(world, v -> Collections.synchronizedMap(new TreeMap<>()));
    }

    public TransportPipesContainer getContainerAtLoc(World world, BlockLocation blockLoc) {
        Map<BlockLocation, TransportPipesContainer> containerMap = getContainers(world);
        return containerMap.get(blockLoc);
    }

    public TransportPipesContainer getContainerAtLoc(Location location) {
        return getContainerAtLoc(location.getWorld(), new BlockLocation(location));
    }

    @Override
    public void updateNonDuctConnections(Duct duct) {
        Pipe pipe = (Pipe) duct;
        pipe.getContainerConnections().clear();
        for (TPDirection tpDir : TPDirection.values()) {
            TransportPipesContainer neighborContainer = getContainerAtLoc(pipe.getWorld(), pipe.getBlockLoc().getNeighbor(tpDir));
            if (neighborContainer != null) {
                pipe.getContainerConnections().put(tpDir, neighborContainer);
            }
        }
    }

    @Override
    public void registerDuctTypes() {
        PipeType pipeType;
        BaseDuctType<Pipe> pipeBaseDuctType = ductRegister.baseDuctTypeOf("pipe");

        pipeType = new ColoredPipeType(pipeBaseDuctType, "White", LangConf.Key.PIPES_WHITE.get(), Material.BONE_MEAL, "transportpipes.craft.coloredpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new ColoredPipeType(pipeBaseDuctType, "Blue", LangConf.Key.PIPES_BLUE.get(), Material.LAPIS_LAZULI, "transportpipes.craft.coloredpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new ColoredPipeType(pipeBaseDuctType, "Red", LangConf.Key.PIPES_RED.get(), LegacyUtils.getInstance().getRedDye(), "transportpipes.craft.coloredpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new ColoredPipeType(pipeBaseDuctType, "Yellow", LangConf.Key.PIPES_YELLOW.get(), LegacyUtils.getInstance().getYellowDye(), "transportpipes.craft.coloredpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new ColoredPipeType(pipeBaseDuctType, "Green", LangConf.Key.PIPES_GREEN.get(), LegacyUtils.getInstance().getGreenDye(), "transportpipes.craft.coloredpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new ColoredPipeType(pipeBaseDuctType, "Black", LangConf.Key.PIPES_BLACK.get(), Material.INK_SAC, "transportpipes.craft.coloredpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new PipeType(pipeBaseDuctType, "Golden", LangConf.Key.PIPES_GOLDEN.get(), "transportpipes.craft.goldenpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new PipeType(pipeBaseDuctType, "Iron", LangConf.Key.PIPES_IRON.get(), "transportpipes.craft.ironpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new PipeType(pipeBaseDuctType, "Ice", LangConf.Key.PIPES_ICE.get(), "transportpipes.craft.icepipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new PipeType(pipeBaseDuctType, "Void", LangConf.Key.PIPES_VOID.get(), "transportpipes.craft.voidpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new PipeType(pipeBaseDuctType, "Extraction", LangConf.Key.PIPES_EXTRACTION.get(), "transportpipes.craft.extractionpipe");
        pipeBaseDuctType.registerDuctType(pipeType);
        pipeType = new PipeType(pipeBaseDuctType, "Crafting", LangConf.Key.PIPES_CRAFTING.get(), "transportpipes.craft.craftingpipe");
        pipeBaseDuctType.registerDuctType(pipeType);

        //connect correctly
        pipeBaseDuctType.ductTypeOf("White").connectToAll();
        pipeBaseDuctType.ductTypeOf("Blue").connectToAll().disconnectFromClasses(ColoredPipeType.class).connectTo("White", "Blue");
        pipeBaseDuctType.ductTypeOf("Red").connectToAll().disconnectFromClasses(ColoredPipeType.class).connectTo("White", "Red");
        pipeBaseDuctType.ductTypeOf("Yellow").connectToAll().disconnectFromClasses(ColoredPipeType.class).connectTo("White", "Yellow");
        pipeBaseDuctType.ductTypeOf("Green").connectToAll().disconnectFromClasses(ColoredPipeType.class).connectTo("White", "Green");
        pipeBaseDuctType.ductTypeOf("Black").connectToAll().disconnectFromClasses(ColoredPipeType.class).connectTo("White", "Black");
        pipeBaseDuctType.ductTypeOf("Golden").connectToAll();
        pipeBaseDuctType.ductTypeOf("Iron").connectToAll();
        pipeBaseDuctType.ductTypeOf("Ice").connectToAll();
        pipeBaseDuctType.ductTypeOf("Void").connectToAll();
        pipeBaseDuctType.ductTypeOf("Extraction").connectToAll();
        pipeBaseDuctType.ductTypeOf("Crafting").connectToAll();

    }

    @Override
    public void registerRecipes() {
        BaseDuctType<Pipe> pipeBaseDuctType = ductRegister.baseDuctTypeOf("pipe");

        DuctType ductType;

        ductType = pipeBaseDuctType.ductTypeOf("White");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "white_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "a a", " a "}, 'a', Material.GLASS));
        ductType = pipeBaseDuctType.ductTypeOf("Blue");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "blue_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', Material.LAPIS_LAZULI));
        ductType = pipeBaseDuctType.ductTypeOf("Red");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "red_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', LegacyUtils.getInstance().getRedDye()));
        ductType = pipeBaseDuctType.ductTypeOf("Yellow");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "yellow_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', LegacyUtils.getInstance().getYellowDye()));
        ductType = pipeBaseDuctType.ductTypeOf("Green");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "green_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', LegacyUtils.getInstance().getGreenDye()));
        ductType = pipeBaseDuctType.ductTypeOf("Black");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "black_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', Material.INK_SAC));
        ductType = pipeBaseDuctType.ductTypeOf("Golden");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "golden_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', Material.GOLD_BLOCK));
        ductType = pipeBaseDuctType.ductTypeOf("Iron");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "iron_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', Material.IRON_BLOCK));
        ductType = pipeBaseDuctType.ductTypeOf("Ice");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "ice_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', Material.SNOW_BLOCK));
        ductType = pipeBaseDuctType.ductTypeOf("Void");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "void_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', Material.OBSIDIAN));
        ductType = pipeBaseDuctType.ductTypeOf("Extraction");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "extraction_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', Tag.PLANKS.getValues()));
        ductType = pipeBaseDuctType.ductTypeOf("Crafting");
        ductType.setDuctRecipe(itemService.createShapedRecipe(transportPipes, "crafting_pipe", pipeBaseDuctType.getItemManager().getClonedItem(ductType), new String[]{" a ", "aba", " a "}, 'a', Material.GLASS, 'b', Material.CRAFTING_TABLE));

        wrenchRecipe = itemService.createShapedRecipe(transportPipes, "wrench", itemService.getWrench(), new String[]{" a ", "aba", " a "}, 'a', Material.REDSTONE, 'b', Material.STICK);
        Bukkit.addRecipe(wrenchRecipe);
    }

    public ShapedRecipe getWrenchRecipe() {
        return wrenchRecipe;
    }

    @Override
    public void tick() {
        tickCounter++;
        tickCounter %= BIG_TICK_COUNT;
        boolean bigTick = tickCounter == 0;

        if(bigTick) {
            transportPipes.runTaskSync(() -> {
                Set<World> worlds = globalDuctManager.getDucts().keySet();
                synchronized (globalDuctManager.getDucts()) {
                    for (World world : worlds) {
                        Map<BlockLocation, Duct> ductMap = globalDuctManager.getDucts().get(world);
                        if (ductMap != null) {
                            for (Duct duct : ductMap.values()) {
                                if (duct instanceof Pipe && duct.isInLoadedChunk()) {
                                    duct.syncBigTick(this);
                                }
                            }
                        }
                    }
                }
            });
        }

        Set<World> worlds = globalDuctManager.getDucts().keySet();
        synchronized (globalDuctManager.getDucts()) {
            for (World world : worlds) {
                Map<BlockLocation, Duct> ductMap = globalDuctManager.getDucts().get(world);
                if (ductMap != null) {
                    for (Duct duct : ductMap.values()) {
                        if (duct instanceof Pipe && duct.isInLoadedChunk()) {
                            duct.tick(bigTick, transportPipes, this);
                        }
                    }
                    for (Duct duct : ductMap.values()) {
                        if (duct instanceof Pipe && duct.isInLoadedChunk()) {
                            duct.postTick(bigTick, transportPipes, this, generalConf);
                        }
                    }
                }
            }
        }

    }

    public Set<PipeItem> getPlayerPipeItems(Player player) {
        return playerItems.computeIfAbsent(player, p -> Collections.synchronizedSet(new HashSet<>()));
    }

    public void spawnPipeItem(PipeItem pipeItem) {
        List<Player> playerList = WorldUtils.getPlayerList(pipeItem.getWorld());
        for (Player p : playerList) {
            PlayerSettingsConf conf = playerSettingsService.getOrCreateSettingsConf(p);
            int renderDistance = conf.getRenderDistance();
            if (conf.isShowItems() && p.getLocation().distance(pipeItem.getBlockLoc().toLocation(pipeItem.getWorld())) <= renderDistance) {
                getPlayerPipeItems(p).add(pipeItem);
                protocolService.sendPipeItem(p, pipeItem);
            }
        }
    }

    public void putPipeItemInPipe(PipeItem pipeItem) {
        Pipe pipeAtBlockLoc = (Pipe) globalDuctManager.getDuctAtLoc(pipeItem.getWorld(), pipeItem.getBlockLoc());
        if (pipeAtBlockLoc == null) {
            throw new IllegalStateException("pipe item can't be created because on the given location is no pipe");
        }
        pipeAtBlockLoc.putPipeItem(pipeItem);
    }

    public void updatePipeItemPosition(PipeItem pipeItem) {
        List<Player> playerList = WorldUtils.getPlayerList(pipeItem.getWorld());
        for (Player p : playerList) {
            if (getPlayerPipeItems(p).contains(pipeItem)) {
                protocolService.updatePipeItem(p, pipeItem);
            }
        }
    }

    public void despawnPipeItem(PipeItem pipeItem) {
        List<Player> playerList = WorldUtils.getPlayerList(pipeItem.getWorld());
        for (Player p : playerList) {
            if (getPlayerPipeItems(p).remove(pipeItem)) {
                protocolService.removePipeItem(p, pipeItem);
            }
        }
    }

    @Override
    public void notifyDuctShown(Duct duct, Player p) {
        super.notifyDuctShown(duct, p);
        if (!playerSettingsService.getOrCreateSettingsConf(p).isShowItems()) {
            return;
        }
        Pipe pipe = (Pipe) duct;
        Set<PipeItem> playerPipeItems = getPlayerPipeItems(p);
        for (PipeItem pipeItem : pipe.getItems()) {
            if (playerPipeItems.add(pipeItem)) {
                protocolService.sendPipeItem(p, pipeItem);
            }
        }
    }

    @Override
    public void notifyDuctHidden(Duct duct, Player p) {
        super.notifyDuctHidden(duct, p);
        Pipe pipe = (Pipe) duct;
        Set<PipeItem> playerPipeItems = getPlayerPipeItems(p);
        for (PipeItem pipeItem : pipe.getItems()) {
            if (playerPipeItems.remove(pipeItem)) {
                protocolService.removePipeItem(p, pipeItem);
            }
        }
        for (PipeItem pipeItem : pipe.getFutureItems()) {
            if (playerPipeItems.remove(pipeItem)) {
                protocolService.removePipeItem(p, pipeItem);
            }
        }
        for (PipeItem pipeItem : pipe.getUnloadedItems()) {
            if (playerPipeItems.remove(pipeItem)) {
                protocolService.removePipeItem(p, pipeItem);
            }
        }
    }
} 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 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 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;
    }

} public enum TPDirection {

    EAST(1, 0, 0, BlockFace.EAST, LangConf.Key.DIRECTIONS_EAST.get()),
    WEST(-1, 0, 0, BlockFace.WEST, LangConf.Key.DIRECTIONS_WEST.get()),
    SOUTH(0, 0, 1, BlockFace.SOUTH, LangConf.Key.DIRECTIONS_SOUTH.get()),
    NORTH(0, 0, -1, BlockFace.NORTH, LangConf.Key.DIRECTIONS_NORTH.get()),
    UP(0, 1, 0, BlockFace.UP, LangConf.Key.DIRECTIONS_UP.get()),
    DOWN(0, -1, 0, BlockFace.DOWN, LangConf.Key.DIRECTIONS_DOWN.get());

    private Vector vec;
    private BlockFace blockFace;
    private String displayName;

    TPDirection(int x, int y, int z, BlockFace blockFace, String displayName) {
        this.vec = new Vector(x, y, z);
        this.blockFace = blockFace;
        this.displayName = displayName;
    }

    public Vector getVector() {
        return vec.clone();
    }

    public int getX() {
        return vec.getBlockX();
    }

    public int getY() {
        return vec.getBlockY();
    }

    public int getZ() {
        return vec.getBlockZ();
    }

    public BlockFace getBlockFace() {
        return blockFace;
    }

    public TPDirection getOpposite() {
        return fromBlockFace(getBlockFace().getOppositeFace());
    }

    public boolean isSide() {
        return vec.getBlockY() == 0;
    }

    public String getDisplayName() {
        return displayName;
    }

    public TPDirection next() {
        int ordinal = ordinal();
        ordinal++;
        ordinal %= values().length;
        return values()[ordinal];
    }

    public static TPDirection fromBlockFace(BlockFace blockFace) {
        for (TPDirection tpDir : TPDirection.values()) {
            if (tpDir.getBlockFace().equals(blockFace)) {
                return tpDir;
            }
        }
        return null;
    }

}
import org.bukkit.inventory.ItemStack;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import de.robotricker.transportpipes.api.TransportPipesContainer;
import de.robotricker.transportpipes.duct.Duct;
import de.robotricker.transportpipes.duct.DuctRegister;
import de.robotricker.transportpipes.duct.manager.DuctManager;
import de.robotricker.transportpipes.duct.manager.GlobalDuctManager;
import de.robotricker.transportpipes.duct.manager.PipeManager;
import de.robotricker.transportpipes.duct.pipe.CraftingPipe;
import de.robotricker.transportpipes.duct.pipe.Pipe;
import de.robotricker.transportpipes.location.BlockLocation;
import de.robotricker.transportpipes.location.TPDirection;
package de.robotricker.transportpipes.duct.pipe.filter;





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());