content: package com.alecgorge.minecraft.jsonapi.dynamic;
  
  import java.io.File;
  import java.io.FileReader;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.Callable;
  import java.util.concurrent.ExecutionException;
  import java.util.concurrent.Future;
  import java.util.logging.Logger;
  
  import org.bukkit.plugin.Plugin;
  import org.json.simpleForBukkit.JSONArray;
  import org.json.simpleForBukkit.JSONObject;
  import org.json.simpleForBukkit.parser.JSONParser;
  
  import com.alecgorge.minecraft.jsonapi.JSONAPI;
  import com.alecgorge.minecraft.jsonapi.api.APIMethodName;
  import com.alecgorge.minecraft.jsonapi.api.JSONAPICallHandler;
  import com.alecgorge.minecraft.jsonapi.dynamic.Method;
  
  public class Caller implements JSONAPIMethodProvider {
  	public HashMap<String, HashMap<String, Method>> methods = new HashMap<String, HashMap<String, Method>>();
  	private JSONParser p = new JSONParser();
  	private JSONAPI inst;
  	private Logger outLog = JSONAPI.instance.outLog;
  	public int methodCount = 0;
  
  	private List<JSONAPICallHandler> handlers = new ArrayList<JSONAPICallHandler>();
  	private List<JSONAPIMethodProvider> objectsToCheck = new ArrayList<JSONAPIMethodProvider>();
  
  	public Caller(JSONAPI plugin) {
  		inst = plugin;
  		registerMethods(this);
  	}
  
  	public Object call(String methodAndNamespace, final Object[] params) throws Throwable {
  		String[] methodParts = methodAndNamespace.split("\\.", 2);
  
  		APIMethodName n = new APIMethodName(methodAndNamespace);
  		for (JSONAPICallHandler c : handlers) {
  			if (c.willHandle(n)) {
  				return c.handle(n, params);
  			}
  		}
  
  		checkObjects();
  
  		final Call c;
  		if(methods.get("").containsKey(methodAndNamespace)) {
  			c = methods.get("").get(methodAndNamespace).getCall();
  		}
  		else if (methodParts.length == 1) {
  			c = methods.get("").get(methodParts[0]).getCall();
  		} else {
  			c = methods.get(methodParts[0]).get(methodParts[1]).getCall();
  		}
  
  		if (params.length < c.getNumberOfExpectedArgs()) {
  			throw new Exception("Incorrect number of args: gave " + params.length + " (" + Arrays.asList(params).toString() + "), expected " + c.getNumberOfExpectedArgs() + " for method " + methodAndNamespace);
  		}
  
  		return innerCall(c, params);
  	}
  
  	private void checkObjects() {
  		for (JSONAPIMethodProvider o : objectsToCheck) {
  			for (java.lang.reflect.Method m : o.getClass().getMethods()) {
  				if (m.isAnnotationPresent(API_Method.class)) {
  					API_Method a = m.getAnnotation(API_Method.class);
  
  					if (methods.get(a.namespace()) == null) {
  						methods.put(a.namespace(), new HashMap<String, Method>());
  					}
  
  //					System.out.println(String.format("adding %s.%s: %s %s", a.namespace(), a.name().isEmpty() ? m.getName() : a.name(), o,m));
  					methods.get(a.namespace()).put(a.name().isEmpty() ? m.getName() : a.name(), new Method(o, m, a));
  				}
  			}
  		}
  		objectsToCheck = new ArrayList<JSONAPIMethodProvider>();
  	}
  
  	private Object innerCall(final Call c, final Object[] params) throws Throwable {
  		Future<Object> f = inst.getServer().getScheduler().callSyncMethod(inst, new Callable<Object>() {
  			public Object call() throws Exception {
  				return c.call(params);
  			}
  		});
  
  		try {
  			return f.get();
  		} catch (InterruptedException e) {
  			return f.get();
  		} catch (ExecutionException e) {
  			throw e.getCause();
  		}
  	}
  
  	@API_Method(namespace = "jsonapi", argumentDescriptions = { "The name of the method to test. Should be a FQN. Ex: dynmap.getHost or getPlayers" }, isProvidedByV2API=false)
  	public boolean methodExists(String name) {
  		if(methods.get("").containsKey(name)) {
  			return true;
  		}
  		String[] methodParts = name.split("\\.", 2);
  
  		APIMethodName n = new APIMethodName(name);
  		for (JSONAPICallHandler c : handlers) {
  			if (c.willHandle(n)) {
  				return true;
  			}
  		}
  
  		checkObjects();
  
  		if(methods.get("").containsKey(name)) {
  			return true;
  		}
  		else if (methodParts.length == 1) {
  			return methods.get("").containsKey(methodParts[0]);
  		} else {
  			return methods.containsKey(methodParts[0]) && methods.get(methodParts[0]).containsKey(methodParts[1]);
  		}
  	}
  
  	@API_Method(namespace = "jsonapi", isProvidedByV2API=false)
  	public HashMap<String, List<String>> getMethods() {
  		HashMap<String, List<String>> r = new HashMap<String, List<String>>();
  
  		for (String key : methods.keySet()) {
  			r.put(key, new ArrayList<String>(methods.get(key).keySet()));
  		}
  
  		return r;
  	}
  	
  	@API_Method(
  			namespace = "",
  			name = "jsonapi.methods.all.for_namespace",
  			returnDescription = "Returns details about all the API methods currently available for a given namespace. You should use `jsonapi.methods` unless you want to work with custom methods or ones loaded in a specific namespace."
  	)
  	public List<Method> getMethodsForNamespace(String namespace) {
  		return new ArrayList<Method>(methods.get(namespace).values());
  	}
  	
  	@API_Method(
  			namespace = "",
  			name = "jsonapi.methods.all.namespaces",
  			returnDescription = "Returns a list of all the namespaces currently loaded on the server."
  	)
  	public List<String> getMethodNamespaces() {
  		return new ArrayList<String>(methods.keySet());
  	}
  	
  	@API_Method(
  			namespace = "",
  			name = "jsonapi.methods.all",
  			returnDescription = "Returns details about all API methods in all namespaces. You should use `jsonapi.methods` unless you want to work with custom methods or ones loaded in a specific namespace."
  	)
  	public Map<String, List<Method>> getAllMethodInfo() {
  		Map<String, List<Method>> m = new HashMap<String, List<Method>>();
  		
  		for (String key : methods.keySet()) {
  			m.put(key, getMethodsForNamespace(key));
  		}
  		
  		return m;
  	}
  
  	@API_Method(
  			namespace = "",
  			name = "jsonapi.methods",
  			returnDescription = "Returns details about API methods provided by default with JSONAPI."
  	)
  	public List<Method> getAllV2APIMethods() {
  		List<Method> m = new ArrayList<Method>();
  		for (String key : methods.keySet()) {
  			for (Method meth : methods.get(key).values()) {
  				if (meth.isProvidedByV2API()) {
  					m.add(meth);
  				}
  			}
  		}
  		
  		Collections.sort(m, new Comparator<Method>() {
  			@Override
  			public int compare(Method o1, Method o2) {
  				return o1.getName().compareTo(o2.getName());
  			}
  		});
  
  		return m;
  	}
  	
  	public List<String> getAllMethods() {
  		List<String> r = new ArrayList<String>();
  		
  		for (String key : methods.keySet()) {
  			r.addAll(new ArrayList<String>(methods.get(key).keySet()));
  		}
  		
  		Collections.sort(r);
  		
  		return r;
  	}
  
  	public void loadFile(File methodsFile, boolean jsonapi4) {
  		try {
  			magicWithMethods(p.parse(new FileReader(methodsFile)), jsonapi4);
  		} catch (Exception e) {
  			e.printStackTrace();
  		}
  	}
  	
  	public void loadInputStream(InputStream methodsFile, boolean jsonapi4) {
  		try {
  			magicWithMethods(p.parse(new InputStreamReader(methodsFile)), jsonapi4);
  		} catch (Exception e) {
  			e.printStackTrace();
  		}
  	}
  
  	public void registerAPICallHandler(JSONAPICallHandler handler) {
  		handlers.add(handler);
  	}
  
  	public void registerMethods(JSONAPIMethodProvider o) {
  		objectsToCheck.add(o);
  	}
  
  	public void deregisterAPICallHandler(JSONAPICallHandler handler) {
  		handlers.remove(handler);
  	}
  
  	private void magicWithMethods(Object raw, boolean jsonapi4) throws Exception {
  		if (raw instanceof JSONObject) {
  			JSONObject methods = (JSONObject) raw;
  			String name = "";
  
  			if (methods.containsKey("name")) {
  				name = methods.get("name").toString();
  			} else {
  				throw new Exception("A JSON file is not well formed: missing the key 'name' for the root object.");
  			}
  
  			if (methods.containsKey("depends")) {
  				Object deps = methods.get("depends");
  				List<String> pluginNames = new ArrayList<String>();
  
  				if (deps instanceof JSONArray) {
  					for (Object o : ((JSONArray) deps)) {
  						pluginNames.add(o.toString());
  					}
  				} else {
  					pluginNames.add(deps.toString());
  				}
  
  				for (String plugin : pluginNames) {
  					plugin = plugin.trim();
  					Plugin p = inst.getServer().getPluginManager().getPlugin(plugin);
  
  					if (p == null && !plugin.equals("JSONAPI")) {
  						outLog.info("[JSONAPI] " + name + " cannot be loaded because it depends on a plugin that is not installed: '" + plugin + "'");
  					} else if (plugin.equals("JSONAPI") || p.isEnabled()) {
  						if (methods.containsKey("methods")) {
  							proccessMethodsWithNamespace((JSONArray) methods.get("methods"), methods.containsKey("namespace") ? methods.get("namespace").toString() : "", jsonapi4);
  						} else {
  							throw new Exception("A JSON file is not well formed: missing the key 'methods' for the root object.");
  						}
  					} else {
  						outLog.info("[JSONAPI] " + name + " cannot be loaded because it depends on a plugin that is not enabled: '" + plugin + "'");
  					}
  				}
  			}
  		} else if (raw instanceof JSONArray) {
  			proccessMethodsWithNamespace((JSONArray) raw, "", jsonapi4);
  		} else {
  			throw new Exception("JSON file is not a valid methods file.");
  		}
  	}
  
  	public void proccessMethodsWithNamespace(JSONArray methods, String namespace, boolean jsonapi4) {
  		for (Object o : methods) {
  			if (o instanceof JSONObject) {
  				String name = ((JSONObject) o).get("name").toString();
  
  				if (this.methods.containsKey(name)) {
  					Logger.getLogger("Minecraft").info("[JSONAPI] The method " + name + " already exists! It is being overridden.");
  				}
  
  				if (!this.methods.containsKey(namespace)) {
  					this.methods.put(namespace, new HashMap<String, Method>());
  				}
  
  				methodCount++;
  				this.methods.get(namespace).put(name, new Method((JSONObject) o, jsonapi4));
  			}
  		}
  	}
  
  	public void loadString(String methodsString, boolean jsonapi4) {
  		try {
  			magicWithMethods(p.parse(methodsString), jsonapi4);
  		} catch (Exception e) {
  			e.printStackTrace();
  		}
  	}
  }
  