content: package com.comphenix.protocol.injector;
  
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  import java.util.logging.Level;
  
  import com.google.common.collect.ImmutableSet;
  import org.bukkit.plugin.Plugin;
  import org.bukkit.plugin.PluginLoadOrder;
  
  import com.google.common.collect.Sets;
  
  /**
   * Determine if a plugin using ProtocolLib is correct.
   * 
   * @author Kristian
   */
  class PluginVerifier {
  	/**
  	 * A named plugin cannot be found.
  	 * @author Kristian
  	 */
  	public static class PluginNotFoundException extends RuntimeException {
  		/**
  		 * Generated by Eclipse.
  		 */
  		private static final long serialVersionUID = 8956699101336877611L;
  
  		public PluginNotFoundException() {
  			super();
  		}
  
  		public PluginNotFoundException(String message) {
  			super(message);
  		}
  	}
  	
  	public enum VerificationResult {
  		VALID,
  		
  		/**
  		 * The plugin doesn't depend on ProtocolLib directly or indirectly.
  		 */
  		NO_DEPEND;
  		
  		/**
  		 * Determine if the verification was valid.
  		 */
  		public boolean isValid() {
  			return this == VALID;
  		}
  	}
  	
  	/**
  	 * Contains a list of plugins that will detect and use ProtocolLib dynamically, instead of relying on the dependency system.
  	 */
  	private static final Set<String> DYNAMIC_DEPENDENCY = ImmutableSet.of("mcore", "MassiveCore");
  	
  	/**
  	 * Set of plugins that have been loaded after ProtocolLib.
  	 */
  	private final Set<String> loadedAfter = new HashSet<>();
  	
  	/**
  	 * Reference to ProtocolLib.
  	 */
  	private final Plugin dependency;
  	
  	/**
  	 * Construct a new plugin verifier.
  	 * @param dependency - reference to ProtocolLib, a dependency we require of plugins.
  	 */
  	public PluginVerifier(Plugin dependency) {
  		if (dependency == null)
  			throw new IllegalArgumentException("dependency cannot be NULL.");
  
  		try {
  			// This would screw with the assumption in hasDependency(Plugin, Plugin)
  			if (safeConversion(dependency.getDescription().getLoadBefore()).size() > 0)
  				throw new IllegalArgumentException("dependency cannot have a load directives.");
  		} catch (LinkageError e) {
  			// They're probably using an ancient version of Bukkit
  			dependency.getLogger().log(Level.WARNING, "Failed to determine loadBefore: " + e);
  		}
  
  		this.dependency = dependency;
  	}
  
  	/**
  	 * Retrieve a plugin by name.
  	 * @param pluginName - the non-null name of the plugin to retrieve.
  	 * @return The retrieved plugin.
  	 * @throws PluginNotFoundException If a plugin with the given name cannot be found.
  	 */
  	private Plugin getPlugin(String pluginName) {
  		Plugin plugin = getPluginOrDefault(pluginName);
  		
  		// Ensure that the plugin exists
  		if (plugin != null)
  			return plugin;
  		else
  			throw new PluginNotFoundException("Cannot find plugin " + pluginName);
  	}
  	
  	/**
  	 * Retrieve a plugin by name.
  	 * @param pluginName - the non-null name of the plugin to retrieve.
  	 * @return The retrieved plugin, or NULL if not found.
  	 */
  	private Plugin getPluginOrDefault(String pluginName) {
  		return dependency.getServer().getPluginManager().getPlugin(pluginName);
  	}
  	
  	/**
  	 * Performs simple verifications on the given plugin.
  	 * <p>
  	 * Results may be cached.
  	 * @param pluginName - the plugin to verify.
  	 * @return A verification result.
  	 * @throws IllegalArgumentException If plugin name is NULL.
  	 * @throws PluginNotFoundException If a plugin with the given name cannot be found.
  	 */
  	public VerificationResult verify(String pluginName) {
  		if (pluginName == null)
  			throw new IllegalArgumentException("pluginName cannot be NULL.");
  		return verify(getPlugin(pluginName));
  	}
  	
  	/**
  	 * Performs simple verifications on the given plugin.
  	 * <p>
  	 * Results may be cached.
  	 * @param plugin - the plugin to verify.
  	 * @return A verification result.
  	 * @throws IllegalArgumentException If plugin name is NULL.
  	 * @throws PluginNotFoundException If a plugin with the given name cannot be found.
  	 */
  	public VerificationResult verify(Plugin plugin) {
  		if (plugin == null)
  			throw new IllegalArgumentException("plugin cannot be NULL.");
  		String name = plugin.getName();
  		
  		// Skip the load order check for ProtocolLib itself
  		if (!dependency.equals(plugin)) {
  			if (!loadedAfter.contains(name) && !DYNAMIC_DEPENDENCY.contains(name)) {
  				if (verifyLoadOrder(dependency, plugin)) {
  					// Memorize
  					loadedAfter.add(plugin.getName());
  				} else {
  					return VerificationResult.NO_DEPEND;
  				}
  			}
  		}
  		
  		// Everything seems to be in order
  		return VerificationResult.VALID;
  	}
  	
  	/**
  	 * Determine if a given plugin is guarenteed to be loaded before the other.
  	 * <p>
  	 * Note that the before plugin is assumed to have no "load" directives - that is, plugins to be
  	 * loaded after itself. The after plugin may have "load" directives, but it is irrelevant for our purposes.
  	 * @param beforePlugin - the plugin that is loaded first.
  	 * @param afterPlugin - the plugin that is loaded last.
  	 * @return TRUE if it will, FALSE if it may or must load in the opposite other.
  	 */
  	private boolean verifyLoadOrder(Plugin beforePlugin, Plugin afterPlugin) {
  		// If a plugin has a dependency, it will be loaded after its dependency
  		if (hasDependency(afterPlugin, beforePlugin)) {
  			return true;
  		}
  		
  		// No dependency - check the load order
  		if (beforePlugin.getDescription().getLoad() == PluginLoadOrder.STARTUP &&
  			afterPlugin.getDescription().getLoad() == PluginLoadOrder.POSTWORLD) {
  			return true;
  		}
  		return false;
  	}
  	
  	/**
  	 * Determine if a plugin has a given dependency, either directly or indirectly.
  	 * @param plugin - the plugin to check.
  	 * @param dependency - the dependency to find.
  	 * @return TRUE if the plugin has the given dependency, FALSE otherwise.
  	 */
  	private boolean hasDependency(Plugin plugin, Plugin dependency) {
  		return hasDependency(plugin, dependency, Sets.<String>newHashSet());
  	}
  	
  	/**
  	 * Convert a list to a set.
  	 * <p>
  	 * A null list will be converted to an empty set.
  	 * @param list - the list to convert.
  	 * @return The converted list.
  	 */
  	private Set<String> safeConversion(List<String> list) {
  		if (list == null)
  			return Collections.emptySet();
  		else
  			return new HashSet<>(list);
  	}
  	
  	// Avoid cycles. DFS.
  	private boolean hasDependency(Plugin plugin, Plugin dependency, Set<String> checking) {
  		Set<String> childNames = Sets.union(
  				 safeConversion(plugin.getDescription().getDepend()),
  				 safeConversion(plugin.getDescription().getSoftDepend())
  		);
  		
  		// Ensure that the same plugin isn't processed twice
  		if (!checking.add(plugin.getName())) {
  			throw new IllegalStateException("Cycle detected in dependency graph: " + plugin);
  		}
  		// Look for the dependency in the immediate children
  		if (childNames.contains(dependency.getName())) {
  			return true;
  		}
  		
  		// Recurse through their dependencies
  		for (String childName : childNames) {
  			Plugin childPlugin = getPluginOrDefault(childName);
  			
  			if (childPlugin != null && hasDependency(childPlugin, dependency, checking)) {
  				return true;
  			}
  		}
  		
  		// Cross edges are permitted
  		checking.remove(plugin.getName());
  		
  		// No dependency found!
  		return false;
  	}
  }
  