public class PlayerSettingsService {

    @Inject
    private TransportPipes transportPipes;
    @Inject
    private GeneralConf generalConf;

    private Map<Player, PlayerSettingsConf> cachedSettingsConfs;

    public PlayerSettingsService() {
        this.cachedSettingsConfs = new HashMap<>();
    }

    public PlayerSettingsConf getOrCreateSettingsConf(Player p) {
        if (!cachedSettingsConfs.containsKey(p)) {
            cachedSettingsConfs.put(p, new PlayerSettingsConf(transportPipes, generalConf, p));
        }
        return cachedSettingsConfs.get(p);
    }

} public class ThreadService extends Thread {

    private final Map<Runnable, Long> tasks;

    private LoggerService logger;
    private SentryService sentry;
    private GlobalDuctManager globalDuctManager;
    private PlayerSettingsService playerSettingsService;

    private boolean running = false;
    private int preferredTPS = 10;
    private int currentTPS = 0;

    @Inject
    public ThreadService(JavaPlugin plugin, LoggerService logger, SentryService sentry, GlobalDuctManager globalDuctManager, PlayerSettingsService playerSettingsService) {
        super("TransportPipes-Thread");
        this.logger = logger;
        this.sentry = sentry;
        this.globalDuctManager = globalDuctManager;
        this.playerSettingsService = playerSettingsService;
        this.tasks = Collections.synchronizedMap(new LinkedHashMap<>());

        Bukkit.getScheduler().runTaskTimer(plugin, (Runnable) this::tickDuctSpawnAndDespawn, 20L, 20L);
    }

    @Override
    public void run() {
        logger.info("Started ThreadService");
        running = true;
        sentry.addTag("thread", getName());
        sentry.injectThread(this);

        long lastTick = System.currentTimeMillis();
        long lastSec = lastTick;
        int tpsCounter = 0;
        while (running) {
            long currentTick = System.currentTimeMillis();
            long diff = currentTick - lastTick;
            if (diff >= 1000f / preferredTPS) {

                tick();

                tpsCounter++;
                lastTick = currentTick;

                if (currentTick - lastSec >= 1000) {
                    currentTPS = tpsCounter;
                    tpsCounter = 0;
                    lastSec = currentTick;
                    logger.debug("TPS: " + currentTPS);
                }

            } else {
                long waitTime = (long) (1000f / preferredTPS - diff);
                try {
                    sleep(waitTime);
                } catch (InterruptedException e) {
                    logger.error("ThreadService was terminated while sleeping!", e);
                }
            }
        }
        logger.info("Stopped ThreadService");
    }

    public Map<Runnable, Long> getTasks() {
        return tasks;
    }

    private void tick() {
        //schedule tasks
        Set<Runnable> taskSet = tasks.keySet();
        synchronized (tasks) {
            Iterator<Runnable> taskIt = taskSet.iterator();
            while (taskIt.hasNext()) {
                Runnable task = taskIt.next();
                if (tasks.get(task) > 1) {
                    tasks.put(task, tasks.get(task) - 1);
                } else {
                    taskIt.remove();
                    task.run();
                }
            }
        }

        globalDuctManager.tick();
    }

    /**
     * does the same as tickDuctSpawnAndDespawn(Duct duct) but for all ducts in all worlds
     */
    private void tickDuctSpawnAndDespawn() {
        synchronized (globalDuctManager.getDucts()) {
            for (World world : Bukkit.getWorlds()) {
                Map<BlockLocation, Duct> ductMap = globalDuctManager.getDucts(world);
                if (ductMap != null) {
                    for (Duct duct : ductMap.values()) {
                        tickDuctSpawnAndDespawn(duct);
                    }
                }
            }
        }
    }

    /**
     * does the same as tickDuctSpawnAndDespawn(Duct duct, Player p) but for all players in the duct's world
     */
    public void tickDuctSpawnAndDespawn(Duct duct) {
        List<Player> playerList = WorldUtils.getPlayerList(duct.getWorld());
        for (Player worldPlayer : playerList) {
            tickDuctSpawnAndDespawn(duct, worldPlayer);
        }
    }

    /**
     * sync method which checks if the given duct should be visible or not for the given player and shows / hides the duct
     */
    public void tickDuctSpawnAndDespawn(Duct duct, Player p) {
        int renderDistance = playerSettingsService.getOrCreateSettingsConf(p).getRenderDistance();
        if (duct.getBlockLoc().toLocation(duct.getWorld()).distance(p.getLocation()) <= renderDistance && (duct.obfuscatedWith() == null || duct.getBlockLoc().toBlock(duct.getWorld()).getType() == Material.BARRIER)) {
            // spawn globalDuctManager if not there
            duct.getDuctType().getBaseDuctType().getDuctManager().notifyDuctShown(duct, p);
        } else {
            // despawn globalDuctManager if there
            duct.getDuctType().getBaseDuctType().getDuctManager().notifyDuctHidden(duct, p);
        }
    }

    public int getCurrentTPS() {
        return currentTPS;
    }

    public int getPreferredTPS() {
        return preferredTPS;
    }

    public void setPreferredTPS(int preferredTPS) {
        this.preferredTPS = preferredTPS;
    }

    public boolean isRunning() {
        return running;
    }

    public void stopRunning() {
        running = false;
    }

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

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

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

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

    public DuctSettingsInventory getSettingsInv() {
        return settingsInv;
    }

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

    public DuctType getDuctType() {
        return ductType;
    }

    public BlockLocation getBlockLoc() {
        return blockLoc;
    }

    public World getWorld() {
        return world;
    }

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

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

    public BlockData obfuscatedWith() {
        return obfuscatedWith;
    }

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

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

    }

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

    }

    public void syncBigTick(DuctManager ductManager) {

    }

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

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

    public Material getBreakParticleData() {
        return null;
    }

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

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

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

        return dropItems;
    }

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

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

} public class DuctRegister {

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

    private TransportPipes plugin;
    private GeneralConf generalConf;

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

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

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

        return baseDuctType;
    }

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

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

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

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

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

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

} public class GlobalDuctManager {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                renderSystem.destroyDuctASD(duct);

            }
        }
    }

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

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

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

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

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

} public class DuctType {

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

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

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

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

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

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

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

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

    public String getName() {
        return name;
    }

    public String getDisplayName() {
        return displayName;
    }

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

    public String getFormattedTypeName() {
        return displayName;
    }

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

    public Recipe getDuctRecipe() {
        return ductRecipe;
    }

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

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

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

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

} public class ItemService {

    private ItemStack wrench;
    private YamlConfiguration tempConf;

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

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

    public ItemStack getWrench() {
        return wrench;
    }

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

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

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

        return woodenPickage;
    }

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

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

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

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

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

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

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

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

        skull.setItemMeta(sm);
        return skull;
    }

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

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

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

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

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

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

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

            }
        }
        return null;
    }

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

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

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

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

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

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

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

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

} public class BlockLocation implements Comparable<BlockLocation> {

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

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

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

    public int getX() {
        return x;
    }

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

    public int getY() {
        return y;
    }

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

    public int getZ() {
        return z;
    }

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

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

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

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

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

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

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

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

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

            }
        }
        return null;
    }

} public 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;
    }

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

    private final static int HITBOX_RANGE = 5;
    private final static Set<Material> LINE_OF_SIGHT_SET;

    static {
        LINE_OF_SIGHT_SET = new HashSet<>();
        LINE_OF_SIGHT_SET.add(Material.WATER);
        LINE_OF_SIGHT_SET.add(Material.LAVA);
        LINE_OF_SIGHT_SET.add(Material.AIR);
        // add transprant blocks, so that when you look only a little above the hitbox
        // of e.g. a grass,
        // you will neverless click on the pipe unless you really click on the hitbox of
        // the grass.
        // without this code, the line of sight will stop at this transparent block and
        // won't recognize the pipe behind it.
        for (Material m : Material.values()) {
            if (m.isTransparent()) {
                LINE_OF_SIGHT_SET.add(m);
            }
        }
    }

    public static List<Block> getLineOfSight(Player p) {
        try {
            return p.getLineOfSight(LINE_OF_SIGHT_SET, HITBOX_RANGE);
        } catch (IllegalStateException ignored) {

        }
        return new ArrayList<>();
    }

    public static Duct getDuctLookingTo(GlobalDuctManager globalDuctManager, Player p, Block clickedBlock) {
        List<Block> line = getLineOfSight(p);

        Duct currentDuct = null;
        int indexOfDuctBlock = -1;

        for (int i = 0; currentDuct == null && line.size() > i; i++) {
            // check whether on this block is a duct or not
            Duct tempDuct = globalDuctManager.getDuctAtLoc(line.get(i).getLocation());
            if (tempDuct != null) {
                // check if the player looks on the hitbox of the duct (the player could
                // possibly look on a block with a duct but not on the hitbox itself)
                RenderSystem playerRenderSystem = globalDuctManager.getPlayerRenderSystem(p, tempDuct.getDuctType().getBaseDuctType());
                if (playerRenderSystem.getClickedDuctFace(p, tempDuct) != null) {
                    currentDuct = tempDuct;
                    indexOfDuctBlock = i;
                }
            }
        }

        // calculate the index of the block clicked on
        int indexOfClickedBlock = -1;
        if (clickedBlock != null) {
            if (line.contains(clickedBlock)) {
                indexOfClickedBlock = line.indexOf(clickedBlock);
            }
        }

        // check if the clicked block is before the duct block, so that you can't
        // interact with a duct behind the clicked block
        if (indexOfDuctBlock != -1 && indexOfClickedBlock != -1) {
            if (indexOfClickedBlock <= indexOfDuctBlock) {
                return null;
            }
        }
        return currentDuct;
    }

    /**
     * gets the neighbor block of the duct (where a block would be placed if right clicked) (calculated by the player direction ray
     * and the duct hitbox)
     */
    public static Block getRelativeBlockOfDuct(GlobalDuctManager globalDuctManager, Player p, Block ductLoc) {
        Duct duct = globalDuctManager.getDuctAtLoc(ductLoc.getLocation());
        if (duct == null) {
            return null;
        }
        TPDirection dir = globalDuctManager.getPlayerRenderSystem(p, duct.getDuctType().getBaseDuctType()).getClickedDuctFace(p, duct);
        return dir != null ? ductLoc.getRelative(dir.getBlockFace()) : null;
    }

} public class WorldUtils {

    private static Map<Player, Integer> hidingDuctsTimers = new HashMap<>();

    /**
     * THREAD-SAFE
     */
    public static List<Player> getPlayerList(World world) {
        // Bukkit.getOnlinePlayers is the only thread safe playerlist getter
        List<Player> playerList = new ArrayList<>();
        for (Player p : Bukkit.getOnlinePlayers()) {
            if (p.getLocation().getWorld().equals(world)) {
                playerList.add(p);
            }
        }
        return playerList;
    }

    public static boolean isContainerBlock(Material material) {
        switch (material) {
            case DISPENSER:
            case CHEST:
            case FURNACE:
            case TRAPPED_CHEST:
            case DROPPER:
            case HOPPER:
            case BREWING_STAND:
            case SHULKER_BOX:
            case WHITE_SHULKER_BOX:
            case BLACK_SHULKER_BOX:
            case BLUE_SHULKER_BOX:
            case GRAY_SHULKER_BOX:
            case BROWN_SHULKER_BOX:
            case CYAN_SHULKER_BOX:
            case GREEN_SHULKER_BOX:
            case LIGHT_BLUE_SHULKER_BOX:
            case LIGHT_GRAY_SHULKER_BOX:
            case LIME_SHULKER_BOX:
            case MAGENTA_SHULKER_BOX:
            case ORANGE_SHULKER_BOX:
            case PINK_SHULKER_BOX:
            case PURPLE_SHULKER_BOX:
            case RED_SHULKER_BOX:
            case YELLOW_SHULKER_BOX:
                return true;
            default:
                return false;
        }
    }

    public static boolean lwcProtection(Block b) {
        if (Bukkit.getPluginManager().isPluginEnabled("LWC")) {
            try {
                com.griefcraft.model.Protection protection = com.griefcraft.lwc.LWC.getInstance().findProtection(b);
                return protection != null && protection.getType() != com.griefcraft.model.Protection.Type.PUBLIC;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    public static void startShowHiddenDuctsProcess(Player p, GlobalDuctManager globalDuctManager, ThreadService threadService, TransportPipes transportPipes, GeneralConf generalConf, PlayerSettingsService playerSettingsService) {

        int renderDistance = playerSettingsService.getOrCreateSettingsConf(p).getRenderDistance();

        if (hidingDuctsTimers.containsKey(p)) {
            return;
        }
        Set<Duct> showingDucts = new HashSet<>();
        Map<BlockLocation, Duct> ductMap = globalDuctManager.getDucts(p.getWorld());
        synchronized (globalDuctManager.getDucts()) {
            for (BlockLocation bl : ductMap.keySet()) {
                Duct duct = ductMap.get(bl);
                Block ductBlock = duct.getBlockLoc().toBlock(duct.getWorld());
                if (ductBlock.getLocation().distance(p.getLocation()) > renderDistance) {
                    continue;
                }
                if (duct.obfuscatedWith() != null && ductBlock.getBlockData().getMaterial() != Material.BARRIER) {
                    showingDucts.add(duct);
                    ductBlock.setBlockData(Material.BARRIER.createBlockData(), false);
                    threadService.tickDuctSpawnAndDespawn(duct);
                }
            }
        }

        int duration = generalConf.getShowHiddenDuctsTime();
        int[] task = new int[]{0};
        hidingDuctsTimers.put(p, duration);

        task[0] = Bukkit.getScheduler().scheduleSyncRepeatingTask(transportPipes, () -> {

            WrapperPlayServerTitle titlePacket = new WrapperPlayServerTitle();
            titlePacket.setTitle(WrappedChatComponent.fromText(LangConf.Key.SHOW_HIDDEN_DUCTS.get(hidingDuctsTimers.get(p))));
            titlePacket.setAction(EnumWrappers.TitleAction.ACTIONBAR);
            titlePacket.sendPacket(p);

            if (hidingDuctsTimers.get(p) == 0) {
                hidingDuctsTimers.remove(p);
                for (Duct duct : showingDucts) {
                    if (globalDuctManager.getDuctAtLoc(duct.getWorld(), duct.getBlockLoc()) != duct) {
                        continue;
                    }
                    Block ductBlock = duct.getBlockLoc().toBlock(duct.getWorld());
                    if (ductBlock.getBlockData().getMaterial() == Material.BARRIER) {
                        ductBlock.setBlockData(duct.obfuscatedWith() == null ? Material.AIR.createBlockData() : duct.obfuscatedWith(), false);
                        threadService.tickDuctSpawnAndDespawn(duct);
                    }
                }
                Bukkit.getScheduler().cancelTask(task[0]);
            } else {
                hidingDuctsTimers.put(p, hidingDuctsTimers.get(p) - 1);
            }
        }, 0L, 20L);

    }

}
import de.robotricker.transportpipes.PlayerSettingsService;
import de.robotricker.transportpipes.ThreadService;
import de.robotricker.transportpipes.TransportPipes;
import de.robotricker.transportpipes.config.GeneralConf;
import de.robotricker.transportpipes.config.LangConf;
import de.robotricker.transportpipes.duct.Duct;
import de.robotricker.transportpipes.duct.DuctRegister;
import de.robotricker.transportpipes.duct.manager.GlobalDuctManager;
import de.robotricker.transportpipes.duct.types.DuctType;
import de.robotricker.transportpipes.items.ItemService;
import de.robotricker.transportpipes.location.BlockLocation;
import de.robotricker.transportpipes.location.TPDirection;
import de.robotricker.transportpipes.log.SentryService;
import de.robotricker.transportpipes.utils.HitboxUtils;
import de.robotricker.transportpipes.utils.WorldUtils;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.Vector;
import javax.inject.Inject;
import java.util.*;
package de.robotricker.transportpipes.listener;



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;