001//
002// MIT License
003//
004// Copyright (c) 2021 Alexander Söderberg & Contributors
005//
006// Permission is hereby granted, free of charge, to any person obtaining a copy
007// of this software and associated documentation files (the "Software"), to deal
008// in the Software without restriction, including without limitation the rights
009// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010// copies of the Software, and to permit persons to whom the Software is
011// furnished to do so, subject to the following conditions:
012//
013// The above copyright notice and this permission notice shall be included in all
014// copies or substantial portions of the Software.
015//
016// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022// SOFTWARE.
023//
024package cloud.commandframework.context;
025
026import cloud.commandframework.CommandManager;
027import cloud.commandframework.annotations.AnnotationAccessor;
028import cloud.commandframework.arguments.CommandArgument;
029import cloud.commandframework.arguments.flags.FlagContext;
030import cloud.commandframework.captions.Caption;
031import cloud.commandframework.captions.CaptionRegistry;
032import cloud.commandframework.captions.CaptionVariable;
033import cloud.commandframework.captions.CaptionVariableReplacementHandler;
034import cloud.commandframework.captions.SimpleCaptionVariableReplacementHandler;
035import cloud.commandframework.keys.CloudKey;
036import cloud.commandframework.keys.CloudKeyHolder;
037import cloud.commandframework.keys.SimpleCloudKey;
038import org.checkerframework.checker.nullness.qual.NonNull;
039import org.checkerframework.checker.nullness.qual.Nullable;
040
041import java.util.Collections;
042import java.util.HashMap;
043import java.util.LinkedList;
044import java.util.Map;
045import java.util.Optional;
046import java.util.function.Supplier;
047
048/**
049 * Command context used to assist in the parsing of commands
050 *
051 * @param <C> Command sender type
052 */
053public final class CommandContext<C> {
054
055    private final CaptionVariableReplacementHandler captionVariableReplacementHandler =
056            new SimpleCaptionVariableReplacementHandler();
057    private final Map<CommandArgument<C, ?>, ArgumentTiming> argumentTimings = new HashMap<>();
058    private final FlagContext flagContext = FlagContext.create();
059    private final Map<CloudKey<?>, Object> internalStorage = new HashMap<>();
060    private final C commandSender;
061    private final boolean suggestions;
062    private final CaptionRegistry<C> captionRegistry;
063    private final CommandManager<C> commandManager;
064
065    private CommandArgument<C, ?> currentArgument = null;
066
067    /**
068     * Create a new command context instance
069     *
070     * @param commandSender   Sender of the command
071     * @param captionRegistry Caption registry
072     * @deprecated Provide a command manager instead of a caption registry
073     */
074    @Deprecated
075    public CommandContext(final @NonNull C commandSender, final @NonNull CaptionRegistry<C> captionRegistry) {
076        this(false, commandSender, captionRegistry);
077    }
078
079    /**
080     * Create a new command context instance
081     *
082     * @param commandSender  Sender of the command
083     * @param commandManager Command manager
084     * @since 1.3.0
085     */
086    public CommandContext(final @NonNull C commandSender, final @NonNull CommandManager<C> commandManager) {
087        this(false, commandSender, commandManager);
088    }
089
090    /**
091     * Create a new command context instance
092     *
093     * @param suggestions     Whether or not the context is created for command suggestions
094     * @param commandSender   Sender of the command
095     * @param captionRegistry Caption registry
096     * @deprecated Provide a command manager instead of a caption registry
097     */
098    @Deprecated
099    public CommandContext(
100            final boolean suggestions,
101            final @NonNull C commandSender,
102            final @NonNull CaptionRegistry<C> captionRegistry
103    ) {
104        this.commandSender = commandSender;
105        this.suggestions = suggestions;
106        this.captionRegistry = captionRegistry;
107        this.commandManager = null;
108    }
109
110    /**
111     * Create a new command context instance
112     *
113     * @param suggestions    Whether or not the context is created for command suggestions
114     * @param commandSender  Sender of the command
115     * @param commandManager Command manager
116     * @since 1.3.0
117     */
118    public CommandContext(
119            final boolean suggestions,
120            final @NonNull C commandSender,
121            final @NonNull CommandManager<C> commandManager
122    ) {
123        this.commandSender = commandSender;
124        this.suggestions = suggestions;
125        this.commandManager = commandManager;
126        this.captionRegistry = commandManager.getCaptionRegistry();
127    }
128
129    /**
130     * Format a caption
131     *
132     * @param caption   Caption key
133     * @param variables Replacements
134     * @return Formatted message
135     */
136    public @NonNull String formatMessage(
137            final @NonNull Caption caption,
138            final @NonNull CaptionVariable... variables
139    ) {
140        return this.captionVariableReplacementHandler.replaceVariables(
141                this.captionRegistry.getCaption(caption, this.commandSender),
142                variables
143        );
144    }
145
146    /**
147     * Get the sender that executed the command
148     *
149     * @return Command sender
150     */
151    public @NonNull C getSender() {
152        return this.commandSender;
153    }
154
155    /**
156     * Check if this context was created for tab completion purposes
157     *
158     * @return {@code true} if this context is requesting suggestions, else {@code false}
159     */
160    public boolean isSuggestions() {
161        return this.suggestions;
162    }
163
164    /**
165     * Store a value in the context map. This will overwrite any existing
166     * value stored with the same key
167     *
168     * @param key   Key
169     * @param value Value
170     * @param <T>   Value type
171     */
172    public <T> void store(final @NonNull String key, final @NonNull T value) {
173        this.internalStorage.put(SimpleCloudKey.of(key), value);
174    }
175
176    /**
177     * Store a value in the context map. This will overwrite any existing
178     * value stored with the same key
179     *
180     * @param key   Key
181     * @param value Value
182     * @param <T>   Value type
183     */
184    public <T> void store(final @NonNull CloudKey<T> key, final @NonNull T value) {
185        this.internalStorage.put(key, value);
186    }
187
188    /**
189     * Store a value in the context map. This will overwrite any existing
190     * value stored with the same key
191     *
192     * @param keyHolder Holder of the identifying key
193     * @param value     Value
194     * @param <T>       Value type
195     */
196    public <T> void store(final @NonNull CommandArgument<C, T> keyHolder, final @NonNull T value) {
197        this.store((CloudKeyHolder<T>) keyHolder, value);
198    }
199
200    /**
201     * Store a value in the context map. This will overwrite any existing
202     * value stored with the same key
203     *
204     * @param keyHolder Holder of the identifying key
205     * @param value     Value
206     * @param <T>       Value type
207     * @since 1.4.0
208     */
209    public <T> void store(final @NonNull CloudKeyHolder<T> keyHolder, final @NonNull T value) {
210        this.internalStorage.put(keyHolder.getKey(), value);
211    }
212
213    /**
214     * Store or remove a value in the context map. This will overwrite any existing
215     * value stored with the same key.
216     * <p>
217     * If the provided value is {@code null}, any current value stored for the provided key will be removed.
218     *
219     * @param key   Key
220     * @param value Value
221     * @param <T>   Value type
222     * @since 1.3.0
223     */
224    public <T> void set(final @NonNull String key, final @Nullable T value) {
225        if (value != null) {
226            this.store(key, value);
227        } else {
228            this.remove(key);
229        }
230    }
231
232    /**
233     * Store or remove a value in the context map. This will overwrite any existing
234     * value stored with the same key.
235     * <p>
236     * If the provided value is {@code null}, any current value stored for the provided key will be removed.
237     *
238     * @param key   Key
239     * @param value Value
240     * @param <T>   Value type
241     * @since 1.4.0
242     */
243    public <T> void set(final @NonNull CloudKey<T> key, final @Nullable T value) {
244        if (value != null) {
245            this.store(key, value);
246        } else {
247            this.remove(key);
248        }
249    }
250
251    /**
252     * Check if the context has a value stored for a key
253     *
254     * @param key Key
255     * @return Whether the context has a value for the provided key
256     * @since 1.3.0
257     */
258    public boolean contains(final @NonNull String key) {
259        return this.contains(SimpleCloudKey.of(key));
260    }
261
262    /**
263     * Check if the context has a value stored for a key
264     *
265     * @param key Key
266     * @return Whether the context has a value for the provided key
267     * @since 1.4.0
268     */
269    public boolean contains(final @NonNull CloudKey<?> key) {
270        return this.internalStorage.containsKey(key);
271    }
272
273    /**
274     * Get the current state of this command context as a map of String to context value.
275     *
276     * @return An immutable copy of this command context as a map
277     * @since 1.3.0
278     */
279    public @NonNull Map<@NonNull String, @Nullable ?> asMap() {
280        final Map<String, Object> values = new HashMap<>();
281        this.internalStorage.forEach((key, value) -> values.put(key.getName(), value));
282        return Collections.unmodifiableMap(values);
283    }
284
285    /**
286     * Get a value from its key. Will return {@link Optional#empty()}
287     * if no value is stored with the given key
288     *
289     * @param key Key
290     * @param <T> Value type
291     * @return Value
292     */
293    public <T> @NonNull Optional<T> getOptional(final @NonNull String key) {
294        final Object value = this.internalStorage.get(SimpleCloudKey.of(key));
295        if (value != null) {
296            @SuppressWarnings("unchecked") final T castedValue = (T) value;
297            return Optional.of(castedValue);
298        } else {
299            return Optional.empty();
300        }
301    }
302
303    /**
304     * Get a value from its key. Will return {@link Optional#empty()}
305     * if no value is stored with the given key
306     *
307     * @param key Key
308     * @param <T> Value type
309     * @return Value
310     * @since 1.4.0
311     */
312    public <T> @NonNull Optional<T> getOptional(final @NonNull CloudKey<T> key) {
313        final Object value = this.internalStorage.get(key);
314        if (value != null) {
315            @SuppressWarnings("unchecked") final T castedValue = (T) value;
316            return Optional.of(castedValue);
317        } else {
318            return Optional.empty();
319        }
320    }
321
322    /**
323     * Get a value from its key. Will return {@link Optional#empty()}
324     * if no value is stored with the given key
325     *
326     * @param keyHolder Holder of the key
327     * @param <T>       Value type
328     * @return Value
329     */
330    @SuppressWarnings("unused")
331    public <T> @NonNull Optional<T> getOptional(final @NonNull CommandArgument<C, T> keyHolder) {
332        return this.getOptional((CloudKeyHolder<T>) keyHolder);
333    }
334
335    /**
336     * Get a value from its key. Will return {@link Optional#empty()}
337     * if no value is stored with the given key
338     *
339     * @param keyHolder Holder of the key
340     * @param <T>       Value type
341     * @return Value
342     * @since 1.4.0
343     */
344    @SuppressWarnings("unused")
345    public <T> @NonNull Optional<T> getOptional(final @NonNull CloudKeyHolder<T> keyHolder) {
346        final Object value = this.internalStorage.get(keyHolder.getKey());
347        if (value != null) {
348            @SuppressWarnings("unchecked") final T castedValue = (T) value;
349            return Optional.of(castedValue);
350        } else {
351            return Optional.empty();
352        }
353    }
354
355    /**
356     * Remove a stored value from the context
357     *
358     * @param key Key to remove
359     */
360    public void remove(final @NonNull String key) {
361        this.remove(SimpleCloudKey.of(key));
362    }
363
364    /**
365     * Remove a stored value from the context
366     *
367     * @param key Key to remove
368     * @since 1.4.0
369     */
370    public void remove(final @NonNull CloudKey<?> key) {
371        this.internalStorage.remove(key);
372    }
373
374    /**
375     * Get a required argument from the context. This will thrown an exception
376     * if there's no value associated with the given key
377     *
378     * @param key Argument key
379     * @param <T> Argument type
380     * @return Argument
381     * @throws NullPointerException If no such argument is stored
382     */
383    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
384    public <T> @NonNull T get(final @NonNull String key) {
385        final Object value = this.internalStorage.get(SimpleCloudKey.of(key));
386        if (value == null) {
387            throw new NullPointerException("No such object stored in the context: " + key);
388        }
389        return (T) value;
390    }
391
392    /**
393     * Get a required argument from the context. This will thrown an exception
394     * if there's no value associated with the given key
395     *
396     * @param key Argument key
397     * @param <T> Argument type
398     * @return Argument
399     * @throws NullPointerException If no such argument is stored
400     * @since 1.4.0
401     */
402    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
403    public <T> @NonNull T get(final @NonNull CloudKey<T> key) {
404        final Object value = this.internalStorage.get(key);
405        if (value == null) {
406            throw new NullPointerException("No such object stored in the context: " + key);
407        }
408        return (T) value;
409    }
410
411    /**
412     * Get a required argument from the context. This will thrown an exception
413     * if there's no value associated with the given argument
414     *
415     * @param keyHolder Holder of the identifying key
416     * @param <T>      Argument type
417     * @return Stored value
418     * @throws NullPointerException If no such value is stored
419     */
420    public <T> @NonNull T get(final @NonNull CommandArgument<C, T> keyHolder) {
421        return this.get(keyHolder.getKey());
422    }
423
424    /**
425     * Get a required argument from the context. This will thrown an exception
426     * if there's no value associated with the given argument
427     *
428     * @param keyHolder Holder of the identifying key
429     * @param <T>      Argument type
430     * @return Stored value
431     * @throws NullPointerException If no such value is stored
432     * @since 1.4.0
433     */
434    public <T> @NonNull T get(final @NonNull CloudKeyHolder<T> keyHolder) {
435        return this.get(keyHolder.getKey());
436    }
437
438    /**
439     * Get a value if it exists, else return the provided default value
440     *
441     * @param argument     Argument
442     * @param defaultValue Default value
443     * @param <T>          Argument type
444     * @return Stored value, or supplied default value
445     */
446    public <T> @Nullable T getOrDefault(
447            final @NonNull CommandArgument<C, T> argument,
448            final @Nullable T defaultValue
449    ) {
450        return this.<T>getOptional(argument.getName()).orElse(defaultValue);
451    }
452
453    /**
454     * Get a value if it exists, else return the provided default value
455     *
456     * @param key          Argument key
457     * @param defaultValue Default value
458     * @param <T>          Argument type
459     * @return Argument, or supplied default value
460     */
461    public <T> @Nullable T getOrDefault(
462            final @NonNull String key,
463            final @Nullable T defaultValue
464    ) {
465        return this.<T>getOptional(key).orElse(defaultValue);
466    }
467
468    /**
469     * Get a value if it exists, else return the provided default value
470     *
471     * @param key          Argument key
472     * @param defaultValue Default value
473     * @param <T>          Argument type
474     * @return Argument, or supplied default value
475     * @since 1.4.0
476     */
477    public <T> @Nullable T getOrDefault(
478            final @NonNull CloudKey<T> key,
479            final @Nullable T defaultValue
480    ) {
481        return this.getOptional(key).orElse(defaultValue);
482    }
483
484    /**
485     * Get a value if it exists, else return the value supplied by the given supplier
486     *
487     * @param key             Argument key
488     * @param defaultSupplier Supplier of default value
489     * @param <T>             Argument type
490     * @return Argument, or supplied default value
491     * @since 1.2.0
492     */
493    public <T> @Nullable T getOrSupplyDefault(
494            final @NonNull String key,
495            final @NonNull Supplier<@Nullable T> defaultSupplier
496    ) {
497        return this.<T>getOptional(key).orElseGet(defaultSupplier);
498    }
499
500    /**
501     * Get a value if it exists, else return the value supplied by the given supplier
502     *
503     * @param key             Argument key
504     * @param defaultSupplier Supplier of default value
505     * @param <T>             Argument type
506     * @return Argument, or supplied default value
507     * @since 1.4.0
508     */
509    public <T> @Nullable T getOrSupplyDefault(
510            final @NonNull CloudKey<T> key,
511            final @NonNull Supplier<@Nullable T> defaultSupplier
512    ) {
513        return this.getOptional(key).orElseGet(defaultSupplier);
514    }
515
516    /**
517     * Get the raw input. This should only be used when {@link #isSuggestions()} is {@code true}
518     *
519     * @return Raw input in token form
520     */
521    public @NonNull LinkedList<@NonNull String> getRawInput() {
522        return this.getOrDefault("__raw_input__", new LinkedList<>());
523    }
524
525    /**
526     * Get the raw input as a joined string
527     *
528     * @return {@link #getRawInput()} joined with {@code " "} as the delimiter
529     * @since 1.1.0
530     */
531    public @NonNull String getRawInputJoined() {
532        return String.join(" ", this.getRawInput());
533    }
534
535    /**
536     * Create an argument timing for a specific argument
537     *
538     * @param argument Argument
539     * @return Created timing instance
540     */
541    public @NonNull ArgumentTiming createTiming(final @NonNull CommandArgument<C, ?> argument) {
542        final ArgumentTiming argumentTiming = new ArgumentTiming();
543        this.argumentTimings.put(argument, argumentTiming);
544        return argumentTiming;
545    }
546
547    /**
548     * Get an immutable view of the argument timings map
549     *
550     * @return Argument timings
551     */
552    public @NonNull Map<CommandArgument<@NonNull C, @NonNull ?>, ArgumentTiming> getArgumentTimings() {
553        return Collections.unmodifiableMap(this.argumentTimings);
554    }
555
556    /**
557     * Get the associated {@link FlagContext} instance
558     *
559     * @return Flag context
560     */
561    public @NonNull FlagContext flags() {
562        return this.flagContext;
563    }
564
565    /**
566     * Get the argument that is currently being parsed for this command context.
567     * This value will be updated whenever the context is used to provide new
568     * suggestions or parse a new command argument
569     *
570     * @return Currently parsing {@link CommandArgument} or {@code null}
571     * @since 1.2.0
572     */
573    public @Nullable CommandArgument<C, ?> getCurrentArgument() {
574        return this.currentArgument;
575    }
576
577    /**
578     * Set the argument that is currently being parsed for this command context.
579     * This value should be updated whenever the context is used to provide new
580     * suggestions or parse a new command argument
581     *
582     * @param argument Currently parsing {@link CommandArgument} or {@code null}
583     * @since 1.2.0
584     */
585    public void setCurrentArgument(final @Nullable CommandArgument<C, ?> argument) {
586        this.currentArgument = argument;
587    }
588
589    /**
590     * Attempt to retrieve a value that has been registered to the associated command manager's
591     * {@link cloud.commandframework.annotations.injection.ParameterInjectorRegistry}
592     *
593     * @param clazz Class of type to inject
594     * @param <T>   Type to inject
595     * @return Optional that may contain the created value
596     * @since 1.3.0
597     */
598    @SuppressWarnings("unchecked")
599    public <@NonNull T> @NonNull Optional<T> inject(final @NonNull Class<T> clazz) {
600        if (this.commandManager == null) {
601            throw new UnsupportedOperationException(
602                    "Cannot retrieve injectable values from a command context that is not associated with a command manager"
603            );
604        }
605        return this.commandManager.parameterInjectorRegistry().getInjectable(clazz, this, AnnotationAccessor.empty());
606    }
607
608
609    /**
610     * Used to track performance metrics related to command parsing. This is attached
611     * to the command context, as this depends on the command context that is being
612     * parsed.
613     * <p>
614     * The times are measured in nanoseconds.
615     */
616    public static final class ArgumentTiming {
617
618        private long start;
619        private long end;
620        private boolean success;
621
622        /**
623         * Created a new argument timing instance
624         *
625         * @param start   Start time (in nanoseconds)
626         * @param end     End time (in nanoseconds)
627         * @param success Whether or not the argument was parsed successfully
628         */
629        public ArgumentTiming(final long start, final long end, final boolean success) {
630            this.start = start;
631            this.end = end;
632            this.success = success;
633        }
634
635        /**
636         * Created a new argument timing instance without an end time
637         *
638         * @param start Start time (in nanoseconds)
639         */
640        @SuppressWarnings("unused")
641        public ArgumentTiming(final long start) {
642            this(start, -1, false);
643        }
644
645        /**
646         * Created a new argument timing instance
647         */
648        public ArgumentTiming() {
649            this(-1, -1, false);
650        }
651
652        /**
653         * Get the elapsed time
654         *
655         * @return Elapsed time (in nanoseconds)
656         */
657        public long getElapsedTime() {
658            if (this.end == -1) {
659                throw new IllegalStateException("No end time has been registered");
660            } else if (this.start == -1) {
661                throw new IllegalStateException("No start time has been registered");
662            }
663            return this.end - this.start;
664        }
665
666        /**
667         * Set the end time
668         *
669         * @param end     End time (in nanoseconds)
670         * @param success Whether or not the argument was parsed successfully
671         */
672        public void setEnd(final long end, final boolean success) {
673            this.end = end;
674            this.success = success;
675        }
676
677        /**
678         * Set the start time
679         *
680         * @param start Start time (in nanoseconds)
681         */
682        public void setStart(final long start) {
683            this.start = start;
684        }
685
686        /**
687         * Check whether or not the value was parsed successfully
688         *
689         * @return {@code true} if the value was parsed successfully, {@code false} if not
690         */
691        public boolean wasSuccess() {
692            return this.success;
693        }
694
695    }
696
697}