package org.mctourney.autoreferee.util.commands;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.UnrecognizedOptionException;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrMatcher;
import org.apache.commons.lang.text.StrTokenizer;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.help.HelpTopic;
import org.bukkit.help.IndexHelpTopic;
import org.bukkit.plugin.java.JavaPlugin;
import org.mctourney.autoreferee.AutoRefMatch;
import org.mctourney.autoreferee.AutoReferee;
import org.mctourney.autoreferee.AutoRefMatch.Role;
import org.mctourney.autoreferee.util.commands.CommandManager.AutoRefCommandHelpTopic;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
public class CommandManager implements CommandExecutor, TabCompleter
{
Map<String, HandlerNode> cmap = Maps.newHashMap();
protected class CommandDelegator
{
public Object handler;
public Method method;
Options commandOptions;
AutoRefCommandHelpTopic helpTopic = null;
public CommandDelegator(Object handler, Method method, PluginCommand command)
{
this.method = method; this.handler = handler;
AutoRefCommand cAnnotation = method.getAnnotation(AutoRefCommand.class);
AutoRefPermission pAnnotation = method.getAnnotation(AutoRefPermission.class);
if (!cAnnotation.description().isEmpty())
helpTopic = new AutoRefCommandHelpTopic(cAnnotation, pAnnotation);
commandOptions = new Options();
char options[] = cAnnotation.options().toCharArray();
for (int i = 0; i < options.length; )
{
char arg = options[i++];
if (i < options.length)
{
if (options[i] == '*') { OptionBuilder.hasOptionalArg(); ++i; }
else if (options[i] == '+') { OptionBuilder.hasArg(); ++i; }
}
commandOptions.addOption(OptionBuilder.create(arg));
}
}
public boolean execute(CommandSender sender, AutoRefMatch match, String[] args)
throws CommandPermissionException, UnrecognizedOptionException
{
CommandLine cli = null;
try { cli = new GnuParser().parse(commandOptions, args); args = cli.getArgs(); }
catch (UnrecognizedOptionException e) { throw e; }
catch (ParseException e) { e.printStackTrace(); }
AutoRefCommand command = method.getAnnotation(AutoRefCommand.class);
AutoRefPermission permissions = method.getAnnotation(AutoRefPermission.class);
// perform the args cut at this point
args = (String[]) ArrayUtils.subarray(args, command.name().length - 1, args.length);
// check command permissions
checkPermissions(command, permissions, sender);
// if the number of arguments is incorrect, just return false
if (command.argmin() > args.length || command.argmax() < args.length) return false;
try { return ((Boolean) method.invoke(handler, sender, match, args, cli)).booleanValue(); }
catch (Exception e) { e.printStackTrace(); return false; }
}
}
public class AutoRefCommandHelpTopic extends HelpTopic
{
private AutoRefPermission permissions;
private AutoRefCommand command;
private boolean alias = false;
public AutoRefCommandHelpTopic(AutoRefCommand command, AutoRefPermission permissions)
{
this.command = command;
this.permissions = permissions;
this.name = "/" + StringUtils.join(command.name(), " ");
this.shortText = command.description();
this.setupFullText();
}
protected void setupFullText()
{
String usage = command.usage();
if ("".equals(usage)) usage = command.argmax() == 0
? "<command>" : "<command> [args?]";
this.fullText = command.description() + "\n"
+ "Usage: " + usage.replace("<command>", this.name);
String[] opts = command.opthelp();
if (opts.length > 0)
{
String opt = "Options:";
for (int i = 0; i < opts.length; i += 2) opt += String.format(
"\n%s -%s, %s", ChatColor.RESET.toString(), opts[i], opts[i+1]);
this.fullText += "\n" + opt;
}
}
@Override
public boolean canSee(CommandSender sender)
{
try { checkPermissions(this.command, this.permissions, sender); return true; }
catch (CommandPermissionException e) { return false; }
}
public boolean isAlias()
{ return alias; }
public AutoRefCommandHelpTopic copyAlias(String alias)
{
AutoRefCommandHelpTopic topic = new AutoRefCommandHelpTopic(this.command, this.permissions);
// modify the command to insert the Bukkit alias
String[] cmd = command.name().clone(); cmd[0] = alias;
topic.name = "/" + StringUtils.join(cmd, " ");
topic.alias = true;
topic.setupFullText();
return topic;
}
}
public void checkPermissions(AutoRefCommand command, AutoRefPermission permissions, CommandSender sender)
throws CommandPermissionException
{
if (sender instanceof ConsoleCommandSender && permissions != null && !permissions.console())
throw new CommandPermissionException(command, "Command not available from console");
if (sender instanceof Player)
{
Player player = (Player) sender;
AutoRefMatch match = AutoReferee.getInstance().getMatch(player.getWorld());
Role role = match == null ? AutoRefMatch.Role.NONE : match.getRole(player);
if (!role.atLeast(permissions.role()))
throw new CommandPermissionException(command, match == null
? "Command available only within an AutoReferee match"
: ("Command not available to " + role.toString().toLowerCase()));
for (String node : permissions.nodes()) if (!player.hasPermission(node))
throw new CommandPermissionException(command, "Insufficient permissions");
}
}
public void registerCommands(Object commands, JavaPlugin plugin)
{
for (Method method : commands.getClass().getDeclaredMethods())
{
AutoRefCommand command = method.getAnnotation(AutoRefCommand.class);
if (command == null || command.name().length == 0) continue;
PluginCommand pcommand = plugin.getCommand(command.name()[0]);
if (pcommand == null) throw new CommandRegistrationException(method, "Command not provided in plugin.yml");
if (method.getReturnType() != boolean.class)
throw new CommandRegistrationException(method, "Command method must return type boolean");
if (pcommand.getExecutor() != this) pcommand.setExecutor(this);
if (pcommand.getTabCompleter() != this) pcommand.setTabCompleter(this);
CommandDelegator delegator = new CommandDelegator(commands, method, pcommand);
this.setHandler(delegator, command.name());
}
}
public void generateHelp(JavaPlugin plugin)
{
List<HelpTopic> topics = Lists.newArrayList();
for (Map.Entry<String, HandlerNode> e : cmap.entrySet())
{
PluginCommand pcommand = Bukkit.getPluginCommand(e.getKey());
if (pcommand != null && plugin.equals(pcommand.getPlugin()))
{
HandlerNode handler = e.getValue();
if (handler != null) topics.addAll(handler.getHelpTopics(pcommand));
}
}
Iterator<HelpTopic> iter = topics.iterator();
for (iter = topics.iterator(); iter.hasNext(); )
{
HelpTopic topic = iter.next();
Bukkit.getHelpMap().addTopic(topic);
// remove the aliases from the list before we populate the index
if (topic instanceof AutoRefCommandHelpTopic &&
((AutoRefCommandHelpTopic) topic).isAlias()) iter.remove();
}
Collections.sort(topics, new Comparator<HelpTopic>()
{
@Override
public int compare(HelpTopic a, HelpTopic b)
{ return a.getName().compareToIgnoreCase(b.getName()); }
});
String stext = String.format("Below is a list of all %s commands:", plugin.getName());
Bukkit.getHelpMap().addTopic(new IndexHelpTopic(plugin.getName(), stext, "", topics));
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args)
{
AutoReferee plugin = AutoReferee.getInstance();
World world = plugin.getSenderWorld(sender);
AutoRefMatch match = plugin.getMatch(world);
// reparse the args properly using the string tokenizer from org.apache.commons
args = new StrTokenizer(StringUtils.join(args, ' '), StrMatcher.splitMatcher(),
StrMatcher.quoteMatcher()).setTrimmerMatcher(StrMatcher.trimMatcher()).getTokenArray();
try
{
HandlerNode node = cmap.get(command.getName());
List<String> ccmd = Lists.newArrayList(command.getName());
if (node == null) return false;
// attempt to narrow down the method using the args
for (String arg : args)
{
// lowercase to maintain case insensitivity
arg = arg.toLowerCase();
HandlerNode next = node.cmap.get(arg);
if (next == null) break;
// move on to the next node, increment the cut length
ccmd.add(arg); node = next;
}
String partialcmd = "/" + StringUtils.join(ccmd, " ");
if (node.handler != null)
{
// attempt to execute the handler, if false, print usage
if (node.handler.execute(sender, match, args)) return true;
AutoRefCommand cAnnotation = node.handler.method.getAnnotation(AutoRefCommand.class);
String usage = cAnnotation.usage();
if ("".equals(usage)) usage = cAnnotation.argmax() == 0
? "<command>" : "<command> [args?]";
// show the usage string with the partial command
sender.sendMessage(ChatColor.DARK_RED + usage.replace("<command>", partialcmd));
return true;
}
// show possible branches in the command tree
String options = node.cmap.size() < 5 ? StringUtils.join(node.cmap.keySet(), '|') : "[args]";
sender.sendMessage(ChatColor.DARK_RED + partialcmd + " " + options);
return true;
}
catch (CommandPermissionException e)
{ sender.sendMessage(ChatColor.DARK_GRAY + e.getMessage()); return true; }
catch (UnrecognizedOptionException e)
{ sender.sendMessage(ChatColor.DARK_GRAY + e.getMessage()); return true; }
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args)
{
try
{
HandlerNode node = cmap.get(command.getName());
if (node == null) return null;
// attempt to narrow down the method using the args
for (int i = 0; i < args.length - 1; ++i)
{
// lowercase to maintain case insensitivity
String arg = args[i].toLowerCase();
HandlerNode next = node.cmap.get(arg);
if (next == null) return null;
// move on to the next node, increment the cut length
node = next;
}
if (node.handler != null) return null;
String partial = args[args.length - 1].toLowerCase();
// show possible branches in the command tree
List<String> opts = Lists.newArrayList();
for (String c : node.cmap.keySet())
if (c.toLowerCase().startsWith(partial)) opts.add(c);
return opts;
}
catch (CommandPermissionException e)
{ sender.sendMessage(ChatColor.DARK_GRAY + e.getMessage()); }
return null;
}
private void setHandler(CommandDelegator handler, String[] cmd)
{
Map<String, HandlerNode> curr = cmap;
HandlerNode node = null;
// traverse handler tree, creating nodes if necessary
for (String c : cmd)
{
// lowercase to maintain case insensitivity
c = c.toLowerCase();
if ((node = curr.get(c)) == null)
curr.put(c, node = new HandlerNode(null));
curr = node.cmap;
}
// set the appropriate handler at this node
node.handler = handler;
}
}
class HandlerNode
{
protected Map<String, HandlerNode> cmap;
protected CommandManager.CommandDelegator handler;
public HandlerNode(CommandManager.CommandDelegator handler)
{
this.handler = handler;
this.cmap = Maps.newHashMap();
}
protected List<AutoRefCommandHelpTopic> getHelpTopics(PluginCommand pcmd)
{
List<AutoRefCommandHelpTopic> topics = Lists.newArrayList();
if (handler != null && handler.helpTopic != null)
{
for (String alias : pcmd.getAliases())
topics.add(handler.helpTopic.copyAlias(alias));
topics.add(handler.helpTopic);
}
for (HandlerNode child : cmap.values())
topics.addAll(child.getHelpTopics(pcmd));
return topics;
}
}