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 org.jetbrains.annotations.NotNull;
027
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.function.Supplier;
036import java.util.stream.Collectors;
037import java.util.stream.IntStream;
038
039
040@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
041public class CommandCompletions<C extends CommandCompletionContext> {
042    private final CommandManager manager;
043    private Map<String, CommandCompletionHandler> completionMap = new HashMap<>();
044    private Map<Class, String> defaultCompletions = new HashMap<>();
045
046    public CommandCompletions(CommandManager manager) {
047        this.manager = manager;
048        registerAsyncCompletion("nothing", c -> Collections.emptyList());
049        registerAsyncCompletion("range", (c) -> {
050            String config = c.getConfig();
051            if (config == null) {
052                return Collections.emptyList();
053            }
054            final String[] ranges = ACFPatterns.DASH.split(config);
055            int start;
056            int end;
057            if (ranges.length != 2) {
058                start = 0;
059                end = ACFUtil.parseInt(ranges[0], 0);
060            } else {
061                start = ACFUtil.parseInt(ranges[0], 0);
062                end = ACFUtil.parseInt(ranges[1], 0);
063            }
064            return IntStream.rangeClosed(start, end).mapToObj(Integer::toString).collect(Collectors.toList());
065        });
066        List<String> timeunits = Arrays.asList("minutes", "hours", "days", "weeks", "months", "years");
067        registerAsyncCompletion("timeunits", (c) -> timeunits);
068    }
069
070    /**
071     * Registr a completion handler to provide command completions based on the user input.
072     *
073     * @param id
074     * @param handler
075     * @return
076     */
077    public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler<C> handler) {
078        return this.completionMap.put("@" + id.toLowerCase(), handler);
079    }
080
081    /**
082     * Registr a completion handler to provide command completions based on the user input.
083     * This handler is declared to be safe to be executed asynchronously.
084     * <p>
085     * Not all platforms support this, so if the platform does not support asynchronous execution,
086     * your handler will be executed on the main thread.
087     * <p>
088     * Use this anytime your handler does not need to access state that is not considered thread safe.
089     * <p>
090     * Use context.isAsync() to determine if you are async or not.
091     *
092     * @param id
093     * @param handler
094     * @return
095     */
096    public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler<C> handler) {
097        return this.completionMap.put("@" + id.toLowerCase(), handler);
098    }
099
100    /**
101     * Register a static list of command completions that will never change.
102     * Like @CommandCompletion, values are | (PIPE) separated.
103     * <p>
104     * Example: foo|bar|baz
105     *
106     * @param id
107     * @param list
108     * @return
109     */
110    public CommandCompletionHandler registerStaticCompletion(String id, String list) {
111        return registerStaticCompletion(id, ACFPatterns.PIPE.split(list));
112    }
113
114    /**
115     * Register a static list of command completions that will never change
116     *
117     * @param id
118     * @param completions
119     * @return
120     */
121    public CommandCompletionHandler registerStaticCompletion(String id, String[] completions) {
122        return registerStaticCompletion(id, Arrays.asList(completions));
123    }
124
125    /**
126     * Register a static list of command completions that will never change. The list is obtained from the supplier
127     * immediately as part of this method call.
128     *
129     * @param id
130     * @param supplier
131     * @return
132     */
133    public CommandCompletionHandler registerStaticCompletion(String id, Supplier<Collection<String>> supplier) {
134        return registerStaticCompletion(id, supplier.get());
135    }
136
137    /**
138     * Register a static list of command completions that will never change
139     *
140     * @param id
141     * @param completions
142     * @return
143     */
144    public CommandCompletionHandler registerStaticCompletion(String id, Collection<String> completions) {
145        return registerAsyncCompletion(id, x -> completions);
146    }
147
148    /**
149     * @param id
150     * @param classes
151     * @return
152     * @deprecated Feature Not done yet
153     */
154    CommandCompletionHandler setDefaultCompletion(String id, Class... classes) {
155        // get completion with specified id
156        id = id.toLowerCase();
157        CommandCompletionHandler completion = completionMap.get(id);
158
159        if (completion == null) {
160            // Throw something because no completion with specified id
161            throw new IllegalStateException("Completion not registered for " + id);
162        }
163
164        for (Class clazz : classes) {
165            defaultCompletions.put(clazz, id);
166        }
167
168        return completion;
169    }
170
171    @NotNull
172    List<String> of(RegisteredCommand cmd, CommandIssuer sender, String[] args, boolean isAsync) {
173        String[] completions = ACFPatterns.SPACE.split(cmd.complete);
174        final int argIndex = args.length - 1;
175
176        String input = args[argIndex];
177
178        String completion = argIndex < completions.length ? completions[argIndex] : null;
179        if (completion == null && completions.length > 0) {
180            completion = completions[completions.length - 1];
181        }
182        if (completion == null) {
183            return Collections.singletonList(input);
184        }
185
186        return getCompletionValues(cmd, sender, completion, args, isAsync);
187    }
188
189    List<String> getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) {
190        completion = manager.getCommandReplacements().replace(completion);
191
192        List<String> allCompletions = new ArrayList<>();
193        String input = args.length > 0 ? args[args.length - 1] : "";
194
195        for (String value : ACFPatterns.PIPE.split(completion)) {
196            String[] complete = ACFPatterns.COLONEQUALS.split(value, 2);
197            CommandCompletionHandler handler = this.completionMap.get(complete[0].toLowerCase());
198            if (handler != null) {
199                if (isAsync && !(handler instanceof AsyncCommandCompletionHandler)) {
200                    ACFUtil.sneaky(new SyncCompletionRequired());
201                    return null;
202                }
203                String config = complete.length == 1 ? null : complete[1];
204                CommandCompletionContext context = manager.createCompletionContext(command, sender, input, config, args);
205
206                try {
207                    //noinspection unchecked
208                    Collection<String> completions = handler.getCompletions(context);
209                    if (completions != null) {
210                        allCompletions.addAll(completions);
211                        continue;
212                    }
213                    //noinspection ConstantIfStatement,ConstantConditions
214                    if (false) { // Hack to fool compiler. since its sneakily thrown.
215                        throw new CommandCompletionTextLookupException();
216                    }
217                } catch (CommandCompletionTextLookupException ignored) {
218                    // This should only happen if some other feedback error occured.
219                } catch (Exception e) {
220                    command.handleException(sender, Arrays.asList(args), e);
221                }
222                // Something went wrong in lookup, fall back to input
223                return Collections.singletonList(input);
224            } else {
225                // Plaintext value
226                allCompletions.add(value);
227            }
228        }
229        return allCompletions;
230    }
231
232    public interface CommandCompletionHandler<C extends CommandCompletionContext> {
233        Collection<String> getCompletions(C context) throws InvalidCommandArgument;
234    }
235
236    public interface AsyncCommandCompletionHandler<C extends CommandCompletionContext> extends CommandCompletionHandler<C> {
237    }
238
239    public static class SyncCompletionRequired extends RuntimeException {
240    }
241
242}