001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.util;
020
021import com.google.common.cache.Cache;
022import com.google.common.cache.CacheBuilder;
023import com.plotsquared.core.PlotSquared;
024import com.plotsquared.core.command.Command;
025import com.plotsquared.core.command.CommandCategory;
026import com.plotsquared.core.command.RequiredType;
027import com.plotsquared.core.configuration.Settings;
028import com.plotsquared.core.player.ConsolePlayer;
029import com.plotsquared.core.player.PlotPlayer;
030import com.plotsquared.core.plot.Plot;
031import com.plotsquared.core.plot.PlotArea;
032import com.plotsquared.core.uuid.UUIDMapping;
033import org.checkerframework.checker.nullness.qual.NonNull;
034
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.Collection;
038import java.util.Collections;
039import java.util.List;
040import java.util.Locale;
041import java.util.UUID;
042import java.util.concurrent.TimeUnit;
043import java.util.function.Predicate;
044import java.util.stream.Collectors;
045
046/**
047 * Tab completion utilities
048 */
049public final class TabCompletions {
050
051    private static final Cache<String, List<String>> cachedCompletionValues =
052            CacheBuilder.newBuilder()
053                    .expireAfterWrite(Settings.Tab_Completions.CACHE_EXPIRATION, TimeUnit.SECONDS)
054                    .build();
055
056    private static final Command booleanTrueCompletion = new Command(null, false, "true", "",
057            RequiredType.NONE, null
058    ) {
059    };
060    private static final Command booleanFalseCompletion = new Command(null, false, "false", "",
061            RequiredType.NONE, null
062    ) {
063    };
064
065    private TabCompletions() {
066        throw new UnsupportedOperationException(
067                "This is a utility class and cannot be instantiated");
068    }
069
070    /**
071     * Get a list of tab completions corresponding to player names. This uses the UUID pipeline
072     * cache, so it will complete will all names known to PlotSquared
073     *
074     * @param input    Command input
075     * @param existing Players that should not be included in completions
076     * @return List of completions
077     * @deprecated In favor {@link #completePlayers(PlotPlayer, String, List)}
078     */
079    @Deprecated(forRemoval = true, since = "6.1.3")
080    public static @NonNull List<Command> completePlayers(
081            final @NonNull String input,
082            final @NonNull List<String> existing
083    ) {
084        return completePlayers(ConsolePlayer.getConsole(), input, existing);
085    }
086
087    /**
088     * Get a list of tab completions corresponding to player names. This uses the UUID pipeline
089     * cache, so it will complete will all names known to PlotSquared
090     *
091     * @param input    Command input
092     * @param issuer   The player who issued the tab completion
093     * @param existing Players that should not be included in completions
094     * @return List of completions
095     * @since 6.1.3
096     */
097    public static @NonNull List<Command> completePlayers(
098            final @NonNull PlotPlayer<?> issuer,
099            final @NonNull String input,
100            final @NonNull List<String> existing
101    ) {
102        return completePlayers("players", issuer, input, existing, uuid -> true);
103    }
104
105    /**
106     * Get a list of tab completions corresponding to player names added to the given plot.
107     *
108     * @param plot     Plot to complete added players for
109     * @param input    Command input
110     * @param existing Players that should not be included in completions
111     * @return List of completions
112     *
113     * @deprecated In favor {@link #completeAddedPlayers(PlotPlayer, Plot, String, List)}
114     */
115    @Deprecated(forRemoval = true, since = "6.1.3")
116    public static @NonNull List<Command> completeAddedPlayers(
117            final @NonNull Plot plot,
118            final @NonNull String input, final @NonNull List<String> existing
119    ) {
120        return completeAddedPlayers(ConsolePlayer.getConsole(), plot, input, existing);
121    }
122
123    /**
124     * Get a list of tab completions corresponding to player names added to the given plot.
125     *
126     * @param issuer   The player who issued the tab completion
127     * @param plot     Plot to complete added players for
128     * @param input    Command input
129     * @param existing Players that should not be included in completions
130     * @return List of completions
131     * @since 6.1.3
132     */
133    public static @NonNull List<Command> completeAddedPlayers(
134            final @NonNull PlotPlayer<?> issuer,
135            final @NonNull Plot plot,
136            final @NonNull String input, final @NonNull List<String> existing
137    ) {
138        return completePlayers("added" + plot, issuer, input, existing,
139                uuid -> plot.getMembers().contains(uuid)
140                        || plot.getTrusted().contains(uuid)
141                        || plot.getDenied().contains(uuid)
142        );
143    }
144
145    public static @NonNull List<Command> completePlayersInPlot(
146            final @NonNull Plot plot,
147            final @NonNull String input, final @NonNull List<String> existing
148    ) {
149        List<String> players = cachedCompletionValues.getIfPresent("inPlot" + plot);
150        if (players == null) {
151            final List<PlotPlayer<?>> inPlot = plot.getPlayersInPlot();
152            players = new ArrayList<>(inPlot.size());
153            for (PlotPlayer<?> player : inPlot) {
154                players.add(player.getName());
155            }
156            cachedCompletionValues.put("inPlot" + plot, players);
157        }
158        return filterCached(players, input, existing);
159    }
160
161    /**
162     * Get a list of completions corresponding to WorldEdit(/FastAsyncWorldEdit) patterns. This uses
163     * WorldEdit's pattern completer internally.
164     *
165     * @param input Command input
166     * @return List of completions
167     */
168    public static @NonNull List<Command> completePatterns(final @NonNull String input) {
169        return PatternUtil.getSuggestions(input.trim()).stream()
170                .map(value -> value.toLowerCase(Locale.ENGLISH).replace("minecraft:", ""))
171                .filter(value -> value.startsWith(input.toLowerCase(Locale.ENGLISH)))
172                .map(value -> new Command(null, false, value, "", RequiredType.NONE, null) {
173                }).collect(Collectors.toList());
174    }
175
176    public static @NonNull List<Command> completeBoolean(final @NonNull String input) {
177        if (input.isEmpty()) {
178            return Arrays.asList(booleanTrueCompletion, booleanFalseCompletion);
179        }
180        if ("true".startsWith(input)) {
181            return Collections.singletonList(booleanTrueCompletion);
182        }
183        if ("false".startsWith(input)) {
184            return Collections.singletonList(booleanFalseCompletion);
185        }
186        return Collections.emptyList();
187    }
188
189    /**
190     * Get a list of integer numbers matching the given input. If the input string
191     * is empty, nothing will be returned. The list is unmodifiable.
192     *
193     * @param input        Input to filter with
194     * @param amountLimit  Maximum amount of suggestions
195     * @param highestLimit Highest number to include
196     * @return Unmodifiable list of number completions
197     */
198    public static @NonNull List<Command> completeNumbers(
199            final @NonNull String input,
200            final int amountLimit, final int highestLimit
201    ) {
202        if (input.isEmpty() || input.length() > highestLimit || !MathMan.isInteger(input)) {
203            return Collections.emptyList();
204        }
205        int offset;
206        try {
207            offset = Integer.parseInt(input) * 10;
208        } catch (NumberFormatException ignored) {
209            return Collections.emptyList();
210        }
211        final List<String> commands = new ArrayList<>();
212        for (int i = offset; i < highestLimit && (offset - i + amountLimit) > 0; i++) {
213            commands.add(String.valueOf(i));
214        }
215        return asCompletions(commands.toArray(new String[0]));
216    }
217
218    /**
219     * Get a list of plot areas matching the given input.
220     * The list is unmodifiable.
221     *
222     * @param input Input to filter with
223     * @return Unmodifiable list of area completions
224     */
225    public static @NonNull List<Command> completeAreas(final @NonNull String input) {
226        final List<Command> completions = new ArrayList<>();
227        for (final PlotArea area : PlotSquared.get().getPlotAreaManager().getAllPlotAreas()) {
228            String areaName = area.getWorldName();
229            if (area.getId() != null) {
230                areaName += ";" + area.getId();
231            }
232            if (!areaName.toLowerCase().startsWith(input.toLowerCase())) {
233                continue;
234            }
235            completions.add(new Command(null, false, areaName, "",
236                    RequiredType.NONE, null
237            ) {
238            });
239        }
240        return Collections.unmodifiableList(completions);
241    }
242
243    public static @NonNull List<Command> asCompletions(String... toFilter) {
244        final List<Command> completions = new ArrayList<>();
245        for (String completion : toFilter) {
246            completions.add(new Command(null, false, completion, "",
247                    RequiredType.NONE, null
248            ) {
249            });
250        }
251        return Collections.unmodifiableList(completions);
252    }
253
254    /**
255     * @param cacheIdentifier Cache key
256     * @param input           Command input
257     * @param existing        Players that should not be included in completions
258     * @param uuidFilter      Filter applied before caching values
259     * @return List of completions
260     * @deprecated In favor {@link #completePlayers(String, PlotPlayer, String, List, Predicate)}
261     */
262    @SuppressWarnings("unused")
263    @Deprecated(forRemoval = true, since = "6.1.3")
264    private static List<Command> completePlayers(
265            final @NonNull String cacheIdentifier,
266            final @NonNull String input, final @NonNull List<String> existing,
267            final @NonNull Predicate<UUID> uuidFilter
268    ) {
269        return completePlayers(cacheIdentifier, ConsolePlayer.getConsole(), input, existing, uuidFilter);
270    }
271
272    /**
273     * @param cacheIdentifier Cache key
274     * @param issuer          The player who issued the tab completion
275     * @param input           Command input
276     * @param existing        Players that should not be included in completions
277     * @param uuidFilter      Filter applied before caching values
278     * @return List of completions
279     * @since 6.1.3
280     */
281    private static List<Command> completePlayers(
282            final @NonNull String cacheIdentifier,
283            final @NonNull PlotPlayer<?> issuer,
284            final @NonNull String input, final @NonNull List<String> existing,
285            final @NonNull Predicate<UUID> uuidFilter
286    ) {
287        List<String> players;
288        if (Settings.Enabled_Components.EXTENDED_USERNAME_COMPLETION) {
289            players = cachedCompletionValues.getIfPresent(cacheIdentifier);
290            if (players == null) {
291                final Collection<UUIDMapping> mappings =
292                        PlotSquared.get().getImpromptuUUIDPipeline().getAllImmediately();
293                players = new ArrayList<>(mappings.size());
294                for (final UUIDMapping mapping : mappings) {
295                    if (uuidFilter.test(mapping.getUuid())) {
296                        players.add(mapping.getUsername());
297                    }
298                }
299                cachedCompletionValues.put(cacheIdentifier, players);
300            }
301        } else {
302            final Collection<? extends PlotPlayer<?>> onlinePlayers = PlotSquared.platform().playerManager().getPlayers();
303            players = new ArrayList<>(onlinePlayers.size());
304            for (final PlotPlayer<?> player : onlinePlayers) {
305                if (!uuidFilter.test(player.getUUID())) {
306                    continue;
307                }
308                if (issuer != null && !issuer.canSee(player)) {
309                    continue;
310                }
311                players.add(player.getName());
312            }
313        }
314        return filterCached(players, input, existing);
315    }
316
317    private static List<Command> filterCached(
318            Collection<String> playerNames, String input,
319            List<String> existing
320    ) {
321        final String processedInput = input.toLowerCase(Locale.ENGLISH);
322        return playerNames.stream().filter(player -> player.toLowerCase(Locale.ENGLISH).startsWith(processedInput))
323                .filter(player -> !existing.contains(player)).map(
324                        player -> new Command(null, false, player, "", RequiredType.NONE,
325                                CommandCategory.INFO
326                        ) {
327                        })
328                /* If there are more than 200 suggestions, just send the first 200 */
329                .limit(200)
330                .collect(Collectors.toList());
331    }
332
333}