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.arguments.standard;
025
026import cloud.commandframework.ArgumentDescription;
027import cloud.commandframework.arguments.CommandArgument;
028import cloud.commandframework.arguments.parser.ArgumentParseResult;
029import cloud.commandframework.arguments.parser.ArgumentParser;
030import cloud.commandframework.context.CommandContext;
031import cloud.commandframework.exceptions.parsing.NoInputProvidedException;
032import cloud.commandframework.exceptions.parsing.NumberParseException;
033import org.checkerframework.checker.nullness.qual.NonNull;
034import org.checkerframework.checker.nullness.qual.Nullable;
035
036import java.util.Collections;
037import java.util.LinkedList;
038import java.util.List;
039import java.util.Queue;
040import java.util.Set;
041import java.util.TreeSet;
042import java.util.function.BiFunction;
043
044@SuppressWarnings("unused")
045public final class IntegerArgument<C> extends CommandArgument<C, Integer> {
046
047    private static final int MAX_SUGGESTIONS_INCREMENT = 10;
048    private static final int NUMBER_SHIFT_MULTIPLIER = 10;
049
050    private final int min;
051    private final int max;
052
053    private IntegerArgument(
054            final boolean required,
055            final @NonNull String name,
056            final int min,
057            final int max,
058            final String defaultValue,
059            final @Nullable BiFunction<@NonNull CommandContext<C>, @NonNull String,
060                    @NonNull List<@NonNull String>> suggestionsProvider,
061            final @NonNull ArgumentDescription defaultDescription
062    ) {
063        super(required, name, new IntegerParser<>(min, max), defaultValue, Integer.class, suggestionsProvider, defaultDescription);
064        this.min = min;
065        this.max = max;
066    }
067
068    /**
069     * Create a new builder
070     *
071     * @param name Name of the argument
072     * @param <C>  Command sender type
073     * @return Created builder
074     */
075    public static <C> @NonNull Builder<C> newBuilder(final @NonNull String name) {
076        return new Builder<>(name);
077    }
078
079    /**
080     * Create a new required command argument
081     *
082     * @param name Argument name
083     * @param <C>  Command sender type
084     * @return Created argument
085     */
086    public static <C> @NonNull CommandArgument<C, Integer> of(final @NonNull String name) {
087        return IntegerArgument.<C>newBuilder(name).asRequired().build();
088    }
089
090    /**
091     * Create a new optional command argument
092     *
093     * @param name Argument name
094     * @param <C>  Command sender type
095     * @return Created argument
096     */
097    public static <C> @NonNull CommandArgument<C, Integer> optional(final @NonNull String name) {
098        return IntegerArgument.<C>newBuilder(name).asOptional().build();
099    }
100
101    /**
102     * Create a new required command argument with a default value
103     *
104     * @param name       Argument name
105     * @param defaultNum Default num
106     * @param <C>        Command sender type
107     * @return Created argument
108     */
109    public static <C> @NonNull CommandArgument<C, Integer> optional(
110            final @NonNull String name,
111            final int defaultNum
112    ) {
113        return IntegerArgument.<C>newBuilder(name).asOptionalWithDefault(Integer.toString(defaultNum)).build();
114    }
115
116    /**
117     * Get the minimum accepted integer that could have been parsed
118     *
119     * @return Minimum integer
120     */
121    public int getMin() {
122        return this.min;
123    }
124
125    /**
126     * Get the maximum accepted integer that could have been parsed
127     *
128     * @return Maximum integer
129     */
130    public int getMax() {
131        return this.max;
132    }
133
134    public static final class Builder<C> extends CommandArgument.Builder<C, Integer> {
135
136        private int min = Integer.MIN_VALUE;
137        private int max = Integer.MAX_VALUE;
138
139        private Builder(final @NonNull String name) {
140            super(Integer.class, name);
141        }
142
143        /**
144         * Set a minimum value
145         *
146         * @param min Minimum value
147         * @return Builder instance
148         */
149        public @NonNull Builder<C> withMin(final int min) {
150            this.min = min;
151            return this;
152        }
153
154        /**
155         * Set a maximum value
156         *
157         * @param max Maximum value
158         * @return Builder instance
159         */
160        public @NonNull Builder<C> withMax(final int max) {
161            this.max = max;
162            return this;
163        }
164
165        /**
166         * Builder a new integer argument
167         *
168         * @return Constructed argument
169         */
170        @Override
171        public @NonNull IntegerArgument<C> build() {
172            return new IntegerArgument<>(this.isRequired(), this.getName(), this.min, this.max,
173                    this.getDefaultValue(), this.getSuggestionsProvider(), this.getDefaultDescription()
174            );
175        }
176
177    }
178
179    public static final class IntegerParser<C> implements ArgumentParser<C, Integer> {
180
181        private final int min;
182        private final int max;
183
184        /**
185         * Construct a new integer parser
186         *
187         * @param min Minimum acceptable value
188         * @param max Maximum acceptable value
189         */
190        public IntegerParser(final int min, final int max) {
191            this.min = min;
192            this.max = max;
193        }
194
195        /**
196         * Get integer suggestions. This supports both positive and negative numbers
197         *
198         * @param min   Minimum value
199         * @param max   Maximum value
200         * @param input Input
201         * @return List of suggestions
202         */
203        @SuppressWarnings("MixedMutabilityReturnType")
204        public static @NonNull List<@NonNull String> getSuggestions(
205                final long min,
206                final long max,
207                final @NonNull String input
208        ) {
209            final Set<Long> numbers = new TreeSet<>();
210
211            try {
212                final long inputNum = Long.parseLong(input.equals("-") ? "-0" : input.isEmpty() ? "0" : input);
213                final long inputNumAbsolute = Math.abs(inputNum);
214
215                numbers.add(inputNumAbsolute); /* It's a valid number, so we suggest it */
216                for (int i = 0; i < MAX_SUGGESTIONS_INCREMENT
217                        && (inputNum * NUMBER_SHIFT_MULTIPLIER) + i <= max; i++) {
218                    numbers.add((inputNumAbsolute * NUMBER_SHIFT_MULTIPLIER) + i);
219                }
220
221                final List<String> suggestions = new LinkedList<>();
222                for (long number : numbers) {
223                    if (input.startsWith("-")) {
224                        number = -number; /* Preserve sign */
225                    }
226                    if (number < min || number > max) {
227                        continue;
228                    }
229                    suggestions.add(String.valueOf(number));
230                }
231
232                return suggestions;
233            } catch (final Exception ignored) {
234                return Collections.emptyList();
235            }
236        }
237
238        @Override
239        public @NonNull ArgumentParseResult<Integer> parse(
240                final @NonNull CommandContext<C> commandContext,
241                final @NonNull Queue<@NonNull String> inputQueue
242        ) {
243            final String input = inputQueue.peek();
244            if (input == null) {
245                return ArgumentParseResult.failure(new NoInputProvidedException(
246                        IntegerParser.class,
247                        commandContext
248                ));
249            }
250            try {
251                final int value = Integer.parseInt(input);
252                if (value < this.min || value > this.max) {
253                    return ArgumentParseResult.failure(new IntegerParseException(
254                            input,
255                            this.min,
256                            this.max,
257                            commandContext
258                    ));
259                }
260                inputQueue.remove();
261                return ArgumentParseResult.success(value);
262            } catch (final Exception e) {
263                return ArgumentParseResult.failure(new IntegerParseException(
264                        input,
265                        this.min,
266                        this.max,
267                        commandContext
268                ));
269            }
270        }
271
272        /**
273         * Get the minimum value accepted by this parser
274         *
275         * @return Min value
276         */
277        public int getMin() {
278            return this.min;
279        }
280
281        /**
282         * Get the maximum value accepted by this parser
283         *
284         * @return Max value
285         */
286        public int getMax() {
287            return this.max;
288        }
289
290        @Override
291        public boolean isContextFree() {
292            return true;
293        }
294
295        @Override
296        public @NonNull List<@NonNull String> suggestions(
297                final @NonNull CommandContext<C> commandContext,
298                final @NonNull String input
299        ) {
300            return getSuggestions(this.min, this.max, input);
301        }
302
303    }
304
305
306    public static final class IntegerParseException extends NumberParseException {
307
308        private static final long serialVersionUID = -6933923056628373853L;
309
310        /**
311         * Construct a new integer parse exception
312         *
313         * @param input          String input
314         * @param min            Minimum value
315         * @param max            Maximum value
316         * @param commandContext Command context
317         */
318        public IntegerParseException(
319                final @NonNull String input,
320                final int min,
321                final int max,
322                final @NonNull CommandContext<?> commandContext
323        ) {
324            super(
325                    input,
326                    min,
327                    max,
328                    IntegerParser.class,
329                    commandContext
330            );
331        }
332
333        @Override
334        public boolean hasMin() {
335            return this.getMin().intValue() != Integer.MIN_VALUE;
336        }
337
338        @Override
339        public boolean hasMax() {
340            return this.getMax().intValue() != Integer.MAX_VALUE;
341        }
342
343        @Override
344        public @NonNull String getNumberType() {
345            return "integer";
346        }
347
348    }
349
350}