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