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.command;
020
021import com.plotsquared.core.configuration.caption.Caption;
022import com.plotsquared.core.configuration.caption.CaptionHolder;
023import com.plotsquared.core.configuration.caption.StaticCaption;
024import com.plotsquared.core.configuration.caption.TranslatableCaption;
025import com.plotsquared.core.permissions.PermissionHolder;
026import com.plotsquared.core.player.PlotPlayer;
027import com.plotsquared.core.util.MathMan;
028import com.plotsquared.core.util.StringComparison;
029import com.plotsquared.core.util.StringMan;
030import com.plotsquared.core.util.task.RunnableVal2;
031import com.plotsquared.core.util.task.RunnableVal3;
032import net.kyori.adventure.text.minimessage.MiniMessage;
033import net.kyori.adventure.text.minimessage.Template;
034import org.checkerframework.checker.nullness.qual.Nullable;
035
036import java.lang.reflect.InvocationTargetException;
037import java.lang.reflect.Method;
038import java.util.ArrayList;
039import java.util.Arrays;
040import java.util.Collection;
041import java.util.Collections;
042import java.util.HashMap;
043import java.util.HashSet;
044import java.util.List;
045import java.util.Map;
046import java.util.Set;
047import java.util.concurrent.CompletableFuture;
048
049public abstract class Command {
050
051    static final MiniMessage MINI_MESSAGE = MiniMessage.builder().build();
052
053    // May be none
054    private final ArrayList<Command> allCommands = new ArrayList<>();
055    private final ArrayList<Command> dynamicCommands = new ArrayList<>();
056    private final HashMap<String, Command> staticCommands = new HashMap<>();
057
058    // Parent command (may be null)
059    private final Command parent;
060    private final boolean isStatic;
061    // The command ID
062    private String id;
063    private List<String> aliases;
064    private RequiredType required;
065    private String usage;
066    private Caption description;
067    private String permission;
068    private boolean confirmation;
069    private CommandCategory category;
070    private Argument<?>[] arguments;
071
072    public Command(
073            Command parent, boolean isStatic, String id, String permission,
074            RequiredType required, CommandCategory category
075    ) {
076        this.parent = parent;
077        this.isStatic = isStatic;
078        this.id = id;
079        this.permission = permission;
080        this.required = required;
081        this.category = category;
082        this.aliases = Collections.singletonList(id);
083        if (this.parent != null) {
084            this.parent.register(this);
085        }
086    }
087
088    public Command(Command parent, boolean isStatic) {
089        this.parent = parent;
090        this.isStatic = isStatic;
091        CommandDeclaration cdAnnotation = getClass().getAnnotation(CommandDeclaration.class);
092        if (cdAnnotation != null) {
093            init(cdAnnotation);
094        }
095        for (final Method method : getClass().getDeclaredMethods()) {
096            if (method.isAnnotationPresent(CommandDeclaration.class)) {
097                Class<?>[] types = method.getParameterTypes();
098                // final PlotPlayer<?> player, String[] args, RunnableVal3<Command,Runnable,Runnable> confirm, RunnableVal2<Command, CommandResult>
099                // whenDone
100                if (types.length == 5 && types[0] == Command.class && types[1] == PlotPlayer.class
101                        && types[2] == String[].class && types[3] == RunnableVal3.class
102                        && types[4] == RunnableVal2.class) {
103                    Command tmp = new Command(this, true) {
104                        @Override
105                        public CompletableFuture<Boolean> execute(
106                                PlotPlayer<?> player, String[] args,
107                                RunnableVal3<Command, Runnable, Runnable> confirm,
108                                RunnableVal2<Command, CommandResult> whenDone
109                        ) {
110                            try {
111                                method.invoke(Command.this, this, player, args, confirm, whenDone);
112                                return CompletableFuture.completedFuture(true);
113                            } catch (IllegalAccessException | InvocationTargetException e) {
114                                e.printStackTrace();
115                            }
116                            return CompletableFuture.completedFuture(false);
117                        }
118                    };
119                    tmp.init(method.getAnnotation(CommandDeclaration.class));
120                }
121            }
122        }
123    }
124
125    public Command getParent() {
126        return this.parent;
127    }
128
129    public String getId() {
130        return this.id;
131    }
132
133    public String getFullId() {
134        if (this.parent != null && this.parent.getParent() != null) {
135            return this.parent.getFullId() + "." + this.id;
136        }
137        return this.id;
138    }
139
140    public List<Command> getCommands(PlotPlayer<?> player) {
141        List<Command> commands = new ArrayList<>();
142        for (Command cmd : this.allCommands) {
143            if (cmd.canExecute(player, false)) {
144                commands.add(cmd);
145            }
146        }
147        return commands;
148    }
149
150    public List<Command> getCommands(CommandCategory category, PlotPlayer<?> player) {
151        List<Command> commands = getCommands(player);
152        if (category != null) {
153            commands.removeIf(command -> command.category != category);
154        }
155        return commands;
156    }
157
158    public List<Command> getCommands() {
159        return this.allCommands;
160    }
161
162    public boolean hasConfirmation(PermissionHolder player) {
163        return this.confirmation && !player.hasPermission(getPermission() + ".confirm.bypass");
164    }
165
166    public List<String> getAliases() {
167        return this.aliases;
168    }
169
170    public Caption getDescription() {
171        return this.description;
172    }
173
174    public RequiredType getRequiredType() {
175        return this.required;
176    }
177
178    public Argument<?>[] getRequiredArguments() {
179        return this.arguments;
180    }
181
182    public void setRequiredArguments(Argument<?>[] arguments) {
183        this.arguments = arguments;
184    }
185
186    public void init(CommandDeclaration declaration) {
187        this.id = declaration.command();
188        this.permission = declaration.permission();
189        this.required = declaration.requiredType();
190        this.category = declaration.category();
191
192        List<String> aliasOptions = new ArrayList<>();
193        aliasOptions.add(this.id);
194        aliasOptions.addAll(Arrays.asList(declaration.aliases()));
195
196        this.aliases = aliasOptions;
197        if (declaration.description().isEmpty()) {
198            Command parent = getParent();
199            // we're collecting the "path" of the command
200            List<String> path = new ArrayList<>();
201            path.add(this.id);
202            while (parent != null && !parent.equals(MainCommand.getInstance())) {
203                path.add(parent.getId());
204                parent = parent.getParent();
205            }
206            Collections.reverse(path);
207            String descriptionKey = String.join(".", path);
208            this.description = TranslatableCaption.of(String.format("commands.description.%s", descriptionKey));
209        } else {
210            this.description = StaticCaption.of(declaration.description());
211        }
212        this.usage = declaration.usage();
213        this.confirmation = declaration.confirmation();
214
215        if (this.parent != null) {
216            this.parent.register(this);
217        }
218    }
219
220    public void register(Command command) {
221        if (command.isStatic) {
222            for (String alias : command.aliases) {
223                this.staticCommands.put(alias.toLowerCase(), command);
224            }
225        } else {
226            this.dynamicCommands.add(command);
227        }
228        this.allCommands.add(command);
229    }
230
231    public String getPermission() {
232        if (this.permission != null && !this.permission.isEmpty()) {
233            return this.permission;
234        }
235        if (this.parent == null) {
236            return "plots.use";
237        }
238        return "plots." + getFullId();
239    }
240
241    public <T> void paginate(
242            PlotPlayer<?> player, List<T> c, int size, int page,
243            RunnableVal3<Integer, T, CaptionHolder> add, String baseCommand, Caption header
244    ) {
245        // Calculate pages & index
246        if (page < 0) {
247            page = 0;
248        }
249        int totalPages = (int) Math.floor((double) c.size() / size);
250        if (page > totalPages) {
251            page = totalPages;
252        }
253        int max = page * size + size;
254        if (max > c.size()) {
255            max = c.size();
256        }
257        // Send the header
258        Template curTemplate = Template.of("cur", String.valueOf(page + 1));
259        Template maxTemplate = Template.of("max", String.valueOf(totalPages + 1));
260        Template amountTemplate = Template.of("amount", String.valueOf(c.size()));
261        player.sendMessage(header, curTemplate, maxTemplate, amountTemplate);
262        // Send the page content
263        List<T> subList = c.subList(page * size, max);
264        int i = page * size;
265        for (T obj : subList) {
266            i++;
267            final CaptionHolder msg = new CaptionHolder();
268            add.run(i, obj, msg);
269            player.sendMessage(msg.get(), msg.getTemplates());
270        }
271        // Send the footer
272        Template command1 = Template.of("command1", baseCommand + " " + page);
273        Template command2 = Template.of("command2", baseCommand + " " + (page + 2));
274        Template clickable = Template.of("clickable", TranslatableCaption.of("list.clickable").getComponent(player));
275        player.sendMessage(TranslatableCaption.of("list.page_turn"), command1, command2, clickable);
276    }
277
278    /**
279     * @param player   Caller
280     * @param args     Arguments
281     * @param confirm  Instance, Success, Failure
282     * @param whenDone task to run when done
283     * @return CompletableFuture {@code true} if the command executed fully, {@code false} in
284     *         any other case
285     */
286    public CompletableFuture<Boolean> execute(
287            PlotPlayer<?> player, String[] args,
288            RunnableVal3<Command, Runnable, Runnable> confirm,
289            RunnableVal2<Command, CommandResult> whenDone
290    ) throws CommandException {
291        if (args.length == 0 || args[0] == null) {
292            if (this.parent == null) {
293                MainCommand.getInstance().help.displayHelp(player, null, 0);
294            } else {
295                sendUsage(player);
296            }
297            return CompletableFuture.completedFuture(false);
298        }
299        if (this.allCommands.isEmpty()) {
300            player.sendMessage(
301                    StaticCaption.of("Not Implemented: https://github.com/IntellectualSites/PlotSquared/issues"));
302            return CompletableFuture.completedFuture(false);
303        }
304        Command cmd = getCommand(args[0]);
305        if (cmd == null) {
306            if (this.parent != null) {
307                sendUsage(player);
308                return CompletableFuture.completedFuture(false);
309            }
310            // Help command
311            try {
312                if (!MathMan.isInteger(args[0])) {
313                    CommandCategory.valueOf(args[0].toUpperCase());
314                }
315                // This will default certain syntax to the help command
316                // e.g. /plot, /plot 1, /plot claiming
317                MainCommand.getInstance().help.execute(player, args, null, null);
318                return CompletableFuture.completedFuture(false);
319            } catch (IllegalArgumentException ignored) {
320            }
321            // Command recommendation
322            player.sendMessage(TranslatableCaption.of("commandconfig.not_valid_subcommand"));
323            List<Command> commands = getCommands(player);
324            if (commands.isEmpty()) {
325                player.sendMessage(
326                        TranslatableCaption.of("commandconfig.did_you_mean"),
327                        Template.of("value", MainCommand.getInstance().help.getUsage())
328                );
329                return CompletableFuture.completedFuture(false);
330            }
331            HashSet<String> setArgs = new HashSet<>(args.length);
332            for (String arg : args) {
333                setArgs.add(arg.toLowerCase());
334            }
335            String[] allArgs = setArgs.toArray(new String[0]);
336            int best = 0;
337            for (Command current : commands) {
338                int match = getMatch(allArgs, current, player);
339                if (match > best) {
340                    cmd = current;
341                }
342            }
343            if (cmd == null) {
344                cmd = new StringComparison<>(args[0], this.allCommands).getMatchObject();
345            }
346            player.sendMessage(
347                    TranslatableCaption.of("commandconfig.did_you_mean"),
348                    Template.of("value", cmd.getUsage())
349            );
350            return CompletableFuture.completedFuture(false);
351        }
352        String[] newArgs = Arrays.copyOfRange(args, 1, args.length);
353        if (!cmd.checkArgs(player, newArgs) || !cmd.canExecute(player, true)) {
354            return CompletableFuture.completedFuture(false);
355        }
356        try {
357            cmd.execute(player, newArgs, confirm, whenDone);
358        } catch (CommandException e) {
359            e.perform(player);
360        }
361        return CompletableFuture.completedFuture(true);
362    }
363
364    public boolean checkArgs(PlotPlayer<?> player, String[] args) {
365        Argument<?>[] reqArgs = getRequiredArguments();
366        if (reqArgs != null && reqArgs.length > 0) {
367            boolean failed = args.length < reqArgs.length;
368            String[] baseSplit = getCommandString().split(" ");
369            String[] fullSplit = getUsage().split(" ");
370            if (fullSplit.length - baseSplit.length < reqArgs.length) {
371                String[] tmp = new String[baseSplit.length + reqArgs.length];
372                System.arraycopy(fullSplit, 0, tmp, 0, fullSplit.length);
373                fullSplit = tmp;
374            }
375            for (int i = 0; i < reqArgs.length; i++) {
376                fullSplit[i + baseSplit.length] = reqArgs[i].getExample().toString();
377                failed = failed || reqArgs[i].parse(args[i]) == null;
378            }
379            if (failed) {
380                // TODO improve or remove the Argument system
381                player.sendMessage(
382                        TranslatableCaption.of("commandconfig.command_syntax"),
383                        Template.of("value", StringMan.join(fullSplit, " "))
384                );
385                return false;
386            }
387        }
388        return true;
389    }
390
391    public int getMatch(String[] args, Command cmd, PlotPlayer<?> player) {
392        String perm = cmd.getPermission();
393        int count = cmd.getAliases().stream().filter(alias -> alias.startsWith(args[0]))
394                .mapToInt(alias -> 5).sum();
395        HashSet<String> desc = new HashSet<>();
396        Collections.addAll(desc, cmd.getDescription().getComponent(player).split(" "));
397        for (String arg : args) {
398            if (perm.startsWith(arg)) {
399                count++;
400            }
401            if (desc.contains(arg)) {
402                count++;
403            }
404        }
405        String[] usage = cmd.getUsage().split(" ");
406        for (int i = 0; i < Math.min(4, usage.length); i++) {
407            int require;
408            if (usage[i].startsWith("<")) {
409                require = 1;
410            } else {
411                require = 0;
412            }
413            String[] split = usage[i].split("\\|| |\\>|\\<|\\[|\\]|\\{|\\}|\\_|\\/");
414            for (String aSplit : split) {
415                for (String arg : args) {
416                    if (arg.equalsIgnoreCase(aSplit)) {
417                        count += 5 - i + require;
418                    }
419                }
420            }
421        }
422        count += StringMan.intersection(desc, args);
423        return count;
424    }
425
426    public Command getCommand(String arg) {
427        Command cmd = this.staticCommands.get(arg.toLowerCase());
428        if (cmd == null) {
429            for (Command command : this.dynamicCommands) {
430                if (command.matches(arg)) {
431                    return command;
432                }
433            }
434        }
435        return cmd;
436    }
437
438    public Command getCommand(Class<?> clazz) {
439        for (Command cmd : this.allCommands) {
440            if (cmd.getClass() == clazz) {
441                return cmd;
442            }
443        }
444        return null;
445    }
446
447    public Command getCommandById(String id) {
448        Command exact = this.staticCommands.get(id);
449        if (exact != null) {
450            return exact;
451        }
452        for (Command cmd : this.allCommands) {
453            if (cmd.getId().equals(id)) {
454                return cmd;
455            }
456        }
457        return null;
458    }
459
460    public boolean canExecute(PlotPlayer<?> player, boolean message) {
461        if (player == null) {
462            return true;
463        }
464        if (!this.required.allows(player)) {
465            if (message) {
466                player.sendMessage(this.required.getErrorMessage());
467            }
468        } else if (!player.hasPermission(getPermission())) {
469            if (message) {
470                player.sendMessage(
471                        TranslatableCaption.of("permission.no_permission"),
472                        Template.of("node", getPermission())
473                );
474            }
475        } else {
476            return true;
477        }
478        return false;
479    }
480
481    public boolean matches(String arg) {
482        arg = arg.toLowerCase();
483        return StringMan.isEqual(arg, this.id) || this.aliases.contains(arg);
484    }
485
486    public String getCommandString() {
487        if (this.parent == null) {
488            return "/" + toString();
489        } else {
490            return this.parent.getCommandString() + " " + toString();
491        }
492    }
493
494    public void sendUsage(PlotPlayer<?> player) {
495        player.sendMessage(
496                TranslatableCaption.of("commandconfig.command_syntax"),
497                Template.of("value", getUsage())
498        );
499    }
500
501    public String getUsage() {
502        if (this.usage != null && !this.usage.isEmpty()) {
503            if (this.usage.startsWith("/")) {
504                return this.usage;
505            }
506            return getCommandString() + " " + this.usage;
507        }
508        if (this.allCommands.isEmpty()) {
509            return getCommandString();
510        }
511        StringBuilder args = new StringBuilder("[");
512        String prefix = "";
513        for (Command cmd : this.allCommands) {
514            args.append(prefix).append(cmd.isStatic ? cmd.toString() : "<" + cmd + ">");
515            prefix = "|";
516        }
517        return getCommandString() + " " + args + "]";
518    }
519
520    public Collection<Command> tabOf(
521            PlotPlayer<?> player, String[] input, boolean space,
522            String... args
523    ) {
524        if (!space) {
525            return null;
526        }
527        List<Command> result = new ArrayList<>();
528        int index = input.length;
529        for (String arg : args) {
530            arg = arg.replace(getCommandString() + " ", "");
531            String[] split = arg.split(" ");
532            if (split.length <= index) {
533                continue;
534            }
535            arg = StringMan.join(Arrays.copyOfRange(split, index, split.length), " ");
536            Command cmd = new Command(null, false, arg, getPermission(), getRequiredType(), null) {
537            };
538            result.add(cmd);
539        }
540        return result;
541    }
542
543    public Collection<Command> tab(PlotPlayer<?> player, String[] args, boolean space) {
544        switch (args.length) {
545            case 0:
546                return this.allCommands;
547            case 1:
548                String arg = args[0].toLowerCase();
549                if (space) {
550                    Command cmd = getCommand(arg);
551                    if (cmd != null && cmd.canExecute(player, false)) {
552                        return cmd.tab(player, Arrays.copyOfRange(args, 1, args.length), space);
553                    } else {
554                        return null;
555                    }
556                } else {
557                    Set<Command> commands = new HashSet<>();
558                    for (Map.Entry<String, Command> entry : this.staticCommands.entrySet()) {
559                        if (entry.getKey().startsWith(arg) && entry.getValue()
560                                .canExecute(player, false)) {
561                            commands.add(entry.getValue());
562                        }
563                    }
564                    return commands;
565                }
566            default:
567                Command cmd = getCommand(args[0]);
568                if (cmd != null) {
569                    return cmd.tab(player, Arrays.copyOfRange(args, 1, args.length), space);
570                } else {
571                    return null;
572                }
573        }
574    }
575
576    @Override
577    public String toString() {
578        return !this.aliases.isEmpty() ? this.aliases.get(0) : this.id;
579    }
580
581    @Override
582    public boolean equals(Object obj) {
583        if (this == obj) {
584            return true;
585        }
586        if (getClass() != obj.getClass()) {
587            return false;
588        }
589        Command other = (Command) obj;
590        if (this.hashCode() != other.hashCode()) {
591            return false;
592        }
593        return this.getFullId().equals(other.getFullId());
594    }
595
596    @Override
597    public int hashCode() {
598        return this.getFullId().hashCode();
599    }
600
601    public void checkTrue(boolean mustBeTrue, Caption message, Template... args) {
602        if (!mustBeTrue) {
603            throw new CommandException(message, args);
604        }
605    }
606
607    public <T> T check(T object, Caption message, Template... args) {
608        if (object == null) {
609            throw new CommandException(message, args);
610        }
611        return object;
612    }
613
614
615    public enum CommandResult {
616        FAILURE,
617        SUCCESS
618    }
619
620
621    public static class CommandException extends RuntimeException {
622
623        private final Template[] args;
624        private final Caption message;
625
626        public CommandException(final @Nullable Caption message, final Template... args) {
627            this.message = message;
628            this.args = args;
629        }
630
631        public void perform(final @Nullable PlotPlayer<?> player) {
632            if (player != null && message != null) {
633                player.sendMessage(message, args);
634            }
635        }
636
637    }
638
639}