001/*
002 * Copyright (c) 2016-2017 Daniel Ennis (Aikar) - MIT License
003 *
004 *  Permission is hereby granted, free of charge, to any person obtaining
005 *  a copy of this software and associated documentation files (the
006 *  "Software"), to deal in the Software without restriction, including
007 *  without limitation the rights to use, copy, modify, merge, publish,
008 *  distribute, sublicense, and/or sell copies of the Software, and to
009 *  permit persons to whom the Software is furnished to do so, subject to
010 *  the following conditions:
011 *
012 *  The above copyright notice and this permission notice shall be
013 *  included in all copies or substantial portions of the Software.
014 *
015 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
016 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
017 *  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
018 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
019 *  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
020 *  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
021 *  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
022 */
023
024package co.aikar.commands;
025
026import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil;
027import org.jetbrains.annotations.NotNull;
028
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Locale;
036import java.util.Map;
037import java.util.function.Supplier;
038import java.util.stream.Collectors;
039import java.util.stream.IntStream;
040
041
042@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
043public class CommandCompletions<C extends CommandCompletionContext> {
044    private static final String DEFAULT_ENUM_ID = "@__defaultenum__";
045    private final CommandManager manager;
046    // TODO: use a CompletionProvider that can return a delegated Id or provide values such as enum support
047    private Map<String, CommandCompletionHandler> completionMap = new HashMap<>();
048    private Map<Class, String> defaultCompletions = new HashMap<>();
049
050    public CommandCompletions(CommandManager manager) {
051        this.manager = manager;
052        registerStaticCompletion("empty", Collections.emptyList());
053        registerStaticCompletion("nothing", Collections.emptyList());
054        registerStaticCompletion("timeunits", Arrays.asList("minutes", "hours", "days", "weeks", "months", "years"));
055        registerAsyncCompletion("range", (c) -> {
056            String config = c.getConfig();
057            if (config == null) {
058                return Collections.emptyList();
059            }
060            final String[] ranges = ACFPatterns.DASH.split(config);
061            int start;
062            int end;
063            if (ranges.length != 2) {
064                start = 0;
065                end = ACFUtil.parseInt(ranges[0], 0);
066            } else {
067                start = ACFUtil.parseInt(ranges[0], 0);
068                end = ACFUtil.parseInt(ranges[1], 0);
069            }
070            return IntStream.rangeClosed(start, end).mapToObj(Integer::toString).collect(Collectors.toList());
071        });
072    }
073
074    /**
075     * Registr a completion handler to provide command completions based on the user input.
076     *
077     * @param id
078     * @param handler
079     * @return
080     */
081    public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler<C> handler) {
082        return this.completionMap.put(prepareCompletionId(id), handler);
083    }
084
085    /**
086     * Unregister a completion handler.
087     * @param id
088     * @return
089     * @throws IllegalStateException If the completion couldn't be found
090     */
091    public CommandCompletionHandler unregisterCompletion(String id) {
092        if (!this.completionMap.containsKey(id)) {
093            throw new IllegalStateException("The supplied key " + id + " does not exist in any completions");
094        }
095
096        return this.completionMap.remove(id);
097    }
098
099    /**
100     * Registr a completion handler to provide command completions based on the user input.
101     * This handler is declared to be safe to be executed asynchronously.
102     * <p>
103     * Not all platforms support this, so if the platform does not support asynchronous execution,
104     * your handler will be executed on the main thread.
105     * <p>
106     * Use this anytime your handler does not need to access state that is not considered thread safe.
107     * <p>
108     * Use context.isAsync() to determine if you are async or not.
109     *
110     * @param id
111     * @param handler
112     * @return
113     */
114    public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler<C> handler) {
115        return this.completionMap.put(prepareCompletionId(id), handler);
116    }
117
118    /**
119     * Register a static list of command completions that will never change.
120     * Like @CommandCompletion, values are | (PIPE) separated.
121     * <p>
122     * Example: foo|bar|baz
123     *
124     * @param id
125     * @param list
126     * @return
127     */
128    public CommandCompletionHandler registerStaticCompletion(String id, String list) {
129        return registerStaticCompletion(id, ACFPatterns.PIPE.split(list));
130    }
131
132    /**
133     * Register a static list of command completions that will never change
134     *
135     * @param id
136     * @param completions
137     * @return
138     */
139    public CommandCompletionHandler registerStaticCompletion(String id, String[] completions) {
140        return registerStaticCompletion(id, Arrays.asList(completions));
141    }
142
143    /**
144     * Register a static list of command completions that will never change. The list is obtained from the supplier
145     * immediately as part of this method call.
146     *
147     * @param id
148     * @param supplier
149     * @return
150     */
151    public CommandCompletionHandler registerStaticCompletion(String id, Supplier<Collection<String>> supplier) {
152        return registerStaticCompletion(id, supplier.get());
153    }
154
155    /**
156     * Register a static list of command completions that will never change
157     *
158     * @param id
159     * @param completions
160     * @return
161     */
162    public CommandCompletionHandler registerStaticCompletion(String id, Collection<String> completions) {
163        return registerAsyncCompletion(id, x -> completions);
164    }
165
166    /**
167     * Registers a completion handler such as @players to default apply to all command parameters of the specified types
168     * <p>
169     * This enables automatic completion support for parameters without manually defining it for custom objects
170     *
171     * @param id
172     * @param classes
173     */
174    public void setDefaultCompletion(String id, Class... classes) {
175        // get completion with specified id
176        id = prepareCompletionId(id);
177        CommandCompletionHandler completion = completionMap.get(id);
178
179        if (completion == null) {
180            // Throw something because no completion with specified id
181            throw new IllegalStateException("Completion not registered for " + id);
182        }
183
184        for (Class clazz : classes) {
185            defaultCompletions.put(clazz, id);
186        }
187    }
188
189    @NotNull
190    private static String prepareCompletionId(String id) {
191        return (id.startsWith("@") ? "" : "@") + id.toLowerCase(Locale.ENGLISH);
192    }
193
194    @NotNull
195    List<String> of(RegisteredCommand cmd, CommandIssuer sender, String[] args, boolean isAsync) {
196        String[] completions = ACFPatterns.SPACE.split(cmd.complete);
197        final int argIndex = args.length - 1;
198
199        String input = args[argIndex];
200
201        String completion = argIndex < completions.length ? completions[argIndex] : null;
202        if (completion == null || completion.isEmpty() || "*".equals(completion)) {
203            completion = findDefaultCompletion(cmd, args);
204        }
205
206        if (completion == null && completions.length > 0) {
207            String last = completions[completions.length - 1];
208            if (last.startsWith("repeat@")) {
209                completion = last;
210            } else if (argIndex >= completions.length && cmd.parameters[cmd.parameters.length - 1].consumesRest) {
211                completion = last;
212            }
213        }
214
215        if (completion == null) {
216            return Collections.singletonList(input);
217        }
218
219        return getCompletionValues(cmd, sender, completion, args, isAsync);
220    }
221
222    String findDefaultCompletion(RegisteredCommand cmd, String[] args) {
223        int i = 0;
224        for (CommandParameter param : cmd.parameters) {
225            if (param.canConsumeInput() && ++i == args.length) {
226                Class type = param.getType();
227                while (type != null) {
228                    String completion = this.defaultCompletions.get(type);
229                    if (completion != null) {
230                        return completion;
231                    }
232                    type = type.getSuperclass();
233                }
234                if (param.getType().isEnum()) {
235                    CommandOperationContext ctx = CommandManager.getCurrentCommandOperationContext();
236                    //noinspection unchecked
237                    ctx.enumCompletionValues = ACFUtil.enumNames((Class<? extends Enum<?>>) param.getType());
238                    return DEFAULT_ENUM_ID;
239                }
240                break;
241            }
242        }
243        return null;
244    }
245
246    List<String> getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) {
247        if (DEFAULT_ENUM_ID.equals(completion)) {
248            CommandOperationContext<?> ctx = CommandManager.getCurrentCommandOperationContext();
249            return ctx.enumCompletionValues;
250        }
251        boolean repeat = completion.startsWith("repeat@");
252        if (repeat) {
253            completion = completion.substring(6);
254        }
255        completion = manager.getCommandReplacements().replace(completion);
256
257        List<String> allCompletions = new ArrayList<>();
258        String input = args.length > 0 ? args[args.length - 1] : "";
259
260        for (String value : ACFPatterns.PIPE.split(completion)) {
261            String[] complete = ACFPatterns.COLONEQUALS.split(value, 2);
262            CommandCompletionHandler handler = this.completionMap.get(complete[0].toLowerCase(Locale.ENGLISH));
263            if (handler != null) {
264                if (isAsync && !(handler instanceof AsyncCommandCompletionHandler)) {
265                    ACFUtil.sneaky(new SyncCompletionRequired());
266                    return null;
267                }
268                String config = complete.length == 1 ? null : complete[1];
269                CommandCompletionContext context = manager.createCompletionContext(command, sender, input, config, args);
270
271                try {
272                    //noinspection unchecked
273                    Collection<String> completions = handler.getCompletions(context);
274
275                    //Handle completions with more than one word:
276                    if (!repeat && completions != null
277                            && command.parameters[command.parameters.length - 1].consumesRest
278                            && args.length > ACFPatterns.SPACE.split(command.complete).length) {
279                        String start = String.join(" ", args);
280                        completions = completions.stream()
281                                .map(s -> {
282                                    if (s != null && s.split(" ").length >= args.length && ApacheCommonsLangUtil.startsWithIgnoreCase(s, start)) {
283                                        String[] completionArgs = s.split(" ");
284                                        return String.join(" ", Arrays.copyOfRange(completionArgs, args.length - 1, completionArgs.length));
285                                    } else {
286                                        return s;
287                                    }
288                                }).collect(Collectors.toList());
289                    }
290
291                    if (completions != null) {
292                        allCompletions.addAll(completions);
293                        continue;
294                    }
295                    //noinspection ConstantIfStatement,ConstantConditions
296                    if (false) { // Hack to fool compiler. since its sneakily thrown.
297                        throw new CommandCompletionTextLookupException();
298                    }
299                } catch (CommandCompletionTextLookupException ignored) {
300                    // This should only happen if some other feedback error occured.
301                } catch (Exception e) {
302                    command.handleException(sender, Arrays.asList(args), e);
303                }
304                // Something went wrong in lookup, fall back to input
305                return Collections.singletonList(input);
306            } else {
307                // Plaintext value
308                allCompletions.add(value);
309            }
310        }
311        return allCompletions;
312    }
313
314    public interface CommandCompletionHandler<C extends CommandCompletionContext> {
315        Collection<String> getCompletions(C context) throws InvalidCommandArgument;
316    }
317
318    public interface AsyncCommandCompletionHandler<C extends CommandCompletionContext> extends CommandCompletionHandler<C> {
319    }
320
321    public static class SyncCompletionRequired extends RuntimeException {
322    }
323
324}