content: package com.comphenix.protocol;
  
  import java.util.ArrayList;
  import java.util.Deque;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Set;
  
  import javax.script.Invocable;
  import javax.script.ScriptEngine;
  import javax.script.ScriptEngineManager;
  import javax.script.ScriptException;
  
  import org.bukkit.ChatColor;
  import org.bukkit.command.CommandSender;
  import org.bukkit.conversations.Conversable;
  import org.bukkit.conversations.Conversation;
  import org.bukkit.conversations.ConversationAbandonedEvent;
  import org.bukkit.conversations.ConversationAbandonedListener;
  import org.bukkit.conversations.ConversationContext;
  import org.bukkit.conversations.ConversationFactory;
  import org.bukkit.plugin.Plugin;
  
  import com.comphenix.protocol.MultipleLinesPrompt.MultipleConversationCanceller;
  import com.comphenix.protocol.error.ErrorReporter;
  import com.comphenix.protocol.error.Report;
  import com.comphenix.protocol.error.ReportType;
  import com.comphenix.protocol.events.PacketEvent;
  
  /**
   * A command to apply JavaScript filtering to the packet command.
   * 
   * @author Kristian
   */
  public class CommandFilter extends CommandBase {
  	public static final ReportType REPORT_FALLBACK_ENGINE = new ReportType("Falling back to the Rhino engine.");
  	public static final ReportType REPORT_CANNOT_LOAD_FALLBACK_ENGINE = new ReportType("Could not load Rhino either. Please upgrade your JVM or OS.");
  	public static final ReportType REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE = new ReportType("Unable to initialize packages for JavaScript engine.");
  	public static final ReportType REPORT_FILTER_REMOVED_FOR_ERROR = new ReportType("Removing filter %s for causing %s.");
  	public static final ReportType REPORT_CANNOT_HANDLE_CONVERSATION = new ReportType("Cannot handle conversation.");
  	
  	public interface FilterFailedHandler{
  		/**
  		 * Invoked when a given filter has failed.
  		 * @param event - the packet event.
  		 * @param filter - the filter that failed.
  		 * @param ex - the failure.
  		 * @return TRUE to keep processing this filter, FALSE to remove it.
  		 */
  		public boolean handle(PacketEvent event, Filter filter, Exception ex);
  	}
  	
  	/**
  	 * Possible sub commands.
  	 * 
  	 * @author Kristian
  	 */
  	private enum SubCommand {
  		ADD, REMOVE;
  	}
  	
  	/**
  	 * A filter that will be used to process a packet event.
  	 * @author Kristian
  	 */
  	public static class Filter {
  		private final String name;
  		private final String predicate;
  		
  		private final Set<PacketType> packets;
  		
  		/**
  		 * Construct a new immutable filter.
  		 * @param name - the unique name of the filter.
   		 * @param predicate - the JavaScript predicate that will be used to filter packet events.
  		 * @param packets - a list of packet types this filter applies to.
  		 */
  		public Filter(String name, String predicate, Set<PacketType> packets) {
  			this.name = name;
  			this.predicate = predicate;
  			this.packets = new HashSet<>(packets);
  		}
  		
  		/**
  		 * Retrieve the unique name of the filter.
  		 * @return Unique name of the filter.
  		 */
  		public String getName() {
  			return name;
  		}
  		
  		/**
  		 * Retrieve the JavaScript predicate that will be used to filter packet events.
  		 * @return Predicate itself.
  		 */
  		public String getPredicate() {
  			return predicate;
  		}
  		
  		/**
  		 * Retrieve a copy of the set of packets this filter applies to.
  		 * @return Set of packets this filter applies to.
  		 */
  		public Set<PacketType> getRanges() {
  			return new HashSet<>(packets);
  		}
  		
  		/**
  		 * Determine whether or not a packet event needs to be passed to this filter.
  		 * @param event - the event to test.
  		 * @return TRUE if it does, FALSE otherwise.
  		 */
  		private boolean isApplicable(PacketEvent event) {
  			return packets.contains(event.getPacketType());
  		}
  		
  		/**
  		 * Evaluate the current filter using the provided ScriptEngine as context.
  		 * <p>
  		 * This context may be modified with additional code.
  		 * @param context - the current script context.
  		 * @param event - the packet event to evaluate.
  		 * @return TRUE to pass this packet event on to the debug listeners, FALSE otherwise.
  		 * @throws ScriptException If the compilation failed or the filter is not valid.
  		 */
  		public boolean evaluate(ScriptEngine context, PacketEvent event) throws ScriptException {
  			if (!isApplicable(event))
  				return true;
  			// Ensure that the predicate has been compiled
  			compile(context);
  			
  			try {
  				Object result = ((Invocable) context).invokeFunction(name, event, event.getPacket().getHandle());
  				
  				if (result instanceof Boolean)
  					return (Boolean) result;
  				else
  					throw new ScriptException("Filter result wasn't a boolean: " + result);
  				
  			} catch (NoSuchMethodException e) {
  				// Must be a fault with the script engine itself
  				throw new IllegalStateException("Unable to compile " + name + " into current script engine.", e);
  			}
  		}
  		
  		/**
  		 * Force the compilation of a specific filter.
  		 * @param context - the current script context.
  		 * @throws ScriptException If the compilation failed.
  		 */
  		public void compile(ScriptEngine context) throws ScriptException {
  			if (context.get(name) == null) {
  				context.eval("var " + name + " = function(event, packet) {\n" + predicate);
  			}
  		}
  		
  		/**
  		 * Clean up all associated code from this filter in the provided script engine.
  		 * @param context - the current script context.
  		 */
  		public void close(ScriptEngine context) {
  			context.put(name, null);
  		}
  	}
  	
  	private class CompilationSuccessCanceller implements MultipleConversationCanceller {
  		@Override
  		public boolean cancelBasedOnInput(ConversationContext context, String in) {
  			throw new UnsupportedOperationException("Cannot cancel on the last line alone.");
  		}
  
  		@Override
  		public void setConversation(Conversation conversation) {
  			// Ignore
  		}
  
  		@Override
  		public boolean cancelBasedOnInput(ConversationContext context, String currentLine, StringBuilder lines, int lineCount) {
  			try {
  				engine.eval("function(event, packet) {\n" + lines.toString());
  				
  				// It compiles - accept the filter!
  				return true;
  			} catch (ScriptException e) {
  				// We also have the function() line
  				int realLineCount = lineCount + 1;
  				
  				// Only possible to recover from an error on the last line.
  				return e.getLineNumber() < realLineCount;
  			}
  		}
  		
  		@Override
  		public CompilationSuccessCanceller clone() {
  			return new CompilationSuccessCanceller();
  		}
  	}
  	
  	/**
  	 * Name of this command.
  	 */
  	public static final String NAME = "filter";
  	
  	// Default error handler
  	private FilterFailedHandler defaultFailedHandler;
  	
  	// Currently registered filters
  	private List<Filter> filters = new ArrayList<Filter>();
  	
  	// Owner plugin
  	private final Plugin plugin;
  	
  	// Whether or not the command is enabled
  	private ProtocolConfig config;
  	
  	// Script engine
  	private ScriptEngine engine;
  	private boolean uninitialized;
  	
  	public CommandFilter(ErrorReporter reporter, Plugin plugin, ProtocolConfig config) {
  		super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 2);
  		this.plugin = plugin;
  		this.config = config;
  		
  		// Tell the filter system to initialize the script first chance it gets
  		this.uninitialized = true;
  	}
  	
  	private void initalizeScript() {
  		try {
  			// First attempt
  			initializeEngine();
  			
  			// Oh for ..
  			if (!isInitialized()) {
  				throw new ScriptException("A JavaScript engine could not be found.");
  			} else {
  				plugin.getLogger().info("Loaded command filter engine.");
  			}
  		} catch (ScriptException e1) {
  			// It's not a huge deal
  			printPackageWarning(e1);
  			
  			if (!config.getScriptEngineName().equals("rhino")) {
  				reporter.reportWarning(this, Report.newBuilder(REPORT_FALLBACK_ENGINE));
  				config.setScriptEngineName("rhino");
  				config.saveAll();
  				
  				try {
  					initializeEngine();
  					
  					if (!isInitialized()) {
  						reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_FALLBACK_ENGINE));
  					}
  				} catch (ScriptException e2) {
  					// And again ..
  					printPackageWarning(e2);
  				}
  			}
  		}
  	}
  	
  	private void printPackageWarning(ScriptException e) {
  		reporter.reportWarning(this, Report.newBuilder(REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE).error(e));
  	}
  	
  	/**
  	 * Initialize the current configured engine.
  	 * @throws ScriptException If we are unable to import packages.
  	 */
  	private void initializeEngine() throws ScriptException {
  		ScriptEngineManager manager = new ScriptEngineManager();
  		engine = manager.getEngineByName(config.getScriptEngineName());
  		
  		// Import useful packages
  		if (engine != null) {
  			engine.eval("importPackage(org.bukkit);");
  			engine.eval("importPackage(com.comphenix.protocol.reflect);");
  		}
  	}
  	
  	/**
  	 * Determine if the filter engine has been successfully initialized.
  	 * @return TRUE if it has, FALSE otherwise.
  	 */
  	public boolean isInitialized() {
  		return engine != null;
  	}
  
  	private FilterFailedHandler getDefaultErrorHandler() {
  		// No need to create a new object every time
  		if (defaultFailedHandler == null) {
  			defaultFailedHandler = new FilterFailedHandler() {
  				@Override
  				public boolean handle(PacketEvent event, Filter filter, Exception ex) {
  					reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event);
  					reporter.reportWarning(this,
  							Report.newBuilder(REPORT_FILTER_REMOVED_FOR_ERROR).messageParam(filter.getName(), ex.getClass().getSimpleName())
  					);
  					return false;
  				}
  			};
  		}
  		return defaultFailedHandler;
  	}
  	
  	/**
  	 * Determine whether or not to pass the given packet event to the packet listeners.
  	 * <p>
  	 * Uses a default filter failure handler that simply prints the error message and removes the filter.
  	 * @param event - the event.
  	 * @return TRUE if we should, FALSE otherwise.
  	 */
  	public boolean filterEvent(PacketEvent event) {
  		return filterEvent(event, getDefaultErrorHandler());
  	}
  	
  	/**
  	 * Determine whether or not to pass the given packet event to the packet listeners.
  	 * @param event - the event.
  	 * @param handler - failure handler.
  	 * @return TRUE if we should, FALSE otherwise.
  	 */
  	public boolean filterEvent(PacketEvent event, FilterFailedHandler handler) {
  		for (Iterator<Filter> it = filters.iterator(); it.hasNext(); ) {
  			Filter filter = it.next();
  			
  			try {
  				if (!filter.evaluate(engine, event)) {
  					return false;
  				}
  			} catch (Exception ex) {
  				if (!handler.handle(event, filter, ex)) {
  					it.remove();
  				}
  			}
  		}
  		// Pass!
  		return true;
  	}
  
  	/**
  	 * Initialize the script engine if necessary.
  	 */
  	private void checkScriptStatus() {
  		// Start the engine
  		if (uninitialized) {
  			uninitialized = false;
  			initalizeScript();
  		}
  	}
  	
  	/*
  	 * Description: Adds or removes a simple packet filter.
         Usage:       /<command> add|remove name [packet IDs]
  	 */
  	@Override
  	protected boolean handleCommand(CommandSender sender, String[] args) {
  		checkScriptStatus();
  		
  		if (!config.isDebug()) {
  			sender.sendMessage(ChatColor.RED + "Debug mode must be enabled in the configuration first!");
  			return true;
  		}
  		if (!isInitialized()) {
  			sender.sendMessage(ChatColor.RED + "JavaScript engine was not present. Filter system is disabled.");
  			return true;
  		}
  		
  		final SubCommand command = parseCommand(args, 0);
  		final String name = args[1];
  		
  		switch (command) {
  			case ADD:
  				// Never overwrite an existing filter
  				if (findFilter(name) != null) {
  					sender.sendMessage(ChatColor.RED + "Filter " + name + " already exists. Remove it first.");
  					return true;
  				}
  				
  				// Prepare the input to the packet type parser
  				Deque<String> rangeArguments = toQueue(args, 2);
  				
  				final PacketTypeParser parser = new PacketTypeParser();
  				final Set<PacketType> packets = parser.parseTypes(rangeArguments, PacketTypeParser.DEFAULT_MAX_RANGE);
  				sender.sendMessage("Enter filter program ('}' to complete or CANCEL):");
  				
  				// Make sure we can use the conversable interface
  				if (sender instanceof Conversable) {
  					final MultipleLinesPrompt prompt =
  							new MultipleLinesPrompt(new CompilationSuccessCanceller(), "function(event, packet) {");
  					
  					new ConversationFactory(plugin).
  						withFirstPrompt(prompt).
  						withEscapeSequence("CANCEL").
  						withLocalEcho(false).
  						addConversationAbandonedListener(new ConversationAbandonedListener() {
  							@Override
  							public void conversationAbandoned(ConversationAbandonedEvent event) {
  								try {
  									final Conversable whom = event.getContext().getForWhom();
  									
  									if (event.gracefulExit()) {
  										String predicate = prompt.removeAccumulatedInput(event.getContext());
  										Filter filter = new Filter(name, predicate, packets);
  	
  										// Print the last line as well
  										whom.sendRawMessage(prompt.getPromptText(event.getContext()));
  										
  										try {
  											// Force early compilation
  											filter.compile(engine);
  											
  											filters.add(filter);
  											whom.sendRawMessage(ChatColor.GOLD + "Added filter " + name);
  										} catch (ScriptException e) {
  											e.printStackTrace();
  											whom.sendRawMessage(ChatColor.GOLD + "Compilation error: " + e.getMessage());
  										}
  									} else {
  										// Too bad
  										whom.sendRawMessage(ChatColor.RED + "Cancelled filter.");
  									}
  								} catch (Exception e) {
  									reporter.reportDetailed(this,
  											Report.newBuilder(REPORT_CANNOT_HANDLE_CONVERSATION).error(e).callerParam(event)
  									);
  								}
  							}
  						}).
  						buildConversation((Conversable) sender).
  						begin();
  				} else {
  					sender.sendMessage(ChatColor.RED + "Only console and players are supported!");
  				}
  				
  				break;
  				
  			case REMOVE:
  				Filter filter = findFilter(name);
  				
  				// See if it exists before we remove it
  				if (filter != null) {
  					filter.close(engine);
  					filters.remove(filter);
  					sender.sendMessage(ChatColor.GOLD + "Removed filter " + name);
  				} else {
  					sender.sendMessage(ChatColor.RED + "Unable to find a filter by the name " + name);
  				}
  				break;
  		}
  		
  		return true;
  	}
  	
  	/**
  	 * Lookup a filter by its name.
  	 * @param name - the filter name.
  	 * @return The filter, or NULL if not found.
  	 */
  	private Filter findFilter(String name) {
  		// We'll just use a linear scan for now - we don't expect that many filters
  		for (Filter filter : filters) {
  			if (filter.getName().equalsIgnoreCase(name)) {
  				return filter;
  			}
  		}
  		return null;
  	}
  	
  	private SubCommand parseCommand(String[] args, int index) {
  		String text = args[index].toUpperCase();
  		
  		try {
  			return SubCommand.valueOf(text);
  		} catch (IllegalArgumentException e) {
  			throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.", e);
  		}
  	}
  }
  