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 com.google.common.collect.SetMultimap;
027
028import java.util.ArrayList;
029import java.util.Comparator;
030import java.util.HashSet;
031import java.util.Iterator;
032import java.util.List;
033import java.util.Set;
034import java.util.regex.Pattern;
035import java.util.stream.Collectors;
036
037@SuppressWarnings("WeakerAccess")
038public class CommandHelp {
039    private final CommandManager manager;
040    private final CommandIssuer issuer;
041    private final List<HelpEntry> helpEntries = new ArrayList<>();
042    private final String commandName;
043    final String commandPrefix;
044    private int page = 1;
045    private int perPage;
046    List<String> search;
047    private Set<HelpEntry> selectedEntry = new HashSet<>();
048    private int totalResults;
049    private int totalPages;
050    private boolean lastPage;
051
052    public CommandHelp(CommandManager manager, RootCommand rootCommand, CommandIssuer issuer) {
053        this.manager = manager;
054        this.issuer = issuer;
055        this.perPage = manager.defaultHelpPerPage;
056        this.commandPrefix = manager.getCommandPrefix(issuer);
057        this.commandName = rootCommand.getCommandName();
058
059
060        SetMultimap<String, RegisteredCommand> subCommands = rootCommand.getSubCommands();
061        Set<RegisteredCommand> seen = new HashSet<>();
062
063        if (!rootCommand.getDefCommand().hasHelpCommand) {
064            RegisteredCommand defCommand = rootCommand.getDefaultRegisteredCommand();
065            if (defCommand != null) {
066                helpEntries.add(new HelpEntry(this, defCommand));
067                seen.add(defCommand);
068            }
069        }
070
071        subCommands.entries().forEach(e -> {
072            String key = e.getKey();
073            if (key.equals(BaseCommand.DEFAULT) || key.equals(BaseCommand.CATCHUNKNOWN)) {
074                return;
075            }
076
077            RegisteredCommand regCommand = e.getValue();
078
079            if (!regCommand.isPrivate && regCommand.hasPermission(issuer) && !seen.contains(regCommand)) {
080                this.helpEntries.add(new HelpEntry(this, regCommand));
081                seen.add(regCommand);
082            }
083        });
084    }
085
086    @UnstableAPI // Not sure on this one yet even when API becomes unstable
087    protected void updateSearchScore(HelpEntry help) {
088        if (this.search == null || this.search.isEmpty()) {
089            help.setSearchScore(1);
090            return;
091        }
092        final RegisteredCommand<?> cmd = help.getRegisteredCommand();
093
094        int searchScore = 0;
095        for (String word : this.search) {
096            Pattern pattern = Pattern.compile(".*" + Pattern.quote(word) + ".*", Pattern.CASE_INSENSITIVE);
097            for (String subCmd : cmd.registeredSubcommands) {
098                Pattern subCmdPattern = Pattern.compile(".*" + Pattern.quote(subCmd) + ".*", Pattern.CASE_INSENSITIVE);
099                if (pattern.matcher(subCmd).matches()) {
100                    searchScore += 3;
101                } else if (subCmdPattern.matcher(word).matches()) {
102                    searchScore++;
103                }
104            }
105
106
107            if (pattern.matcher(help.getDescription()).matches()) {
108                searchScore += 2;
109            }
110            if (pattern.matcher(help.getParameterSyntax(issuer)).matches()) {
111                searchScore++;
112            }
113            if (help.getSearchTags() != null && pattern.matcher(help.getSearchTags()).matches()) {
114                searchScore += 2;
115            }
116        }
117        help.setSearchScore(searchScore);
118    }
119
120    public CommandManager getManager() {
121        return manager;
122    }
123
124    public boolean testExactMatch(String command) {
125        selectedEntry.clear();
126        for (HelpEntry helpEntry : helpEntries) {
127            if (helpEntry.getCommand().endsWith(" " + command)) {
128                selectedEntry.add(helpEntry);
129            }
130        }
131        return !selectedEntry.isEmpty();
132    }
133
134    public void showHelp() {
135        showHelp(issuer);
136    }
137
138    public void showHelp(CommandIssuer issuer) {
139        CommandHelpFormatter formatter = manager.getHelpFormatter();
140        if (!selectedEntry.isEmpty()) {
141            HelpEntry first = ACFUtil.getFirstElement(selectedEntry);
142            formatter.printDetailedHelpHeader(this, issuer, first);
143
144            for (HelpEntry helpEntry : selectedEntry) {
145                formatter.showDetailedHelp(this, helpEntry);
146            }
147
148            formatter.printDetailedHelpFooter(this, issuer, first);
149            return;
150        }
151
152        List<HelpEntry> helpEntries = getHelpEntries().stream().filter(HelpEntry::shouldShow).collect(Collectors.toList());
153        Iterator<HelpEntry> results = helpEntries.stream()
154                .sorted(Comparator.comparingInt(helpEntry -> helpEntry.getSearchScore() * -1)).iterator();
155        if (!results.hasNext()) {
156            issuer.sendMessage(MessageType.ERROR, MessageKeys.NO_COMMAND_MATCHED_SEARCH, "{search}", ACFUtil.join(this.search, " "));
157            helpEntries = getHelpEntries();
158            results = helpEntries.iterator();
159        }
160        this.totalResults = helpEntries.size();
161        int min = (this.page - 1) * this.perPage; // TODO: per page configurable?
162        int max = min + this.perPage;
163        this.totalPages = (int) Math.ceil((float) totalResults / (float) this.perPage);
164        int i = 0;
165        if (min >= totalResults) {
166            issuer.sendMessage(MessageType.HELP, MessageKeys.HELP_NO_RESULTS);
167            return;
168        }
169
170        List<HelpEntry> printEntries = new ArrayList<>();
171        while (results.hasNext()) {
172            HelpEntry e = results.next();
173            if (i >= max) {
174                break;
175            }
176            if (i++ < min) {
177                continue;
178            }
179            printEntries.add(e);
180        }
181        this.lastPage = max >= totalResults;
182
183        if (search == null) {
184            formatter.showAllResults(this, printEntries);
185        } else {
186            formatter.showSearchResults(this, printEntries);
187        }
188
189    }
190
191    public List<HelpEntry> getHelpEntries() {
192        return helpEntries;
193    }
194
195    public void setPerPage(int perPage) {
196        this.perPage = perPage;
197    }
198
199    public void setPage(int page) {
200        this.page = page;
201    }
202
203    public void setPage(int page, int perPage) {
204        this.setPage(page);
205        this.setPerPage(perPage);
206    }
207
208    public void setSearch(List<String> search) {
209        this.search = search;
210        getHelpEntries().forEach(this::updateSearchScore);
211    }
212
213    public CommandIssuer getIssuer() {
214        return issuer;
215    }
216
217    public String getCommandName() {
218        return commandName;
219    }
220
221    public String getCommandPrefix() {
222        return commandPrefix;
223    }
224
225    public int getPage() {
226        return page;
227    }
228
229    public int getPerPage() {
230        return perPage;
231    }
232
233    public List<String> getSearch() {
234        return search;
235    }
236
237    public Set<HelpEntry> getSelectedEntry() {
238        return selectedEntry;
239    }
240
241    public int getTotalResults() {
242        return totalResults;
243    }
244
245    public int getTotalPages() {
246        return totalPages;
247    }
248
249    public boolean isOnlyPage() {
250        return this.page == 1 && lastPage;
251    }
252
253    public boolean isLastPage() {
254        return lastPage;
255    }
256}