public abstract class BungeeTabListPlusAPI {
    private static BungeeTabListPlusAPI instance = null;

    /**
     * Registers a custom variable
     * <p>
     * You cannot use this to replace existing variables. If registering a variable which already
     * exists there may be an exception thrown but there is no guarantee that an exception
     * is thrown in that case.
     *
     * @param plugin   your plugin
     * @param variable your variable
     */
    public static void registerVariable(Plugin plugin, Variable variable) {
        Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?");
        instance.registerVariable0(plugin, variable);
    }

    protected abstract void registerVariable0(Plugin plugin, Variable variable);

    /**
     * Registers a custom variable bound to a specific server
     * <p>
     * You cannot use this to replace existing variables. If registering a variable which already
     * exists there may be an exception thrown but there is no guarantee that an exception
     * is thrown in that case.
     *
     * @param plugin   your plugin
     * @param variable your variable
     */
    public static void registerVariable(Plugin plugin, ServerVariable variable) {
        Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?");
        instance.registerVariable0(plugin, variable);
    }

    protected abstract void registerVariable0(Plugin plugin, ServerVariable variable);

    /**
     * Create a new {@link CustomTablist}
     *
     * @return thte created {@link CustomTablist}
     */
    public static CustomTablist createCustomTablist() {
        Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized");
        return instance.createCustomTablist0();
    }

    protected abstract CustomTablist createCustomTablist0();

    /**
     * Set a custom tab list for a player
     *
     * @param player        the player
     * @param customTablist the CustomTablist to use
     */
    public static void setCustomTabList(ProxiedPlayer player, CustomTablist customTablist) {
        Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized");
        instance.setCustomTabList0(player, customTablist);
    }

    protected abstract void setCustomTabList0(ProxiedPlayer player, CustomTablist customTablist);

    /**
     * Get the face part of the players skin as an icon for use in the tab list.
     *
     * @param player the player
     * @return the icon
     */
    @Nonnull
    public static Icon getIconFromPlayer(ProxiedPlayer player) {
        Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized");
        return instance.getIconFromPlayer0(player);
    }

    @Nonnull
    protected abstract Icon getIconFromPlayer0(ProxiedPlayer player);

    /**
     * Creates an icon from an 8x8 px image. The creation of the icon can take several
     * minutes. When the icon has been created the callback is invoked.
     *
     * @param image    the image
     * @param callback called when the icon is ready
     */
    public static void createIcon(BufferedImage image, Consumer<Icon> callback) {
        Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized");
        instance.createIcon0(image, callback);
    }

    protected abstract void createIcon0(BufferedImage image, Consumer<Icon> callback);

    /**
     * Removes a custom tab list from a player.
     * If the player hasn't got a custom tab list associated with it this will do nothing.
     *
     * @param player the player
     */
    public static void removeCustomTabList(ProxiedPlayer player) {
        Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized");
        instance.removeCustomTabList0(player);
    }

    protected abstract void removeCustomTabList0(ProxiedPlayer player);

    /**
     * Get the FakePlayerManager instance
     *
     * @return the FakePlayerManager instance
     */
    public static FakePlayerManager getFakePlayerManager() {
        Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized");
        return instance.getFakePlayerManager0();
    }

    protected abstract FakePlayerManager getFakePlayerManager0();

    /**
     * Check if a player is hidden from the tab list.
     *
     * A player is regarded as hidden if one of the following conditions is true:
     * - The player is hidden using a vanish plugin(e.g. SuperVanish, Essentials, ...)
     * - The player has been hidden using the /btlp hide command
     * - The player is in the list of hidden players in the configuration
     * - The player is on one of the hidden servers(configuration)
     *
     * @param player the player
     * @return true if hidden, false otherwise
     */
    public static boolean isHidden(ProxiedPlayer player) {
        Preconditions.checkState(instance != null, "BungeeTabListPlus not initialized");
        return instance.isHidden0(player);
    }

    protected abstract boolean isHidden0(ProxiedPlayer player);
} public class BukkitBridge implements Listener {

    private static final TypeAdapterRegistry typeAdapterRegistry = TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS;
    private static final RateLimitedExecutor rlExecutor = new RateLimitedExecutor(5000);

    private final Map<ProxiedPlayer, PlayerConnectionInfo> playerPlayerConnectionInfoMap = new ConcurrentHashMap<>();
    private final Map<String, ServerBridgeDataCache> serverInformation = new ConcurrentHashMap<>();
    private final int proxyIdentifier = ThreadLocalRandom.current().nextInt();
    private final NetDataKeyIdMap idMap = new NetDataKeyIdMap();

    private final ScheduledExecutorService asyncExecutor;
    private final ScheduledExecutorService mainLoop;
    private final PlayerPlaceholderResolver playerPlaceholderResolver;
    private final ServerPlaceholderResolver serverPlaceholderResolver;
    private final Plugin plugin;
    private final Logger logger;
    private final BungeePlayerProvider bungeePlayerProvider;
    private final BungeeTabListPlus btlp;
    private final Cache cache;

    public BukkitBridge(ScheduledExecutorService asyncExecutor, ScheduledExecutorService mainLoop, PlayerPlaceholderResolver playerPlaceholderResolver, ServerPlaceholderResolver serverPlaceholderResolver, Plugin plugin, Logger logger, BungeePlayerProvider bungeePlayerProvider, BungeeTabListPlus btlp, Cache cache) {
        this.asyncExecutor = asyncExecutor;
        this.mainLoop = mainLoop;
        this.playerPlaceholderResolver = playerPlaceholderResolver;
        this.serverPlaceholderResolver = serverPlaceholderResolver;
        this.plugin = plugin;
        this.logger = logger;
        this.bungeePlayerProvider = bungeePlayerProvider;
        this.btlp = btlp;
        this.cache = cache;
        ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
        asyncExecutor.scheduleWithFixedDelay(this::sendIntroducePackets, 100, 100, TimeUnit.MILLISECONDS);
        asyncExecutor.scheduleWithFixedDelay(this::resendUnconfirmedMessages, 2, 2, TimeUnit.SECONDS);
        asyncExecutor.scheduleWithFixedDelay(this::removeObsoleteServerConnections, 5, 5, TimeUnit.SECONDS);
    }

    @EventHandler
    public void onPlayerConnect(PostLoginEvent event) {
        playerPlayerConnectionInfoMap.put(event.getPlayer(), new PlayerConnectionInfo());
    }

    @EventHandler
    public void onServerChange(ServerConnectedEvent event) {
        PlayerConnectionInfo previous = playerPlayerConnectionInfoMap.put(event.getPlayer(), new PlayerConnectionInfo());
        if (previous != null) {
            PlayerBridgeDataCache playerBridgeData = previous.playerBridgeData;
            if (playerBridgeData != null) {
                synchronized (playerBridgeData) {
                    playerBridgeData.connection = null;
                    playerBridgeData.reset();
                }
            }
        }
    }

    @EventHandler
    public void onPlayerDisconnect(PlayerDisconnectEvent event) {
        playerPlayerConnectionInfoMap.remove(event.getPlayer());
    }

    @EventHandler
    public void onPluginMessage(PluginMessageEvent event) {
        if (event.getTag().equals(BridgeProtocolConstants.CHANNEL)) {
            if (event.getReceiver() instanceof ProxiedPlayer && event.getSender() instanceof Server) {

                ProxiedPlayer player = (ProxiedPlayer) event.getReceiver();
                Server server = (Server) event.getSender();

                event.setCancelled(true);

                DataInput input = new DataInputStream(new ByteArrayInputStream(event.getData()));

                try {
                    handlePluginMessage(player, server, input);
                } catch (Throwable e) {
                    rlExecutor.execute(() -> {
                        logger.log(Level.SEVERE, "Unexpected error: ", e);
                    });
                }
            } else {
                // hacking attempt
                event.setCancelled(true);
            }
        }
    }

    public String getStatus(ServerInfo server) {
        Collection<ProxiedPlayer> players = server.getPlayers();
        if (players.isEmpty()) {
            return "no players";
        }
        int error_bungee = 0, unavailable = 0, working = 0, stale = 0, incomplete = 0;
        for (ProxiedPlayer player : players) {
            PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player);

            if (connectionInfo == null) {
                error_bungee++;
                continue;
            }

            if (connectionInfo.isConnectionValid == false) {
                unavailable++;
                continue;
            }

            if (connectionInfo.hasReceived == false) {
                incomplete++;
                continue;
            }

            PlayerBridgeDataCache bridgeData = connectionInfo.playerBridgeData;
            if (bridgeData == null) {
                error_bungee++;
                continue;
            }

            if (bridgeData.nextOutgoingMessageId - bridgeData.lastConfirmed > 2 && System.currentTimeMillis() - bridgeData.lastMessageSent > 5000) {
                stale++;
                continue;
            }
        }

        if (unavailable == players.size()) {
            return "not installed or incompatible version";
        }

        String status = "working: " + working;
        if (unavailable != 0) {
            status += ", unavailable: " + unavailable;
        }
        if (incomplete != 0) {
            status += ", unavailable: " + incomplete;
        }
        if (stale != 0) {
            status += ", stale: " + stale;
        }
        if (error_bungee != 0) {
            status += ", error_bungee: " + error_bungee;
        }
        return status;
    }

    private void handlePluginMessage(ProxiedPlayer player, Server server, DataInput input) throws IOException {
        PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player);

        if (connectionInfo == null) {
            return;
        }

        int messageId = input.readUnsignedByte();

        if (messageId == BridgeProtocolConstants.MESSAGE_ID_INTRODUCE) {

            int serverIdentifier = input.readInt();
            int protocolVersion = input.readInt();
            int minimumCompatibleProtocolVersion = input.readInt();
            String proxyPluginVersion = input.readUTF();

            int connectionId = serverIdentifier + proxyIdentifier;

            if (connectionId == connectionInfo.connectionIdentifier) {
                return;
            }

            if (BridgeProtocolConstants.VERSION < minimumCompatibleProtocolVersion) {
                rlExecutor.execute(() -> {
                    logger.log(Level.WARNING, "Incompatible version of BTLP on server " + server.getInfo().getName() + " detected: " + proxyPluginVersion);
                });
                return;
            }

            // reset connection state
            connectionInfo.connectionIdentifier = connectionId;
            connectionInfo.isConnectionValid = true;
            connectionInfo.nextIntroducePacketDelay = 1;
            connectionInfo.introducePacketDelay = 1;
            connectionInfo.hasReceived = false;
            connectionInfo.protocolVersion = Integer.min(BridgeProtocolConstants.VERSION, protocolVersion);

            BungeePlayer bungeePlayer = bungeePlayerProvider.getPlayerIfPresent(player);
            if (bungeePlayer == null) {
                logger.severe("Internal error - Bridge functionality not available for " + player.getName());
            } else {
                connectionInfo.playerBridgeData = bungeePlayer.getBridgeDataCache();
                connectionInfo.playerBridgeData.setConnectionId(connectionId);
                connectionInfo.playerBridgeData.connection = server;
                connectionInfo.playerBridgeData.requestMissingData();
            }
            connectionInfo.serverBridgeData = getServerDataCache(server.getInfo().getName());
            connectionInfo.serverBridgeData.setConnectionId(connectionId);
            connectionInfo.serverBridgeData.addConnection(server);
            connectionInfo.serverBridgeData.requestMissingData();

            // send ACK 0
            ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
            DataOutput output = new DataOutputStream(byteArrayOutput);

            output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK);
            output.writeInt(connectionId);
            output.writeInt(0);

            byte[] message = byteArrayOutput.toByteArray();
            server.sendData(BridgeProtocolConstants.CHANNEL, message);
        } else {

            if (!connectionInfo.isConnectionValid) {
                return;
            }

            int connectionId = input.readInt();

            if (connectionId != connectionInfo.connectionIdentifier) {
                return;
            }

            connectionInfo.hasReceived = true;

            int sequenceNumber = input.readInt();

            BridgeData bridgeData;
            boolean isServerMessage;

            if ((messageId & 0x80) == 0) {
                bridgeData = connectionInfo.playerBridgeData;
                isServerMessage = false;
            } else {
                bridgeData = connectionInfo.serverBridgeData;
                isServerMessage = true;
            }

            if (bridgeData == null) {
                return;
            }

            messageId = messageId & 0x7f;

            synchronized (bridgeData) {

                if (messageId == BridgeProtocolConstants.MESSAGE_ID_ACK) {

                    int confirmed = sequenceNumber - bridgeData.lastConfirmed;

                    if (confirmed > bridgeData.messagesPendingConfirmation.size()) {
                        return;
                    }

                    while (confirmed-- > 0) {
                        bridgeData.lastConfirmed++;
                        bridgeData.messagesPendingConfirmation.remove();
                    }
                } else if (messageId == BridgeProtocolConstants.MESSAGE_ID_UPDATE_DATA) {

                    if (sequenceNumber > bridgeData.nextIncomingMessageId) {
                        // ignore messages from the future
                        return;
                    }

                    ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
                    DataOutput output = new DataOutputStream(byteArrayOutput);

                    output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK | (isServerMessage ? 0x80 : 0x00));
                    output.writeInt(connectionId);
                    output.writeInt(sequenceNumber);

                    byte[] message = byteArrayOutput.toByteArray();
                    server.sendData(BridgeProtocolConstants.CHANNEL, message);

                    if (sequenceNumber < bridgeData.nextIncomingMessageId) {
                        // ignore messages from the past after sending ACK
                        return;
                    }

                    bridgeData.nextIncomingMessageId++;

                    int size = input.readInt();
                    if (size > 0) {
                        onDataReceived(bridgeData, input, size);
                    }
                } else {
                    throw new IllegalArgumentException("Unexpected message id: " + messageId);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void onDataReceived(DataCache cache, DataInput input, int size) throws IOException {
        if (size == 1) {
            int netId = input.readInt();
            DataKey<?> key = idMap.getKey(netId);

            if (key == null) {
                throw new AssertionError("Received unexpected data key net id " + netId);
            }

            boolean removed = input.readBoolean();

            if (removed) {

                mainLoop.execute(() -> cache.updateValue(key, null));
            } else {

                Object value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input);
                mainLoop.execute(() -> cache.updateValue((DataKey<Object>) key, value));
            }
        } else {
            Object[] update = new Object[size * 2];

            for (int i = 0; i < update.length; i += 2) {
                int netId = input.readInt();
                DataKey<?> key = idMap.getKey(netId);

                if (key == null) {
                    throw new AssertionError("Received unexpected data key net id " + netId);
                }

                boolean removed = input.readBoolean();
                Object value = null;

                if (!removed) {
                    value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input);
                }

                update[i] = key;
                update[i + 1] = value;
            }

            mainLoop.execute(() -> {
                for (int i = 0; i < update.length; i += 2) {
                    cache.updateValue((DataKey<Object>) update[i], update[i + 1]);
                }
            });
        }
    }

    /**
     * Sends introduce packets to the proxy to try to establish a connection.
     * <p>
     * Should be called periodically, recommended interval is 100ms to 1s.
     */
    private void sendIntroducePackets() {
        for (Map.Entry<ProxiedPlayer, PlayerConnectionInfo> entry : playerPlayerConnectionInfoMap.entrySet()) {
            Server server = entry.getKey().getServer();
            PlayerConnectionInfo connectionInfo = entry.getValue();
            if (server != null && !connectionInfo.hasReceived) {
                if (--connectionInfo.nextIntroducePacketDelay <= 0) {
                    connectionInfo.nextIntroducePacketDelay = connectionInfo.introducePacketDelay++;

                    try {

                        ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
                        DataOutput output = new DataOutputStream(byteArrayOutput);

                        output.writeByte(BridgeProtocolConstants.MESSAGE_ID_INTRODUCE);
                        output.writeInt(proxyIdentifier);
                        output.writeInt(BridgeProtocolConstants.VERSION);
                        output.writeInt(BridgeProtocolConstants.VERSION);
                        output.writeUTF(plugin.getDescription().getVersion());

                        byte[] message = byteArrayOutput.toByteArray();
                        server.sendData(BridgeProtocolConstants.CHANNEL, message);
                    } catch (Throwable th) {
                        rlExecutor.execute(() -> {
                            logger.log(Level.SEVERE, "Unexpected error", th);
                        });
                    }
                }
            }
        }
    }

    /**
     * Sends unconfirmed messages to the proxy yet another time, to ensure their arrival.
     * <p>
     * Should be called periodically, recommended interval is 1s to 10s.
     */
    private void resendUnconfirmedMessages() {
        long now = System.currentTimeMillis();

        for (Map.Entry<ProxiedPlayer, PlayerConnectionInfo> e : playerPlayerConnectionInfoMap.entrySet()) {
            ProxiedPlayer player = e.getKey();
            Server server = player.getServer();
            PlayerConnectionInfo connectionInfo = e.getValue();

            if (!connectionInfo.isConnectionValid) {
                continue;
            }

            BridgeData bridgeData = connectionInfo.playerBridgeData;

            if (bridgeData == null) {
                continue;
            }

            if (server != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) {
                for (byte[] message : bridgeData.messagesPendingConfirmation) {
                    server.sendData(BridgeProtocolConstants.CHANNEL, message);
                }
            }
        }

        for (ServerBridgeDataCache bridgeData : serverInformation.values()) {
            Server server = bridgeData.getConnection();

            if (server != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) {
                for (byte[] message : bridgeData.messagesPendingConfirmation) {
                    server.sendData(BridgeProtocolConstants.CHANNEL, message);
                }
            }
        }
    }

    private void checkForThirdPartyVariables(String serverName, ServerBridgeDataCache dataCache) {
        mainLoop.execute(() -> {
            dataCache.addDataChangeListener(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES, () -> updateBridgePlaceholders(serverName, dataCache));
            updateBridgePlaceholders(serverName, dataCache);
            dataCache.addDataChangeListener(BTLPDataKeys.REGISTERED_THIRD_PARTY_SERVER_VARIABLES, () -> updateBridgeServerPlaceholders(serverName, dataCache));
            updateBridgeServerPlaceholders(serverName, dataCache);
            dataCache.addDataChangeListener(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS, () -> updatePlaceholderAPIPlaceholders(serverName, dataCache));
            updatePlaceholderAPIPlaceholders(serverName, dataCache);
        });
    }

    private void updateBridgePlaceholders(String serverName, ServerBridgeDataCache dataCache) {
        List<String> variables = dataCache.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES);
        if (variables != null) {
            for (String variable : variables) {
                playerPlaceholderResolver.addBridgeCustomPlaceholderDataKey(variable, BTLPDataKeys.createThirdPartyVariableDataKey(variable));
            }
            cache.updateCustomPlaceholdersBridge(serverName, variables);
            btlp.scheduleSoftReload();
        }
    }

    private void updateBridgeServerPlaceholders(String serverName, ServerBridgeDataCache dataCache) {
        List<String> variables = dataCache.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_SERVER_VARIABLES);
        if (variables != null) {
            for (String variable : variables) {
                serverPlaceholderResolver.addBridgeCustomPlaceholderServerDataKey(variable, BTLPDataKeys.createThirdPartyServerVariableDataKey(variable));
            }
            cache.updateCustomServerPlaceholdersBridge(serverName, variables);
            btlp.scheduleSoftReload();
        }
    }

    private void updatePlaceholderAPIPlaceholders(String serverName, ServerBridgeDataCache dataCache) {
        List<String> plugins = dataCache.get(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS);
        if (plugins != null) {
            playerPlaceholderResolver.addPlaceholderAPIPluginPrefixes(plugins);
            cache.updatePAPIPrefixes(serverName, plugins);
            btlp.scheduleSoftReload();
        }
    }

    private void removeObsoleteServerConnections() {
        for (ServerBridgeDataCache cache : serverInformation.values()) {
            cache.removeObsoleteConnections();
        }
    }

    public PlayerBridgeDataCache createDataCacheForPlayer(@Nonnull BungeePlayer player) {
        return new PlayerBridgeDataCache();
    }

    public DataHolder getServerDataHolder(@Nonnull String server) {
        return getServerDataCache(server);
    }

    private ServerBridgeDataCache getServerDataCache(@Nonnull String serverName) {
        if (!serverInformation.containsKey(serverName)) {
            serverInformation.computeIfAbsent(serverName, key -> {
                ServerBridgeDataCache dataCache = new ServerBridgeDataCache();
                checkForThirdPartyVariables(serverName, dataCache);
                return dataCache;
            });
        }
        return serverInformation.get(serverName);
    }

    private static class PlayerConnectionInfo {
        boolean isConnectionValid = false;
        boolean hasReceived = false;
        int connectionIdentifier = 0;
        int protocolVersion = 0;
        int nextIntroducePacketDelay = 1;
        int introducePacketDelay = 1;
        @Nullable
        PlayerBridgeDataCache playerBridgeData = null;
        @Nullable
        ServerBridgeDataCache serverBridgeData = null;
    }

    private abstract class BridgeData extends TrackingDataCache {

        final Queue<byte[]> messagesPendingConfirmation = new ConcurrentLinkedQueue<>();
        int lastConfirmed = 0;
        int nextOutgoingMessageId = 1;
        int nextIncomingMessageId = 1;
        long lastMessageSent = 0;
        int connectionId;

        boolean requestAll = false;

        @Nullable
        abstract Server getConnection();

        @Override
        protected <T> void addActiveKey(DataKey<T> key) {
            super.addActiveKey(key);

            try {
                synchronized (this) {
                    Server connection = getConnection();
                    if (connection != null) {
                        ByteArrayDataOutput data = ByteStreams.newDataOutput();
                        data.writeByte(this instanceof PlayerBridgeDataCache ? BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA : BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA_SERVER);
                        data.writeInt(connectionId);
                        data.writeInt(nextOutgoingMessageId++);
                        data.writeInt(1);
                        DataStreamUtils.writeDataKey(data, key);
                        data.writeInt(idMap.getNetId(key));
                        byte[] message = data.toByteArray();
                        messagesPendingConfirmation.add(message);
                        lastMessageSent = System.currentTimeMillis();
                        connection.sendData(BridgeProtocolConstants.CHANNEL, message);
                    } else {
                        requestAll = true;
                    }
                }
            } catch (Throwable th) {
                rlExecutor.execute(() -> {
                    logger.log(Level.SEVERE, "Unexpected exception", th);
                });
                requestAll = true;
            }
        }

        void setConnectionId(int connectionId) {
            if (this.connectionId != connectionId) {
                reset();
            }
            this.connectionId = connectionId;
        }

        void reset() {
            synchronized (this) {
                requestAll = true;
                messagesPendingConfirmation.clear();
                lastConfirmed = 0;
                nextOutgoingMessageId = 1;
                nextIncomingMessageId = 1;
                lastMessageSent = 0;
                Collection<DataKey<?>> queriedKeys = new ArrayList<>(getActiveKeys());
                mainLoop.execute(() -> {
                    for (DataKey<?> key : queriedKeys) {
                        updateValue(key, null);
                    }
                });
            }
        }

        void requestMissingData() throws IOException {
            synchronized (this) {
                if (requestAll) {
                    Server connection = getConnection();
                    if (connection != null) {
                        List<DataKey<?>> keys = new ArrayList<>(getActiveKeys());

                        ByteArrayDataOutput data = ByteStreams.newDataOutput();
                        data.writeByte(this instanceof PlayerBridgeDataCache ? BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA : BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA_SERVER);
                        data.writeInt(connectionId);
                        data.writeInt(nextOutgoingMessageId++);
                        data.writeInt(keys.size());
                        for (DataKey<?> key : keys) {
                            DataStreamUtils.writeDataKey(data, key);
                            data.writeInt(idMap.getNetId(key));
                        }
                        byte[] message = data.toByteArray();
                        messagesPendingConfirmation.add(message);
                        lastMessageSent = System.currentTimeMillis();
                        connection.sendData(BridgeProtocolConstants.CHANNEL, message);
                    }
                    requestAll = false;
                }
            }
        }
    }

    public class PlayerBridgeDataCache extends BridgeData {
        @Nullable
        private volatile Server connection = null;

        @Override
        @Nullable
        Server getConnection() {
            return connection;
        }
    }

    private class ServerBridgeDataCache extends BridgeData {
        private final ReferenceSet<Server> connections = new ReferenceOpenHashSet<>();

        private void addConnection(@Nonnull Server server) {
            synchronized (this) {
                connections.add(server);
            }
        }

        @Override
        @Nullable
        Server getConnection() {
            synchronized (this) {
                ObjectIterator<Server> iterator = connections.iterator();
                while (iterator.hasNext()) {
                    Server server = iterator.next();
                    if (server.isConnected()) {
                        return server;
                    } else {
                        iterator.remove();
                    }
                }
                return null;
            }
        }

        private void removeObsoleteConnections() {
            synchronized (this) {
                ObjectIterator<Server> iterator = connections.iterator();
                while (iterator.hasNext()) {
                    Server server = iterator.next();
                    if (!server.isConnected()) {
                        iterator.remove();
                    }
                }
            }
        }

        @Override
        void reset() {
            synchronized (this) {
                super.reset();
                connections.clear();
            }
        }
    }
} public class Cache implements Serializable {

    private transient File file;

    private Map<String, List<String>> cachedPAPIPrefixes = new HashMap<>();
    private Map<String, List<String>> cachedCustomPlaceholdersBridge = new HashMap<>();
    private Map<String, List<String>> cachedCustomServerPlaceholdersBridge = new HashMap<>();

    public synchronized void updatePAPIPrefixes(String server, List<String> prefixes) {
        cachedPAPIPrefixes.put(server, new ArrayList<>(prefixes));
    }

    public synchronized Set<String> getPAPIPrefixes() {
        return getServerNames()
                .map(cachedPAPIPrefixes::get)
                .filter(Objects::nonNull)
                .flatMap(List::stream)
                .collect(Collectors.toSet());
    }

    private Stream<String> getServerNames() {
        for (int i = 0; i < 3; i++) {
            try {
                return ProxyServer.getInstance().getServers().keySet()
                        .stream()
                        .filter(Objects::nonNull);
            } catch (Throwable ignored) {
            }
        }
        return Stream.empty();
    }

    public synchronized void updateCustomPlaceholdersBridge(String server, List<String> prefixes) {
        cachedCustomPlaceholdersBridge.put(server, new ArrayList<>(prefixes));
    }

    public synchronized Set<String> getCustomPlaceholdersBridge() {
        return getServerNames()
                .map(cachedCustomPlaceholdersBridge::get)
                .filter(Objects::nonNull)
                .flatMap(List::stream)
                .collect(Collectors.toSet());
    }

    public synchronized void updateCustomServerPlaceholdersBridge(String server, List<String> prefixes) {
        cachedCustomServerPlaceholdersBridge.put(server, new ArrayList<>(prefixes));
    }

    public synchronized Set<String> getCustomServerPlaceholdersBridge() {
        return getServerNames()
                .map(cachedCustomServerPlaceholdersBridge::get)
                .filter(Objects::nonNull)
                .flatMap(List::stream)
                .collect(Collectors.toSet());
    }

    private Cache(File file) {
        this.file = file;
    }

    public static Cache load(File file) {
        try (FileInputStream is = new FileInputStream(file)) {
            ObjectInputStream ois = new ObjectInputStream(is);
            Cache cache = (Cache) ois.readObject();
            cache.file = file;
            return cache;
        } catch (Throwable th) {
            return new Cache(file);
        }
    }

    public synchronized void save() {
        try (FileOutputStream os = new FileOutputStream(file)) {
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(this);
            oos.flush();
        } catch (Throwable th) {
            BungeeTabListPlus.getInstance().getLogger().log(Level.SEVERE, "Failed to write file: " + th.getMessage(), th);
        }
    }
} public class CommandBungeeTabListPlus extends CommandExecutor {
    public CommandBungeeTabListPlus() {
        super("BungeeTabListPlus", "bungeetablistplus.command", "btlp");

        init();
    }

    private void init() {
        addSubCommand(new CommandBase("reload", "bungeetablistplus.admin", this::commandReload));
        addSubCommand(new CommandHide());
        addSubCommand(new CommandFakePlayers());
        addSubCommand(new CommandBase("help", null, this::commandHelp, "?"));
        addSubCommand(new CommandBase("status", null, this::commandStatus));
        addSubCommand(new CommandDebug());
        setDefaultAction(this::commandHelp);
    }

    private void commandReload(CommandSender sender) {
        boolean success = BungeeTabListPlus.getInstance().reload();
        if (success) {
            sender.sendMessage(ChatUtil.parseBBCode("&aSuccessfully reloaded BungeeTabListPlus."));
        } else {
            sender.sendMessage(ChatUtil.parseBBCode("&cAn error occurred while reloaded BungeeTabListPlus."));
        }
    }

    private void commandHelp(CommandSender sender) {
        sender.sendMessage(ChatUtil.parseBBCode("&e&lAvailable Commands:"));
        sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp reload[/suggest] &f&oReload the configuration"));
        sender.sendMessage(ChatUtil.parseBBCode("&e[suggest=/btlp hide]/btlp hide [on|off|toggle][/suggest] &f&oHide yourself from the tab list."));
        sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp fake[/suggest] &f&oManage fake players."));
        sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp status[/suggest] &f&oDisplays info about plugin version, updates and the bridge plugin."));
        sender.sendMessage(ChatUtil.parseBBCode("&e[suggest]/btlp help[/suggest] &f&oYou already found this one :P"));
    }

    private void commandStatus(CommandSender sender) {
        // Version
        String version = BungeeTabListPlus.getInstance().getPlugin().getDescription().getVersion();
        sender.sendMessage(ChatUtil.parseBBCode("&eYou are running BungeeTabListPlus version " + version));

        // Update?
        sender.sendMessage(ChatUtil.parseBBCode("&eLooking for an update..."));
        UpdateChecker updateChecker = new UpdateChecker(BungeeTabListPlus.getInstance().getPlugin());
        updateChecker.run();
        if (updateChecker.isUpdateAvailable()) {
            sender.sendMessage(ChatUtil.parseBBCode("&aAn update is available at [url]http://www.spigotmc.org/resources/bungeetablistplus.313/[/url]"));
        } else if (updateChecker.isNewDevBuildAvailable()) {
            sender.sendMessage(ChatUtil.parseBBCode("&aA new dev-build is available at [url]https://ci.codecrafter47.de/job/BungeeTabListPlus/[/url]"));
        } else {
            sender.sendMessage(ChatUtil.parseBBCode("&aYou are already running the latest version."));
        }

        // Bridge plugin status
        BukkitBridge bridge = BungeeTabListPlus.getInstance().getBridge();
        List<ServerInfo> servers = new ArrayList<>(ProxyServer.getInstance().getServers().values());
        List<String> withBridge = new ArrayList<>();
        List<ServerInfo> withoutBridge = new ArrayList<>();
        List<String> maybeBridge = new ArrayList<>();
        for (ServerInfo server : servers) {
            DataHolder dataHolder = bridge.getServerDataHolder(server.getName());
            if (dataHolder != null && dataHolder.get(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES) != null) {
                withBridge.add(server.getName());
            } else {
                if (server.getPlayers().isEmpty()) {
                    maybeBridge.add(server.getName());
                } else {
                    withoutBridge.add(server);
                }
            }
        }
        List<String> withPAPI = servers.stream()
                .filter(server -> {
                    DataHolder dataHolder = bridge.getServerDataHolder(server.getName());
                    Boolean b;
                    return dataHolder != null && (b = dataHolder.get(BTLPDataKeys.PLACEHOLDERAPI_PRESENT)) != null && b;
                })
                .map(ServerInfo::getName)
                .collect(Collectors.toList());

        sender.sendMessage(ChatUtil.parseBBCode("&eBridge plugin status:"));
        if (!withBridge.isEmpty()) {
            sender.sendMessage(ChatUtil.parseBBCode("&fInstalled on: &a" + Joiner.on("&f,&a ").join(withBridge)));
        }
        if (!withPAPI.isEmpty()) {
            sender.sendMessage(ChatUtil.parseBBCode("&fServers with PlaceholderAPI: &a" + Joiner.on("&f, &a").join(withPAPI)));
        }
        for (ServerInfo server : withoutBridge) {
            sender.sendMessage(ChatUtil.parseBBCode("&c" + server.getName() + "&f: " + bridge.getStatus(server)));
        }
        if (!maybeBridge.isEmpty()) {
            sender.sendMessage(ChatUtil.parseBBCode("&eBridge status is not available for servers without players."));
        }

        // That's it
        sender.sendMessage(ChatUtil.parseBBCode("&aThank you for using BungeeTabListPlus."));
    }
} public class BridgeProtocolConstants {

    public static final String CHANNEL = "btlp:bridge";

    public static final int VERSION = 5;

    public static final int MESSAGE_ID_INTRODUCE = 0x00;

    public static final int MESSAGE_ID_ACK = 0x01;
    public static final int MESSAGE_ID_REQUEST_DATA = 0x02;
    public static final int MESSAGE_ID_UPDATE_DATA = 0x03;

    public static final int MESSAGE_ID_ACK_SERVER = 0x81;
    public static final int MESSAGE_ID_REQUEST_DATA_SERVER = 0x82;
    public static final int MESSAGE_ID_UPDATE_DATA_SERVER = 0x83;
} public class SortingRuleAliasProcessor implements SortingRulePreprocessor {

    private static final ImmutableMap<String, RewriteData> map = ImmutableMap.<String, RewriteData>builder()
            .put("you", new RewriteData("name viewer-first", true))
            .put("youfirst", new RewriteData("name viewer-first", false))
            .put("alpha", new RewriteData("name", true))
            .put("alphabet", new RewriteData("name", true))
            .put("alphabetic", new RewriteData("name", true))
            .put("alphabetical", new RewriteData("name", true))
            .put("alphabetically", new RewriteData("name", false))
            .put("teamfirst", new RewriteData("team viewer-first", false))
            .put("teams", new RewriteData("team", false))
            .put("factionfirst", new RewriteData("faction_name viewer-first", false))
            .put("factions", new RewriteData("faction_name", false))
            .put("worldname", new RewriteData("world", true))
            .put("playerworld", new RewriteData("world viewer-first", true))
            .put("playerworldfirst", new RewriteData("world viewer-first", true))
            .put("serveralphabetically", new RewriteData("server", true))
            .put("playerserverfirst", new RewriteData("server viewer-first", true))
            .put("afklast", new RewriteData("essentials_afk as number asc", true))
            .put("vaultgroupinfo", new RewriteData("vault_primary_group_weight asc", true))
            .put("vaultgroupinforeversed", new RewriteData("vault_primary_group_weight desc", true))
            .put("bungeepermsgroupinfo", new RewriteData("bungeeperms_primary_group_weight asc", true))
            .put("bungeepermsgroupinforeversed", new RewriteData("bungeeperms_primary_group_weight desc", true))
            .put("luckpermsgroupinfo", new RewriteData("luckpermsbungee_primary_group_weight asc", true))
            .put("luckpermsgroupinforeversed", new RewriteData("luckpermsbungee_primary_group_weight desc", true))
            .put("vaultprefix", new RewriteData("vault_prefix asc", true))
            .put("connectedfirst", new RewriteData("session_duration_total_seconds desc", false))
            .put("connectedlast", new RewriteData("session_duration_total_seconds asc", false))
            .build();

    @Override
    public String process(String sortingRule, ErrorHandler errorHandler, Mark mark) {
        RewriteData rewriteData = map.get(sortingRule.toLowerCase());
        if (rewriteData != null) {
            if (rewriteData.deprecated) {
                errorHandler.addWarning("Sorting rule '" + sortingRule + "' has been deprecated. Use '" + rewriteData.rewrite + "' instead.", mark);
            }
            return rewriteData.rewrite;
        }
        return sortingRule;
    }

    @Value
    private static class RewriteData {
        String rewrite;
        boolean deprecated;
    }
} public class MainConfig implements UpdateableConfig {

    @Comment({
            "if enabled the plugin checks for new versions automatically.",
            "Use /BTLP to see whether a new version is available",
            "this does NOT automatically install an update"
    })
    public boolean checkForUpdates = true;

    @Comment({
            "this notifies admins (everyone with the permission `bungeetablistplus.admin`) if an update is available"
    })

    public boolean notifyAdminsIfUpdateAvailable = true;

    @Comment({
            "Interval (in seconds) at which all servers of your network get pinged to check whether they are online",
            "If you intend to use the {onlineState:SERVER} variable set this to 2 or any value you like",
            "setting this to -1 disables this feature"
    })
    public int pingDelay = -1;

    @Comment({
            "those fakeplayers will randomly appear on the tablist. If you don't put any names there then no fakeplayers will appear"
    })
    public List<String> fakePlayers = new ArrayList<>();

    @Comment({
            "Servers which you wish to show their own tabList (The one provided by bukkit)",
            "Players on these servers don't see the custom tab list provided by BungeeTabListPlus"
    })
    public List<String> excludeServers = new ArrayList<>();

    @Comment({
            "Players on these servers are hidden from the tab list.",
            "Doesn't necessarily hide the server from the tab list."
    })
    public List<String> hiddenServers = new ArrayList<>();

    @Comment({
            "players which are permanently hidden from the tab list",
            "you can either put your username or your uuid (with dashes) here",
            "don't use this. you have absolutely no reason to hide from anyone. on your own server."
    })
    public List<String> hiddenPlayers = new ArrayList<>();

    @Comment({
            "Time zone to use for the {time} variable",
            "Can be full name like \"America/Los_Angeles\"",
            "or custom id like \"GMT+8\""
    })
    @Path("time-zone")
    public String time_zone = TimeZone.getDefault().getID();

    @Comment("Custom placeholders")
    public Map<String, CustomPlaceholderConfiguration> customPlaceholders = new HashMap<>();

    public TimeZone getTimeZone() {
        return TimeZone.getTimeZone(time_zone);
    }

    @Comment({
            "Disables the custom tab list for players in spectators mode.",
            "As a result those players will see the vanilla tab list of the server.",
            "If you do not use this option players in spectator mode will see the ",
            "fake players created by BungeeTabListPlus in the teleport menu."
    })
    public boolean disableCustomTabListForSpectators = true;

    @Comment({
            "Removes the `~BTLP Slot ##` entries from tab completion if the.",
            "size of the tab list is 80 slots."
    })
    public boolean experimentalTabCompleteFixForTabSize80 = false;

    @Comment({
            "Replaces the `~BTLP Slot ##` entries in tab completion with smileys"
    })
    public boolean experimentalTabCompleteSmileys = false;

    public transient boolean needWrite = false;

    @Override
    public void update(MappingNode node) {
        val outdatedConfigOptions = ImmutableList.<String>of("tablistUpdateIntervall",
                "tablistUpdateInterval",
                "updateOnPlayerJoinLeave",
                "updateOnServerChange",
                "offline",
                "offline-text",
                "online",
                "online-text",
                "permissionSource",
                "useScoreboardToBypass16CharLimit",
                "autoExcludeServers",
                "showPlayersInGamemode3",
                "serverAlias",
                "worldAlias",
                "serverPrefixes",
                "prefixes",
                "charLimit",
                "automaticallySendBugReports");

        for (String option : outdatedConfigOptions) {
            needWrite |= YamlUtil.contains(node, option);
            YamlUtil.remove(node, option);
        }

        val newConfigOptions = ImmutableList.<String>of(
                "disableCustomTabListForSpectators",
                "experimentalTabCompleteFixForTabSize80",
                "experimentalTabCompleteSmileys"
        );

        for (String option : newConfigOptions) {
            needWrite |= !YamlUtil.contains(node, option);
        }
    }

    public void writeWithComments(Writer writer, Yaml yaml) throws IOException {
        writeCommentLine(writer, "This is the configuration file of BungeeTabListPlus");
        writeCommentLine(writer, "See https://github.com/CodeCrafter47/BungeeTabListPlus/wiki for additional information");

        String ser = yaml.dumpAs(this, Tag.MAP, null);

        Map<String, String[]> comments = new HashMap<>();
        for (Field field : MainConfig.class.getDeclaredFields()) {
            Comment comment = field.getAnnotation(Comment.class);
            if (comment != null) {
                int modifiers = field.getModifiers();
                if (!Modifier.isStatic(modifiers) && !Modifier.isTransient(modifiers)) {
                    if (Modifier.isPublic(modifiers)) {
                        Path path = field.getAnnotation(Path.class);
                        comments.put(path != null ? path.value() : field.getName(), comment.value());
                    }
                }
            }
        }

        ArrayList<String> lines = new ArrayList<>(Arrays.asList(ser.split("\n")));

        ListIterator<String> iterator = lines.listIterator();

        while (iterator.hasNext()) {
            String line = iterator.next();
            for (Map.Entry<String, String[]> entry : comments.entrySet()) {
                if (line.startsWith(entry.getKey())) {
                    String[] value = entry.getValue();
                    iterator.previous();
                    iterator.add("");
                    for (String comment : value) {
                        iterator.add("# " + comment);
                    }
                    iterator.next();
                }
            }
        }

        for (String line : lines) {
            writer.write(line);
            writer.write("\n");
        }

        writer.close();
    }

    private static void writeCommentLine(Writer writer, String comment) throws IOException {
        writer.write("# " + comment + "\n");
    }
} @Getter
@Setter
public class PlayersByServerComponentConfiguration extends MarkedPropertyBase implements ComponentConfiguration {

    private PlayerOrderConfiguration playerOrder = PlayerOrderConfiguration.DEFAULT;
    private MarkedStringProperty playerSet;
    private ComponentConfiguration playerComponent = new BasicComponentConfiguration("${player name}");
    @Nullable
    private ComponentConfiguration morePlayersComponent;
    private boolean fillSlotsVertical = false;
    private MarkedIntegerProperty minSize = new MarkedIntegerProperty(0);
    private MarkedIntegerProperty maxSize = new MarkedIntegerProperty(-1);
    private MarkedIntegerProperty minSizePerServer = new MarkedIntegerProperty(0);
    private MarkedIntegerProperty maxSizePerServer = new MarkedIntegerProperty(-1);
    @Nullable
    private ComponentConfiguration serverHeader;
    @Nullable
    private ComponentConfiguration serverFooter;
    @Nullable
    private ComponentConfiguration serverSeparator;

    private MarkedListProperty<String> hiddenServers = new MarkedListProperty<>();

    private ServerOptions showServers = null;
    private MarkedBooleanProperty includeEmptyServers = null;

    private MarkedStringProperty serverOrder = null;
    private Map<String, Integer> customServerOrder = null;
    private boolean prioritizeViewerServer = true;

    private Map<String, List<String>> mergeServers = null;

    public List<String> getCustomServerOrder() {
        // dummy method for snakeyaml to detect property type
        return null;
    }

    public void setCustomServerOrder(List<String> customServerOrder) {
        if (customServerOrder == null) {
            this.customServerOrder = null;
        } else {
            val builder = ImmutableMap.<String, Integer>builder();
            int rank = 0;
            for (String server : customServerOrder) {
                builder.put(server, rank++);
            }
            this.customServerOrder = builder.build();
        }
    }

    @Override
    public ComponentTemplate toTemplate(TemplateCreationContext tcc) {
        if (ConfigValidationUtil.checkNotNull(tcc, "!players_by_server component", "playerSet", playerSet, getStartMark())) {
            if (!tcc.getPlayerSets().containsKey(playerSet.getValue())) {
                tcc.getErrorHandler().addError("No player set definition available for player set \"" + playerSet.getValue() + "\"", playerSet.getStartMark());
            }
        }

        PlayerOrderTemplate playerOrderTemplate = PlayerOrderConfiguration.DEFAULT.toTemplate(tcc);
        if (ConfigValidationUtil.checkNotNull(tcc, "!players_by_server component", "playerOrder", playerOrder, getStartMark())) {
            playerOrderTemplate = this.playerOrder.toTemplate(tcc);

        }
        if (minSize.getValue() < 0) {
            tcc.getErrorHandler().addError("Failed to configure players component. MinSize is negative", minSize.getStartMark());
        }
        if (maxSize.getValue() != -1 && minSize.getValue() > maxSize.getValue()) {
            tcc.getErrorHandler().addError("Failed to configure players component. MaxSize is lower than minSize", maxSize.getStartMark());
        }

        if (minSizePerServer.getValue() < 0) {
            tcc.getErrorHandler().addError("Failed to configure players component. MinSizePerWorld is negative", minSizePerServer.getStartMark());
        }
        if (maxSizePerServer.getValue() != -1 && minSizePerServer.getValue() > maxSizePerServer.getValue()) {
            tcc.getErrorHandler().addError("Failed to configure players component. MaxSizePerWorld is lower than minSizePerWorld", maxSizePerServer.getStartMark());
        }


        TemplateCreationContext childContextS = tcc.clone();
        if (fillSlotsVertical) {
            childContextS.setColumns(1);
        }
        BungeeTabListPlus btlp = BungeeTabListPlus.getInstance();
        childContextS.addPlaceholderResolver(new ComponentServerPlaceholderResolver(btlp.getServerPlaceholderResolver(), btlp.getDataManager()));

        TemplateCreationContext childContextP = childContextS.clone();
        childContextP.setDefaultIcon(new PlayerIconTemplate(PlayerPlaceholderResolver.BindPoint.PLAYER, tcc.getPlayerIconDataKey()));
        childContextP.setDefaultPing(new PlayerPingTemplate(PlayerPlaceholderResolver.BindPoint.PLAYER, tcc.getPlayerPingDataKey()));
        childContextP.setPlayerAvailable(true);

        TemplateCreationContext childContextM = tcc.clone();
        if (fillSlotsVertical) {
            childContextM.setColumns(1);
        }
        childContextM.addPlaceholderResolver(new OtherCountPlaceholderResolver());

        ComponentTemplate playerComponentTemplate = tcc.emptyComponent(); // dummy
        if (ConfigValidationUtil.checkNotNull(tcc, "!players_by_server component", "playerComponent", playerComponent, getStartMark())) {
            playerComponentTemplate = this.playerComponent.toTemplate(childContextP);
            ComponentTemplate.LayoutInfo layoutInfo = playerComponentTemplate.getLayoutInfo();
            if (!layoutInfo.isConstantSize()) {
                tcc.getErrorHandler().addError("Failed to configure !players_by_server component. Attribute playerComponent must not have variable size.", playerComponent.getStartMark());
            }
            if (layoutInfo.isBlockAligned()) {
                tcc.getErrorHandler().addError("Failed to configure !players_by_server component. Attribute playerComponent must not require block alignment.", playerComponent.getStartMark());
            }
        }

        if (!ConfigValidationUtil.checkNotNull(tcc, "!players_by_server component", "hiddenServers", hiddenServers, getStartMark())) {
            hiddenServers = new MarkedListProperty<>();
        }

        if (includeEmptyServers != null) {
            if (includeEmptyServers.isValue()) {
                tcc.getErrorHandler().addWarning("'includeEmptyServers: true' is deprecated and will be removed. Use 'showServers: ALL' instead.", includeEmptyServers.getStartMark());
            } else {
                tcc.getErrorHandler().addWarning("'includeEmptyServers: false' is deprecated and will be removed. Use 'showServers: NON_EMPTY' instead.", includeEmptyServers.getStartMark());
            }
        }

        if (showServers == null) {
            if (includeEmptyServers != null) {
                showServers = includeEmptyServers.isValue() ? ServerOptions.ALL : ServerOptions.NON_EMPTY;
            } else {
                showServers = ServerOptions.ALL;
            }
        }

        ContextAwareOrdering<Context, PlayerSetPartition, String> serverComparator = null;

        if (serverOrder != null) {
            List<ContextAwareOrdering<Context, PlayerSetPartition, String>> list = Stream.of(serverOrder.getValue().split(","))
                    .filter((s1) -> !s1.isEmpty())
                    .map(String::toLowerCase)
                    .map((String rule) -> getServerComparator(rule, tcc, serverOrder.getStartMark()))
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
            if (!list.isEmpty()) {
                serverComparator = ContextAwareOrdering.compound(list);
            }
        }

        ComponentTemplate morePlayersComponentTemplate;
        if (this.morePlayersComponent != null) {

            morePlayersComponentTemplate = this.morePlayersComponent.toTemplate(childContextM);
            ComponentTemplate.LayoutInfo layoutInfo = morePlayersComponentTemplate.getLayoutInfo();
            if (!layoutInfo.isConstantSize()) {
                tcc.getErrorHandler().addError("Failed to configure !players_by_server component. Attribute playerComponent cannot have variable size.", morePlayersComponent.getStartMark());
            }
            if (layoutInfo.isBlockAligned()) {
                tcc.getErrorHandler().addError("Failed to configure !players_by_server component. Attribute playerComponent must not require block alignment.", morePlayersComponent.getStartMark());
            }
        } else {
            morePlayersComponentTemplate = childContextM.emptyComponent();
        }

        Map<String, String> serverMap = new HashMap<>();
        if (mergeServers != null) {
            for (Map.Entry<String, List<String>> entry : mergeServers.entrySet()) {
                String groupName = entry.getKey();
                List<String> serverNames = entry.getValue();
                if (serverNames != null) {
                    for (String serverName : serverNames) {
                        serverMap.put(serverName, groupName);
                    }
                }
            }
        }

        return PlayersByServerComponentTemplate.builder()
                .playerOrder(playerOrderTemplate)
                .playerSet(tcc.getPlayerSets().get(playerSet.getValue()))
                .playerComponent(playerComponentTemplate)
                .morePlayersComponent(morePlayersComponentTemplate)
                .serverHeader(serverHeader != null ? serverHeader.toTemplate(childContextS) : null)
                .serverFooter(serverFooter != null ? serverFooter.toTemplate(childContextS) : null)
                .serverSeparator(serverSeparator != null ? serverSeparator.toTemplate(tcc) : null)
                .fillSlotsVertical(fillSlotsVertical)
                .minSize(minSize.getValue())
                .maxSize(maxSize.getValue())
                .minSizePerServer(minSizePerServer.getValue())
                .maxSizePerServer(maxSizePerServer.getValue())
                .columns(tcc.getColumns().orElse(1))
                .defaultIcon(tcc.getDefaultIcon())
                .defaultText(tcc.getDefaultText())
                .defaultPing(tcc.getDefaultPing())
                .partitionFunction(tcc.getExpressionEngine().compile(childContextP, "${player server}", null))
                .mergeSections(new MapFunction(serverMap))
                .hiddenServers(ImmutableSet.copyOf(hiddenServers))
                .showServers(showServers)
                .serverComparator(serverComparator)
                .prioritizeViewerServer(prioritizeViewerServer)
                .build();
    }

    private ContextAwareOrdering<Context, PlayerSetPartition, String> getServerComparator(String rule, TemplateCreationContext tcc, Mark mark) {
        switch (rule) {
            case "alphabetically":
                return ContextAwareOrdering.from(Comparator.<String>naturalOrder());
            case "playercount":
                return new ContextAwareOrdering<Context, PlayerSetPartition, String>() {
                    @Override
                    public int compare(Context context, PlayerSetPartition partition, String first, String second) {
                        PlayerSet ps1 = partition.getPartition(first);
                        PlayerSet ps2 = partition.getPartition(second);
                        int pc1 = ps1 != null ? ps1.getCount() : 0;
                        int pc2 = ps2 != null ? ps2.getCount() : 0;
                        return -Integer.compare(pc1, pc2);
                    }
                };
            case "online":
                return ContextAwareOrdering.from(Comparator.comparing(server -> BungeeTabListPlus.getInstance().getDataManager().getServerDataHolder(server).get(BTLPBungeeDataKeys.DATA_KEY_SERVER_ONLINE), Comparator.reverseOrder()));
            case "custom":
                if (customServerOrder == null) {
                    tcc.getErrorHandler().addWarning("Selected serverOrder option 'custom', but 'customServerOrder' option is not set.", serverOrder.getStartMark());
                }
                Map<String, Integer> order = customServerOrder != null ? customServerOrder : Collections.emptyMap();
                return ContextAwareOrdering.from(Comparator.comparing(server -> order.getOrDefault(server, Integer.MAX_VALUE)));
            case "yourserverfirst":
                return new ContextAwareOrdering<Context, PlayerSetPartition, String>() {
                    @Override
                    public int compare(Context context, PlayerSetPartition partition, String first, String second) {
                        String server = context.getViewer().get(BungeeData.BungeeCord_Server);
                        if (first.equals(server)) {
                            return -1;
                        } else if (second.equals(server)) {
                            return 1;
                        } else {
                            return 0;
                        }
                    }
                };
            default:
                tcc.getErrorHandler().addWarning("Unknown serverOrder option: '" + rule + "'", mark);
                return null;
        }
    }

    public enum ServerOptions {
        ALL, ONLINE, NON_EMPTY
    }

} public class BTLPBungeeDataKeys implements DataKeyCatalogue {
    public static final DataKey<Integer> DATA_KEY_Server_Count = new DataKey<>("btlp:server_count", BungeeData.SCOPE_BUNGEE_PROXY, TypeToken.INTEGER);
    public static final DataKey<Integer> DATA_KEY_Server_Count_Online = new DataKey<>("btlp:server_count_online", BungeeData.SCOPE_BUNGEE_PROXY, TypeToken.INTEGER);

    public static final DataKey<String> ThirdPartyPlaceholderBungee = new DataKey<>("btlp:thirdPartyPlaceholderBungee", BungeeData.SCOPE_BUNGEE_PLAYER, TypeToken.STRING);

    public static final DataKey<String> ThirdPartyServerPlaceholderBungee = new DataKey<>("btlp:thirdPartyServerPlaceholderBungee", BungeeData.SCOPE_BUNGEE_SERVER, TypeToken.STRING);

    public static final DataKey<Integer> DATA_KEY_GAMEMODE = new DataKey<>("btlp:gamemode", BungeeData.SCOPE_BUNGEE_PLAYER, TypeToken.INTEGER);
    public static final DataKey<Icon> DATA_KEY_ICON = new DataKey<>("btlp:icon", BungeeData.SCOPE_BUNGEE_PLAYER, BTLPDataTypes.ICON);

    public static final DataKey<String> DATA_KEY_RedisBungee_ServerId = new DataKey<>("btlp:redisbungee:serverId", BungeeData.SCOPE_BUNGEE_PLAYER, TypeToken.STRING);

    public static final DataKey<String> DATA_KEY_ServerName = new DataKey<>("btlp:serverName", BungeeData.SCOPE_BUNGEE_SERVER, TypeToken.STRING);

    public static final DataKey<Boolean> DATA_KEY_SERVER_ONLINE = new DataKey<>("btlp:server_online", BungeeData.SCOPE_BUNGEE_SERVER, TypeToken.BOOLEAN);

    public static final DataKey<Boolean> DATA_KEY_IS_HIDDEN = new DataKey<>("btlp:is_hidden", BungeeData.SCOPE_BUNGEE_PLAYER, TypeToken.BOOLEAN);
    public static final DataKey<String> DATA_KEY_CLIENT_VERSION = new DataKey<>("btlp:client_version", BungeeData.SCOPE_BUNGEE_PLAYER, TypeToken.STRING);
    public static final DataKey<Boolean> DATA_KEY_CLIENT_VERSION_BELOW_1_8 = new DataKey<>("btlp:client_version_below_1_8", BungeeData.SCOPE_BUNGEE_PLAYER, TypeToken.BOOLEAN);

    public static final DataKey<Boolean> DATA_KEY_IS_HIDDEN_PLAYER_CONFIG = new DataKey<>("btlp:is_hidden_player_config", BungeeData.SCOPE_BUNGEE_PLAYER, TypeToken.BOOLEAN);
    public static final DataKey<Boolean> DATA_KEY_IS_HIDDEN_PLAYER_COMMAND = new DataKey<>("btlp:is_hidden_player_command", BungeeData.SCOPE_BUNGEE_PLAYER, TypeToken.BOOLEAN);
    public static final DataKey<Boolean> DATA_KEY_IS_HIDDEN_SERVER_CONFIG = new DataKey<>("btlp:is_hidden_server_config", BungeeData.SCOPE_BUNGEE_SERVER, TypeToken.BOOLEAN);

    public static DataKey<String> createBungeeThirdPartyVariableDataKey(String name) {
        return ThirdPartyPlaceholderBungee.withParameter(name);
    }

    public static DataKey<String> createBungeeThirdPartyServerVariableDataKey(String name) {
        return ThirdPartyServerPlaceholderBungee.withParameter(name);
    }
} public class TabListListener implements Listener {

    private final BungeeTabListPlus btlp;

    public TabListListener(BungeeTabListPlus btlp) {
        this.btlp = btlp;
    }

    @EventHandler(priority = EventPriority.LOWEST)
    public void onPlayerJoin(PostLoginEvent e) {
        try {
            BungeePlayer player = btlp.getBungeePlayerProvider().onPlayerConnected(e.getPlayer());
            TabView tabView = btlp.getTabViewManager().onPlayerJoin(e.getPlayer());
            tabView.getTabOverlayProviders().addProvider(new ExcludedServersTabOverlayProvider(player, btlp));
            for (EventListener listener : btlp.getListeners()) {
                listener.onTabViewAdded(tabView, player);
            }
        } catch (Throwable th) {
            BungeeTabListPlus.getInstance().reportError(th);
        }
    }

    @EventHandler(priority = EventPriority.HIGHEST)
    public void onPlayerDisconnect(PlayerDisconnectEvent e) {
        try {
            TabView tabView = btlp.getTabViewManager().onPlayerDisconnect(e.getPlayer());
            tabView.deactivate();
            for (EventListener listener : btlp.getListeners()) {
                listener.onTabViewRemoved(tabView);
            }
            btlp.getBungeePlayerProvider().onPlayerDisconnected(e.getPlayer());

            // hack to revert changes from https://github.com/SpigotMC/BungeeCord/commit/830f18a35725f637d623594eaaad50b566376e59
            Server server = e.getPlayer().getServer();
            if (server != null) {
                server.disconnect("Quitting");
            }
            ((UserConnection) e.getPlayer()).setServer(null);
        } catch (Throwable th) {
            BungeeTabListPlus.getInstance().reportError(th);
        }
    }

    @EventHandler
    public void onReload(ProxyReloadEvent event) {
        btlp.reload();
    }
} public class GlobalServerPlaceholderResolver implements PlaceholderResolver<Context> {

    private final DataManager dataManager;
    private final ServerPlaceholderResolver serverPlaceholderResolver;

    public GlobalServerPlaceholderResolver(DataManager dataManager, ServerPlaceholderResolver serverPlaceholderResolver) {
        this.dataManager = dataManager;
        this.serverPlaceholderResolver = serverPlaceholderResolver;
    }

    @Nonnull
    @Override
    public PlaceholderBuilder<?, ?> resolve(PlaceholderBuilder<Context, ?> builder, List<PlaceholderArg> args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException {
        if (args.size() >= 1 && args.get(0) instanceof PlaceholderArg.Text && args.get(0).getText().startsWith("server:")) {
            String serverName = args.remove(0).getText().substring(7);
            DataHolder serverDataHolder = dataManager.getServerDataHolder(serverName);
            return serverPlaceholderResolver.resolve(builder.transformContext(context -> serverDataHolder), args, tcc);
        }
        throw new UnknownPlaceholderException();
    }

} public class PlayerPlaceholderResolver extends AbstractPlayerPlaceholderResolver {

    private final ServerPlaceholderResolver serverPlaceholderResolver;
    private final Cache cache;

    private final Set<String> placeholderAPIPluginPrefixes = Collections.synchronizedSet(new HashSet<>());
    private final Map<String, DataKey<String>> bridgeCustomPlaceholderDataKeys = Collections.synchronizedMap(new HashMap<>());
    private final Map<String, DataKey<String>> customPlaceholderDataKeys = Collections.synchronizedMap(new HashMap<>());

    private final Map<String, String> aliasMap = new HashMap<>();

    public PlayerPlaceholderResolver(ServerPlaceholderResolver serverPlaceholderResolver, Cache cache) {
        super();
        this.serverPlaceholderResolver = serverPlaceholderResolver;
        this.cache = cache;
        addPlaceholder("ping", create(BungeeData.BungeeCord_Ping));
        addPlaceholder("bungeecord_primary_group", create(BungeeData.BungeeCord_PrimaryGroup));
        addPlaceholder("bungeeperms_display", create(BungeeData.BungeePerms_DisplayPrefix));
        addPlaceholder("bungeeperms_prefix", create(BungeeData.BungeePerms_Prefix));
        addPlaceholder("bungeeperms_suffix", create(BungeeData.BungeePerms_Suffix));
        addPlaceholder("bungeeperms_primary_group", create(BungeeData.BungeePerms_PrimaryGroup));
        addPlaceholder("bungeeperms_primary_group_prefix", create(BungeeData.BungeePerms_PrimaryGroupPrefix));
        addPlaceholder("bungeeperms_user_prefix", create(BungeeData.BungeePerms_PlayerPrefix));
        addPlaceholder("bungeeperms_primary_group_rank", create(BungeeData.BungeePerms_Rank));
        addPlaceholder("bungeeperms_primary_group_weight", create(BungeeData.BungeePerms_Weight));
        addPlaceholder("luckpermsbungee_prefix", create(BungeeData.LuckPerms_Prefix));
        addPlaceholder("luckpermsbungee_suffix", create(BungeeData.LuckPerms_Suffix));
        addPlaceholder("luckpermsbungee_primary_group", create(BungeeData.LuckPerms_PrimaryGroup));
        addPlaceholder("luckpermsbungee_primary_group_weight", create(BungeeData.LuckPerms_Weight));
        addPlaceholder("client_version", create(BTLPBungeeDataKeys.DATA_KEY_CLIENT_VERSION));
        addPlaceholder("client_version_below_1_8", create(BTLPBungeeDataKeys.DATA_KEY_CLIENT_VERSION_BELOW_1_8));
        addPlaceholder("client_version_atleast_1_8", create(BTLPBungeeDataKeys.DATA_KEY_CLIENT_VERSION_BELOW_1_8, b -> !b, TypeToken.BOOLEAN));
        addPlaceholder("world", create(MinecraftData.World));
        addPlaceholder("team", create(MinecraftData.Team));
        addPlaceholder("vault_balance", create(MinecraftData.Economy_Balance));
        addPlaceholder("vault_balance2", create(MinecraftData.Economy_Balance, b -> {
            if (b == null) {
                return "";
            } else if (b >= 10_000_000) {
                return String.format("%1.0fM", b / 1_000_000);
            } else if (b >= 10_000) {
                return String.format("%1.0fK", b / 1_000);
            } else if (b >= 100) {
                return String.format("%1.0f", b);
            } else {
                return String.format("%1.2f", b);
            }
        }, TypeToken.STRING));
        addPlaceholder("multiverse_world_alias", create(BukkitData.Multiverse_WorldAlias));
        addPlaceholder("faction_name", create(BukkitData.Factions_FactionName));
        addPlaceholder("faction_member_count", create(BukkitData.Factions_FactionMembers));
        addPlaceholder("faction_online_member_count", create(BukkitData.Factions_OnlineFactionMembers));
        addPlaceholder("faction_at_current_location", create(BukkitData.Factions_FactionsWhere));
        addPlaceholder("faction_power", create(BukkitData.Factions_FactionPower));
        addPlaceholder("faction_player_power", create(BukkitData.Factions_PlayerPower));
        addPlaceholder("faction_rank", create(BukkitData.Factions_FactionsRank));
        addPlaceholder("SimpleClans_ClanName", create(BukkitData.SimpleClans_ClanName));
        addPlaceholder("SimpleClans_ClanMembers", create(BukkitData.SimpleClans_ClanMembers));
        addPlaceholder("SimpleClans_OnlineClanMembers", create(BukkitData.SimpleClans_OnlineClanMembers));
        addPlaceholder("SimpleClans_ClanTag", create(BukkitData.SimpleClans_ClanTag));
        addPlaceholder("SimpleClans_ClanTagLabel", create(BukkitData.SimpleClans_ClanTagLabel));
        addPlaceholder("SimpleClans_ClanColorTag", create(BukkitData.SimpleClans_ClanColorTag));
        addPlaceholder("vault_primary_group", create(MinecraftData.Permissions_PermissionGroup));
        addPlaceholder("vault_prefix", create(MinecraftData.Permissions_Prefix));
        addPlaceholder("vault_suffix", create(MinecraftData.Permissions_Suffix));
        addPlaceholder("vault_primary_group_prefix", create(MinecraftData.Permissions_PrimaryGroupPrefix));
        addPlaceholder("vault_player_prefix", create(MinecraftData.Permissions_PlayerPrefix));
        addPlaceholder("vault_primary_group_weight", create(MinecraftData.Permissions_PermissionGroupWeight));
        addPlaceholder("health", create(MinecraftData.Health));
        addPlaceholder("max_health", create(MinecraftData.MaxHealth));
        addPlaceholder("location_x", create(MinecraftData.PosX));
        addPlaceholder("location_y", create(MinecraftData.PosY));
        addPlaceholder("location_z", create(MinecraftData.PosZ));
        addPlaceholder("xp", create(MinecraftData.XP));
        addPlaceholder("total_xp", create(MinecraftData.TotalXP));
        addPlaceholder("level", create(MinecraftData.Level));
        addPlaceholder("player_points", create(BukkitData.PlayerPoints_Points));
        addPlaceholder("vault_currency", create(MinecraftData.Economy_CurrencyNameSingular));
        addPlaceholder("vault_currency_plural", create(MinecraftData.Economy_CurrencyNamePlural));
        addPlaceholder("tab_name", create(BukkitData.PlayerListName, (player, name) -> name == null ? player.getName() : name, TypeToken.STRING));
        addPlaceholder("display_name", create(MinecraftData.DisplayName, (player, name) -> name == null ? player.getName() : name, TypeToken.STRING));
        addPlaceholder("bungeecord_display_name", create(BungeeData.BungeeCord_DisplayName, (player, name) -> name == null ? player.getName() : name, TypeToken.STRING));
        addPlaceholder("session_duration_seconds", create(BungeeData.BungeeCord_SessionDuration, duration -> duration == null ? null : (int) (duration.getSeconds() % 60), TypeToken.INTEGER));
        addPlaceholder("session_duration_total_seconds", create(BungeeData.BungeeCord_SessionDuration, duration -> duration == null ? null : (int) duration.getSeconds(), TypeToken.INTEGER));
        addPlaceholder("session_duration_minutes", create(BungeeData.BungeeCord_SessionDuration, duration -> duration == null ? null : (int) ((duration.getSeconds() % 3600) / 60), TypeToken.INTEGER));
        addPlaceholder("session_duration_hours", create(BungeeData.BungeeCord_SessionDuration, duration -> duration == null ? null : (int) (duration.getSeconds() / 3600), TypeToken.INTEGER));
        addPlaceholder("essentials_afk", create(BukkitData.Essentials_IsAFK));
        addPlaceholder("is_hidden", create(BTLPBungeeDataKeys.DATA_KEY_IS_HIDDEN));
        addPlaceholder("gamemode", create(BTLPBungeeDataKeys.DATA_KEY_GAMEMODE));
        addPlaceholder("bungeeonlinetime_seconds", create(BungeeData.BungeeOnlineTime_OnlineTime, duration -> duration == null ? null : (int) (duration.getSeconds() % 60), TypeToken.INTEGER));
        addPlaceholder("bungeeonlinetime_minutes", create(BungeeData.BungeeOnlineTime_OnlineTime, duration -> duration == null ? null : (int) ((duration.getSeconds() % 3600) / 60), TypeToken.INTEGER));
        addPlaceholder("bungeeonlinetime_hours", create(BungeeData.BungeeOnlineTime_OnlineTime, duration -> duration == null ? null : (int) (duration.getSeconds() / 3600), TypeToken.INTEGER));
        addPlaceholder("bungeeonlinetime_hours_of_24", create(BungeeData.BungeeOnlineTime_OnlineTime, duration -> duration == null ? null : (int) ((duration.getSeconds() / 3600) % 24), TypeToken.INTEGER));
        addPlaceholder("bungeeonlinetime_days", create(BungeeData.BungeeOnlineTime_OnlineTime, duration -> duration == null ? null : (int) (duration.getSeconds() / 86400), TypeToken.INTEGER));
        addPlaceholder("redisbungee_server_id", create(BTLPBungeeDataKeys.DATA_KEY_RedisBungee_ServerId));
        addPlaceholder("askyblock_island_level", create(BukkitData.ASkyBlock_IslandLevel));
        addPlaceholder("askyblock_island_name", create(BukkitData.ASkyBlock_IslandName));
        addPlaceholder("askyblock_team_leader", create(BukkitData.ASkyBlock_TeamLeader));
        addPlaceholder("paf_clans_clan_name", create(BungeeData.PAFClans_ClanName));
        addPlaceholder("paf_clans_clan_tag", create(BungeeData.PAFClans_ClanTag));
        addPlaceholder("paf_clans_clan_member_count", create(BungeeData.PAFClans_MemberCount));
        addPlaceholder("paf_clans_clan_online_member_count", create(BungeeData.PAFClans_OnlineMemberCount));
        addPlaceholder("paf_clans_is_leader", create(BungeeData.PAFClans_IsLeader));
        addPlaceholder("permission", this::resolvePermissionPlaceholder);
        addPlaceholder("nucleus_afk", create(SpongeData.Nucleus_IsAFK));
        addPlaceholder("nucleus_nick", create(SpongeData.Nucleus_Nick));

        // Server
        addPlaceholder("server", this::resolveServerPlaceholder);
    }

    @Nonnull
    private PlaceholderBuilder<?, ?> resolveServerPlaceholder(PlaceholderBuilder<Player, ?> builder, List<PlaceholderArg> args, TemplateCreationContext tcc) throws PlaceholderException {
        try {
            // The player's data holder should allow transparent access to the server data, so this is fine
            return serverPlaceholderResolver.resolve(builder.transformContext(c -> c), args, tcc);
        } catch (UnknownPlaceholderException e) {
            throw new PlaceholderException("Unknown placeholder");
        }
    }

    @Nonnull
    private PlaceholderBuilder<?, ?> resolvePermissionPlaceholder(PlaceholderBuilder<Player, ?> builder, List<PlaceholderArg> args, TemplateCreationContext tcc) throws PlaceholderException {
        if (args.isEmpty()) {
            throw new PlaceholderException("Use of permission placeholder lacks specification of specific permission");
        }
        if (args.get(0) instanceof PlaceholderArg.Text) {
            String permission = args.remove(0).getText();
            return builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.BOOLEAN, MinecraftData.permission(permission), (p, d) -> d), TypeToken.BOOLEAN);
        } else {
            ExpressionTemplate permission = args.remove(0).getExpression();
            Function<Context, Player> playerFunction = builder.getContextTransformation();
            return PlaceholderBuilder.create().acquireData(() -> new PermissionDataProvider(permission.instantiateWithStringResult(), playerFunction), TypeToken.BOOLEAN, builder.isRequiresViewerContext() || permission.requiresViewerContext());
        }
    }

    @Nonnull
    @Override
    public PlaceholderBuilder<?, ?> resolve(PlaceholderBuilder<Player, ?> builder, List<PlaceholderArg> args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException {
        if (!args.isEmpty() && aliasMap.containsKey(args.get(0).getText())) {
            String replacement = aliasMap.get(args.get(0).getText());
            tcc.getErrorHandler().addWarning("Placeholder '" + args.get(0).getText() + "' has been deprecated. Use '" + replacement + "' instead.", null);
            args.set(0, new PlaceholderArg.Text(replacement));
        }
        try {
            return super.resolve(builder, args, tcc);
        } catch (UnknownPlaceholderException e) {
            if (!args.isEmpty()) {
                String id = args.get(0).getText().toLowerCase();
                PlaceholderBuilder<?, ?> result = null;
                if (customPlaceholderDataKeys.containsKey(id)) {
                    DataKey<String> dataKey = customPlaceholderDataKeys.get(id);
                    result = builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.STRING, dataKey, (player, replacement) -> replacement), TypeToken.STRING);
                } else if (bridgeCustomPlaceholderDataKeys.containsKey(id)) {
                    DataKey<String> dataKey = bridgeCustomPlaceholderDataKeys.get(id);
                    result = builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.STRING, dataKey, (player, replacement) -> replacement), TypeToken.STRING);
                } else {
                    for (String prefix : placeholderAPIPluginPrefixes) {
                        if (id.length() >= prefix.length() && id.substring(0, prefix.length()).equalsIgnoreCase(prefix)) {
                            id = args.remove(0).getText();
                            val resolver = create(BTLPDataKeys.createPlaceholderAPIDataKey("%" + id + "%"));
                            addPlaceholder(id, resolver);
                            return resolver.resolve(builder, args, tcc);
                        }
                    }
                }
                if (result == null) {
                    // prevent errors because bridge information has not been synced yet
                    if (cache.getCustomPlaceholdersBridge().contains(id)) {
                        result = builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.STRING, BTLPDataKeys.createThirdPartyVariableDataKey(id), (player, replacement) -> replacement), TypeToken.STRING);
                    } else {
                        for (String prefix : cache.getPAPIPrefixes()) {
                            if (id.length() >= prefix.length() && id.substring(0, prefix.length()).equalsIgnoreCase(prefix)) {
                                result = builder.acquireData(new PlayerPlaceholderDataProviderSupplier<>(TypeToken.STRING, BTLPDataKeys.createPlaceholderAPIDataKey("%" + id + "%"), (player, replacement) -> replacement), TypeToken.STRING);
                            }
                        }
                    }
                }
                if (result != null) {
                    args.remove(0);
                    return result;
                }
            }
            throw e;
        }
    }

    public void addPlaceholderAPIPluginPrefixes(Collection<String> prefixes) {
        placeholderAPIPluginPrefixes.addAll(prefixes);
    }

    public void addCustomPlaceholderDataKey(String id, DataKey<String> dataKey) {
        customPlaceholderDataKeys.put(id.toLowerCase(), dataKey);
    }

    public void addBridgeCustomPlaceholderDataKey(String id, DataKey<String> dataKey) {
        bridgeCustomPlaceholderDataKeys.put(id.toLowerCase(), dataKey);
    }

    private static class PermissionDataProvider extends AbstractActiveElement<Runnable> implements PlaceholderDataProvider<Context, Boolean>, ExpressionUpdateListener {
        private final ToStringExpression permission;
        private final Function<Context, Player> playerFunction;
        private DataKey<Boolean> permissionDataKey;

        private PermissionDataProvider(ToStringExpression permission, Function<Context, Player> playerFunction) {
            this.permission = permission;
            this.playerFunction = playerFunction;
        }

        @Override
        protected void onActivation() {
            permission.activate(getContext(), this);
            permissionDataKey = MinecraftData.permission(permission.evaluate());
            if (hasListener()) {
                playerFunction.apply(getContext()).addDataChangeListener(permissionDataKey, getListener());
            }
        }

        @Override
        protected void onDeactivation() {
            permission.deactivate();
            if (hasListener()) {
                playerFunction.apply(getContext()).removeDataChangeListener(permissionDataKey, getListener());
            }
        }

        @Override
        public Boolean getData() {
            return playerFunction.apply(getContext()).get(permissionDataKey);
        }

        @Override
        public void onExpressionUpdate() {
            if (hasListener()) {
                playerFunction.apply(getContext()).removeDataChangeListener(permissionDataKey, getListener());
            }
            permissionDataKey = MinecraftData.permission(permission.evaluate());
            if (hasListener()) {
                playerFunction.apply(getContext()).addDataChangeListener(permissionDataKey, getListener());
                getListener().run();
            }
        }
    }
} public class ServerCountPlaceholderResolver implements PlaceholderResolver<Context> {

    private final DataManager dataManager;

    public ServerCountPlaceholderResolver(DataManager dataManager) {
        this.dataManager = dataManager;
    }

    @Nonnull
    @Override
    public PlaceholderBuilder<?, ?> resolve(PlaceholderBuilder<Context, ?> builder, List<PlaceholderArg> args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException {
        if (args.size() >= 2 && "server_count".equalsIgnoreCase(args.get(0).getText()) && "total".equalsIgnoreCase(args.get(1).getText())) {
            args.remove(0);
            args.remove(0);
            return builder.transformContext(context -> dataManager.getProxyData())
                    .acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.INTEGER, BTLPBungeeDataKeys.DATA_KEY_Server_Count, (server, replacement) -> replacement), TypeToken.INTEGER);
        }
        if (args.size() >= 2 && "server_count".equalsIgnoreCase(args.get(0).getText()) && "online".equalsIgnoreCase(args.get(1).getText())) {
            args.remove(0);
            args.remove(0);
            return builder.transformContext(context -> dataManager.getProxyData())
                    .acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.INTEGER, BTLPBungeeDataKeys.DATA_KEY_Server_Count_Online, (server, replacement) -> replacement), TypeToken.INTEGER);
        }
        throw new UnknownPlaceholderException();
    }

} public class ServerPlaceholderResolver extends AbstractDataHolderPlaceholderResolver<DataHolder> {

    private final Map<String, DataKey<String>> bridgeCustomPlaceholderServerDataKeys = Collections.synchronizedMap(new HashMap<>());
    private final Map<String, DataKey<String>> customPlaceholderServerDataKeys = Collections.synchronizedMap(new HashMap<>());

    private final Cache cache;

    public ServerPlaceholderResolver(Cache cache) {
        this.cache = cache;

        setDefaultPlaceholder(builder -> builder.acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.STRING, BTLPBungeeDataKeys.DATA_KEY_ServerName, (c, d) -> d), TypeToken.STRING));
        addPlaceholder("tps", create(MinecraftData.TPS));
        addPlaceholder("name", create(BTLPBungeeDataKeys.DATA_KEY_ServerName));
        addPlaceholder("online", create(BTLPBungeeDataKeys.DATA_KEY_SERVER_ONLINE));
    }

    @Nonnull
    @Override
    public PlaceholderBuilder<?, ?> resolve(PlaceholderBuilder<DataHolder, ?> builder, List<PlaceholderArg> args, TemplateCreationContext tcc) throws UnknownPlaceholderException, PlaceholderException {
        try {
            return super.resolve(builder, args, tcc);
        } catch (UnknownPlaceholderException e) {
            if (!args.isEmpty()) {
                String id = args.get(0).getText().toLowerCase();
                PlaceholderBuilder<?, ?> result = null;
                if (customPlaceholderServerDataKeys.containsKey(id)) {
                    DataKey<String> dataKey = customPlaceholderServerDataKeys.get(id);
                    result = builder.acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.STRING, dataKey, (server, replacement) -> replacement), TypeToken.STRING);
                } else if (bridgeCustomPlaceholderServerDataKeys.containsKey(id)) {
                    DataKey<String> dataKey = bridgeCustomPlaceholderServerDataKeys.get(id);
                    result = builder.acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.STRING, dataKey, (player, replacement) -> replacement), TypeToken.STRING);
                } else if (cache.getCustomServerPlaceholdersBridge().contains(id)) {
                    // prevent warnings because bridge data has not been synced yet
                    result = builder.acquireData(new DataHolderPlaceholderDataProviderSupplier<>(TypeToken.STRING, BTLPDataKeys.createThirdPartyServerVariableDataKey(id), (player, replacement) -> replacement), TypeToken.STRING);
                }
                if (result != null) {
                    args.remove(0);
                    return result;
                }
            }
            throw e;
        }
    }

    public void addCustomPlaceholderServerDataKey(String id, DataKey<String> dataKey) {
        customPlaceholderServerDataKeys.put(id.toLowerCase(), dataKey);
    }

    public void addBridgeCustomPlaceholderServerDataKey(String id, DataKey<String> dataKey) {
        bridgeCustomPlaceholderServerDataKeys.put(id.toLowerCase(), dataKey);
    }
} public class FakePlayerManagerImpl implements FakePlayerManager, PlayerProvider {
    private List<FakePlayer> online = new ArrayList<>();
    private List<String> offline;
    private boolean randomJoinLeaveEventsEnabled;

    private final Set<Listener> listeners = new ReferenceOpenHashSet<>();

    private final Plugin plugin;
    private final IconManager iconManager;
    private final EventExecutor mainThread;

    public FakePlayerManagerImpl(final Plugin plugin, IconManager iconManager, EventExecutor mainThread) {
        this.plugin = plugin;
        this.iconManager = iconManager;
        this.mainThread = mainThread;

        randomJoinLeaveEventsEnabled = true;
        if (BungeeTabListPlus.getInstance().getConfig().fakePlayers.size() > 0) {
            offline = new ArrayList<>(BungeeTabListPlus.getInstance().getConfig().fakePlayers);
            sanitizeFakePlayerNames();
        } else {
            offline = new ArrayList<>();
        }
        mainThread.scheduleAtFixedRate(this::triggerRandomEvent, 10, 10, TimeUnit.SECONDS);
    }

    private void triggerRandomEvent() {
        try {
            if (Math.random() <= 0.5 && online.size() > 0) {
                // do a server switch
                FakePlayer player = online.get((int) (Math.random() * online.size()));
                if (player.isRandomServerSwitchEnabled()) {
                    player.changeServer(getRandomServer());
                }
            } else if (randomJoinLeaveEventsEnabled) {
                if (Math.random() < 0.8 && offline.size() > 0) {
                    // add player
                    String name = offline.remove((int) (Math.random() * offline.size()));
                    createFakePlayer(name, getRandomServer(), true, true);
                } else if (online.size() > 0) {
                    // remove player
                    FakePlayer fakePlayer = online.get((int) (online.size() * Math.random()));
                    if (BungeeTabListPlus.getInstance().getConfig().fakePlayers.contains(fakePlayer.getName())) {
                        removeFakePlayer(fakePlayer);
                    }
                }
            }
        } catch (Throwable th) {
            plugin.getLogger().log(Level.SEVERE, "An error occurred while processing random fake player events", th);
        }
    }

    private static ServerInfo getRandomServer() {
        ArrayList<ServerInfo> servers = new ArrayList<>(ProxyServer.getInstance().getServers().values());
        return servers.get((int) (Math.random() * servers.size()));
    }

    public void removeConfigFakePlayers() {
        Set<String> configFakePlayers = new HashSet<>(BungeeTabListPlus.getInstance().getConfig().fakePlayers);
        mainThread.execute(() -> {
            offline.clear();
            for (FakePlayer fakePlayer : ImmutableList.copyOf(online)) {
                if (configFakePlayers.contains(fakePlayer.getName())) {
                    removeFakePlayer(fakePlayer);
                }
            }
        });
    }

    public void reload() {
        mainThread.execute(() -> {
            offline = new ArrayList<>(BungeeTabListPlus.getInstance().getConfig().fakePlayers);
            sanitizeFakePlayerNames();
            for (int i = offline.size() * 4; i > 0; i--) {
                triggerRandomEvent();
            }
        });
    }

    private void sanitizeFakePlayerNames() {
        for (Iterator<?> iterator = offline.iterator(); iterator.hasNext(); ) {
            Object name = iterator.next();
            if (name == null || !(name instanceof String)) {
                plugin.getLogger().warning("Invalid name used for fake player, removing. (" + name + ")");
                iterator.remove();
            }
        }
    }

    @Override
    @SneakyThrows
    public Collection<FakePlayer> getPlayers() {
        if (!mainThread.inEventLoop()) {
            return mainThread.submit(this::getPlayers).get();
        }
        return ImmutableList.copyOf(online);
    }

    @Override
    public void registerListener(Listener listener) {
        listeners.add(listener);
    }

    @Override
    public void unregisterListener(Listener listener) {
        listeners.remove(listener);
    }

    @Override
    @SneakyThrows
    public Collection<codecrafter47.bungeetablistplus.api.bungee.tablist.FakePlayer> getOnlineFakePlayers() {
        if (!mainThread.inEventLoop()) {
            return mainThread.submit(this::getOnlineFakePlayers).get();
        }
        return ImmutableList.copyOf(online);
    }

    @Override
    public boolean isRandomJoinLeaveEnabled() {
        return randomJoinLeaveEventsEnabled;
    }

    @Override
    public void setRandomJoinLeaveEnabled(boolean value) {
        this.randomJoinLeaveEventsEnabled = value;
    }

    @Override
    @SneakyThrows
    public codecrafter47.bungeetablistplus.api.bungee.tablist.FakePlayer createFakePlayer(String name, ServerInfo server) {
        return createFakePlayer(name, server, false, false);
    }

    @SneakyThrows
    public FakePlayer createFakePlayer(String name, ServerInfo server, boolean randomServerSwitch, boolean skinFromName) {
        if (!mainThread.inEventLoop()) {
            return mainThread.submit(() -> createFakePlayer(name, server, randomServerSwitch, skinFromName)).get();
        }
        FakePlayer fakePlayer = new FakePlayer(name, server, randomServerSwitch, mainThread);
        online.add(fakePlayer);
        listeners.forEach(listener -> listener.onPlayerAdded(fakePlayer));
        if (skinFromName) {
            iconManager.createIconFromName(fakePlayer.getName()).thenAcceptAsync(icon -> {
                if (null != fakePlayer.get(BTLPBungeeDataKeys.DATA_KEY_ICON)) {
                    fakePlayer.data.updateValue(BTLPBungeeDataKeys.DATA_KEY_ICON, icon);
                }
            }, mainThread);
        }
        return fakePlayer;
    }

    @Override
    @SneakyThrows
    public void removeFakePlayer(codecrafter47.bungeetablistplus.api.bungee.tablist.FakePlayer fakePlayer) {
        if (!mainThread.inEventLoop()) {
            mainThread.submit(() -> removeFakePlayer(fakePlayer)).sync();
            return;
        }
        FakePlayer player = (FakePlayer) fakePlayer;
        if (online.remove(player)) {
            if (BungeeTabListPlus.getInstance().getConfig().fakePlayers.contains(player.getName())) {
                offline.add(player.getName());
            }
            listeners.forEach(listener -> listener.onPlayerRemoved(player));
        }
    }
} public class ExcludedServersTabOverlayProvider extends AbstractTabOverlayProvider<TabOverlayHandle, TabOverlayHandle> implements Runnable {

    private final Player player;
    private final BungeeTabListPlus btlp;
    private boolean shouldBeActive = false;
    private TabOverlayProviderSet tabOverlayProviderSet;
    private static Set<ExcludedServersTabOverlayProvider> attachedProviders = new HashSet<>();

    public ExcludedServersTabOverlayProvider(Player player, BungeeTabListPlus btlp) {
        super("excluded-servers", 10003, ContentOperationMode.PASS_TROUGH, HeaderAndFooterOperationMode.PASS_TROUGH);
        this.player = player;
        this.btlp = btlp;
    }


    @Override
    protected void activate(TabView tabView, TabOverlayHandle contentHandle, TabOverlayHandle headerAndFooterHandle) {

    }

    @Override
    @SneakyThrows
    protected void attach(TabView tabView) {
        btlp.getMainThreadExecutor().submit(() -> {
            tabOverlayProviderSet = tabView.getTabOverlayProviders();
            player.addDataChangeListener(BungeeData.BungeeCord_Server, this);
            String server = player.get(BungeeData.BungeeCord_Server);
            shouldBeActive = server == null || btlp.getExcludedServers().contains(server);
            attachedProviders.add(this);
        }).sync();
    }

    @Override
    @SneakyThrows
    protected void detach(TabView tabView) {
        btlp.getMainThreadExecutor().submit(() -> {
            attachedProviders.remove(this);
            player.removeDataChangeListener(BungeeData.BungeeCord_Server, this);
        }).sync();
    }

    @Override
    protected void deactivate(TabView tabView) {

    }

    @Override
    protected boolean shouldActivate(TabView tabView) {
        return shouldBeActive;
    }

    @Override
    public void run() {
        String server = player.get(BungeeData.BungeeCord_Server);
        shouldBeActive = server == null || btlp.getExcludedServers().contains(server);
        tabOverlayProviderSet.scheduleUpdate();
    }

    public static void onReload() {
        BungeeTabListPlus.getInstance().getMainThreadExecutor().execute(() -> {
            for (ExcludedServersTabOverlayProvider provider : attachedProviders) {
                provider.run();
            }
        });
    }
} public class UpdateChecker implements Runnable {

    private final Plugin plugin;

    private boolean updateAvailable = false;
    private boolean newDevBuildAvailable = false;

    public static final long interval = 120;

    private boolean error = false;

    public UpdateChecker(Plugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public void run() {
        if (updateAvailable) {
            return;
        }
        if (error) {
            return;
        }
        try {
            InputStreamReader ir;
            URL url = new URL(
                    "http://updates.codecrafter47.dyndns.eu/" + plugin.
                            getDescription().getName() + "/version.txt");
            HttpURLConnection con = (HttpURLConnection) url.openConnection();
            con.connect();
            int res = con.getResponseCode();
            if (res != 200) {
                con.disconnect();
                return;
            }
            InputStream inputStream = con.getInputStream();
            ir = new InputStreamReader(inputStream, Charsets.UTF_8);

            BufferedReader input = new BufferedReader(ir);

            String newVersion = "";
            for (int i = 0; i < 10; i++) {
                String s = input.readLine();
                if (s == null) {
                    break;
                }
                if (!s.isEmpty() && s.length() > 2) {
                    newVersion = s;
                }
            }

            // compare versions
            String runningVersion = plugin.getDescription().getVersion();
            updateAvailable = compareVersions(newVersion, runningVersion);

            input.close();
            ir.close();
            inputStream.close();
            con.disconnect();

            if (updateAvailable) {
                plugin.getLogger().info("A new version of BungeeTabListPlus (" + newVersion + ") is available. Download from http://www.spigotmc.org/resources/bungeetablistplus.313/");
            }

            if (!updateAvailable && !newDevBuildAvailable && runningVersion.endsWith("-SNAPSHOT")) {
                // Check whether there is a new dev-build available
                try {
                    Properties current = new Properties();
                    current.load(getClass().getClassLoader().getResourceAsStream("version.properties"));
                    String currentVersion = current.getProperty("build", "unknown");
                    if (!currentVersion.equals("unknown")) {
                        int buildNumber = Integer.valueOf(currentVersion);
                        Properties latest = new Properties();
                        latest.load(new URL("http://ci.codecrafter47.dyndns.eu/job/BungeeTabListPlus/lastSuccessfulBuild/artifact/bungee/target/classes/version.properties").openStream());
                        String latestVersion = latest.getProperty("build", "unknown");
                        if (!latestVersion.isEmpty() && !latestVersion.equals("unknown")) {
                            int latestBuildNumber = Integer.valueOf(latestVersion);
                            if (latestBuildNumber > buildNumber) {
                                newDevBuildAvailable = true;
                                plugin.getLogger().info("A new dev-build is available at http://ci.codecrafter47.dyndns.eu/job/BungeeTabListPlus/");
                            }
                        }
                    }
                } catch (Throwable ignored) {
                }
            }
        } catch (Throwable t) {
            error = true;
        }
    }

    static boolean compareVersions(String newVersion, String runningVersion) {
        boolean usesDevBuild = false;
        if (runningVersion.endsWith("-SNAPSHOT")) {
            usesDevBuild = true;
            runningVersion = runningVersion.replace("-SNAPSHOT", "");
        }

        String[] current = runningVersion.split("\\.");
        String[] latest = newVersion.split("\\.");

        int i = 0;
        boolean higher = false;
        boolean equal = true;
        for (; i < current.length && i < latest.length; i++) {
            if (Integer.valueOf(current[i]) < Integer.valueOf(latest[i])) {
                higher = true;
                break;
            } else if (Objects.equals(Integer.valueOf(current[i]), Integer.valueOf(latest[i]))) {
                equal = true;
            } else {
                equal = false;
                break;
            }
        }

        boolean updateAvailable = false;
        if (higher) {
            updateAvailable = true;
        }

        if (equal) {
            if (current.length < latest.length) {
                updateAvailable = true;
            } else if (current.length == latest.length && usesDevBuild) {
                updateAvailable = true;
            }
        }
        return updateAvailable;
    }

    public boolean isUpdateAvailable() {
        return updateAvailable;
    }

    public boolean isNewDevBuildAvailable() {
        return newDevBuildAvailable;
    }
} public class UpdateNotifier implements Runnable {

    private final BungeeTabListPlus plugin;

    public UpdateNotifier(BungeeTabListPlus plugin) {
        this.plugin = plugin;
    }

    @Override
    public void run() {
        if (!plugin.getConfig().notifyAdminsIfUpdateAvailable) {
            return;
        }
        if (!plugin.isUpdateAvailable() && !plugin.isNewDevBuildAvailable()) {
            return;
        }
        for (ProxiedPlayer player : plugin.getProxy().getPlayers()) {
            if (player.hasPermission("bungeetablistplus.admin")) {
                if (plugin.isUpdateAvailable()) {
                    player.sendMessage(getPrefix().append(
                            "A new version is available. Download ").color(
                            ChatColor.GOLD).append("here").color(
                            ChatColor.LIGHT_PURPLE).
                            underlined(true).event(
                            new ClickEvent(ClickEvent.Action.OPEN_URL,
                                    "http://www.spigotmc.org/resources/bungeetablistplus.313/")).
                            create());
                } else {
                    player.sendMessage(getPrefix().append(
                            "A new dev-build is available. Download ").color(
                            ChatColor.GOLD).append("here").color(
                            ChatColor.LIGHT_PURPLE).
                            underlined(true).event(
                            new ClickEvent(ClickEvent.Action.OPEN_URL,
                                    "http://ci.codecrafter47.dyndns.eu/job/BungeeTabListPlus/")).
                            create());
                }
            }
        }
        if (plugin.isUpdateAvailable()) {
            plugin.getLogger().info("A new version of BungeeTabListPlus is available. Download from http://www.spigotmc.org/resources/bungeetablistplus.313/");
        } else {
            plugin.getLogger().info("A new dev-build is available at http://ci.codecrafter47.dyndns.eu/job/BungeeTabListPlus/");
        }
    }

    private ComponentBuilder getPrefix() {
        return new ComponentBuilder("[").color(ChatColor.BLUE).append(
                "BungeeTabListPlus").color(ChatColor.YELLOW).event(
                new ClickEvent(ClickEvent.Action.OPEN_URL,
                        "http://www.spigotmc.org/resources/bungeetablistplus.313/")).
                append("] ").color(ChatColor.BLUE).event((ClickEvent) null);
    }

} public class ExceptionHandlingEventExecutor extends SingleThreadEventExecutor {

    private final Logger logger;

    public ExceptionHandlingEventExecutor(EventExecutorGroup parent, Executor executor, Logger logger) {
        super(parent, executor, true);
        this.logger = logger;
    }

    @Override
    protected void run() {
        do {
            Runnable task = this.takeTask();
            if (task != null) {
                try {
                    task.run();
                } catch (Throwable th) {
                    logger.log(Level.WARNING, "An unexpected error occurred: " + th.getMessage(), th);
                }
                this.updateLastExecutionTime();
            }
        } while (!this.confirmShutdown());
    }
} public class MatchingStringsCollection {

    private final List<String> patterns;
    private final Object2BooleanMap<String> cache = new Object2BooleanOpenHashMap<>();

    public MatchingStringsCollection(List<String> patterns) {
        this.patterns = patterns.stream()
                .filter(regex -> {
                    try {
                        Pattern.compile(regex);
                        return true;
                    } catch (PatternSyntaxException e) {
                        BungeeTabListPlus.getInstance().getLogger().log(Level.WARNING, "Illegal regex", e);
                        return false;
                    }
                }).collect(Collectors.toList());
    }

    public boolean contains(String s) {
        if (cache.containsKey(s)) {
            return cache.getBoolean(s);
        } else {
            boolean r = compute(s);
            cache.put(s, r);
            return r;
        }
    }

    private boolean compute(String s) {
        for (String pattern : patterns) {
            try {
                if (s.matches(pattern)) {
                    return true;
                }
            } catch (PatternSyntaxException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
} public class BungeeProtocolVersionProvider implements ProtocolVersionProvider {

    private final BungeeClientVersionProvider clientVersionProvider = new BungeeClientVersionProvider();

    @Override
    public boolean has18OrLater(ProxiedPlayer player) {
        return player.getPendingConnection().getVersion() >= 47;
    }

    @Override
    public boolean has113OrLater(ProxiedPlayer player) {
        return player.getPendingConnection().getVersion() >= 393;
    }

    @Override
    public boolean is18(ProxiedPlayer player) {
        return player.getPendingConnection().getVersion() == 47;
    }

    @Override
    public String getVersion(ProxiedPlayer player) {
        return clientVersionProvider.apply(player);
    }

} public class ProtocolSupportVersionProvider implements ProtocolVersionProvider {

    private static final boolean psb12 = ProxyServer.getInstance().getPluginManager().getPlugin("ProtocolSupportBungee").getDescription().getVersion().equals("1.2");

    @Override
    public boolean has18OrLater(ProxiedPlayer player) {
        ProtocolVersion protocolVersion = ProtocolSupportAPI.getProtocolVersion(player);
        if (psb12) {
            switch (protocolVersion) {
                case MINECRAFT_1_8:
                    return true;
                default:
                    return false;
            }
        } else {
            return protocolVersion.isAfterOrEq(ProtocolVersion.MINECRAFT_1_8);
        }
    }

    @Override
    public boolean has113OrLater(ProxiedPlayer player) {
        ProtocolVersion protocolVersion = ProtocolSupportAPI.getProtocolVersion(player);
        if (psb12) {
            return false;
        } else {
            return protocolVersion.getId() >= 393;
        }
    }

    @Override
    public boolean is18(ProxiedPlayer player) {
        ProtocolVersion protocolVersion = ProtocolSupportAPI.getProtocolVersion(player);
        return protocolVersion == ProtocolVersion.MINECRAFT_1_8;
    }

    @Override
    public String getVersion(ProxiedPlayer player) {
        return ProtocolSupportAPI.getProtocolVersion(player).getName();
    }

} public interface ProtocolVersionProvider {

    boolean has18OrLater(ProxiedPlayer player);

    boolean has113OrLater(ProxiedPlayer player);

    boolean is18(ProxiedPlayer player);

    String getVersion(ProxiedPlayer player);
} public class ViaVersionProtocolVersionProvider implements ProtocolVersionProvider {

    @Override
    public boolean has18OrLater(ProxiedPlayer player) {
        int version = Via.getAPI().getPlayerVersion(player);
        return ProtocolVersion.getIndex(ProtocolVersion.getProtocol(version)) >= ProtocolVersion.getIndex(ProtocolVersion.v1_8);
    }

    @Override
    public boolean has113OrLater(ProxiedPlayer player) {
        // Note this doesn't care about the client version, but about the server version
        return player.getPendingConnection().getVersion() >= 393;
    }

    @Override
    public boolean is18(ProxiedPlayer player) {
        return Via.getAPI().getPlayerVersion(player) == 47;
    }

    @Override
    public String getVersion(ProxiedPlayer player) {
        return ProtocolVersion.getProtocol(Via.getAPI().getPlayerVersion(player)).getName();
    }

}
import codecrafter47.bungeetablistplus.api.bungee.BungeeTabListPlusAPI;
import codecrafter47.bungeetablistplus.bridge.BukkitBridge;
import codecrafter47.bungeetablistplus.cache.Cache;
import codecrafter47.bungeetablistplus.command.CommandBungeeTabListPlus;
import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants;
import codecrafter47.bungeetablistplus.compat.SortingRuleAliasProcessor;
import codecrafter47.bungeetablistplus.config.MainConfig;
import codecrafter47.bungeetablistplus.config.PlayersByServerComponentConfiguration;
import codecrafter47.bungeetablistplus.data.BTLPBungeeDataKeys;
import codecrafter47.bungeetablistplus.listener.TabListListener;
import codecrafter47.bungeetablistplus.managers.*;
import codecrafter47.bungeetablistplus.placeholder.GlobalServerPlaceholderResolver;
import codecrafter47.bungeetablistplus.placeholder.PlayerPlaceholderResolver;
import codecrafter47.bungeetablistplus.placeholder.ServerCountPlaceholderResolver;
import codecrafter47.bungeetablistplus.placeholder.ServerPlaceholderResolver;
import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl;
import codecrafter47.bungeetablistplus.tablist.ExcludedServersTabOverlayProvider;
import codecrafter47.bungeetablistplus.updater.UpdateChecker;
import codecrafter47.bungeetablistplus.updater.UpdateNotifier;
import codecrafter47.bungeetablistplus.util.ExceptionHandlingEventExecutor;
import codecrafter47.bungeetablistplus.util.MatchingStringsCollection;
import codecrafter47.bungeetablistplus.version.BungeeProtocolVersionProvider;
import codecrafter47.bungeetablistplus.version.ProtocolSupportVersionProvider;
import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider;
import codecrafter47.bungeetablistplus.version.ViaVersionProtocolVersionProvider;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import de.codecrafter47.data.bukkit.api.BukkitData;
import de.codecrafter47.data.bungee.api.BungeeData;
import de.codecrafter47.data.minecraft.api.MinecraftData;
import de.codecrafter47.data.sponge.api.SpongeData;
import de.codecrafter47.taboverlay.config.ComponentSpec;
import de.codecrafter47.taboverlay.config.ConfigTabOverlayManager;
import de.codecrafter47.taboverlay.config.ErrorHandler;
import de.codecrafter47.taboverlay.config.dsl.CustomPlaceholderConfiguration;
import de.codecrafter47.taboverlay.config.icon.DefaultIconManager;
import de.codecrafter47.taboverlay.config.platform.EventListener;
import de.codecrafter47.taboverlay.config.platform.Platform;
import de.codecrafter47.taboverlay.config.player.JoinedPlayerProvider;
import de.codecrafter47.taboverlay.config.player.PlayerProvider;
import de.codecrafter47.taboverlay.spectator.SpectatorPassthroughTabOverlayManager;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.MultithreadEventExecutorGroup;
import lombok.Getter;
import lombok.val;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.Connection;
import net.md_5.bungee.api.plugin.Plugin;
import org.bstats.bungeecord.Metrics;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.error.YAMLException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/*
 *     Copyright (C) 2020 Florian Stober
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package codecrafter47.bungeetablistplus;



/**
 * Main Class of BungeeTabListPlus
 *
 * @author Florian Stober
 */
public class BungeeTabListPlus {

    /**
     * Holds an INSTANCE of itself if the plugin is enabled
     */
    private static BungeeTabListPlus INSTANCE;
    @Getter
    private final Plugin plugin;

    public PlayerProvider playerProvider;
    @Getter
    private EventExecutor mainThreadExecutor;
    @Getter
    private EventExecutorGroup asyncExecutor;

    @Getter
    private RedisPlayerManager redisPlayerManager;
    @Getter
    private DataManager dataManager;
    private ServerStateManager serverStateManager;
    @Getter
    private ServerPlaceholderResolver serverPlaceholderResolver;

    private Yaml yaml;
    @Getter
    private HiddenPlayersManager hiddenPlayersManager;
    private PlayerPlaceholderResolver playerPlaceholderResolver;
    private API api;

    public BungeeTabListPlus(Plugin plugin) {
        this.plugin = plugin;
    }

    /**
     * Static getter for the current instance of the plugin
     *
     * @return the current instance of the plugin, null if the plugin is
     * disabled
     */
    public static BungeeTabListPlus getInstance(Plugin plugin) {
        if (INSTANCE == null) {
            INSTANCE = new BungeeTabListPlus(plugin);
        }
        return INSTANCE;
    }

    public static BungeeTabListPlus getInstance() {
        return INSTANCE;
    }

    @Getter
    private MainConfig config;

    private Cache cache;
    @Getter
    MatchingStringsCollection excludedServers;