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 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 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 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 enum ExtractAmount {

    EXTRACT_1(LangConf.Key.EXTRACT_AMOUNT_EXTRACT_1.get(), 1),
    EXTRACT_16(LangConf.Key.EXTRACT_AMOUNT_EXTRACT_16.get(), 16);

    private String displayName;
    private ItemStack displayItem;

    ExtractAmount(String displayName, int amount) {
        this.displayName = displayName;
        this.displayItem = new ItemStack(Material.BRICKS, amount);
    }

    public int getAmount() {
        return displayItem.getAmount();
    }

    public String getDisplayName() {
        return displayName;
    }

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

    public ItemStack getDisplayItem() {
        return displayItem.clone();
    }

} public enum ExtractCondition {

    NEEDS_REDSTONE(LangConf.Key.EXTRACT_CONDITION_NEEDS_REDSTONE.get(), Material.REDSTONE),
    ALWAYS_EXTRACT(LangConf.Key.EXTRACT_CONDITION_ALWAYS_EXTRACT.get(), Material.LIME_DYE),
    NEVER_EXTRACT(LangConf.Key.EXTRACT_CONDITION_NEVER_EXTRACT.get(), Material.BARRIER);

    private String displayName;
    private ItemStack displayItem;

    ExtractCondition(String displayName, Material type) {
        this.displayName = displayName;
        this.displayItem = new ItemStack(type);
    }

    public String getDisplayName() {
        return displayName;
    }

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

    public ItemStack getDisplayItem() {
        return displayItem.clone();
    }

} public enum FilterMode {

    NORMAL(LangConf.Key.FILTER_MODE_NORMAL.get()),
    INVERTED(LangConf.Key.FILTER_MODE_INVERTED.get()),
    BLOCK_ALL(LangConf.Key.FILTER_MODE_BLOCKALL.get());

    private String displayName;

    FilterMode(String displayName) {
        this.displayName = displayName;
    }

    public String getDisplayName() {
        return displayName;
    }

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

} public enum FilterStrictness {

    MATERIAL(LangConf.Key.FILTER_STRICTNESS_MATERIAL.get()),
    MATERIAL_METADATA(LangConf.Key.FILTER_STRICTNESS_MATERIAL_METADATA.get());

    private String displayName;

    FilterStrictness(String displayName) {
        this.displayName = displayName;
    }

    public String getDisplayName() {
        return displayName;
    }

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

} public class ItemData {

    private ItemStack backedItem;

    public ItemData(ItemStack item) {
        this.backedItem = item.clone();
        this.backedItem.setAmount(1);
    }

    public ItemStack toItemStack() {
        return backedItem.clone();
    }

    @Override
    public int hashCode() {
        int hash = 1;

        hash = hash * 31 + backedItem.getType().ordinal();
        hash = hash * 31 + (backedItem.hasItemMeta() ? backedItem.getItemMeta().hashCode() : 0);

        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ItemData other = (ItemData) obj;
        if (backedItem == null) {
            return other.backedItem == null;
        } else return backedItem.isSimilar(other.backedItem);
    }

    @Override
    public String toString() {
        return backedItem.toString();
    }
} public class ItemFilter {

    public static final int MAX_ITEMS_PER_ROW = 32;

    private ItemData[] filterItems;
    private FilterMode filterMode;
    private FilterStrictness filterStrictness;

    public ItemFilter() {
        filterItems = new ItemData[MAX_ITEMS_PER_ROW];
        filterMode = FilterMode.NORMAL;
        filterStrictness = FilterStrictness.MATERIAL_METADATA;
    }

    public ItemData[] getFilterItems() {
        return filterItems;
    }

    public FilterMode getFilterMode() {
        return filterMode;
    }

    public void setFilterMode(FilterMode filterMode) {
        this.filterMode = filterMode;
    }

    public FilterStrictness getFilterStrictness() {
        return filterStrictness;
    }

    public void setFilterStrictness(FilterStrictness filterStrictness) {
        this.filterStrictness = filterStrictness;
    }

    public int applyFilter(ItemStack item) {
        if (item == null || getFilterMode() == FilterMode.BLOCK_ALL) {
            return 0;
        }
        if (getFilterMode() == FilterMode.NORMAL) {
            int weight = 0;
            for (ItemData id : filterItems) {
                if (id != null && matchesItemStrictness(id.toItemStack(), item)) {
                    weight++;
                }
            }
            return weight;
        }
        if (getFilterMode() == FilterMode.INVERTED) {
            for (ItemData id : filterItems) {
                if (id != null && matchesItemStrictness(id.toItemStack(), item)) {
                    return 0;
                }
            }
            return 1;
        }
        return 0;
    }

    private boolean matchesItemStrictness(ItemStack mask, ItemStack itemStack) {
        switch (getFilterStrictness()) {
            case MATERIAL:
                return mask.getType() == itemStack.getType();
            case MATERIAL_METADATA:
                return mask.getType() == itemStack.getType() && Bukkit.getItemFactory().equals(mask.getItemMeta(), itemStack.getItemMeta());
            default:
                return false;
        }
    }

    public List<ItemStack> getAsItemStacks() {
        List<ItemStack> itemStacks = new ArrayList<>();
        for (int i = 0; i < MAX_ITEMS_PER_ROW; i++) {
            if (filterItems[i] != null) {
                itemStacks.add(filterItems[i].toItemStack());
            }
        }
        return itemStacks;
    }

    public void saveToNBTTag(CompoundTag compoundTag, ItemService itemService) {
        compoundTag.putInt("filterMode", filterMode.ordinal());
        compoundTag.putInt("filterStrictness", filterStrictness.ordinal());
        ListTag<StringTag> itemDataListTag = new ListTag<>(StringTag.class);
        for (int i = 0; i < MAX_ITEMS_PER_ROW; i++) {
            ItemData itemData = filterItems[i];
            if (itemData == null) {
                itemDataListTag.add(new StringTag(null));
            } else {
                itemDataListTag.addString(itemService.serializeItemStack(itemData.toItemStack()));
            }
        }
        compoundTag.put("filterItems", itemDataListTag);
    }

    public void loadFromNBTTag(CompoundTag compoundTag, ItemService itemService) {
        filterMode = FilterMode.values()[compoundTag.getInt("filterMode")];
        filterStrictness = FilterStrictness.values()[compoundTag.getInt("filterStrictness")];
        ListTag<StringTag> itemDataListTag = (ListTag<StringTag>) compoundTag.getListTag("filterItems");
        for (int i = 0; i < MAX_ITEMS_PER_ROW; i++) {
            if(i >= itemDataListTag.size()) {
                filterItems[i] = null;
                continue;
            }
            ItemStack deserialized = itemService.deserializeItemStack(itemDataListTag.get(i).getValue());
            filterItems[i] = deserialized != null ? new ItemData(deserialized) : null;
        }
    }

} public class PipeItem {

    private ArmorStandData asd;
    private ItemStack item;
    private World world;
    private BlockLocation blockLoc;
    private RelativeLocation oldRelativeLocation;
    private RelativeLocation relativeLocation;
    private TPDirection movingDir;

    public PipeItem() {
    }

    public PipeItem(ItemStack item, World world, BlockLocation blockLoc, TPDirection movingDir) {
        this.item = item;
        this.blockLoc = blockLoc;
        this.movingDir = movingDir;
        init(world, true);
    }

    public PipeItem(ItemStack item, World world, BlockLocation blockLoc, RelativeLocation relLoc, TPDirection movingDir) {
        this.item = item;
        this.blockLoc = blockLoc;
        this.relativeLocation = relLoc;
        this.movingDir = movingDir;
        init(world, false);
    }

    public void init(World world, boolean initRelLoc) {
        this.world = world;
        this.asd = new ArmorStandData(new RelativeLocation(0.25f, 0f, 0.33f), true, new Vector(1, 0, 0), new Vector(0f, 0f, 0f), new Vector(-30f, 0f, 0f), null, item);
        if (initRelLoc)
            this.relativeLocation = new RelativeLocation(movingDir.getX() > 0 ? 0 : (movingDir.getX() < 0 ? 1 : 0.5f), movingDir.getY() > 0 ? 0 : (movingDir.getY() < 0 ? 1 : 0.5f), movingDir.getZ() > 0 ? 0 : (movingDir.getZ() < 0 ? 1 : 0.5f));
        resetOldRelativeLocation();
    }

    public ArmorStandData getAsd() {
        return asd;
    }

    public ItemStack getItem() {
        return item;
    }

    public World getWorld() {
        return world;
    }

    public BlockLocation getBlockLoc() {
        return blockLoc;
    }

    public void setBlockLoc(BlockLocation blockLoc) {
        this.blockLoc = blockLoc;
    }

    public RelativeLocation getOldRelativeLocation() {
        return oldRelativeLocation;
    }

    public RelativeLocation getRelativeLocation() {
        return relativeLocation;
    }

    public RelativeLocation getRelativeLocationDifference() {
        return relativeLocation.clone().add(-oldRelativeLocation.getLongX(), -oldRelativeLocation.getLongY(), -oldRelativeLocation.getLongZ());
    }

    public void resetOldRelativeLocation() {
        oldRelativeLocation = relativeLocation.clone();
    }

    public TPDirection getMovingDir() {
        return movingDir;
    }

    public void setMovingDir(TPDirection movingDir) {
        this.movingDir = movingDir;
    }

    public void saveToNBTTag(CompoundTag compoundTag, ItemService itemService) {
        compoundTag.putString("itemStack", itemService.serializeItemStack(item));
        compoundTag.putString("blockLoc", blockLoc.toString());
        compoundTag.putString("relLoc", relativeLocation.toString());
        compoundTag.putInt("movingDir", movingDir.ordinal());
    }

    public void loadFromNBTTag(CompoundTag compoundTag, World world, ItemService itemService) {
        item = itemService.deserializeItemStack(compoundTag.getString("itemStack"));
        blockLoc = BlockLocation.fromString(compoundTag.getString("blockLoc"));
        relativeLocation = RelativeLocation.fromString(compoundTag.getString("relLoc"));
        movingDir = TPDirection.values()[compoundTag.getInt("movingDir")];
        init(world, false);
    }

} 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 ItemService {

    private ItemStack wrench;
    private YamlConfiguration tempConf;

    @Inject
    public ItemService(GeneralConf generalConf) {
        Material wrenchMaterial = Material.getMaterial(generalConf.getWrenchItem().toUpperCase(Locale.ENGLISH));
        Objects.requireNonNull(wrenchMaterial, "The material for the wrench item set in the config file is not valid.");

        wrench = generalConf.getWrenchGlowing() ? createGlowingItem(wrenchMaterial) : new ItemStack(wrenchMaterial);
        wrench = changeDisplayNameAndLoreConfig(wrench, LangConf.Key.WRENCH.getLines());
        tempConf = new YamlConfiguration();
    }

    public ItemStack getWrench() {
        return wrench;
    }

    public boolean isWrench(ItemStack item) {
        if (item == null || !item.hasItemMeta() || !item.getItemMeta().hasDisplayName()) {
            return false;
        }
        return item.getItemMeta().getDisplayName().equals(LangConf.Key.WRENCH.getLines().get(0));
    }

    public ItemStack createModelledItem(int damage) {
        ItemStack woodenPickage = new ItemStack(Material.WOODEN_PICKAXE);
        ItemMeta meta = woodenPickage.getItemMeta();

        ((Damageable) meta).setDamage(damage);
        meta.setUnbreakable(true);
        woodenPickage.setItemMeta(meta);

        return woodenPickage;
    }

    public ItemStack createGlowingItem(Material material) {
        ItemStack is = new ItemStack(material);
        ItemMeta im = is.getItemMeta();
        im.addEnchant(Enchantment.PROTECTION_ENVIRONMENTAL, 1, true);
        im.addItemFlags(ItemFlag.HIDE_ENCHANTS);
        is.setItemMeta(im);
        return is;
    }

    public ItemStack changeDisplayName(ItemStack is, String displayName) {
        ItemMeta im = is.getItemMeta();
        im.setDisplayName(displayName);
        is.setItemMeta(im);
        return is;
    }

    public ItemStack changeDisplayNameAndLore(ItemStack is, String... displayNameAndLore) {
        ItemMeta meta = is.getItemMeta();
        if (displayNameAndLore.length > 0)
            meta.setDisplayName(displayNameAndLore[0]);
        if (displayNameAndLore.length > 1)
            meta.setLore(Arrays.asList(displayNameAndLore).subList(1, displayNameAndLore.length));
        is.setItemMeta(meta);
        return is;
    }

    public ItemStack changeDisplayNameAndLoreConfig(ItemStack is, String displayName, List<String> lore) {
        ItemMeta meta = is.getItemMeta();
        meta.setDisplayName(displayName);
        meta.setLore(lore);
        is.setItemMeta(meta);
        return is;
    }

    public ItemStack changeDisplayNameAndLoreConfig(ItemStack is, List<String> displayNameAndLore) {
        ItemMeta meta = is.getItemMeta();
        if (displayNameAndLore.size() > 0)
            meta.setDisplayName(displayNameAndLore.get(0));
        if (displayNameAndLore.size() > 1)
            meta.setLore(displayNameAndLore.subList(1, displayNameAndLore.size()));
        is.setItemMeta(meta);
        return is;
    }

    public ItemStack createHeadItem(String uuid, String textureValue, String textureSignature) {
        WrappedGameProfile wrappedProfile = new WrappedGameProfile(UUID.fromString(uuid), null);
        wrappedProfile.getProperties().put("textures", new WrappedSignedProperty("textures", textureValue, textureSignature));

        ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
        SkullMeta sm = (SkullMeta) skull.getItemMeta();
        sm.setOwningPlayer(Bukkit.getOfflinePlayer(UUID.fromString(uuid)));

        Field profileField;
        try {
            profileField = sm.getClass().getDeclaredField("profile");
            profileField.setAccessible(true);
            profileField.set(sm, wrappedProfile.getHandle());
        } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e1) {
            e1.printStackTrace();
        }

        skull.setItemMeta(sm);
        return skull;
    }

    public ItemStack setDuctNBTTags(DuctType dt, ItemStack item) {
        item = NMSUtils.manipulateItemStackNBT(item, "basicDuctType", dt.getBaseDuctType().getName(), String.class, "String");
        item = NMSUtils.manipulateItemStackNBT(item, "ductType", dt.getName(), String.class, "String");
        return item;
    }

    public DuctType readDuctNBTTags(ItemStack item, DuctRegister ductRegister) {
        String basicDuctTypeSerialized = (String) NMSUtils.readItemStackNBT(item, "basicDuctType", "String");
        if (basicDuctTypeSerialized != null && !basicDuctTypeSerialized.isEmpty()) {
            BaseDuctType bdt = ductRegister.baseDuctTypeOf(basicDuctTypeSerialized);
            String ductTypeSerialized = (String) NMSUtils.readItemStackNBT(item, "ductType", "String");
            if (ductTypeSerialized != null && !ductTypeSerialized.isEmpty()) {
                return bdt.ductTypeOf(ductTypeSerialized);
            }
        } else {
            //legacy ductType loading (with the gameprofile uuids of the skulls)
            if (item.getItemMeta() instanceof SkullMeta) {

                SkullMeta sm = (SkullMeta) item.getItemMeta();
                try {

                    Field profileField = sm.getClass().getDeclaredField("profile");
                    profileField.setAccessible(true);
                    Object profile = profileField.get(sm);
                    if (profile == null)
                        return null;

                    Field idField = Class.forName("com.mojang.authlib.GameProfile").getDeclaredField("id");
                    idField.setAccessible(true);
                    UUID id = (UUID) idField.get(profile);
                    if (id == null)
                        return null;

                    for (DuctType dt : ductRegister.baseDuctTypeOf("pipe").ductTypes()) {
                        Object dtProfile = profileField.get(dt.getBaseDuctType().getItemManager().getItem(dt).getItemMeta());
                        UUID dtId = (UUID) idField.get(dtProfile);

                        if (id.compareTo(dtId) == 0) {
                            return dt;
                        }
                    }
                } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | ClassNotFoundException e) {
                    e.printStackTrace();
                }

            }
        }
        return null;
    }

    public void populateInventoryLine(Inventory inv, int row, ItemStack... items) {
        for (int i = 0; i < 9; i++) {
            if (items.length > i && items[i] != null) {
                ItemStack is = items[i];
                inv.setItem(row * 9 + i, is);
            }
        }
    }

    public ItemStack createWildcardItem(Material material) {
        ItemStack glassPane = new ItemStack(material);
        return changeDisplayNameAndLore(glassPane, ChatColor.RESET.toString());
    }

    public ItemStack createBarrierItem() {
        return changeDisplayNameAndLore(new ItemStack(Material.BARRIER), ChatColor.RESET.toString());
    }

    public boolean isItemWildcardOrBarrier(ItemStack item) {
        if (item != null) {
            switch (item.getType()) {
                case GRAY_STAINED_GLASS_PANE:
                case BLACK_STAINED_GLASS_PANE:
                case RED_STAINED_GLASS_PANE:
                case BLUE_STAINED_GLASS_PANE:
                case LIME_STAINED_GLASS_PANE:
                case WHITE_STAINED_GLASS_PANE:
                case YELLOW_STAINED_GLASS_PANE:
                case BROWN_STAINED_GLASS_PANE:
                case CYAN_STAINED_GLASS_PANE:
                case GREEN_STAINED_GLASS_PANE:
                case LIGHT_BLUE_STAINED_GLASS_PANE:
                case LIGHT_GRAY_STAINED_GLASS_PANE:
                case MAGENTA_STAINED_GLASS_PANE:
                case ORANGE_STAINED_GLASS_PANE:
                case PINK_STAINED_GLASS_PANE:
                case PURPLE_STAINED_GLASS_PANE:
                case BARRIER:
                    if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) {
                        return item.getItemMeta().getDisplayName().equals(ChatColor.RESET.toString());
                    }
                default:
                    return false;
            }
        }
        return false;
    }

    public String serializeItemStack(ItemStack itemStack) {
        tempConf.set("itemStack", itemStack);
        String string = tempConf.saveToString();
        tempConf.set("itemStack", null);
        return string;
    }

    public ItemStack deserializeItemStack(String string) {
        try {
            tempConf.loadFromString(string);
        } catch (InvalidConfigurationException e) {
            e.printStackTrace();
        }
        ItemStack itemStack = tempConf.getItemStack("itemStack");
        tempConf.set("itemStack", null);
        return itemStack;
    }

    public ShapedRecipe createShapedRecipe(TransportPipes transportPipes, String recipeKey, ItemStack resultItem, String[] shape, Object... ingredientMap) {
        ShapedRecipe recipe = new ShapedRecipe(new NamespacedKey(transportPipes, recipeKey), resultItem);
        recipe.shape(shape);
        for (int i = 0; i < ingredientMap.length; i += 2) {
            char c = (char) ingredientMap[i];
            if (ingredientMap[i + 1] instanceof Material) {
                recipe.setIngredient(c, (Material) ingredientMap[i + 1]);
            } else {
                recipe.setIngredient(c, new RecipeChoice.MaterialChoice(((Collection<Material>) ingredientMap[i + 1]).stream().collect(Collectors.toList())));
            }
        }
        return recipe;
    }

    public ShapelessRecipe createShapelessRecipe(TransportPipes transportPipes, String recipeKey, ItemStack resultItem, Material... ingredients) {
        ShapelessRecipe recipe = new ShapelessRecipe(new NamespacedKey(transportPipes, recipeKey), resultItem);
        for (int i = 0; i < ingredients.length; i += 2) {
            recipe.addIngredient(ingredients[i]);
        }
        return recipe;
    }

} 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 class RelativeLocation implements Cloneable {

    public static final long PRECISION = 100000;

    private long x;
    private long y;
    private long z;

    public RelativeLocation(double x, double y, double z) {
        set(x, y, z);
    }

    public RelativeLocation(long x, long y, long z) {
        set(x, y, z);
    }

    public long getLongX() {
        return x;
    }

    public long getLongY() {
        return y;
    }

    public long getLongZ() {
        return z;
    }

    public double getDoubleX() {
        return x / 1d / PRECISION;
    }

    public double getDoubleY() {
        return y / 1d / PRECISION;
    }

    public double getDoubleZ() {
        return z / 1d / PRECISION;
    }

    public RelativeLocation set(double x, double y, double z) {
        this.x = (long) (x * PRECISION);
        this.y = (long) (y * PRECISION);
        this.z = (long) (z * PRECISION);
        return this;
    }

    public RelativeLocation set(long x, long y, long z) {
        this.x = x;
        this.y = y;
        this.z = z;
        return this;
    }

    public RelativeLocation add(double x, double y, double z) {
        set(getDoubleX() + x, getDoubleY() + y, getDoubleZ() + z);
        return this;
    }

    public RelativeLocation add(long x, long y, long z) {
        set(getLongX() + x, getLongY() + y, getLongZ() + z);
        return this;
    }

    public boolean isXEquals(double x) {
        return this.x == x * PRECISION;
    }

    public boolean isYEquals(double y) {
        return this.y == y * PRECISION;
    }

    public boolean isZEquals(double z) {
        return this.z == z * PRECISION;
    }

    public boolean isEquals(double x, double y, double z) {
        return isXEquals(x) && isYEquals(y) && isZEquals(z);
    }

    /**
     * switches all values in the following system:<br>
     * 1: 0<br>
     * 0: 1<br>
     * 0.5: 0.5
     */
    public void switchValues() {
        set(1d - getDoubleX(), 1d - getDoubleY(), 1d - getDoubleZ());
    }

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

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

    @Override
    public RelativeLocation clone() {
        return new RelativeLocation(getDoubleX(), getDoubleY(), getDoubleZ());
    }

    @Override
    public String toString() {
        return getLongX() + ", " + getLongY() + ", " + getLongZ();
    }

    public static RelativeLocation fromString(String s) {
        if (s == null) {
            return null;
        }
        String[] split = s.split(", ");
        if (split.length == 3) {
            try {
                long x = Long.parseLong(split[0]);
                long y = Long.parseLong(split[1]);
                long z = Long.parseLong(split[2]);
                return new RelativeLocation(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 net.querz.nbt.CompoundTag;
import net.querz.nbt.ListTag;
import org.bukkit.World;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import de.robotricker.transportpipes.duct.Duct;
import de.robotricker.transportpipes.duct.DuctRegister;
import de.robotricker.transportpipes.duct.manager.GlobalDuctManager;
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.IronPipe;
import de.robotricker.transportpipes.duct.pipe.Pipe;
import de.robotricker.transportpipes.duct.pipe.extractionpipe.ExtractAmount;
import de.robotricker.transportpipes.duct.pipe.extractionpipe.ExtractCondition;
import de.robotricker.transportpipes.duct.pipe.filter.FilterMode;
import de.robotricker.transportpipes.duct.pipe.filter.FilterStrictness;
import de.robotricker.transportpipes.duct.pipe.filter.ItemData;
import de.robotricker.transportpipes.duct.pipe.filter.ItemFilter;
import de.robotricker.transportpipes.duct.pipe.items.PipeItem;
import de.robotricker.transportpipes.duct.types.DuctType;
import de.robotricker.transportpipes.items.ItemService;
import de.robotricker.transportpipes.location.BlockLocation;
import de.robotricker.transportpipes.location.RelativeLocation;
import de.robotricker.transportpipes.location.TPDirection;
package de.robotricker.transportpipes.saving;






public class LegacyDuctLoader_v4_3_1 extends DuctLoader {

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

    @Override
    public void loadDuctsSync(World world, CompoundTag compoundTag) {
        ListTag<CompoundTag> listTag = (ListTag<CompoundTag>) compoundTag.getListTag("Ducts");

        synchronized (globalDuctManager.getDucts()) {
            Map<Duct, CompoundTag> ductCompoundTagMap = new HashMap<>();
            for (CompoundTag ductTag : listTag) {
                if (!ductTag.getString("DuctType").equals("PIPE")) {
                    continue;
                }
                DuctType ductType;

                String ductDetails = ductTag.getString("DuctDetails");
                String pipeType = ductDetails.split(";")[1].split(":")[1];
                if (pipeType.equals("COLORED")) {
                    String color = ductDetails.split(";")[2].split(":")[1];
                    ductType = ductRegister.baseDuctTypeOf("pipe").ductTypeOf(color);
                } else {
                    ductType = ductRegister.baseDuctTypeOf("pipe").ductTypeOf(pipeType);
                }

                String locString = ductTag.getString("DuctLocation");
                BlockLocation blockLoc = new BlockLocation((int) Double.parseDouble(locString.split(":")[1]), (int) Double.parseDouble(locString.split(":")[2]), (int) Double.parseDouble(locString.split(":")[3]));