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