public class TransportPipesAPI {

    private static TransportPipesAPI instance;

    @Inject
    private GlobalDuctManager globalDuctManager;

    @Inject
    private DuctRegister ductRegister;

    @Inject
    private ThreadService threadService;

    @Inject
    private TPContainerListener tpContainerListener;

    public TransportPipesAPI() {
        instance = this;
    }

    public void buildDuct(String baseDuctTypeName, String ductTypeName, BlockLocation blockLocation, World world, Chunk chunk) throws Exception {

        if (globalDuctManager.getDuctAtLoc(world, blockLocation) != null) {
            throw new Exception("Another duct exists at this location");
        }

        DuctType ductType = ductRegister.baseDuctTypeOf(baseDuctTypeName).ductTypeOf(ductTypeName);

        if (ductType.getBaseDuctType().is("pipe")) {
            for (TPDirection dir : TPDirection.values()) {
                if (WorldUtils.lwcProtection(blockLocation.toBlock(world).getRelative(dir.getBlockFace()))) {
                    throw new Exception("Cannot place duct next to protected container block");
                }
            }
        }

        Duct duct = globalDuctManager.createDuctObject(ductType, blockLocation, world, chunk);
        globalDuctManager.registerDuct(duct);
        globalDuctManager.updateDuctConnections(duct);
        globalDuctManager.registerDuctInRenderSystems(duct, true);
        globalDuctManager.updateNeighborDuctsConnections(duct);
        globalDuctManager.updateNeighborDuctsInRenderSystems(duct, true);
    }

    public void destroyDuct(BlockLocation blockLocation, World world) throws Exception {
        Duct duct = globalDuctManager.getDuctAtLoc(world, blockLocation);
        if (duct == null) {
            throw new Exception("There is no duct at this location");
        }
        globalDuctManager.unregisterDuct(duct);
        globalDuctManager.unregisterDuctInRenderSystem(duct, true);
        globalDuctManager.updateNeighborDuctsConnections(duct);
        globalDuctManager.updateNeighborDuctsInRenderSystems(duct, true);
        globalDuctManager.playDuctDestroyActions(duct, null);
    }

    public void putItemInPipe(Pipe pipe, ItemStack item, TPDirection direction) {
        ((PipeManager) (DuctManager<?>) ductRegister.baseDuctTypeOf("pipe").getDuctManager()).putPipeItemInPipe(new PipeItem(item, pipe.getWorld(), pipe.getBlockLoc(), direction));
    }

    public void registerTransportPipesContainer(TransportPipesContainer container, BlockLocation blockLocation, World world) throws Exception {
        PipeManager pipeManager = (PipeManager) (DuctManager<?>) ductRegister.baseDuctTypeOf("pipe").getDuctManager();
        if (pipeManager.getContainerAtLoc(blockLocation.toLocation(world)) != null || globalDuctManager.getDuctAtLoc(world, blockLocation) != null) {
            throw new Exception("The given location is not empty");
        }
        pipeManager.getContainers(world).put(blockLocation, container);

        for (TPDirection dir : TPDirection.values()) {
            Duct duct = globalDuctManager.getDuctAtLoc(world, blockLocation.getNeighbor(dir));
            if (duct instanceof Pipe) {
                globalDuctManager.updateDuctConnections(duct);
                globalDuctManager.updateDuctInRenderSystems(duct, true);
            }
        }
    }

    public void unregisterTransportPipesContainer(BlockLocation blockLocation, World world) throws Exception {
        PipeManager pipeManager = (PipeManager) (DuctManager<?>) ductRegister.baseDuctTypeOf("pipe").getDuctManager();
        TransportPipesContainer container = pipeManager.getContainerAtLoc(blockLocation.toLocation(world));
        if (container == null) {
            throw new Exception("There is no TransportPipesContainer at the given location");
        }
        pipeManager.getContainers(world).remove(blockLocation);

        for (TPDirection dir : TPDirection.values()) {
            Duct duct = globalDuctManager.getDuctAtLoc(world, blockLocation.getNeighbor(dir));
            if (duct instanceof Pipe) {
                globalDuctManager.updateDuctConnections(duct);
                globalDuctManager.updateDuctInRenderSystems(duct, true);
            }
        }
    }

    public int getTPS() {
        return threadService.getCurrentTPS();
    }

    public int getPreferredTPS() {
        return threadService.getPreferredTPS();
    }

    public void updateVanillaContainerBlock(Block block, boolean placed) {
        tpContainerListener.updateContainerBlock(block, placed, true);
    }

    public Duct getDuct(BlockLocation blockLocation, World world) {
        return globalDuctManager.getDuctAtLoc(world, blockLocation);
    }

    public int getDuctCount(World world) {
        return globalDuctManager.getDucts(world).size();
    }

    public static TransportPipesAPI getInstance() throws Exception {
        if (instance == null) {
            throw new Exception("TransportPipes is not yet initialized");
        }
        return instance;
    }
} @CommandAlias("tpipes|transportpipes|transportpipe|tpipe|pipes|pipe")
public class TPCommand extends BaseCommand {

    @Inject
    private ThreadService threadService;
    @Inject
    private JavaPlugin plugin;
    @Inject
    private GlobalDuctManager globalDuctManager;
    @Inject
    private CreativeInventory creativeDuctInv;
    @Inject
    private PlayerSettingsInventory playerSettingsInventory;

    @Subcommand("tps")
    @CommandPermission("transportpipes.tps")
    @Description("Shows some basic information about the plugin and it's runtime")
    public void onTPS(CommandSender cs) {

        int tps = threadService.getCurrentTPS();
        int pref_tps = threadService.getPreferredTPS();
        ChatColor tpsColor = ChatColor.DARK_GREEN;
        if (tps <= 1) {
            tpsColor = ChatColor.DARK_RED;
        } else if (tps <= 3) {
            tpsColor = ChatColor.RED;
        } else if (tps <= 4) {
            tpsColor = ChatColor.GOLD;
        } else if (tps <= 5) {
            tpsColor = ChatColor.GREEN;
        }

        cs.sendMessage(MessageUtils.formatColoredMsg("&6TransportPipes &7v" + plugin.getDescription().getVersion()));
        cs.sendMessage(MessageUtils.formatColoredMsg("&6TPS: " + tpsColor + tps + " &6/ &2" + pref_tps));

        synchronized (globalDuctManager.getDucts()) {
            for (World world : Bukkit.getWorlds()) {
                int worldPipes = 0;
                int worldItems = 0;
                Map<BlockLocation, Duct> ductMap = globalDuctManager.getDucts(world);
                for (Duct duct : ductMap.values()) {
                    if (duct.getDuctType().getBaseDuctType().is("Pipe")) {
                        Pipe pipe = (Pipe) duct;
                        worldPipes++;
                        worldItems += 0;
                    }
                }
                cs.sendMessage(MessageUtils.formatColoredMsg("&6" + world.getName() + ": &e" + worldPipes + " &6" + "pipes, &e" + worldItems + " &6items"));
            }
        }
    }

    @Subcommand("creative")
    @CommandPermission("transportpipes.creative")
    public void onCreativeDuctInv(Player p) {
        creativeDuctInv.openInv(p);
    }

    @Subcommand("settings")
    @CommandPermission("transportpipes.settings")
    public void onSettingsInv(Player p) {
        playerSettingsInventory.openInv(p);
    }

    @HelpCommand
    @Syntax("[command]")
    public void onDefault(CommandSender cs, CommandHelp help) {
        help.showHelp();
    }

} public class GeneralConf extends Conf {

    @Inject
    public GeneralConf(Plugin configPlugin) {
        super(configPlugin, "config.yml", "config.yml", true);
    }

    public int getMaxItemsPerPipe() {
        return (int) read("max_items_per_pipe");
    }

    public boolean isCraftingEnabled() {
        return (boolean) read("crafting_enabled");
    }

    @SuppressWarnings("unchecked")
    public List<String> getAnticheatPlugins() {
        return (List<String>) read("anticheat_plugins");
    }

    public String getDefaultRenderSystemName() {
        return (String) read("default_render_system");
    }

    public boolean isDefaultShowItems() {
        return (boolean) read("default_show_items");
    }

    public int getDefaultRenderDistance() {
        return (int) read("default_render_distance");
    }

    @SuppressWarnings("unchecked")
    public List<String> getDisabledWorlds() {
        return (List<String>) read("disabled_worlds");
    }

    public String getWrenchItem() {
        return (String) read("wrench.item");
    }

    public boolean getWrenchGlowing() {
        return (boolean) read("wrench.glowing");
    }

    public String getLanguage() {
        return (String) read("language");
    }

    public int getShowHiddenDuctsTime() {
        return (int) read("show_hidden_ducts_time");
    }

    public ResourcepackService.ResourcepackMode getResourcepackMode() {
        String url = (String) read("resourcepack_mode");
        if (url == null || url.equalsIgnoreCase("default")) {
            return ResourcepackService.ResourcepackMode.DEFAULT;
        } else if (url.equalsIgnoreCase("none")) {
            return ResourcepackService.ResourcepackMode.NONE;
        } else if (url.equalsIgnoreCase("server")) {
            return ResourcepackService.ResourcepackMode.SERVER;
        }
        return ResourcepackService.ResourcepackMode.DEFAULT;
    }

} public class LangConf extends Conf {

    private static LangConf langConf;

    public LangConf(Plugin configPlugin, String language) {
        super(configPlugin, "lang_" + language.toLowerCase(Locale.ENGLISH) + ".yml", "lang.yml", true);
        langConf = this;
    }

    public enum Key {

        PIPES_WHITE("pipes.white"),
        PIPES_YELLOW("pipes.yellow"),
        PIPES_GREEN("pipes.green"),
        PIPES_BLUE("pipes.blue"),
        PIPES_RED("pipes.red"),
        PIPES_BLACK("pipes.black"),
        PIPES_ICE("pipes.ice"),
        PIPES_GOLDEN("pipes.golden"),
        PIPES_IRON("pipes.iron"),
        PIPES_VOID("pipes.void"),
        PIPES_EXTRACTION("pipes.extraction"),
        PIPES_CRAFTING("pipes.crafting"),
        WRENCH("wrench"),
        COLORS_WHITE("colors.white"),
        COLORS_YELLOW("colors.yellow"),
        COLORS_GREEN("colors.green"),
        COLORS_BLUE("colors.blue"),
        COLORS_RED("colors.red"),
        COLORS_BLACK("colors.black"),
        DIRECTIONS_EAST("directions.east"),
        DIRECTIONS_WEST("directions.west"),
        DIRECTIONS_SOUTH("directions.south"),
        DIRECTIONS_NORTH("directions.north"),
        DIRECTIONS_UP("directions.up"),
        DIRECTIONS_DOWN("directions.down"),
        DIRECTIONS_NONE("directions.none"),
        RESOURCEPACK_FAIL("resourcepack_fail"),
        RENDERSYSTEM_BLOCK("rendersystem_block"),
        PROTECTED_BLOCK("protected_block"),
        RENDERSYSTEM_NAME_VANILLA("rendersystem_name.vanilla"),
        RENDERSYSTEM_NAME_MODELLED("rendersystem_name.modelled"),
        PLAYER_SETTINGS_TITLE("player_settings.title"),
        PLAYER_SETTINGS_RENDERDISTANCE("player_settings.renderdistance"),
        PLAYER_SETTINGS_DECREASE_RENDERDISTANCE("player_settings.decrease_renderdistance"),
        PLAYER_SETTINGS_INCREASE_RENDERDISTANCE("player_settings.increase_renderdistance"),
        PLAYER_SETTINGS_RENDERSYSTEM("player_settings.rendersystem"),
        PLAYER_SETTINGS_ITEM_VISIBILITY_SHOW("player_settings.item_visibility_show"),
        PLAYER_SETTINGS_ITEM_VISIBILITY_HIDE("player_settings.item_visibility_hide"),
        DUCT_INVENTORY_TITLE("duct_inventory.title"),
        DUCT_INVENTORY_LEFTARROW("duct_inventory.leftarrow"),
        DUCT_INVENTORY_RIGHTARROW("duct_inventory.rightarrow"),
        DUCT_INVENTORY_FILTER_MODE_AND_STRICTNESS("duct_inventory.filter_mode_and_strictness"),
        DUCT_INVENTORY_GOLDENPIPE_FILTERTITLE("duct_inventory.goldenpipe.filtertitle"),
        DUCT_INVENTORY_EXTRACTIONPIPE_EXTRACTDIRECTION("duct_inventory.extractionpipe.extractdirection"),
        DUCT_INVENTORY_EXTRACTIONPIPE_EXTRACTCONDITION("duct_inventory.extractionpipe.extractcondition"),
        DUCT_INVENTORY_EXTRACTIONPIPE_EXTRACTAMOUNT("duct_inventory.extractionpipe.extractamount"),
        DUCT_INVENTORY_EXTRACTIONPIPE_FILTERTITLE("duct_inventory.extractionpipe.filtertitle"),
        DUCT_INVENTORY_CRAFTINGPIPE_OUTPUTDIRECTION("duct_inventory.craftingpipe.outputdirection"),
        DUCT_INVENTORY_CRAFTINGPIPE_RETRIEVECACHEDITEMS("duct_inventory.craftingpipe.retrievecacheditems"),
        DUCT_INVENTORY_CREATIVE_INVENTORY_TITLE("duct_inventory.creative_inventory_title"),
        FILTER_STRICTNESS_MATERIAL("filter_strictness.material"),
        FILTER_STRICTNESS_MATERIAL_METADATA("filter_strictness.material_metadata"),
        FILTER_MODE_NORMAL("filter_mode.normal"),
        FILTER_MODE_INVERTED("filter_mode.inverted"),
        FILTER_MODE_BLOCKALL("filter_mode.blockall"),
        EXTRACT_CONDITION_NEEDS_REDSTONE("extract_condition.needs_redstone"),
        EXTRACT_CONDITION_ALWAYS_EXTRACT("extract_condition.always_extract"),
        EXTRACT_CONDITION_NEVER_EXTRACT("extract_condition.never_extract"),
        EXTRACT_AMOUNT_EXTRACT_1("extract_amount.extract_1"),
        EXTRACT_AMOUNT_EXTRACT_16("extract_amount.extract_16"),
        SHOW_HIDDEN_DUCTS("show_hidden_ducts");

        private String key;

        Key(String key) {
            this.key = key;
        }

        public String get(Object... replacements) {
            String value = (String) langConf.read(key);
            if (value == null) {
                value = "&cMissing Language Entry: &4" + key + "&r";
            }
            for (int i = 0; i < replacements.length; i++) {
                value = value.replaceAll("%" + (i + 1) + "%", replacements[i].toString());
            }
            return ChatColor.translateAlternateColorCodes('&', value);
        }

        public List<String> getLines(Object... replacements) {
            return Arrays.asList(get(replacements).split("\\\\n"));
        }

        public void sendMessage(Player p) {
            for (String line : getLines()) {
                p.sendMessage(line);
            }
        }

    }

} public class PlayerSettingsConf extends Conf {

    private GeneralConf generalConf;

    public PlayerSettingsConf(TransportPipes transportPipes, GeneralConf generalConf, Player p) {
        super(transportPipes, "playerconfig.yml", "playersettings/" + p.getUniqueId().toString() + ".yml", false);
        this.generalConf = generalConf;
    }

    public int getRenderDistance() {
        if (!getYamlConf().contains("render_distance")) {
            setRenderDistance(generalConf.getDefaultRenderDistance());
        }
        return (int) read("render_distance");
    }

    public void setRenderDistance(int renderDistance) {
        overrideAsync("render_distance", renderDistance);
    }

    public String getRenderSystemName() {
        if (!getYamlConf().contains("render_system")) {
            setRenderSystemName(generalConf.getDefaultRenderSystemName());
        }
        return (String) read("render_system");
    }

    public void setRenderSystemName(String name) {
        overrideAsync("render_system", name);
    }

    public RenderSystem getRenderSystem(BaseDuctType baseDuctType) {
        return RenderSystem.getRenderSystem(getRenderSystemName(), baseDuctType);
    }

    public boolean isShowItems() {
        if (!getYamlConf().contains("show_items")) {
            setShowItems(generalConf.isDefaultShowItems());
        }
        return (int) read("show_items") == 1;
    }

    public void setShowItems(boolean showItems) {
        overrideAsync("show_items", showItems ? 1 : 0);
    }

} 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 PipeFactory extends DuctFactory<Pipe> {

    @Inject
    private ItemDistributorService itemDistributor;

    @Override
    public Pipe createDuct(DuctType ductType, BlockLocation blockLoc, World world, Chunk chunk) {
        Pipe pipe = null;
        if (ductType instanceof ColoredPipeType) {
            pipe = new ColoredPipe(ductType, blockLoc, world, chunk, null, globalDuctManager, itemDistributor);
        } else if (ductType.is("Golden")) {
            GoldenPipeSettingsInventory inv = transportPipes.getInjector().newInstance(GoldenPipeSettingsInventory.class);
            pipe = new GoldenPipe(ductType, blockLoc, world, chunk, inv, globalDuctManager, itemDistributor);
            pipe.initSettingsInv(transportPipes);
        } else if (ductType.is("Iron")) {
            pipe = new IronPipe(ductType, blockLoc, world, chunk, null, globalDuctManager, itemDistributor);
        } else if (ductType.is("Ice")) {
            pipe = new IcePipe(ductType, blockLoc, world, chunk, null, globalDuctManager, itemDistributor);
        } else if (ductType.is("Void")) {
            pipe = new VoidPipe(ductType, blockLoc, world, chunk, null, globalDuctManager, itemDistributor);
        } else if (ductType.is("Extraction")) {
            ExtractionPipeSettingsInventory inv = transportPipes.getInjector().newInstance(ExtractionPipeSettingsInventory.class);
            pipe = new ExtractionPipe(ductType, blockLoc, world, chunk, inv, globalDuctManager, itemDistributor);
            pipe.initSettingsInv(transportPipes);
        } else if (ductType.is("Crafting")) {
            CraftingPipeSettingsInventory inv = transportPipes.getInjector().newInstance(CraftingPipeSettingsInventory.class);
            pipe = new CraftingPipe(ductType, blockLoc, world, chunk, inv, globalDuctManager, itemDistributor);
            pipe.initSettingsInv(transportPipes);
        }
        return pipe;
    }

} 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 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 final class BaseDuctType<T extends Duct> {

    private String name;
    private DuctManager<T> ductManager;
    private DuctFactory<T> ductFactory;
    private ItemManager<T> itemManager;
    private VanillaRenderSystem vanillaRenderSystem;
    private ModelledRenderSystem modelledRenderSystem;

    private List<DuctType> ductTypes;

    public BaseDuctType(String name, DuctManager<T> ductManager, DuctFactory<T> ductFactory, ItemManager<T> itemManager) {
        this.name = name;
        this.ductManager = ductManager;
        this.ductFactory = ductFactory;
        this.itemManager = itemManager;
        this.ductTypes = new ArrayList<>();
    }

    public String getName() {
        return name;
    }

    public DuctManager<T> getDuctManager() {
        return ductManager;
    }

    public DuctFactory<T> getDuctFactory() {
        return ductFactory;
    }

    public ItemManager<T> getItemManager() {
        return itemManager;
    }

    public VanillaRenderSystem getVanillaRenderSystem() {
        return vanillaRenderSystem;
    }

    public ModelledRenderSystem getModelledRenderSystem() {
        return modelledRenderSystem;
    }

    public void setVanillaRenderSystem(VanillaRenderSystem vanillaRenderSystem) {
        this.vanillaRenderSystem = vanillaRenderSystem;
    }

    public void setModelledRenderSystem(ModelledRenderSystem modelledRenderSystem) {
        this.modelledRenderSystem = modelledRenderSystem;
    }

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

    // ****************************************************
    // DUCT MATERIAL
    // ****************************************************

    public List<DuctType> ductTypes() {
        return ductTypes;
    }

    public <S extends DuctType> S ductTypeOf(String displayName) {
        for (DuctType dt : ductTypes) {
            if (dt.getName().equalsIgnoreCase(displayName)) {
                return (S) dt;
            }
        }
        return null;
    }

    public void registerDuctType(DuctType ductType) {
        if (ductTypes.stream().anyMatch(dt -> dt.getName().equalsIgnoreCase(ductType.getName()))) {
            throw new IllegalArgumentException("DuctType '" + ductType.getName() + "' already exists");
        }
        ductTypes.add(ductType);
    }

} public class PlayerSettingsInventory extends IndividualInventory implements Listener {

    @Inject
    private ItemService itemService;
    @Inject
    private PlayerSettingsService playerSettingsService;
    @Inject
    private DuctRegister ductRegister;
    @Inject
    private ResourcepackService resourcepackService;
    @Inject
    private TransportPipes transportPipes;

    private Set<Inventory> inventories;

    public PlayerSettingsInventory() {
        inventories = new HashSet<>();
    }

    @Override
    Inventory create(Player p) {
        Inventory inv = Bukkit.createInventory(null, 2 * 9, LangConf.Key.PLAYER_SETTINGS_TITLE.get());
        inventories.add(inv);

        PlayerSettingsConf playerSettingsConf = playerSettingsService.getOrCreateSettingsConf(p);

        ItemStack decreaseBtn = new ItemStack(Material.SUNFLOWER);
        itemService.changeDisplayName(decreaseBtn, LangConf.Key.PLAYER_SETTINGS_DECREASE_RENDERDISTANCE.get());
        ItemStack increaseBtn = new ItemStack(Material.SUNFLOWER);
        itemService.changeDisplayName(increaseBtn, LangConf.Key.PLAYER_SETTINGS_INCREASE_RENDERDISTANCE.get());

        ItemStack eye = new ItemStack(Material.ENDER_EYE, playerSettingsConf.getRenderDistance());
        itemService.changeDisplayNameAndLore(eye, LangConf.Key.PLAYER_SETTINGS_RENDERDISTANCE.get(playerSettingsConf.getRenderDistance()));

        ItemStack glassPane = itemService.createWildcardItem(Material.GRAY_STAINED_GLASS_PANE);

        itemService.populateInventoryLine(inv, 0, glassPane, glassPane, decreaseBtn, glassPane, eye, glassPane, increaseBtn, glassPane, glassPane);

        String renderSystemName = playerSettingsConf.getRenderSystemName();

        ItemStack renderSystemRepresentationItem = RenderSystem.getItem(renderSystemName, itemService, ductRegister);
        itemService.changeDisplayNameAndLore(renderSystemRepresentationItem, LangConf.Key.PLAYER_SETTINGS_RENDERSYSTEM.get(RenderSystem.getLocalizedRenderSystemName(renderSystemName)));

        boolean showItems = playerSettingsConf.isShowItems();
        ItemStack itemVisibilityItem = showItems ? itemService.changeDisplayNameAndLore(new ItemStack(Material.GLASS), LangConf.Key.PLAYER_SETTINGS_ITEM_VISIBILITY_SHOW.get()) : itemService.changeDisplayNameAndLore(new ItemStack(Material.BARRIER), LangConf.Key.PLAYER_SETTINGS_ITEM_VISIBILITY_HIDE.get());

        itemService.populateInventoryLine(inv, 1, glassPane, glassPane, glassPane, renderSystemRepresentationItem, glassPane, itemVisibilityItem, glassPane, glassPane, glassPane);

        return inv;
    }

    @EventHandler
    public void onInvClick(InventoryClickEvent e) {
        if (e.getInventory() != null && inventories.contains(e.getInventory()) && e.getWhoClicked() instanceof Player) {
            if (itemService.isItemWildcardOrBarrier(e.getCurrentItem())) {
                e.setCancelled(true);
                return;
            }

            Player p = (Player) e.getWhoClicked();
            PlayerSettingsConf playerSettingsConf = playerSettingsService.getOrCreateSettingsConf(p);

            e.setCancelled(true);

            if (e.getRawSlot() == 2) {
                // decrease render distance
                int before = playerSettingsConf.getRenderDistance();
                int after = before - 1;
                if (after >= 1) {
                    p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1f, 1f);
                    playerSettingsConf.setRenderDistance(after);
                    openInv(p);
                } else {
                    p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f);
                }
            } else if (e.getRawSlot() == 6) {
                // increase render distance
                int before = playerSettingsConf.getRenderDistance();
                int after = before + 1;
                if (after <= 64) {
                    p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1f, 1f);
                    playerSettingsConf.setRenderDistance(after);
                    openInv(p);
                } else {
                    p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f);

                }
            } else if (e.getRawSlot() == 12) {

                if (resourcepackService.getResourcepackMode() == ResourcepackService.ResourcepackMode.NONE) {
                    LangConf.Key.RENDERSYSTEM_BLOCK.sendMessage((Player) e.getWhoClicked());
                    return;
                }

                // change render system
                String oldRenderSystemName = playerSettingsConf.getRenderSystemName();
                String newRenderSystemName = null;
                if (oldRenderSystemName.equalsIgnoreCase(VanillaRenderSystem.getDisplayName())) {
                    if(resourcepackService.getResourcepackMode() == ResourcepackService.ResourcepackMode.DEFAULT && !resourcepackService.getResourcepackPlayers().contains((Player) e.getWhoClicked())) {
                        p.closeInventory();
                        resourcepackService.loadResourcepackForPlayer((Player) e.getWhoClicked());
                        return;
                    }
                    newRenderSystemName = ModelledRenderSystem.getDisplayName();
                } else if (oldRenderSystemName.equalsIgnoreCase(ModelledRenderSystem.getDisplayName())) {
                    newRenderSystemName = VanillaRenderSystem.getDisplayName();
                }

                transportPipes.changeRenderSystem((Player) e.getWhoClicked(), newRenderSystemName);

                p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1f, 1f);
                openInv(p);
            } else if (e.getRawSlot() == 14) {
                // change item visibility
                boolean showItems = playerSettingsConf.isShowItems();
                playerSettingsConf.setShowItems(!showItems);

                p.playSound(p.getLocation(), Sound.UI_BUTTON_CLICK, 1f, 1f);
                openInv(p);
            }

        }
    }

    @EventHandler
    public void onInvClose(InventoryCloseEvent e) {
        if (e.getInventory() != null && inventories.contains(e.getInventory()) && e.getPlayer() instanceof Player) {
            inventories.remove(e.getInventory());
        }
    }

} public class DuctListener implements Listener {

    private final List<Material> interactables = new ArrayList<>();

    //makes sure that "callInteraction" is called with the mainHand and with the offHand every single time
    private Map<Player, Interaction> interactions = new HashMap<>();

    private ItemService itemService;
    private DuctRegister ductRegister;
    private GlobalDuctManager globalDuctManager;
    private TPContainerListener tpContainerListener;
    private GeneralConf generalConf;
    private SentryService sentry;
    private TransportPipes transportPipes;
    private ThreadService threadService;
    private PlayerSettingsService playerSettingsService;

    @Inject
    public DuctListener(ItemService itemService, JavaPlugin plugin, DuctRegister ductRegister, GlobalDuctManager globalDuctManager, TPContainerListener tpContainerListener, GeneralConf generalConf, SentryService sentry, TransportPipes transportPipes, ThreadService threadService, PlayerSettingsService playerSettingsService) {
        this.itemService = itemService;
        this.ductRegister = ductRegister;
        this.globalDuctManager = globalDuctManager;
        this.tpContainerListener = tpContainerListener;
        this.generalConf = generalConf;
        this.sentry = sentry;
        this.transportPipes = transportPipes;
        this.threadService = threadService;
        this.playerSettingsService = playerSettingsService;

        for (Material m : Material.values()) {
            if (m.isInteractable()) {
                interactables.add(m);
            }
        }

        Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, this::updateInteractSet, 0L, 1L);
    }

    private void updateInteractSet() {
        Iterator<Player> events = interactions.keySet().iterator();
        while (events.hasNext()) {
            Player p = events.next();
            if (interactions.get(p) != null)
                callInteraction(interactions.get(p));
            events.remove();
        }
    }

    @EventHandler(priority = EventPriority.LOW)
    public void onInteract(PlayerInteractEvent e) {
        Player p = e.getPlayer();
        Block clickedBlock = e.getClickedBlock();

        if (e.getAction() == Action.PHYSICAL) {
            return;
        }

        if (e.getHand() == EquipmentSlot.HAND) {
            Interaction offHandInteraction = new Interaction(p, EquipmentSlot.OFF_HAND, p.getInventory().getItemInOffHand(), clickedBlock, e.getBlockFace(), e.getAction());
            interactions.put(p, offHandInteraction);

            Interaction mainHandInteraction = new Interaction(p, EquipmentSlot.HAND, p.getInventory().getItemInMainHand(), clickedBlock, e.getBlockFace(), e.getAction());
            callInteraction(mainHandInteraction);
            if (mainHandInteraction.cancel) e.setCancelled(true);
            if (mainHandInteraction.denyBlockUse) e.setUseInteractedBlock(Event.Result.DENY);
            if (mainHandInteraction.successful) {
                interactions.put(p, null);
            }
        } else if (e.getHand() == EquipmentSlot.OFF_HAND) {
            if (interactions.containsKey(p) && interactions.get(p) == null) {
                e.setCancelled(true);
                return;
            }
            if (interactions.containsKey(p)) {
                interactions.remove(p);
            } else {
                Interaction mainHandInteraction = new Interaction(p, EquipmentSlot.HAND, p.getInventory().getItemInMainHand(), clickedBlock, e.getBlockFace(), e.getAction());
                callInteraction(mainHandInteraction);
                if (mainHandInteraction.successful) {
                    return;
                }
            }
            Interaction offHandInteraction = new Interaction(p, EquipmentSlot.OFF_HAND, p.getInventory().getItemInOffHand(), clickedBlock, e.getBlockFace(), e.getAction());
            callInteraction(offHandInteraction);
            if (offHandInteraction.cancel) e.setCancelled(true);
            if (offHandInteraction.denyBlockUse) e.setUseInteractedBlock(Event.Result.DENY);
        }

    }

    private void callInteraction(Interaction interaction) {
        if (interaction.action == Action.RIGHT_CLICK_AIR || interaction.action == Action.RIGHT_CLICK_BLOCK) {
            if (interaction.item != null) {
                Duct clickedDuct = HitboxUtils.getDuctLookingTo(globalDuctManager, interaction.p, interaction.clickedBlock);
                DuctType itemDuctType = itemService.readDuctNBTTags(interaction.item, ductRegister);
                boolean manualPlaceable = itemDuctType != null || interaction.item.getType().isSolid();

                // ********************** WRENCH DUCT CLICK ****************************
                if (clickedDuct != null && itemService.isWrench(interaction.item)) {
                    //wrench click
                    if (buildAllowed(interaction.p, clickedDuct.getBlockLoc().toBlock(interaction.p.getWorld()))) {
                        clickedDuct.notifyClick(interaction.p, interaction.p.isSneaking());
                    }

                    interaction.cancel = true;
                    interaction.successful = true;
                    return;
                }

                // ********************** WRENCH NON DUCT CLICK ****************************
                if (clickedDuct == null && itemService.isWrench(interaction.item) && interaction.p.isSneaking()) {
                    //wrench click

                    WorldUtils.startShowHiddenDuctsProcess(interaction.p, globalDuctManager, threadService, transportPipes, generalConf, playerSettingsService);

                    interaction.cancel = true;
                    interaction.successful = true;
                    return;
                }

                // ********************** DUCT OBFUSCATION ****************************
                if (clickedDuct != null && !interaction.p.isSneaking() && canBeUsedToObfuscate(interaction.item.getType())) {
                    // block can be used to obfuscate and player is not sneaking
                    // this block will be used to obfuscate the duct
                    Block ductBlock = clickedDuct.getBlockLoc().toBlock(interaction.p.getWorld());
                    if (buildAllowed(interaction.p, ductBlock)) {

                        BlockData bd = interaction.item.getType().createBlockData();
                        setDirectionalBlockFace(ductBlock.getLocation(), bd, interaction.p);
                        ductBlock.setBlockData(bd, true);
                        clickedDuct.obfuscatedWith(bd);

                        decreaseHandItem(interaction.p, interaction.hand);
                    }

                    interaction.cancel = true;
                    interaction.successful = true;
                    return;
                }

                // ********************** PREPARATIONS FOR DUCT / BLOCK PLACE ****************************
                Block placeBlock = null;
                if (clickedDuct != null) {
                    placeBlock = HitboxUtils.getRelativeBlockOfDuct(globalDuctManager, interaction.p, clickedDuct.getBlockLoc().toBlock(interaction.p.getWorld()));
                } else if (interaction.clickedBlock != null) {
                    placeBlock = interaction.clickedBlock.getRelative(interaction.blockFace);
                }
                if (placeBlock != null && (placeBlock.getType().isSolid() || globalDuctManager.getDuctAtLoc(placeBlock.getLocation()) != null)) {
                    placeBlock = null;
                }
                if (clickedDuct == null && interaction.clickedBlock != null && interactables.contains(interaction.clickedBlock.getType()) && !interaction.p.isSneaking()) {
                    return;
                } else if (placeBlock == null || (clickedDuct != null && !manualPlaceable)) {
                    if (!interaction.item.getType().isEdible()) {
                        interaction.denyBlockUse = true;
                    }
                    return;
                }

                // ********************** DUCT AND BLOCK PLACE ****************************
                if (manualPlaceable) {
                    if (itemDuctType != null) {

                        // duct placement
                        if (buildAllowed(interaction.p, placeBlock)) {
                            boolean lwcAllowed = true;
                            for (TPDirection dir : TPDirection.values()) {
                                if (WorldUtils.lwcProtection(placeBlock.getRelative(dir.getBlockFace()))) {
                                    lwcAllowed = false;
                                }
                            }
                            if (lwcAllowed) {
                                Duct duct = globalDuctManager.createDuctObject(itemDuctType, new BlockLocation(placeBlock.getLocation()), placeBlock.getWorld(), placeBlock.getChunk());
                                globalDuctManager.registerDuct(duct);
                                globalDuctManager.updateDuctConnections(duct);
                                globalDuctManager.registerDuctInRenderSystems(duct, true);
                                globalDuctManager.updateNeighborDuctsConnections(duct);
                                globalDuctManager.updateNeighborDuctsInRenderSystems(duct, true);

                                decreaseHandItem(interaction.p, interaction.hand);
                            } else {
                                LangConf.Key.PROTECTED_BLOCK.sendMessage(interaction.p);
                            }
                        }

                        interaction.cancel = true;
                        interaction.successful = true;
                    } else if (clickedDuct != null) {
                        //block placement next to duct
                        if (buildAllowed(interaction.p, placeBlock)) {

                            BlockData bd = interaction.item.getType().createBlockData();
                            setDirectionalBlockFace(placeBlock.getLocation(), bd, interaction.p);
                            placeBlock.setBlockData(bd, true);

                            // create TransportPipesContainer from placed block if it is such
                            if (WorldUtils.isContainerBlock(interaction.item.getType())) {
                                tpContainerListener.updateContainerBlock(placeBlock, true, true);
                            }
                            decreaseHandItem(interaction.p, interaction.hand);
                        }
                        interaction.cancel = true;
                        interaction.successful = true;
                    }
                }

            }
        } else if (interaction.action == Action.LEFT_CLICK_AIR || interaction.action == Action.LEFT_CLICK_BLOCK) {
            Duct clickedDuct = HitboxUtils.getDuctLookingTo(globalDuctManager, interaction.p, interaction.clickedBlock);
            // duct destruction
            if (clickedDuct != null) {
                if (buildAllowed(interaction.p, clickedDuct.getBlockLoc().toBlock(interaction.p.getWorld()))) {
                    globalDuctManager.unregisterDuct(clickedDuct);
                    globalDuctManager.unregisterDuctInRenderSystem(clickedDuct, true);
                    globalDuctManager.updateNeighborDuctsConnections(clickedDuct);
                    globalDuctManager.updateNeighborDuctsInRenderSystems(clickedDuct, true);
                    globalDuctManager.playDuctDestroyActions(clickedDuct, interaction.p);
                }

                interaction.cancel = true;
                interaction.successful = true;
            }
        }
    }

    private boolean canBeUsedToObfuscate(Material type) {
        return type.isOccluding() && !type.isInteractable() && !type.hasGravity();
    }

    private void setDirectionalBlockFace(Location b, BlockData bd, Player p) {
        if (bd instanceof Directional) {
            Vector dir = new Vector(b.getX() + 0.5d, b.getY() + 0.5d, b.getZ() + 0.5d);
            dir.subtract(p.getEyeLocation().toVector());
            double absX = Math.abs(dir.getX());
            double absY = Math.abs(dir.getY());
            double absZ = Math.abs(dir.getZ());
            if (((Directional) bd).getFaces().contains(BlockFace.UP) && ((Directional) bd).getFaces().contains(BlockFace.DOWN)) {
                if (absX >= absY && absX >= absZ) {
                    ((Directional) bd).setFacing(dir.getX() > 0 ? BlockFace.WEST : BlockFace.EAST);
                } else if (absY >= absX && absY >= absZ) {
                    ((Directional) bd).setFacing(dir.getY() > 0 ? BlockFace.DOWN : BlockFace.UP);
                } else {
                    ((Directional) bd).setFacing(dir.getZ() > 0 ? BlockFace.NORTH : BlockFace.SOUTH);
                }
            } else {
                if (absX >= absZ) {
                    ((Directional) bd).setFacing(dir.getX() > 0 ? BlockFace.WEST : BlockFace.EAST);
                } else {
                    ((Directional) bd).setFacing(dir.getZ() > 0 ? BlockFace.NORTH : BlockFace.SOUTH);
                }
            }
        }
    }

    private void decreaseHandItem(Player p, EquipmentSlot hand) {
        if (p.getGameMode() == GameMode.CREATIVE) {
            return;
        }
        ItemStack item = hand == EquipmentSlot.HAND ? p.getInventory().getItemInMainHand() : p.getInventory().getItemInOffHand();
        if (item != null) {
            if (item.getAmount() <= 1) {
                item = null;
            } else {
                item.setAmount(item.getAmount() - 1);
            }
            if (hand == EquipmentSlot.HAND) p.getInventory().setItemInMainHand(item);
            else p.getInventory().setItemInOffHand(item);
        }
    }


    private boolean buildAllowed(Player p, Block b) {
        if (generalConf.getDisabledWorlds().contains(b.getWorld().getName())) {
            return false;
        }
        if (p.isOp()) {
            return true;
        }

        BlockBreakEvent event = new BlockBreakEvent(b, p);

        // unregister anticheat listeners
        List<RegisteredListener> unregisterListeners = new ArrayList<>();
        for (RegisteredListener rl : event.getHandlers().getRegisteredListeners()) {
            for (String antiCheat : generalConf.getAnticheatPlugins()) {
                if (rl.getPlugin().getName().equalsIgnoreCase(antiCheat)) {
                    unregisterListeners.add(rl);
                }
            }
            if (rl.getListener().equals(tpContainerListener)) {
                unregisterListeners.add(rl);
            }
        }
        for (RegisteredListener rl : unregisterListeners) {
            event.getHandlers().unregister(rl);
        }

        Bukkit.getPluginManager().callEvent(event);

        // register anticheat listeners
        event.getHandlers().registerAll(unregisterListeners);

        return !event.isCancelled();
    }

    private class Interaction {
        Player p;
        EquipmentSlot hand;
        ItemStack item;
        Block clickedBlock;
        BlockFace blockFace;
        Action action;
        boolean cancel;
        boolean denyBlockUse;
        boolean successful = false;

        Interaction(Player p, EquipmentSlot hand, ItemStack item, Block clickedBlock, BlockFace blockFace, Action action) {
            this.p = p;
            this.hand = hand;
            this.item = item;
            this.clickedBlock = clickedBlock;
            this.blockFace = blockFace;
            this.action = action;
        }
    }

} public class PlayerListener implements Listener {

    @Inject
    private GlobalDuctManager globalDuctManager;

    @Inject
    private DuctRegister ductRegister;

    @Inject
    private ItemService itemService;

    @Inject
    private GeneralConf generalConf;

    @EventHandler
    public void onQuit(PlayerQuitEvent e) {
        globalDuctManager.getPlayerDucts(e.getPlayer()).clear();
        ((PipeManager) (DuctManager<? extends Duct>) ductRegister.baseDuctTypeOf("pipe").getDuctManager()).getPlayerPipeItems(e.getPlayer()).clear();
    }

    @EventHandler
    public void onWorldChange(PlayerChangedWorldEvent e) {
        //make sure that all duct that were visible to the player get removed so they will spawn again when the player is nearby
        globalDuctManager.getPlayerDucts(e.getPlayer()).clear();
        ((PipeManager) (DuctManager<? extends Duct>) ductRegister.baseDuctTypeOf("pipe").getDuctManager()).getPlayerPipeItems(e.getPlayer()).clear();
    }

    @EventHandler
    public void onJoin(PlayerJoinEvent event) {
        if (generalConf.isCraftingEnabled()) {
            List<NamespacedKey> keys = new ArrayList<>();
            for (BaseDuctType bdt : ductRegister.baseDuctTypes()) {
                for (Object type : bdt.ductTypes()) {
                    DuctType dt = (DuctType) type;
                    if (dt.getDuctRecipe() != null) {
                        NamespacedKey key = ((Keyed) dt.getDuctRecipe()).getKey();
                        keys.add(key);
                    }
                }
                if (bdt.is("pipe")) {
                    keys.add(((PipeManager) bdt.getDuctManager()).getWrenchRecipe().getKey());
                }
            }
            event.getPlayer().discoverRecipes(keys);
        }
    }

    @EventHandler
    public void onCraft(PrepareItemCraftEvent e) {
        if (e.getInventory().getViewers().isEmpty() || e.getInventory().getResult() == null) {
            return;
        }
        Player p = (Player) e.getInventory().getViewers().get(0);
        for (BaseDuctType bdt : ductRegister.baseDuctTypes()) {
            for (Object dt : bdt.ductTypes()) {
                if (e.getRecipe().getResult().isSimilar(bdt.getItemManager().getItem((DuctType) dt))) {
                    if (!((DuctType) dt).hasPlayerCraftingPermission(p)) {
                        e.getInventory().setResult(null);
                        return;
                    }
                }
            }
        }
        if (itemService.isWrench(e.getRecipe().getResult())) {
            if (!p.hasPermission("transportpipes.craft.wrench")) {
                e.getInventory().setResult(null);
            }
        }
    }

} public class TPContainerListener implements Listener {

    @Inject
    private DuctRegister ductRegister;

    @Inject
    private GlobalDuctManager globalDuctManager;

    @Inject
    private TransportPipes transportPipes;

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onBlockPlace(BlockPlaceEvent e) {
        notifyBlockUpdate(e.getBlock(), true);
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onBlockBreak(BlockBreakEvent e) {
        notifyBlockUpdate(e.getBlock(), false);
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onBlockExplode(BlockExplodeEvent e) {
        for (Block b : e.blockList()) {
            notifyBlockUpdate(b, false);
        }
    }

    @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
    public void onEntityExplode(EntityExplodeEvent e) {
        for (Block b : e.blockList()) {
            notifyBlockUpdate(b, false);
        }
    }

    private void notifyBlockUpdate(Block block, boolean place) {
        if (WorldUtils.isContainerBlock(block.getType())) {
            updateContainerBlock(block, place, true);
        }
        if (!place) {
            //undo obfuscation
            Duct duct = globalDuctManager.getDuctAtLoc(block.getLocation());
            if (duct != null) {
                duct.obfuscatedWith(null);
            }
        }
    }

    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onChunkLoad(ChunkLoadEvent e) {
        handleChunkLoadSync(e.getChunk(), false);
    }

    public void updateContainerBlock(Block block, boolean add, boolean updateNeighborPipes) {
        PipeManager pipeManager = (PipeManager) (DuctManager<? extends Duct>) ductRegister.baseDuctTypeOf("pipe").getDuctManager();

        BlockLocation blockLoc = new BlockLocation(block.getLocation());
        if (add) {

            if (pipeManager.getContainerAtLoc(block.getLocation()) == null) {
                TransportPipesContainer container = createContainerFromBlock(block);
                pipeManager.getContainers(block.getWorld()).put(blockLoc, container);

                // only update the neighbor pipes if this updateContainerBlock method call is because of a chunk load that was not issued inside the onEnable method
                if (updateNeighborPipes) {
                    for (TPDirection dir : TPDirection.values()) {
                        Duct duct = globalDuctManager.getDuctAtLoc(block.getWorld(), blockLoc.getNeighbor(dir));
                        if (duct instanceof Pipe) {
                            globalDuctManager.updateDuctConnections(duct);
                            globalDuctManager.updateDuctInRenderSystems(duct, true);
                        }
                    }
                }

                transportPipes.runTaskSync(() -> {
                    //checks for double chest neighbor and updates the neighbors TransportPipesContainer if present
                    Block neighborDoubleChestBlock = checkForDoubleChestNeighbor(block);
                    if (neighborDoubleChestBlock != null) {
                        TransportPipesContainer neighborContainer = pipeManager.getContainerAtLoc(neighborDoubleChestBlock.getWorld(), new BlockLocation(neighborDoubleChestBlock.getLocation()));
                        if (neighborContainer instanceof BlockContainer) {
                            ((BlockContainer) neighborContainer).updateBlock();
                        }
                        if (pipeManager.getContainers(block.getWorld()).get(blockLoc) == container) {
                            ((BlockContainer) container).updateBlock();
                        }
                    }
                });

            }

        } else {

            TransportPipesContainer container = pipeManager.getContainerAtLoc(block.getLocation());
            if (container != null) {
                pipeManager.getContainers(block.getWorld()).remove(blockLoc);

                // only update the neighbor pipes if this updateContainerBlock method call is because of a chunk load that was not issued inside the onEnable method
                if (updateNeighborPipes) {
                    for (TPDirection dir : TPDirection.values()) {
                        Duct duct = globalDuctManager.getDuctAtLoc(block.getWorld(), blockLoc.getNeighbor(dir));
                        if (duct instanceof Pipe) {
                            globalDuctManager.updateDuctConnections(duct);
                            globalDuctManager.updateDuctInRenderSystems(duct, true);
                        }
                    }
                }
            }
        }
    }

    public Block checkForDoubleChestNeighbor(Block block) {
        if (block.getState() instanceof InventoryHolder) {
            if (((InventoryHolder) block.getState()).getInventory().getHolder() instanceof DoubleChest) {
                for (TPDirection dir : TPDirection.values()) {
                    if (dir.isSide()) {
                        Block neighborBlock = block.getRelative(dir.getBlockFace());
                        if (neighborBlock.getState() instanceof InventoryHolder) {
                            if (((InventoryHolder) neighborBlock.getState()).getInventory().getHolder() instanceof DoubleChest) {
                                DoubleChest blockChest = (DoubleChest) ((InventoryHolder) block.getState()).getInventory().getHolder();
                                DoubleChest neighborBlockChest = (DoubleChest) ((InventoryHolder) neighborBlock.getState()).getInventory().getHolder();
                                if (blockChest.getLocation().equals(neighborBlockChest.getLocation())) {
                                    return neighborBlock;
                                }
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    public TransportPipesContainer createContainerFromBlock(Block block) {
        if (block.getState() instanceof Furnace) {
            return new FurnaceContainer(block);
        } else if (block.getState() instanceof BrewingStand) {
            return new BrewingStandContainer(block);
        } else if (block.getState() instanceof InventoryHolder) {
            return new SimpleInventoryContainer(block);
        }
        return null;
    }

    public void handleChunkLoadSync(Chunk loadedChunk, boolean onServerStart) {
        PipeManager pipeManager = (PipeManager) (DuctManager<? extends Duct>) ductRegister.baseDuctTypeOf("pipe").getDuctManager();

        if (loadedChunk.getTileEntities() != null) {
            for (BlockState bs : loadedChunk.getTileEntities()) {
                if (WorldUtils.isContainerBlock(bs.getType())) {

                    //automatically ignores this block if it is already registered as container block
                    updateContainerBlock(bs.getBlock(), true, !onServerStart);

                    //if this block is already registered, update the block, because the blockState object changes after a chunk unload and load
                    TransportPipesContainer container = pipeManager.getContainerAtLoc(bs.getLocation());
                    if (container instanceof BlockContainer) {
                        ((BlockContainer) container).updateBlock();
                    }
                }
            }
        }
    }


} public class WorldListener implements Listener {

    @Inject
    private TransportPipes plugin;

    @EventHandler
    public void onWorldSave(WorldSaveEvent e) {
        plugin.saveWorld(e.getWorld());
    }

} public class LoggerService {

    private Logger logger;

    @Inject
    public LoggerService(Logger logger) {
        this.logger = logger;
        setDebug(false);
    }

    public void error(String message) {
        logger.severe(message);
    }

    public void error(String message, Throwable throwable) {
        logger.log(Level.SEVERE, message, throwable);
    }

    public void warning(String message) {
        logger.warning(message);
    }

    public void warning(String message, Throwable throwable) {
        logger.log(Level.WARNING, message, throwable);
    }

    public void info(String message) {
        logger.info(message);
    }

    public void debug(String message) {
        logger.fine(message);
    }

    public void setDebug(boolean debug) {
        if (debug) {
            logger.setLevel(Level.FINE);
        } else {
            logger.setLevel(Level.INFO);
        }
    }

} public class SentryService {

    @Inject
    private LoggerService logger;

    private SentryClient client;

    public boolean init(String dsn) {
        try {
            client = Sentry.init(dsn, new SentryClientFactory() {
                @Override
                public SentryClient createSentryClient(Dsn dsn) {
                    return null;
                }
            });
        } catch (Throwable t) {
            return false;
        }
        return true;
    }

    public boolean isInitialized() {
        return client != null;
    }

    public void addTag(String name, String value) {
        if (!isInitialized()) {
            return;
        }
        client.getContext().addTag(name, value);
    }

    public void record(Throwable throwable) {
        client.sendException(throwable);
    }

    public void breadcrumb(Breadcrumb.Level level, String category, String message) {
        if (!isInitialized()) {
            return;
        }
        client.getContext().recordBreadcrumb(new BreadcrumbBuilder().setLevel(level).setCategory(category).setMessage(message).build());
    }

    public void injectThread(Thread thread) {
        if (!isInitialized()) {
            return;
        }
        thread.setUncaughtExceptionHandler((currentThread, throwable) -> {
            logger.error("An uncaught error occurred!", throwable);
            record(throwable);
        });
    }

} public class ProtocolService {

    private WrappedDataWatcher.Serializer INT_SERIALIZER;
    private WrappedDataWatcher.Serializer BYTE_SERIALIZER;
    private WrappedDataWatcher.Serializer VECTOR_SERIALIZER;
    private WrappedDataWatcher.Serializer BOOLEAN_SERIALIZER;

    private TransportPipes plugin;

    @Inject
    public ProtocolService(TransportPipes plugin) {
        INT_SERIALIZER = WrappedDataWatcher.Registry.get(Integer.class);
        BYTE_SERIALIZER = WrappedDataWatcher.Registry.get(Byte.class);
        VECTOR_SERIALIZER = WrappedDataWatcher.Registry.get(NMSUtils.getVector3fClass());
        BOOLEAN_SERIALIZER = WrappedDataWatcher.Registry.get(Boolean.class);

        this.plugin = plugin;
    }

    private int nextEntityID = 99999;
    private UUID uuid = UUID.randomUUID();

    public void sendPipeItem(Player p, PipeItem item) {
        sendASD(p, item.getBlockLoc(), item.getRelativeLocation().clone().add(-0.5d, -0.5d, -0.5d), item.getAsd());
    }

    public void updatePipeItem(Player p, PipeItem item) {
        try {
            WrapperPlayServerRelEntityMove moveWrapper = new WrapperPlayServerRelEntityMove();
            moveWrapper.setEntityID(item.getAsd().getEntityID());
            moveWrapper.setDx((int) ((item.getRelativeLocationDifference().getDoubleX() * 32d) * 128));
            moveWrapper.setDy((int) ((item.getRelativeLocationDifference().getDoubleY() * 32d) * 128));
            moveWrapper.setDz((int) ((item.getRelativeLocationDifference().getDoubleZ() * 32d) * 128));
            moveWrapper.setOnGround(true);
            moveWrapper.sendPacket(p);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void removePipeItem(final Player p, PipeItem item) {
        removeASD(p, Collections.singletonList(item.getAsd()));
    }

    public void sendASD(Player p, BlockLocation blockLoc, RelativeLocation offset, ArmorStandData asd) {
        int serverVersion = NMSUtils.gatherProtocolVersion();

        try {
            if (asd.getEntityID() == -1) {
                asd.setEntityID(++nextEntityID);
            }

            // SPAWN ENTITY
            WrapperPlayServerSpawnEntity spawnWrapper = new WrapperPlayServerSpawnEntity();
            spawnWrapper.setEntityID(asd.getEntityID());
            spawnWrapper.setUniqueId(uuid);
            spawnWrapper.setType(78); // object id: ArmorStand (http://wiki.vg/Protocol#Spawn_Object)
            spawnWrapper.setX(blockLoc.getX() + asd.getRelLoc().getDoubleX() + offset.getDoubleX());
            spawnWrapper.setY(blockLoc.getY() + asd.getRelLoc().getDoubleY() + offset.getDoubleY());
            spawnWrapper.setZ(blockLoc.getZ() + asd.getRelLoc().getDoubleZ() + offset.getDoubleZ());
            spawnWrapper.setOptionalSpeedX(0);
            spawnWrapper.setOptionalSpeedY(0);
            spawnWrapper.setOptionalSpeedZ(0);
            spawnWrapper.setPitch(0);

            double x = asd.getDirection().getX();
            double z = asd.getDirection().getZ();

            double theta = Math.atan2(-x, z);
            double yaw = Math.toDegrees((theta + 2 * Math.PI) % (2 * Math.PI));

            spawnWrapper.setYaw((float) yaw);
            spawnWrapper.setObjectData(0); // without random velocity
            spawnWrapper.sendPacket(p);

            // ENTITYMETADATA
            WrapperPlayServerEntityMetadata metaWrapper = new WrapperPlayServerEntityMetadata();
            metaWrapper.setEntityID(asd.getEntityID());

            byte bitMask = (byte) ((asd.isSmall() ? 0x01 : 0x00) | 0x04 | 0x08 | 0x10); // (small) + hasArms + noBasePlate + Marker

            List<WrappedWatchableObject> metaList = new ArrayList<>();
            metaList.add(new WrappedWatchableObject(new WrappedDataWatcher.WrappedDataWatcherObject(3, BOOLEAN_SERIALIZER), false));// customname
            // invisible
            metaList.add(new WrappedWatchableObject(new WrappedDataWatcher.WrappedDataWatcherObject(serverVersion <= 110 ? 10 : 11, BYTE_SERIALIZER), bitMask));// armorstand
            // specific
            // data...
            metaList.add(new WrappedWatchableObject(new WrappedDataWatcher.WrappedDataWatcherObject(0, BYTE_SERIALIZER), (byte) (0x20)));// invisible
            // (entity
            // specific
            // data)
            metaList.add(new WrappedWatchableObject(new WrappedDataWatcher.WrappedDataWatcherObject(serverVersion <= 110 ? 11 : 12, VECTOR_SERIALIZER), NMSUtils.createVector3f((float) asd.getHeadRotation().getX(), (float) asd.getHeadRotation().getY(), (float) asd.getHeadRotation().getZ())));// head rot
            metaList.add(new WrappedWatchableObject(new WrappedDataWatcher.WrappedDataWatcherObject(serverVersion <= 110 ? 14 : 15, VECTOR_SERIALIZER), NMSUtils.createVector3f((float) asd.getArmRotation().getX(), (float) asd.getArmRotation().getY(), (float) asd.getArmRotation().getZ())));// right arm rot

            metaWrapper.setMetadata(metaList);
            metaWrapper.sendPacket(p);

            // ENTITYEQUIPMENT
            final WrapperPlayServerEntityEquipment equipmentWrapper = new WrapperPlayServerEntityEquipment();
            equipmentWrapper.setEntityID(asd.getEntityID());

            // HAND ITEM
            if (asd.getHandItem() != null) {
                equipmentWrapper.setSlot(EnumWrappers.ItemSlot.MAINHAND);
                equipmentWrapper.setItem(asd.getHandItem());
            }

            // HEAD ITEM
            if (asd.getHeadItem() != null) {
                equipmentWrapper.setSlot(EnumWrappers.ItemSlot.HEAD);
                equipmentWrapper.setItem(asd.getHeadItem());
            }

            // ENTITYMETADATA 2 (fire)
            final WrapperPlayServerEntityMetadata meta2Wrapper = new WrapperPlayServerEntityMetadata();
            meta2Wrapper.setEntityID(asd.getEntityID());

            List<WrappedWatchableObject> meta2List = new ArrayList<>();
            meta2List.add(new WrappedWatchableObject(new WrappedDataWatcher.WrappedDataWatcherObject(0, BYTE_SERIALIZER), (byte) (0x01 | 0x20)));// on
            // fire
            meta2Wrapper.setMetadata(meta2List);

            plugin.runTaskAsync(() -> {
                try {
                    meta2Wrapper.sendPacket(p);
                    equipmentWrapper.sendPacket(p);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, 1);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void sendASD(Player p, BlockLocation blockLoc, List<ArmorStandData> armorStandData) {
        for (ArmorStandData asd : armorStandData) {
            sendASD(p, blockLoc, new RelativeLocation(0d, 0d, 0d), asd);
        }
    }

    public void removeASD(Player p, List<ArmorStandData> armorStandData) {
        WrapperPlayServerEntityDestroy destroyWrapper = new WrapperPlayServerEntityDestroy();
        int[] ids = armorStandData.stream().mapToInt(ArmorStandData::getEntityID).toArray();
        destroyWrapper.setEntityIds(ids);
        destroyWrapper.sendPacket(p);
    }

} public abstract class RenderSystem {

    private BaseDuctType baseDuctType;

    public RenderSystem(BaseDuctType baseDuctType) {
        this.baseDuctType = baseDuctType;
    }

    // ***************************************************************
    // ASD UTILS
    // ***************************************************************

    /**
     * creates the ASD information of this duct in respect of its connections and saves it internally.
     * This is only for duct creation, not for duct updates!
     */
    public abstract void createDuctASD(Duct duct, Collection<TPDirection> connections);

    /**
     * does the same as createDuctASD but for duct that exist already. The difference in ASD is put into "removeASD" and "addASD" so the caller can send it to the clients.
     */
    public abstract void updateDuctASD(Duct duct, Collection<TPDirection> connections, List<ArmorStandData> removeASD, List<ArmorStandData> addASD);

    /**
     * simply removes the ASD information of this duct
     */
    public abstract void destroyDuctASD(Duct duct);

    /**
     * retrieves all ASD information of the given duct and returns it
     */
    public abstract List<ArmorStandData> getASDForDuct(Duct duct);

    // ***************************************************************
    // AABB UTILS
    // ***************************************************************

    public abstract AxisAlignedBB getOuterHitbox(Duct duct);

    public abstract TPDirection getClickedDuctFace(Player player, Duct duct);

    // ***************************************************************
    //  MISC
    // ***************************************************************

    public BaseDuctType getBaseDuctType() {
        return baseDuctType;
    }

    public static ItemStack getItem(String renderSystemDisplayName, ItemService itemService, DuctRegister ductRegister) {
        if (ModelledRenderSystem.getDisplayName().equalsIgnoreCase(renderSystemDisplayName)) {
            return ModelledRenderSystem.getItem(itemService);
        }
        if (VanillaRenderSystem.getDisplayName().equalsIgnoreCase(renderSystemDisplayName)) {
            return VanillaRenderSystem.getItem(ductRegister);
        }
        return null;
    }

    public static RenderSystem getRenderSystem(String renderSystemName, BaseDuctType baseDuctType) {
        if (baseDuctType.getVanillaRenderSystem() != null && VanillaRenderSystem.getDisplayName().equalsIgnoreCase(renderSystemName)) {
            return baseDuctType.getVanillaRenderSystem();
        }
        if (baseDuctType.getModelledRenderSystem() != null && ModelledRenderSystem.getDisplayName().equalsIgnoreCase(renderSystemName)) {
            return baseDuctType.getModelledRenderSystem();
        }
        return null;
    }

    public static String getLocalizedRenderSystemName(String renderSystemName) {
        if (renderSystemName.equalsIgnoreCase("modelled")) {
            return LangConf.Key.RENDERSYSTEM_NAME_MODELLED.get();
        } else if (renderSystemName.equalsIgnoreCase("vanilla")) {
            return LangConf.Key.RENDERSYSTEM_NAME_VANILLA.get();
        }
        return null;
    }

} public class ModelledPipeRenderSystem extends ModelledRenderSystem {

    private ModelledPipeModel model = new ModelledPipeModel();

    private Map<Pipe, ArmorStandData> midASD = new HashMap<>();
    private Map<Pipe, Map<TPDirection, ArmorStandData>> connASD = new HashMap<>();
    private AxisAlignedBB midAABB;
    private Map<TPDirection, AxisAlignedBB> connAABBs = new HashMap<>();

    @Inject
    public ModelledPipeRenderSystem(ItemService itemService, DuctRegister ductRegister) {
        super(ductRegister.baseDuctTypeOf("Pipe"));
        ModelledPipeModel.init(itemService, ductRegister);

        midAABB = new AxisAlignedBB(4d / 16d, 4d / 16d, 4d / 16d, 12d / 16d, 12d / 16d, 12d / 16d);
        connAABBs.put(TPDirection.NORTH, new AxisAlignedBB(4d / 16d, 4d / 16d, 0d / 16d, 12d / 16d, 12d / 16d, 4d / 16d));
        connAABBs.put(TPDirection.EAST, new AxisAlignedBB(12d / 16d, 4d / 16d, 4d / 16d, 16d / 16d, 12d / 16d, 12d / 16d));
        connAABBs.put(TPDirection.SOUTH, new AxisAlignedBB(4d / 16d, 4d / 16d, 12d / 16d, 12d / 16d, 12d / 16d, 16d / 16d));
        connAABBs.put(TPDirection.WEST, new AxisAlignedBB(0d / 16d, 4d / 16d, 4d / 16d, 4d / 16d, 12d / 16d, 12d / 16d));
        connAABBs.put(TPDirection.UP, new AxisAlignedBB(4d / 16d, 12d / 16d, 4d / 16d, 12d / 16d, 16d / 16d, 12d / 16d));
        connAABBs.put(TPDirection.DOWN, new AxisAlignedBB(4d / 16d, 0d / 16d, 4d / 16d, 12d / 16d, 4d / 16d, 12d / 16d));

    }

    @Override
    public void createDuctASD(Duct duct, Collection<TPDirection> connections) {
        Pipe pipe = (Pipe) duct;
        if (midASD.containsKey(pipe)) {
            return;
        }

        midASD.put(pipe, model.createMidASD(pipe.getDuctType()));
        Map<TPDirection, ArmorStandData> connMap = new HashMap<>();
        for (TPDirection connDir : connections) {
            connMap.put(connDir, model.createConnASD(ModelledPipeConnectionModelData.createConnectionModelData(pipe, connDir)));
        }
        connASD.put(pipe, connMap);
    }

    @Override
    public void updateDuctASD(Duct duct, Collection<TPDirection> connections, List<ArmorStandData> removeASD, List<ArmorStandData> addASD) {
        Pipe pipe = (Pipe) duct;
        if (!midASD.containsKey(pipe) || !connASD.containsKey(pipe)) {
            return;
        }
        Map<TPDirection, ArmorStandData> connMap = connASD.get(pipe);

        for (TPDirection connDir : TPDirection.values()) {
            if (connMap.containsKey(connDir) && connections.contains(connDir)) {
                // direction was active before and after update
                ArmorStandData newASD = model.createConnASD(ModelledPipeConnectionModelData.createConnectionModelData(pipe, connDir));
                if (!connMap.get(connDir).isSimilar(newASD)) {
                    // ASD changed after update in this direction
                    removeASD.add(connMap.get(connDir));
                    addASD.add(newASD);
                    connMap.put(connDir, newASD);
                }
            } else if (!connMap.containsKey(connDir) && connections.contains(connDir)) {
                // direction wasn't active before update but direction IS active after update
                ArmorStandData newASD = model.createConnASD(ModelledPipeConnectionModelData.createConnectionModelData(pipe, connDir));
                addASD.add(newASD);
                connMap.put(connDir, newASD);
            } else if (connMap.containsKey(connDir) && !connections.contains(connDir)) {
                // direction was active before update but isn't active after update
                removeASD.add(connMap.get(connDir));
                connMap.remove(connDir);
            }
        }
    }

    @Override
    public void destroyDuctASD(Duct duct) {
        Pipe pipe = (Pipe) duct;
        midASD.remove(pipe);
        connASD.remove(pipe);
    }

    @Override
    public List<ArmorStandData> getASDForDuct(Duct duct) {
        Pipe pipe = (Pipe) duct;

        List<ArmorStandData> asd = new ArrayList<>();
        if (midASD.containsKey(pipe)) {
            asd.add(midASD.get(pipe));
        }
        if (connASD.containsKey(pipe)) {
            asd.addAll(connASD.get(pipe).values());
        }
        return asd;
    }

    @Override
    public AxisAlignedBB getOuterHitbox(Duct duct) {
        Pipe pipe = (Pipe) duct;
        if (pipe == null) {
            return null;
        }
        List<AxisAlignedBB> aabbs = new ArrayList<>();
        aabbs.add(midAABB);
        Collection<TPDirection> pipeConns = pipe.getAllConnections();
        for (TPDirection connDir : pipeConns) {
            aabbs.add(connAABBs.get(connDir));
        }
        double minx = Double.MAX_VALUE;
        double miny = Double.MAX_VALUE;
        double minz = Double.MAX_VALUE;
        double maxx = Double.MIN_VALUE;
        double maxy = Double.MIN_VALUE;
        double maxz = Double.MIN_VALUE;
        for (AxisAlignedBB aabb : aabbs) {
            minx = Math.min(aabb.getMin().getDoubleX(), minx);
            miny = Math.min(aabb.getMin().getDoubleY(), miny);
            minz = Math.min(aabb.getMin().getDoubleZ(), minz);
            maxx = Math.max(aabb.getMax().getDoubleX(), maxx);
            maxy = Math.max(aabb.getMax().getDoubleY(), maxy);
            maxz = Math.max(aabb.getMax().getDoubleZ(), maxz);
        }
        return new AxisAlignedBB(minx, miny, minz, maxx, maxy, maxz);
    }

    @Override
    public TPDirection getClickedDuctFace(Player player, Duct duct) {
        Pipe pipe = (Pipe) duct;
        Vector ray = player.getEyeLocation().getDirection();
        Vector origin = player.getEyeLocation().toVector();

        Collection<TPDirection> connDirs = pipe.getAllConnections();
        TPDirection clickedMidFace = midAABB.performRayIntersection(ray, origin, pipe.getBlockLoc());
        if (clickedMidFace != null && !connDirs.contains(clickedMidFace)) {
            return clickedMidFace;
        } else {
            double nearestDistanceSquared = Double.MAX_VALUE;
            TPDirection currentClickedConnFace = null;
            for (TPDirection connDir : connDirs) {
                AxisAlignedBB connAABB = connAABBs.get(connDir);
                double newDistanceSquared = connAABB.getAABBMiddle(pipe.getBlockLoc()).distanceSquared(origin);
                if (newDistanceSquared < nearestDistanceSquared) {
                    TPDirection clickedConnFace = connAABB.performRayIntersection(ray, origin, pipe.getBlockLoc());
                    if (clickedConnFace != null) {
                        nearestDistanceSquared = newDistanceSquared;
                        currentClickedConnFace = clickedConnFace;
                    }
                }
            }
            return currentClickedConnFace;
        }
    }

} public class VanillaPipeRenderSystem extends VanillaRenderSystem {

    private VanillaPipeModel midModel = new VanillaPipeModelMID();
    private VanillaPipeModel ewModel = new VanillaPipeModelEW();
    private VanillaPipeModel nsModel = new VanillaPipeModelNS();
    private VanillaPipeModel udModel = new VanillaPipeModelUD();

    private Map<Pipe, List<ArmorStandData>> pipeASD = new HashMap<>();

    @Inject
    public VanillaPipeRenderSystem(DuctRegister ductRegister) {
        super(ductRegister.baseDuctTypeOf("Pipe"));
        VanillaPipeModel.init(ductRegister);
    }

    @Override
    public void createDuctASD(Duct duct, Collection<TPDirection> connections) {
        Pipe pipe = (Pipe) duct;
        if (pipeASD.containsKey(pipe)) {
            return;
        }
        pipeASD.put(pipe, getModel(pipe.getDuctType(), connections).createASD(VanillaPipeModelData.createModelData(pipe)));
    }

    @Override
    public void updateDuctASD(Duct duct, Collection<TPDirection> connections, List<ArmorStandData> removeASD, List<ArmorStandData> addASD) {
        Pipe pipe = (Pipe) duct;
        if (!pipeASD.containsKey(pipe)) {
            return;
        }

        List<ArmorStandData> oldASD = pipeASD.get(pipe);
        List<ArmorStandData> newASD = getModel(pipe.getDuctType(), connections).createASD(VanillaPipeModelData.createModelData(pipe));

        List<ArmorStandData> tempASD = new ArrayList<>();

        for (int i = 0; i < Math.max(oldASD.size(), newASD.size()); i++) {
            ArmorStandData ASDOld = i < oldASD.size() ? oldASD.get(i) : null;
            ArmorStandData ASDNew = i < newASD.size() ? newASD.get(i) : null;
            if (!((ASDOld != null && ASDOld.isSimilar(ASDNew)) || ASDOld == ASDNew)) {
                if (ASDOld != null) {
                    removeASD.add(ASDOld);
                }
                if (ASDNew != null) {
                    addASD.add(ASDNew);
                    tempASD.add(ASDNew);
                }
            } else if (ASDOld != null) {
                tempASD.add(ASDOld);
            }
        }

        oldASD.clear();
        oldASD.addAll(tempASD);
    }

    @Override
    public void destroyDuctASD(Duct duct) {
        Pipe pipe = (Pipe) duct;
        pipeASD.remove(pipe);
    }

    @Override
    public List<ArmorStandData> getASDForDuct(Duct duct) {
        Pipe pipe = (Pipe) duct;
        return pipeASD.get(pipe);
    }

    @Override
    public AxisAlignedBB getOuterHitbox(Duct duct) {
        Pipe pipe = (Pipe) duct;
        return getModel(pipe.getDuctType(), duct.getAllConnections()).getAABB();
    }

    @Override
    public TPDirection getClickedDuctFace(Player player, Duct duct) {
        return getOuterHitbox(duct).performRayIntersection(player.getEyeLocation().getDirection(), player.getEyeLocation().toVector(), duct.getBlockLoc());
    }

    private VanillaPipeModel getModel(PipeType pt, Collection<TPDirection> conns) {
        List<TPDirection> connList = new ArrayList<>(conns);
        if (!(pt.is("Golden") || pt.is("Iron") || pt.is("Void") || pt.is("Crafting"))) {
            if (connList.size() == 1) {
                if (connList.get(0) == TPDirection.EAST || connList.get(0) == TPDirection.WEST) {
                    return ewModel;
                } else if (connList.get(0) == TPDirection.NORTH || connList.get(0) == TPDirection.SOUTH) {
                    return nsModel;
                } else if (connList.get(0) == TPDirection.UP || connList.get(0) == TPDirection.DOWN) {
                    return udModel;
                }
            } else if (connList.size() == 2 && connList.get(0).getOpposite().equals(connList.get(1))) {
                if (connList.get(0) == TPDirection.EAST || connList.get(0) == TPDirection.WEST) {
                    return ewModel;
                } else if (connList.get(0) == TPDirection.NORTH || connList.get(0) == TPDirection.SOUTH) {
                    return nsModel;
                } else if (connList.get(0) == TPDirection.UP || connList.get(0) == TPDirection.DOWN) {
                    return udModel;
                }
            }
        }
        return midModel;
    }

} public class DiskService {

    @Inject
    private TransportPipes transportPipes;
    @Inject
    private DuctSaver ductSaver;
    @Inject
    private SentryService sentry;

    public void loadDuctsSync(World world) {
        try {
            Path p = Paths.get(world.getWorldFolder().getAbsolutePath(), "ducts.dat");
            if (!Files.isRegularFile(p)) {
                p = Paths.get(world.getWorldFolder().getAbsolutePath(), "pipes.dat");
            }
            CompoundTag compoundTag = (CompoundTag) NBTUtil.readTag(p.toFile());
            String version = compoundTag.getString("version");
            if(version == null || version.isEmpty()) {
                version = compoundTag.getString("PluginVersion");
            }

            long versionLong = transportPipes.convertVersionToLong(version);
            DuctLoader ductLoader;

            //one more than the real version to handle all versions below
            if (versionLong <= transportPipes.convertVersionToLong("4.3.2")) {
                ductLoader = transportPipes.getInjector().getSingleton(LegacyDuctLoader_v4_3_1.class);
            } else {
                ductLoader = transportPipes.getInjector().getSingleton(DuctLoader.class);
            }

            if (ductLoader == null) {
                throw new IOException("Could not load ducts.dat file because version " + version + " is not supported!");
            }

            ductLoader.loadDuctsSync(world, compoundTag);

        } catch (FileNotFoundException ignored) {
        } catch (Exception e) {
            e.printStackTrace();
            sentry.record(e);
        }
    }

    public void saveDuctsSync(World world) {
        try {
            ductSaver.saveDuctsSync(world);
        } catch (Exception e) {
            sentry.record(e);
        }
    }

} public class LWCUtils extends JavaModule {

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

    @Override
    public void onPostRegistration(LWCProtectionRegistrationPostEvent e) {
        if (e.getProtection().getType() == Protection.Type.PUBLIC) {
            return;
        }
        boolean destroyedAtLeastOneDuct = false;

        PipeManager pipeManager = ((PipeManager) (DuctManager<?>) ductRegister.baseDuctTypeOf("pipe").getDuctManager());

        Location protectionLoc = e.getProtection().getBlock().getLocation();
        BlockLocation protectionBlockLoc = new BlockLocation(protectionLoc);
        Map<BlockLocation, TransportPipesContainer> containerMap = pipeManager.getContainers(e.getProtection().getBukkitWorld());
        if (containerMap != null && containerMap.containsKey(protectionBlockLoc)) {
            Map<BlockLocation, Duct> ductMap = globalDuctManager.getDucts(e.getProtection().getBukkitWorld());
            if (ductMap != null) {
                for (TPDirection dir : TPDirection.values()) {
                    if (ductMap.containsKey(protectionBlockLoc.getNeighbor(dir))) {
                        Duct duct = ductMap.get(protectionBlockLoc.getNeighbor(dir));
                        if (duct instanceof Pipe) {
                            globalDuctManager.unregisterDuct(duct);
                            globalDuctManager.unregisterDuctInRenderSystem(duct, true);
                            globalDuctManager.updateNeighborDuctsConnections(duct);
                            globalDuctManager.updateNeighborDuctsInRenderSystems(duct, true);
                            globalDuctManager.playDuctDestroyActions(duct, null);
                            destroyedAtLeastOneDuct = true;
                        }
                    }
                }
            }
        }

        if (destroyedAtLeastOneDuct) {
            LangConf.Key.PROTECTED_BLOCK.sendMessage(e.getPlayer());
        }

    }
} public abstract class LegacyUtils {

    private static LegacyUtils instance;

    public abstract Material getRedDye();

    public abstract Material getYellowDye();

    public abstract Material getGreenDye();

    public static LegacyUtils getInstance() {
        return instance;
    }

    public static void setInstance(LegacyUtils instance) {
        LegacyUtils.instance = instance;
    }
} public class LegacyUtils_1_13 extends LegacyUtils {

    @Override
    public Material getRedDye() {
        return Material.valueOf("ROSE_RED");
    }

    @Override
    public Material getYellowDye() {
        return Material.valueOf("DANDELION_YELLOW");
    }

    @Override
    public Material getGreenDye() {
        return Material.valueOf("CACTUS_GREEN");
    }
}
import ch.jalu.injector.Injector;
import ch.jalu.injector.InjectorBuilder;
import co.aikar.commands.PaperCommandManager;
import de.robotricker.transportpipes.api.TransportPipesAPI;
import de.robotricker.transportpipes.commands.TPCommand;
import de.robotricker.transportpipes.config.GeneralConf;
import de.robotricker.transportpipes.config.LangConf;
import de.robotricker.transportpipes.config.PlayerSettingsConf;
import de.robotricker.transportpipes.duct.Duct;
import de.robotricker.transportpipes.duct.DuctRegister;
import de.robotricker.transportpipes.duct.factory.PipeFactory;
import de.robotricker.transportpipes.duct.manager.GlobalDuctManager;
import de.robotricker.transportpipes.duct.manager.PipeManager;
import de.robotricker.transportpipes.duct.pipe.Pipe;
import de.robotricker.transportpipes.duct.types.BaseDuctType;
import de.robotricker.transportpipes.inventory.PlayerSettingsInventory;
import de.robotricker.transportpipes.items.PipeItemManager;
import de.robotricker.transportpipes.listener.DuctListener;
import de.robotricker.transportpipes.listener.PlayerListener;
import de.robotricker.transportpipes.listener.TPContainerListener;
import de.robotricker.transportpipes.listener.WorldListener;
import de.robotricker.transportpipes.log.LoggerService;
import de.robotricker.transportpipes.log.SentryService;
import de.robotricker.transportpipes.protocol.ProtocolService;
import de.robotricker.transportpipes.rendersystems.RenderSystem;
import de.robotricker.transportpipes.rendersystems.pipe.modelled.ModelledPipeRenderSystem;
import de.robotricker.transportpipes.rendersystems.pipe.vanilla.VanillaPipeRenderSystem;
import de.robotricker.transportpipes.saving.DiskService;
import de.robotricker.transportpipes.utils.LWCUtils;
import de.robotricker.transportpipes.utils.legacy.LegacyUtils;
import de.robotricker.transportpipes.utils.legacy.LegacyUtils_1_13;
import io.sentry.event.Breadcrumb;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.logging.Logger;
import java.util.stream.Collectors;
package de.robotricker.transportpipes;



public class TransportPipes extends JavaPlugin {

    private Injector injector;

    private SentryService sentry;
    private ThreadService thread;
    private DiskService diskService;

    @Override
    public void onEnable() {

        if (Bukkit.getVersion().contains("1.13")) {
            LegacyUtils.setInstance(new LegacyUtils_1_13());
        } else {
            System.err.println("------------------------------------------");
            System.err.println("TransportPipes currently only works with Minecraft 1.13.1 and 1.13.2");
            System.err.println("------------------------------------------");
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }

        try {
            Class.forName("org.bukkit.inventory.RecipeChoice");
        } catch (ClassNotFoundException e) {
            System.err.println("------------------------------------------");
            System.err.println("TransportPipes currently only works with Minecraft 1.13.1 and 1.13.2");
            System.err.println("------------------------------------------");
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }

        if (Files.isRegularFile(Paths.get(getDataFolder().getPath(), "recipes.yml"))) {
            System.err.println("------------------------------------------");
            System.err.println("Please delete the old plugins/TransportPipes directory so TransportPipes can recreate it with a bunch of new config values");
            System.err.println("------------------------------------------");
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }

        //Initialize dependency injector
        injector = new InjectorBuilder().addDefaultHandlers("de.robotricker.transportpipes").create();
        injector.register(Logger.class, getLogger());
        injector.register(Plugin.class, this);
        injector.register(JavaPlugin.class, this);
        injector.register(TransportPipes.class, this);

        //Initialize logger
        LoggerService logger = injector.getSingleton(LoggerService.class);

        //Initialize sentry
        sentry = injector.getSingleton(SentryService.class);
        if (!sentry.init("https://84937d8c6bc2435d860021667341c87c@sentry.io/1281889?stacktrace.app.packages=de.robotricker&release=" + getDescription().getVersion())) {
            logger.warning("Unable to initialize sentry!");
        }
        sentry.addTag("thread", Thread.currentThread().getName());
        sentry.injectThread(Thread.currentThread());
        sentry.breadcrumb(Breadcrumb.Level.INFO, "MAIN", "enabling plugin");

        //Initialize configs
        injector.getSingleton(GeneralConf.class);
        injector.register(LangConf.class, new LangConf(this, injector.getSingleton(GeneralConf.class).getLanguage()));

        //Initialize API
        injector.getSingleton(TransportPipesAPI.class);

        //Initialize thread
        thread = injector.getSingleton(ThreadService.class);
        thread.start();

        //Register pipe
        BaseDuctType<Pipe> baseDuctType = injector.getSingleton(DuctRegister.class).registerBaseDuctType("Pipe", PipeManager.class, PipeFactory.class, PipeItemManager.class);