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);
        baseDuctType.setModelledRenderSystem(injector.newInstance(ModelledPipeRenderSystem.class));
        baseDuctType.setVanillaRenderSystem(injector.newInstance(VanillaPipeRenderSystem.class));

        //Register listeners
        Bukkit.getPluginManager().registerEvents(injector.getSingleton(TPContainerListener.class), this);
        Bukkit.getPluginManager().registerEvents(injector.getSingleton(PlayerListener.class), this);
        Bukkit.getPluginManager().registerEvents(injector.getSingleton(DuctListener.class), this);
        Bukkit.getPluginManager().registerEvents(injector.getSingleton(WorldListener.class), this);
        Bukkit.getPluginManager().registerEvents(injector.getSingleton(PlayerSettingsInventory.class), this);
        Bukkit.getPluginManager().registerEvents(injector.getSingleton(ResourcepackService.class), this);

        //Register commands
        PaperCommandManager commandManager = new PaperCommandManager(this);
        commandManager.enableUnstableAPI("help");
        commandManager.registerCommand(injector.getSingleton(TPCommand.class));
        commandManager.getCommandCompletions().registerCompletion("baseDuctType", c -> injector.getSingleton(DuctRegister.class).baseDuctTypes().stream().map(BaseDuctType::getName).collect(Collectors.toList()));

        sentry.breadcrumb(Breadcrumb.Level.INFO, "MAIN", "enabled plugin");

        diskService = injector.getSingleton(DiskService.class);

        TPContainerListener tpContainerListener = injector.getSingleton(TPContainerListener.class);
        runTaskSync(() -> {
            for (World world : Bukkit.getWorlds()) {
                for (Chunk loadedChunk : world.getLoadedChunks()) {
                    tpContainerListener.handleChunkLoadSync(loadedChunk, true);
                }
                diskService.loadDuctsSync(world);
            }
        });

        if (Bukkit.getPluginManager().isPluginEnabled("LWC")) {
            try {
                com.griefcraft.scripting.Module module = injector.getSingleton(LWCUtils.class);
                com.griefcraft.lwc.LWC.getInstance().getModuleLoader().registerModule(this, module);
            } catch (Exception e) {
                e.printStackTrace();
                sentry.record(e);
            }
        }

    }

    @Override
    public void onDisable() {
        if (sentry != null && thread != null) {
            sentry.breadcrumb(Breadcrumb.Level.INFO, "MAIN", "disabling plugin");
            // Stop tpThread gracefully
            try {
                thread.stopRunning();
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (World world : Bukkit.getWorlds()) {
                saveWorld(world);
            }
            sentry.breadcrumb(Breadcrumb.Level.INFO, "MAIN", "disabled plugin");
        }
    }

    public void saveWorld(World world) {
        sentry.breadcrumb(Breadcrumb.Level.INFO, "MAIN", "saving world " + world.getName());
        diskService.saveDuctsSync(world);
        sentry.breadcrumb(Breadcrumb.Level.INFO, "MAIN", "saved world " + world.getName());
    }

    public void runTaskSync(Runnable task) {
        if (isEnabled()) {
            Bukkit.getScheduler().runTask(this, task);
        }
    }

    public void runTaskSyncLater(Runnable task, long delay) {
        if (isEnabled()) {
            Bukkit.getScheduler().runTaskLater(this, task, delay);
        }
    }

    public void runTaskAsync(Runnable runnable, long delay) {
        thread.getTasks().put(runnable, delay);
    }

    public Injector getInjector() {
        return injector;
    }

    public void changeRenderSystem(Player p, String newRenderSystemName) {
        PlayerSettingsConf playerSettingsConf = injector.getSingleton(PlayerSettingsService.class).getOrCreateSettingsConf(p);
        DuctRegister ductRegister = injector.getSingleton(DuctRegister.class);
        GlobalDuctManager globalDuctManager = injector.getSingleton(GlobalDuctManager.class);
        ProtocolService protocolService = injector.getSingleton(ProtocolService.class);

        // change render system
        String oldRenderSystemName = playerSettingsConf.getRenderSystemName();
        if (oldRenderSystemName.equalsIgnoreCase(newRenderSystemName)) {
            return;
        }
        playerSettingsConf.setRenderSystemName(newRenderSystemName);

        for (BaseDuctType baseDuctType : ductRegister.baseDuctTypes()) {
            RenderSystem oldRenderSystem = RenderSystem.getRenderSystem(oldRenderSystemName, baseDuctType);

            // switch render system
            synchronized (globalDuctManager.getPlayerDucts(p)) {
                Iterator<Duct> ductIt = globalDuctManager.getPlayerDucts(p).iterator();
                while (ductIt.hasNext()) {
                    Duct nextDuct = ductIt.next();
                    protocolService.removeASD(p, oldRenderSystem.getASDForDuct(nextDuct));
                    ductIt.remove();
                }
            }

        }
    }

    public long convertVersionToLong(String version) {
        long versionLong = 0;
        try {
            if (version.contains("-")) {
                for (String subVersion : version.split("-")) {
                    if (subVersion.startsWith("b")) {
                        int buildNumber = 0;
                        String buildNumberString = subVersion.substring(1);
                        if (!buildNumberString.equalsIgnoreCase("CUSTOM")) {
                            buildNumber = Integer.parseInt(buildNumberString);
                        }
                        versionLong |= buildNumber;
                    } else if (!subVersion.equalsIgnoreCase("SNAPSHOT")) {
                        versionLong |= (long) convertMainVersionStringToInt(subVersion) << 32;
                    }
                }
            } else {
                versionLong = (long) convertMainVersionStringToInt(version) << 32;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return versionLong;
    }

    private int convertMainVersionStringToInt(String mainVersion) {
        int versionInt = 0;
        if (mainVersion.contains(".")) {
            // shift for every version number 1 byte to the left
            int leftShift = (mainVersion.split("\\.").length - 1) * 8;
            for (String subVersion : mainVersion.split("\\.")) {
                byte v = Byte.parseByte(subVersion);
                versionInt |= ((int) v << leftShift);
                leftShift -= 8;
            }
        } else {
            versionInt = Byte.parseByte(mainVersion);
        }
        return versionInt;
    }

} 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 abstract class DuctManager<T extends Duct> {

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

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

    public abstract void registerDuctTypes();

    public abstract void registerRecipes();

    public abstract void tick();

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

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

    public void updateNonDuctConnections(Duct duct) {

    }

} public class GlobalDuctManager {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                renderSystem.destroyDuctASD(duct);

            }
        }
    }

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

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

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

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

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

} public class ItemDistributorService {

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

    private Map<Pipe, Integer> pipeItemDistributionCounter;

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

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

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

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

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

        return freeSpaceMap;
    }

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

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

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

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

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

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

            distributionCounter++;
        }

        pipeItemDistributionCounter.put(pipe, distributionCounter);

        return splitMap;

    }

} public class ItemFilter {

    public static final int MAX_ITEMS_PER_ROW = 32;

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

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

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

    public FilterMode getFilterMode() {
        return filterMode;
    }

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

    public FilterStrictness getFilterStrictness() {
        return filterStrictness;
    }

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

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

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

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

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

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

} public class PipeItem {

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

    public PipeItem() {
    }

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

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

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

    public ArmorStandData getAsd() {
        return asd;
    }

    public ItemStack getItem() {
        return item;
    }

    public World getWorld() {
        return world;
    }

    public BlockLocation getBlockLoc() {
        return blockLoc;
    }

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

    public RelativeLocation getOldRelativeLocation() {
        return oldRelativeLocation;
    }

    public RelativeLocation getRelativeLocation() {
        return relativeLocation;
    }

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

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

    public TPDirection getMovingDir() {
        return movingDir;
    }

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

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

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

} public class DuctType {

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

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

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

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

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

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

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

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

    public String getName() {
        return name;
    }

    public String getDisplayName() {
        return displayName;
    }

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

    public String getFormattedTypeName() {
        return displayName;
    }

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

    public Recipe getDuctRecipe() {
        return ductRecipe;
    }

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

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

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

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

} public abstract class DuctSettingsInventory extends GlobalInventory implements Listener {

    @Inject
    protected ItemService itemService;

    @Inject
    protected TransportPipes transportPipes;

    protected Duct duct;

    public final void setDuct(Duct duct) {
        this.duct = duct;
    }

    public void closeForAllPlayers(TransportPipes transportPipes){
        transportPipes.runTaskSync(() -> {
            for (int i = 0; i < inv.getViewers().size(); i++) {
                inv.getViewers().get(i).closeInventory();
            }
        });
    }

    public abstract void create();

    public abstract void populate();

    /**
     * returns whether to cancel the click event or not
     */
    protected abstract boolean click(Player p, int rawSlot, ClickType ct);

    protected boolean drag(Player p, Set<Integer> rawSlots, DragType dragType) {
        return false;
    }

    protected boolean collect_to_cursor(Player p, ItemStack cursorItem, int rawSlot) {
        return false;
    }

    public abstract void save(Player p);

    @EventHandler
    public void onInvClick(InventoryClickEvent e) {
        if (e.getInventory() != null && e.getInventory().equals(inv) && e.getWhoClicked() instanceof Player) {
            if (itemService.isItemWildcardOrBarrier(e.getCurrentItem())) {
                e.setCancelled(true);
                return;
            }
            //don't call click method when double clicked, because before a double click, left click was already registered twice
            if (e.getClick() != ClickType.DOUBLE_CLICK && click((Player) e.getWhoClicked(), e.getRawSlot(), e.getClick())) {
                e.setCancelled(true);
            } else if (e.getClick() == ClickType.DOUBLE_CLICK && e.getAction() == InventoryAction.COLLECT_TO_CURSOR && collect_to_cursor((Player) e.getWhoClicked(), e.getCursor(), e.getRawSlot())) {
                e.setCancelled(true);
            }
        }
    }

    @EventHandler
    public void onDrag(InventoryDragEvent e) {
        if (e.getInventory() != null && e.getInventory().equals(inv) && e.getWhoClicked() instanceof Player) {
            if (drag((Player) e.getWhoClicked(), e.getRawSlots(), e.getType())) {
                e.setCancelled(true);
            }
        }
    }

    @EventHandler
    public void onClose(InventoryCloseEvent e) {
        if (e.getInventory() != null && e.getInventory().equals(inv) && e.getPlayer() instanceof Player) {
            save((Player) e.getPlayer());
        }
    }

} public class GoldenPipeSettingsInventory extends DuctSettingsInventory {

    private Map<GoldenPipe.Color, Integer> scrollValues;

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

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

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

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

            inv.setItem(line * 9, wool);

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

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

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

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

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

            save(p);
            populate();

            return true;
        }

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

            save(p);

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

            populate();

            return true;
        }

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

            save(p);

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

            populate();

            return true;
        }

        return false;
    }

    @Override
    public void save(Player p) {

        GoldenPipe pipe = (GoldenPipe) duct;

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

    }
} public class ItemService {

    private ItemStack wrench;
    private YamlConfiguration tempConf;

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

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

    public ItemStack getWrench() {
        return wrench;
    }

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

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

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

        return woodenPickage;
    }

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

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

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

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

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

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

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

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

        skull.setItemMeta(sm);
        return skull;
    }

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

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

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

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

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

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

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

            }
        }
        return null;
    }

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

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

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

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

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

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

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

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

} public class BlockLocation implements Comparable<BlockLocation> {

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

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

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

    public int getX() {
        return x;
    }

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

    public int getY() {
        return y;
    }

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

    public int getZ() {
        return z;
    }

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

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

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

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

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

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

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

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

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

            }
        }
        return null;
    }

} public enum TPDirection {

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

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

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

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

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

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

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

    public BlockFace getBlockFace() {
        return blockFace;
    }

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

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

    public String getDisplayName() {
        return displayName;
    }

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

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

}
import net.querz.nbt.CompoundTag;
import net.querz.nbt.ListTag;
import org.bukkit.ChatColor;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.robotricker.transportpipes.TransportPipes;
import de.robotricker.transportpipes.config.LangConf;
import de.robotricker.transportpipes.duct.manager.DuctManager;
import de.robotricker.transportpipes.duct.manager.GlobalDuctManager;
import de.robotricker.transportpipes.duct.pipe.filter.ItemDistributorService;
import de.robotricker.transportpipes.duct.pipe.filter.ItemFilter;
import de.robotricker.transportpipes.duct.pipe.items.PipeItem;
import de.robotricker.transportpipes.duct.types.DuctType;
import de.robotricker.transportpipes.inventory.DuctSettingsInventory;
import de.robotricker.transportpipes.inventory.GoldenPipeSettingsInventory;
import de.robotricker.transportpipes.items.ItemService;
import de.robotricker.transportpipes.location.BlockLocation;
import de.robotricker.transportpipes.location.TPDirection;
package de.robotricker.transportpipes.duct.pipe;





public class GoldenPipe extends Pipe {

    private ItemFilter[] itemFilters;

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

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

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

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

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

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

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

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

    }

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

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

        settingsInv.populate();
    }

    public enum Color {

        BLUE(Material.BLUE_WOOL, Material.BLUE_STAINED_GLASS_PANE, LangConf.Key.COLORS_BLUE.get(), TPDirection.EAST),