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