import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;

import se.rupy.http.Async;
import se.rupy.http.Daemon;
import se.rupy.http.Deploy;
import se.rupy.http.Event;
import se.rupy.http.Output;
import se.rupy.http.Query;
import se.rupy.http.Service;

/**
 * Secure Unique Identity Cluster.
 *
 * This solution is "wasteful"; but with 16-digit Base-58 keys and
 * long ids, collisions should be rare, at least over the network.
 *
 * @author Marc
 */
public class Root extends Service {
	static int LENGTH = 16;
	static String local;//"89.221.241.32", "89.221.241.33", "one", "two",
	static String[] ip = {"195.67.191.192", "195.67.191.192", "195.67.191.192"};
	static String[] host = {"fem", "six", "sju"}; // oct, nio, ten
	static String[] node_type = {"data", "node", "user", "task", "type"};
	static String[] link_type = {"data", "node", "user", "task", "date"};
	static String[] meta_type = {"data", "node", "user", "task"};

	static String root = "/";
	static Deploy.Archive archive;
	static URLDecoder decoder = new URLDecoder();

	// example data for tutorial
	final String key = "TvaaS3cqJhQyK6sn";
	final String word = "/node/data/word/four";
	final String roll = "/meta/user/data/fuse";

	static String type(String[] type, String name, String selected) {
		StringBuilder select = new StringBuilder("<select id=\"type\" name=\"" + name + "\">");

		for(int i = 0; i < type.length; i++) {
			select.append("<option" + (type[i].equals(selected) ? " selected" : "") + ">" + type[i] + "</option>");
		}

		select.append("</select>");
		return select.toString();
	}

	static int type(String[] type, String name) {
		for(int i = 0; i < type.length; i++) {
			if(type[i].equals(name))
				return i;
		}

		return -1;
	}

	public static String local() { return local; }
	public String path() { return root; }

	public void create(Daemon daemon) throws Exception {
		local = System.getProperty("host");
		archive = (Deploy.Archive) Thread.currentThread().getContextClassLoader();

		String data = System.getProperty("data", "root.rupy.se");

		if(data.indexOf("binarytask") > -1) {
												ip = new String[3];
												ip[0] = "104.199.80.227";
												ip[1] = "104.198.30.4";
												ip[2] = "104.199.173.122";

												host = new String[3];
												host[0] = "euro";
												host[1] = "iowa";
												host[2] = "asia";
								}

								if(data.indexOf("radiomesh") > -1) {
												ip = new String[3];
												ip[0] = "195.67.191.192";
												ip[1] = "195.67.191.192";
												ip[2] = "195.67.191.192";
												//ip[3] = "195.67.191.192";
												//ip[4] = "195.67.191.192";
												//ip[5] = "195.67.191.192";

												host = new String[3];
												host[0] = "ett";
												host[1] = "tva";
												host[2] = "thr";
												//host[3] = "fyr";
												//host[4] = "fiv";
												//host[5] = "sex";
								}
	}

	public static String host() {
		Deploy.Archive archive = (Deploy.Archive) Thread.currentThread().getContextClassLoader();
		return archive.host();
		//return System.getProperty("data", "root.rupy.se");
	}

	public static String home() {
		return "app/" + host() + "/root";
	}

	public void filter(Event event) throws Event, Exception {
		Output out = event.output();

		if(event.query().method() == Query.POST) {
			ByteArrayOutputStream data = new ByteArrayOutputStream();
			Deploy.pipe(event.input(), data);
			String file = decrypt(data.toByteArray());
			String path = event.query().header("path");

			boolean make = path.startsWith("/make");
			boolean link = path.startsWith("/link");
			boolean meta = path.startsWith("/meta");

			String type = "user";
			String sort = "key";

			int question = path.indexOf("?");

			if(question > 0) {
				StringTokenizer amp = new StringTokenizer(path.substring(question + 1), "&");

				while(amp.hasMoreTokens()) {
					String equ = amp.nextToken();
					int pos = equ.indexOf('=');

					String key = null;
					String value = "false";

					if(pos == -1) {
						pos = equ.length();
						key = equ.substring(0, pos);
					}
					else {
						key = equ.substring(0, pos);
						value = equ.length() > pos + 1 ? decoder.decode(equ.substring(pos + 1), "UTF-8") : "";
					}

					if(key.equals("type"))
						type = value;
					if(key.equals("sort"))
						sort = value;
				}
			}

			try {
																//System.err.println("#1");
				if(link)
					link(new JSONObject(file), null);
				else if(meta)
					meta(new JSONObject(file), null);
				else
					store(new JSONObject(file), type, sort, make, ((String) event.query().header("host")).equals("test.rupy.se"));
																//System.err.println("#2");
																out.print(1);
			}
			catch(SortFail e) {
				out.print(e);
			}
			catch(KeyFail e) {
				out.print(0);
			}
			catch(Exception e) {
				e.printStackTrace();
				out.print(e + "[" + local + "]");
			}
		}
		else {
			out.println("<html><head><title>Root Cloud Store</title>");
			out.println("<style>");
			out.println(" a:link, a:hover, a:active, a:visited { color: #666666; font-style: italic; }");
			out.println(" a.red:link, a.red:hover, a.red:active, a.red:visited { color: #ff3300; font-style: italic; }");
			out.println(" a.green:link, a.green:hover, a.green:active, a.green:visited { color: #00cc33; font-style: italic; }");
			out.println(" a.blue:link, a.blue:hover, a.blue:active, a.blue:visited { color: #6699ff; font-style: italic; }");
			out.println(" a.orange:link, a.orange:hover, a.orange:active, a.orange:visited { color: #ff9900; font-style: italic; }");
			out.println("</style>");
			out.println("</head>");
			out.println("<body><pre>ROOT replicates JSON objects async<font color=\"red\"><sup>*</sup></font> over HTTP<br>across a cluster with guaranteed uniqueness<br>and DNS roundrobin for 100% read uptime.<br>");
			out.println(" <i>Usage:</i>");
			out.println(" <font color=\"#ff3300\"><i>user</i></font> <font color=\"#00cc33\"><i>node</i></font> <font color=\"##6699ff\"><i>link</i></font> <font color=\"#ff9900\"><i>meta</i></font>");
			out.println(" ┌──────┬──────┬──────┬──────┐");
			out.println(" <i>make</i> │ <a class=\"red\" href=\"/user?url=talk.binarytask.com\">join</a> │ <a class=\"green\" href=\"/node?make\">form</a> │ <a class=\"blue\" href=\"/link\">bind</a> │ <a class=\"orange\" href=\"/meta\">bond</a> │ <font color=\"grey\"><i>\"insert\"</i></font>");
			out.println(" <i>find</i> │ <a class=\"red\" href=\"http://talk.binarytask.com\">sign</a> │ <a class=\"green\" href=\"/node/user/key/" + key + "\">load</a> │ <a class=\"blue\" href=\"/link/user/data/" + key + "\">list</a> │ <a class=\"orange\" href=\"" + roll + "\">roll</a> │ <font color=\"grey\"><i>\"select\"</i></font>");
			out.println(" <i>edit</i> │ │ <a class=\"green\" href=\"/node\">save</a> │ │ <a class=\"orange\" href=\"/meta?edit\">yoke</a> │ <font color=\"grey\"><i>\"update\"</i></font>");
			out.println(" <i>drop</i> │ │ │ │ <a class=\"orange\" href=\"/meta?tear=true\">trim</a> │ <font color=\"grey\"><i>\"delete\"</i></font>");
			out.println(" ├──────┼──────┼──────┼──────┤");
			out.println(" <i>open</i> │ <a href=\"" + word + "\">word</a> │ <a href=\"/node/data/date/14/11/25/example\">path</a> │ <a href=\"/link/user/data/" + hash(key) + "\">list</a> │ <a href=\"/tree\">tree</a> │");
			out.println(" └──────┴──────┴──────┴──────┘");
			out.println("");
			out.println("<font color=\"red\"><sup>*</sup></font><i>Source:</i> <a href=\"http://root.rupy.se/code\">Async</a>, <a href=\"http://root.rupy.se/code?path=/User.java\">User</a>, <a href=\"http://root.rupy.se/code?path=/Root.java\">Root</a>");
			out.println(" <i>Host:</i> " + Root.local);
			out.println("</pre>");
			out.println("</body>");
			out.println("</html>");
		}
	}

	private static void send(final int i, final String key, final byte[] data, Event event, final boolean trace) throws Exception {
		if(trace) {
			Output out = event.output();
			out.println("> " + host[i] + " (" + key + ")");
			out.flush();
		}

		Async.Work work = new Async.Work(event) {
			public void send(Async.Call post) throws Exception {
				String path = null;
				Query query = event.query();

				String param = "?type=" + query.string("type", "user") + "&sort=" + query.string("sort", "key");

				if(event.query().bit("create", true))
					path = "/make" + param;
				else
					path = event.query().path() + param;

				post.post(root, "Host:" + Root.host[i] + "." + host() + "\r\nHead:less\r\nPath:" + path, data);
			}

			public void read(String host, String body) throws Exception {
				if(!event.query().string(Root.host[i]).equals("-2"))
					event.query().put(Root.host[i], body);

				event.query().put("in_" + Root.host[i] + "_key", key);

																//System.out.println(i);

																if(trace)
					event.query().put("wakeup", Root.host[i]);

																event.reply().wakeup(true);

																//System.err.println(i + " " + );
			}

			public void fail(String host, Exception e) throws Exception {
				if(e instanceof Async.Timeout) {
					event.daemon().client().send(host, this, 60);
				}
				else {
					event.query().put(Root.host[i], e.toString());

					if(trace)
						event.query().put("wakeup", Root.host[i]);

					e.printStackTrace();

					event.reply().wakeup(true);
				}
			}
		};

		event.daemon().client().send(ip[i], work, 60);
	}

	/**
	 * Create and synchronize a unique node key across the cluster.
	 * The id is generated by hashing the key with {@link #hash(String)}.
	 */
	public static JSONObject sync(Event event, String type, String sort, boolean create) throws Exception {
		JSONObject json = json(event);
		if(create) {
			json = create(event, json);
		}
		while(exists(json, type, sort, event, create, ((String) event.query().header("host")).equals("test.rupy.se")) == 0) {
			json = create(event, json);
		}
		return json;
	}

	private static JSONObject create(Event event, JSONObject json) throws Exception {
		boolean test = ((String) event.query().header("host")).equals("test.rupy.se");
		String key = Event.random(test ? 2 : LENGTH);

		if(event.query().bit("create", true)) { // From /node but with create flag.
			json.put("key", key);
		}
		else { // From /make.
			return new JSONObject("{\"key\":\"" + key + "\"}");
		}

		return json;
	}

	/**
	 * Fetch the id for a key.
	 */
	public static long id(String key, String type) throws Exception {
		JSONObject o = new JSONObject(file(home() + "/node/" + type + "/id" + path(hash(key))));

		if(o.getString("key").equals(key))
			return hash(key);
		else
			throw new KeyFail("Root [" + type + "/" + key + "] not found!");
	}

	private static int exists(JSONObject json, String type, String sort, Event event, boolean create, boolean test) throws Exception {
		//if(json.has("root"))
		// throw new RootFail("contains key 'root'.");

		String key = json.getString("key");
		long id = hash(key);

		if(id < 4) // Reserved for developer
			return 0;

		boolean key_exist = Files.exists(Paths.get(home() + "/node/" + type + "/key" + path(key)));
		boolean id_exist = Files.exists(Paths.get(home() + "/node/" + type + "/id" + path(id)));

		//if(event == null) // TODO: This is to test remote collisions, set LENGTH to 2 and uncomment this.

		boolean skip = false;

		if(test && event != null)
			skip = true;

		if(!skip && create && (key_exist || id_exist))
			return 0;

		if(!test)
			sort(json, null, type, sort, create); // TODO: To test remote sort collision comment this out.

		if(event == null)
			return 1;

		if(!create && event.query().path().startsWith("/node") &&
				!key_exist && !id_exist)
			throw new Exception("Node does not exist! [type=" + type + "]");

		byte[] encrypted = encrypt(json.toString());

		for(int i = 0; i < host.length; i++) {
			String state = event.string(host[i]);
			String out_key = event.string("out_" + host[i] + "_key");

			if(!host[i].equals(local)) {
				if(((state.equals("") || state.equals("0")) && !out_key.equals(key)) || state.equals("-2")) {
					event.query().put(host[i], "2");
					event.query().put("out_" + host[i] + "_key", key);
					send(i, key, encrypted, event, event.query().bit("trace", true));
				}
			}
		}

		return 1;
	}

	public static class KeyFail extends Exception {
		public KeyFail(String message) { super(message); }
	}

	public static class SortFail extends Exception {
		public SortFail(String message) { super(message); }
	}

	public static class LinkFail extends Exception {
		public LinkFail(String message) { super(message); }
	}

	public static class MetaFail extends Exception {
		public MetaFail(String message) { super(message); }
	}

	private static void store(JSONObject json, String type, String sort, boolean create, boolean test) throws Exception {
		if(exists(json, type, sort, null, create, test) == 0)
			throw new KeyFail("Node [" + local + "] collision");

		String path = home() + "/node/" + type + "/id" + path(hash(json.getString("key")));

								final long time = System.currentTimeMillis();

		new File(path.substring(0, path.lastIndexOf("/"))).mkdirs();

		long t1 = System.currentTimeMillis() - time;

		File file = new File(path);

		//if(create)
		// file.createNewFile();

								long t2 = System.currentTimeMillis() - time;

		write(file, json);

								long t3 = System.currentTimeMillis() - time;

		sort(json, path, type, sort, create);

								//System.err.println("time " + (System.currentTimeMillis() - time) + " " + t1 + " " + t2 + " " + t3);
	}

	private static void write(File file, JSONObject json) throws Exception {
		write(file, json, false);
	}

				private static void write(File file, JSONObject json, boolean async) throws Exception {
								if(async) {
												/* This was to try and get faulty Toshiba 256 GB cards with high peak latencies
												to work without punishing the users with sometime up to 5000ms of wait.
												It failed because ROOT uses the file system at so many places and the cards
												locked up on both read and write so this would require a whole new IO JNI subsystem
												just for these faulty products. It was an interesting exercise non the less because
												I suspect that even SanDisc cards will get more latency as they wear down.
												 */
												AsynchronousFileChannel channel = AsynchronousFileChannel.open(
																				Paths.get(file.getPath()),
																				StandardOpenOption.WRITE,
																				StandardOpenOption.CREATE);

												ByteBuffer buffer = ByteBuffer.allocate(1024);

												buffer.put(json.toString().getBytes());
												buffer.flip();

												final long time = System.currentTimeMillis();

												channel.write(
																				buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
																								public void completed(Integer result, ByteBuffer attachment) {
																												//System.err.println("yes " + (System.currentTimeMillis() - time));
																								}
																								public void failed(Throwable exc, ByteBuffer attachment) {
																												//System.err.println("no");
																								}
																				});
								}
								else {
												BufferedWriter output = new BufferedWriter(new FileWriter(file));
												output.write(json.toString());
												output.close();
								}
				}

	private static void sort(JSONObject json, String path, String type, String sort, boolean create) throws Exception {
		String home = home() + "/node/" + type;
		long id = hash(json.getString("key"));
		String[] name = sort.split(",");

		for(int i = 0; i < name.length; i++) {
			String key = name[i].trim();
			String value = json.getString(key);
			boolean full = false;

			if(value.contains(" ") || key.equals("word"))
				full = true;
			else if(value.matches("[0-9]+") || !value.matches("[a-zA-Z0-9/.@\\-\\+]+") || value.toLowerCase().matches("root"))
				throw new SortFail("Validation [" + key + "=" + json.getString(key) + "]");

			if(full) {
				String[] words = value.toLowerCase().split("\\s+");

				for(int j = 0; j < words.length; j++) {
					String word = words[j];

					// remove punctuation
					while(word.endsWith(".")
							|| word.endsWith(":")
							|| word.endsWith(",")
							|| word.endsWith(";")
							|| word.endsWith("!")
							|| word.endsWith("?"))
						word = word.substring(0, word.length() - 1);

					if(word.matches("[\\p{L}]+") && word.length() > 3) { // UTF-8 character
						sort = home + "/" + key + "/" + word;
						new File(sort.substring(0, sort.lastIndexOf("/"))).mkdirs();
						RandomAccessFile file = new RandomAccessFile(sort, "rw");
						write(file, id);
						file.close();
					}
				}
			}
			else {
				if(value.indexOf("/") > 0)
					sort = home + "/" + key + "/" + value;
				else
					sort = home + "/" + key + path(value);

				boolean exists = new File(sort).exists();

				if(exists && create) {
					throw new SortFail("Collision [" + key + "=" + json.getString(key) + "]");
				}

				if(!exists && path != null) {
					new File(sort.substring(0, sort.lastIndexOf("/"))).mkdirs();
					Files.createLink(Paths.get(sort), Paths.get(path));
				}
			}
		}
	}

	private static void link(JSONObject json, final Event event) throws Exception {
		JSONObject parent = json.getJSONObject("parent");
		JSONObject child = json.getJSONObject("child");

		String ptype = parent.getString("type");
		String pkey = parent.getString("key");
		String ctype = child.getString("type");
		String ckey = child.getString("key");

		if(event == null) {
			link(ptype, pkey, ctype, ckey);
		}
		else {
			byte[] data = Root.encrypt(json.toString());

			for(int i = 0; i < host.length; i++) {
				if(!host[i].equals(local)) {
					send_link(i, data, event);
				}
			}
		}
	}

	private static void meta(JSONObject json, final Event event) throws Exception {
		JSONObject parent = json.getJSONObject("parent");
		JSONObject child = json.getJSONObject("child");
		JSONObject node = json.optJSONObject("json");
		boolean echo = json.optBoolean("echo");
		boolean tear = json.optBoolean("tear");
		String path = json.getString("path");
		String ptype = parent.getString("type");
		String pkey = parent.getString("key");
		String ctype = child.getString("type");
		String ckey = child.getString("key");

		if(event == null) {
			meta(ptype, pkey, ctype, ckey, path, node, echo, tear);
		}
		else {
			byte[] data = Root.encrypt(json.toString());

			for(int i = 0; i < host.length; i++) {
				if(!host[i].equals(local)) {
					send_meta(i, data, event);
				}
			}
		}
	}

	/*
	 * Link parent and child.
	 * Root relations is self referencing lists of node types, so you can get last 10 registered users f.ex.
	 * Atomic relation is node-to-node, both one-to-one and one-to-many, allowing for regular give me all articles written by user f.ex.
	 * These lists have no edit or delete, see meta!
	 */
	private static void link(String ptype, String pkey, String ctype, String ckey) throws Exception {
		String path = home() + "/link/";

		long pid = hash(pkey);
		long cid = ctype.equals("date") ? Long.parseLong(ckey) : hash(ckey);

		if(!ctype.endsWith("date") && !new File(home() + "/node/" + ctype + "/id" + path(cid)).exists())
			throw new LinkFail("Child '" + cid + "' doesn't exist.");

		if(!new File(home() + "/node/" + ptype + "/id" + path(pid)).exists())
			throw new LinkFail("Parent '" + pid + "' doesn't exist.");

		path += ptype + "/" + ctype + "/" + path(pkey);

		new File(path.substring(0, path.lastIndexOf("/"))).mkdirs();

		RandomAccessFile file = new RandomAccessFile(path, "rw");

		write(file, cid);

		file.close();
	}

	/*
	 * Link parent and child with meta files, potentially with tree structure.
	 */
	private static void meta(String ptype, String pkey, String ctype, String ckey, String path, JSONObject data, boolean echo, boolean tear) throws Exception {
		File parent = new File(home() + "/node/" + ptype + "/key" + path(pkey));
		File child = new File(home() + "/node/" + ctype + "/key" + path(ckey));

		if(!parent.exists())
			throw new MetaFail("Parent node doesn't exist. (" + parent + ")");

		JSONObject p = new JSONObject(file(parent));
		JSONObject c = null;

		if(child.exists())
			c = new JSONObject(file(child));

		String pname = pkey;
		String cname = ckey;

		if(c != null && c.has("name"))
			cname = c.getString("name");

		String ppname = path(pname);
		String ccname = path(cname);

		if(c != null) {
			ccname = path(ckey);

			if(p.has("name"))
				pname = p.getString("name");
		}

		pname = "/" + pname;
		cname = "/" + cname;

		String root = home() + "/meta/" + ptype + "/" + ctype + ppname + cname + path;
		String link = home() + "/meta/" + ctype + "/" + ptype + ccname + pname + path;

		if(tear) {
			try {
				new File(root).delete();
				new File(link).delete();
			}
			catch (Exception e) {
				// OK
			}
		}
		else {
			File folder = new File(root.substring(0, root.lastIndexOf("/")));
			folder.mkdirs();
			// Neat but leads to edit wars...
			//folder.setLastModified(new Date().getTime());

			if(data == null) {
				if(ctype.equals("user"))
					throw new MetaFail("You can't hardlink 'user' type.");

				try {
					Files.createLink(Paths.get(root), child.toPath());
				}
				catch(FileAlreadyExistsException e) {
					// OK
				}
			}
			else {
				BufferedWriter output = new BufferedWriter(new FileWriter(root));
				output.write(data.toString());
				output.close();
			}

			if(echo && c != null && data != null) {
				new File(link.substring(0, link.lastIndexOf("/"))).mkdirs();

				try {
					Files.createLink(Paths.get(link), Paths.get(root));
				}
				catch(FileAlreadyExistsException e) {
					// OK
				}
			}
		}
	}

	// rm -rf
	private static boolean delete(File file) {
		if(file.isDirectory()) {
			String[] files = file.list();

			for(int i = 0; i < files.length; i++) {
				boolean success = delete(new File(file, files[i]));

				if(!success)
					return false;
			}
		}

		return file.delete();
	}

	/*
	 * Finds the references to same nodes in full word index files.
	 *
	 * This should search from the end until the size asked for is found
	 * with infinite "next" pagination memory seek position instead of size.
	 */
	private static List first_compare(RandomAccessFile one, RandomAccessFile two) throws Exception {
		if(one.length() > 0 && two.length() > 0) {
			LinkedList list = new LinkedList();

			byte[] one_data = new byte[4096];
			byte[] two_data = new byte[4096];

			// loop one
			for(int i = 0; i < one.length() / one_data.length + 1; i++) {
				one.seek(i * one_data.length);
				int one_read = one.read(one_data);

				if(one_read > 0) {
					ByteBuffer one_buffer = ByteBuffer.wrap(one_data, 0, one_read);

					// for each one id
					for(int j = 0; j < one_read / 8; j++) {
						long one_id = one_buffer.getLong();

						// loop two
						for(int k = 0; k < two.length() / two_data.length + 1; k++) {
							two.seek(k * two_data.length);
							int two_read = two.read(two_data);

							if(two_read > 0) {
								ByteBuffer two_buffer = ByteBuffer.wrap(two_data, 0, two_read);

								// for each two id
								for(int l = 0; l < two_read / 8; l++) {
									long two_id = two_buffer.getLong();

									// match
									if(one_id == two_id) {
										list.add(new Long(one_id));
									}
								}
							}
						}
					}
				}
			}

			return list;
		}

		return null;
	}

	private synchronized static List remove(RandomAccessFile file, List list) throws Exception {
		// check for non duplicate

		LinkedList both = new LinkedList();

		if(file.length() > 0) {
			byte[] data = new byte[4096];

			for(int i = 0; i < file.length() / data.length + 1; i++) {
				file.seek(i * data.length);
				int read = file.read(data);

				ByteBuffer buffer = ByteBuffer.wrap(data, 0, read);

				for(int j = 0; j < read / 8; j++) {
					Long id = new Long(buffer.getLong());

					if(list.contains(id)) {
						both.add(id);
					}
				}
			}
		}

		return both;
	}

	private synchronized static void write(RandomAccessFile file, long id) throws Exception {
		/* check for duplicate
		 *
		 * here we should probably check for zero long values since we need to delete references by zeroing them.
		 * but we want to write the latest entry last for chronology.
		 */
		if(file.length() > 0) {
			byte[] data = new byte[4096];

			for(int i = 0; i < file.length() / data.length + 1; i++) {
				file.seek(i * data.length);
				int read = file.read(data);

				ByteBuffer buffer = ByteBuffer.wrap(data, 0, read);

				for(int j = 0; j < read / 8; j++) {
					if(id == buffer.getLong()) {
						return;
					}
				}
			}
		}

		// write

		file.seek(file.length());
		file.writeLong(id);
		file.close();
	}

	private static void send_link(final int i, final byte[] data, Event event) throws Exception {
		final Async.Work work = new Async.Work(event) {
			public void send(Async.Call post) throws Exception {
				post.post(root, "Host:" + Root.host[i] + "." + host() + "\r\nHead:less\r\nPath:/link", data);
			}

			public void read(String host, String body) throws Exception {
				int working = 0, complete = 0, failed = 0;
				event.query().put(Root.host[i], body);

				for(int j = 0; j < Root.host.length; j++) {
					if(!Root.host[i].equals(local)) {
						String result = event.string(Root.host[j]);

						if(result.equals("2"))
							working++;
						else if(result.equals("1"))
							complete++;
						else if(result.length() > 0)
							failed++;
					}
				}

				if(complete == Root.host.length - 1) {
					event.query().put("result", "1");
					link((JSONObject) event.query().get("json"), null);
					int state = event.reply().wakeup(true);
				}
				else if(failed > 0) {
					event.query().put("result", body + "[" + local + "]");
					int state = event.reply().wakeup(true);
				}
			}

			public void fail(String host, Exception e) throws Exception {
				if(e instanceof Async.Timeout) {
					event.daemon().client().send(host, this, 60);
				}
				else {
					e.printStackTrace();
					event.query().put("result", e.toString());
					event.reply().wakeup(true);
				}
			}
		};

		event.daemon().client().send(ip[i], work, 60);
	}

	private static void send_meta(final int i, final byte[] data, Event event) throws Exception {
		final Async.Work work = new Async.Work(event) {
			public void send(Async.Call post) throws Exception {
				post.post(root, "Host:" + Root.host[i] + "." + host() + "\r\nHead:less\r\nPath:/meta", data);
			}

			public void read(String host, String body) throws Exception {
				int working = 0, complete = 0, failed = 0;
				event.query().put(Root.host[i], body);

				for(int j = 0; j < Root.host.length; j++) {
					if(!Root.host[i].equals(local)) {
						String result = event.string(Root.host[j]);

						if(result.equals("2"))
							working++;
						else if(result.equals("1"))
							complete++;
						else if(result.length() > 0)
							failed++;
					}
				}

				if(complete == Root.host.length - 1) {
					event.query().put("result", "1");
					int state = event.reply().wakeup(true);
				}
				else if(failed > 0) {
					event.query().put("result", body + "[" + local + "]");
					int state = event.reply().wakeup(true);
				}
			}

			public void fail(String host, Exception e) throws Exception {
				if(e instanceof Async.Timeout) {
					event.daemon().client().send(host, this, 60);
				}
				else {
					e.printStackTrace();
					event.query().put("result", e.toString());
					event.reply().wakeup(true);
				}
			}
		};

		event.daemon().client().send(ip[i], work, 60);
	}

	public static long hash(String key) throws Exception {
		MessageDigest md = MessageDigest.getInstance("SHA-512");

		byte[] data = key.getBytes();
		md.update(data, 0, data.length);
		byte[] hash = md.digest();

		long h = 2166136261L;

		for(int i = 0; i < hash.length; i++) {
			h = (h ^ hash[i]) * 16777619;
		}

		return Math.abs(h);
	}

	public static String path(long id) {
		return path(String.valueOf(id), 3);
	}

	public static String path(String name) {
		return path(name, 2);
	}

	/* Make a path of the first X chars then a file of the remainder.
	 * 58^2=3364 this is how you calculate if you need more or less folders.
	 * 10^3=1000 this is why the id needs one more folder.
	 */
	private static String path(String name, int length) {
		int index = name.indexOf('.');

		if((index > 0 && index <= length) || name.length() <= length) // Unless we can't!
			return "/" + name;

		StringBuilder path = new StringBuilder();

		for(int i = 0; i < name.length(); i++) {
			if(i <= length) {
				path.append("/");
			}

			path.append(name.charAt(i));
		}

		return path.toString();
	}

	/*
	 * Encryption stuff
	 */

	static String secret;

	static String secret() throws Exception {
		if(secret == null) {
			secret = file("app/" + archive.host() + "/root/secret");
		}

		Deploy.Archive current = (Deploy.Archive) Thread.currentThread().getContextClassLoader();
		//System.out.println(archive.host() + " " + secret + " " + current.host());

		return secret;
	}

	public static String file(String path) throws Exception {
		return file(new File(path));
	}

	public static String file(File file) throws Exception {
		BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
		String content = in.readLine();
		in.close();
		return content;
	}

	static byte[] encrypt(String text) throws Exception {
		Cipher aes = Cipher.getInstance("AES/ECB/PKCS5Padding");
		aes.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret().getBytes(), "AES"));
		return aes.doFinal(text.getBytes());
	}

	static String decrypt(byte[] data) throws Exception {
		Cipher aes = Cipher.getInstance("AES/ECB/PKCS5Padding");
		aes.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret().getBytes(), "AES"));
		return new String(aes.doFinal(data));
	}

	static JSONObject json(Event event) throws Exception {
		Object json = event.query().get("json");

		if(json == null)
			return null;

		if(json instanceof JSONObject)
			return (JSONObject) json;
		else if(json instanceof String)
			return new JSONObject((String) json);
		else
			throw new Exception("JSON is wrong type!");
	}

	public static class Find extends Service {
		public static int NAME = 1;
		public static int DATE = 2;

		public String path() {
			return null;
		}

		public JSONArray recurse(File file, String full, int from, int size, int level, int deep, final int sort, boolean secure, boolean time) throws Exception {
			if(deep > -1 && level > deep)
				return null;

			File[] files = file.listFiles();

			if(sort > 0) {
				Arrays.sort(files, new Comparator<File>() {
					public int compare(File a, File b) {
						if(sort == DATE)
							return Long.compare(b.lastModified(), a.lastModified());
						else
							return a.getName().compareTo(b.getName());
					}
				});
			}

			JSONArray arr = new JSONArray();

			if(files.length > 0) {
				int f = 0, s = files.length;

				if(level == 0) {
					f = from;

					if(size > 0)
						s = size;
				}

				int length = f + s;

				if(length > files.length) {
					f -= length - files.length;
					length = files.length;
				}

				if(f < 0) f = 0;

				for(int i = f; i < length; i++) {
					Path path = Paths.get(full + "/" + files[i].getName());

					JSONObject obj = new JSONObject();
					boolean add = false;

					String name = files[i].getName();

					if(!secure && name.length() == 16)
						name = "" + Root.hash(name);

					if(Files.isDirectory(path)) {
						if(deep == 0) {
							arr.put(name);
						}
						else {
							JSONArray child = recurse(path.toFile(), path.toString(), from, size, level + 1, deep, sort, secure, time);

							if(child != null) {
								add = true;
								obj.put(name, child);
							}
						}
					}
					else {
						add = true;
						JSONObject json = new JSONObject(file(path.toString()));

						if(json.has("key"))
							json.remove("key");

						if(time)
							json.put("time", System.currentTimeMillis() - files[i].lastModified());

						obj.put(name, json);
					}

					if(add)
						arr.put(obj);
				}
			}

			if(arr.length() == 0)
				return null;

			return arr;
		}

		private void writeArray(Event event, JSONArray arr, int length) throws Exception {
			StringBuilder builder = new StringBuilder();
			builder.append("{\"total\": " + length + ", \"list\":");
			builder.append(arr == null ? "[]" : arr.toString(4));
			builder.append("}");

			event.reply().type("application/json; charset=UTF-8");
			byte[] data = builder.toString().getBytes("UTF-8");
			Output out = event.reply().output(data.length);
			out.write(data);
		}

		private void writeObject(Event event, JSONObject obj) throws Exception {
			event.reply().type("application/json; charset=UTF-8");
			byte[] data = obj.toString(4).getBytes("UTF-8");
			Output out = event.reply().output(data.length);
			out.write(data);
		}

		/* Example Public Paths:
		 *
		 * Node:
		 * - /node/user/key/<key>
		 *
		 * Link:
		 * - /link/user/data/<key>
		 *
		 * Meta:
		 * - /meta/user/data/<key>
		 *
		 * Find:
		 * - /node/data/word/full%20word%20search
		 */
		public void filter(Event event) throws Event, Exception {
			event.query().parse();

			String[] path = event.query().path().split("/");

			String full = "";
			String rule = "";
			String head = "";
			String tail = "";
			String last = "";

			try {
				rule = path[1];
				head = path[2];
				tail = path[3];
			}
			catch(Exception e) {
				//e.printStackTrace();
				fail(1, event, full, rule, head, tail, last);
			}

			int from = event.query().medium("from", 0);
			int size = event.query().medium("size", rule.equals("meta") ? -1 : 10);
			int deep = event.query().medium("deep", -1);
			int sort = event.query().medium("sort", 0);
			boolean time = event.query().bit("time", false);
			String algo = event.query().string("algo", "SHA");

			if(size > 50)
				size = 50;

			for(int i = 4; i < path.length; i++) {
				last += path[i];

				if(i < path.length - 1)
					last += "/";
			}

			if(rule.equals("node") && head.equals("user") && (tail.equals("name") || tail.equals("mail") || tail.equals("id"))) {
				event.reply().code("403 Forbidden");
				event.output().print("<pre>Public node/user/" + tail + " is forbidden.</pre>");
				throw event;
			}

			if(rule.equals("link") && head.equals("user") && tail.equals("date") && last.matches("[0-9]+")) {
				event.reply().code("403 Forbidden");
				event.output().print("<pre>Public link/user/date is forbidden.</pre>");
				throw event;
			}

			boolean remove = false; // remove key from node

			try {
				if(rule.equals("link")) {
					if(last.length() > 0) {
						full = home() + "/link/" + head + "/" + tail + Root.path(last);

						if(last.matches("[0-9]+")) {
							full = home() + "/node/" + head + "/id" + Root.path(last, 3);

							File file = new File(full);

							if(file.exists()) {
								JSONObject json = new JSONObject(Root.file(file));
								full = home() + "/link/" + head + "/" + tail + Root.path(json.getString("key"));
								remove = true;
							}
						}
					}
					else {
						event.reply().code("400 Bad Request");
						event.output().print("<pre>Sort without key.</pre>");
						throw event;
					}

					File file = new File(full);

					if(file.exists()) {
						RandomAccessFile raf = new RandomAccessFile(file, "rw");
						write_last(event, tail, raf, head, last, from, size, remove, algo);
						raf.close();
					}
					else {
						writeArray(event, null, 0);
					}
				}
				else if(rule.equals("meta")) {
					if(last.length() == 0)
						fail(2, event, full, rule, head, tail, last);

					String[] tree = last.split("/");
					JSONObject json = null;
					String key = null;

					/* this looks nasty, but it's simply
					 * ways to get data with id/key/name
					 * combinations:
					 *
					 * /meta/<head>/<tail>/<id>
					 * /meta/<head>/<tail>/<id>/<id>
					 * /meta/<head>/<tail>/<id>/<name>
					 * /meta/<head>/<tail>/<name>/<id>
					 * /meta/<head>/<tail>/<name>/<name>
					 * /meta/<head>/<tail>/<key>
					 * /meta/<head>/<tail>/<key>/<name>
					 *
					 * more will be added as needed.
					 */
					if(tree[0].matches("[0-9]+")) { // parent id
						long id = Long.parseLong(tree[0]);
						json = new JSONObject(file(home() + "/node/" + head + "/id" + Root.path(id)));
						key = json.getString("key");

						if(tree.length == 1) {
							full = home() + "/meta/" + head + "/" + tail + Root.path(key);
						}
						else if(tree.length > 1) {
							if(tree[1].matches("[0-9]+")) { // child id
								long id2 = Long.parseLong(tree[1]);
								JSONObject json2 = new JSONObject(file(home() + "/node/" + tail + "/id" + Root.path(id2)));
								String key2 = json2.getString("key");

								full = home() + "/meta/" + head + "/" + tail + "/" + Root.path(key) + "/" + key2;
							}
							else {
								full = home() + "/meta/" + head + "/" + tail + "/" + Root.path(key) + "/" + tree[1];
							}
						}
					}
					else if(tree[0].length() < LENGTH) { // parent name
						json = new JSONObject(file(home() + "/node/" + head + "/name" + Root.path(tree[0])));
						key = json.getString("key");

						if(tree.length == 1) {
							full = home() + "/meta/" + head + "/" + tail + Root.path(key);
						}
						else if(tree.length > 1) {
							if(tree[1].matches("[0-9]+")) { // child id
								long id = Long.parseLong(tree[1]);
								json = new JSONObject(file(home() + "/node/" + tail + "/id" + Root.path(id)));
								full = home() + "/meta/" + head + "/" + tail + "/" + Root.path(key) + "/" + json.getString("key");
							}
							else {
								full = home() + "/meta/" + head + "/" + tail + "/" + Root.path(key) + "/" + tree[1];
							}
						}
					}
					else { // parent key
						full = home() + "/meta/" + head + "/" + tail + Root.path(last);
					}

					File file = new File(full);

					if(file.exists() && file.isFile()) {
						json = new JSONObject(file(file));

						if(json.has("key"))
							json.remove("key");

						writeObject(event, json);
					}
					else {
						int length = 0;
						JSONArray arr = new JSONArray();

						if(file.exists() && file.isDirectory()) {
							boolean secure = last.length() == 16 && last.indexOf("/") == -1 && !last.matches("[0-9]+");
							arr = recurse(file, full, from, size, 0, deep, sort, secure, time);
							File[] files = file.listFiles();

							for(int i = 0; i < files.length; i++) {
								Path p = Paths.get(full + "/" + files[i].getName() + "/root");

								if(files[i].isFile())
									length++;
								else if(files[i].isDirectory() && Files.exists(p))
									length++;
							}
						}

						writeArray(event, arr, length);
					}
				}
				else { // node
					full = home() + "/node/" + head + "/" + tail + "/" + last;

					String decoded = decoder.decode(last, "UTF-8");

					if(tail.equals("word")) { // full word search
						full = home() + "/node/" + head + "/" + tail + "/";
						remove = true;

						if(last.contains(" ")) {
							String[] word = last.split("\\s+");

							RandomAccessFile one = new RandomAccessFile(full + word[0], "r");
							RandomAccessFile two = new RandomAccessFile(full + word[1], "r");

							List list = first_compare(one, two);

							for(int i = 2; i < word.length; i++) {
								RandomAccessFile next = new RandomAccessFile(full + word[i], "r");

								list = remove(next, list);

								next.close();
							}

							print_list(event, head, list, null, null, list.size(), remove, algo);

							one.close();
							two.close();
						}
						else {
							full = home() + "/node/" + head + "/" + tail + "/";

							RandomAccessFile raf = new RandomAccessFile(full + last, "r");

							write_last(event, head, raf, null, null, from, size, remove, algo);

							raf.close();
						}
					}
					else { // node sort index
						if(last.matches("[a-zA-Z0-9.@\\-\\+]+"))
							full = home() + "/node/" + head + "/" + tail + Root.path(last);

						if(last.matches("[0-9]+")) {
							full = home() + "/node/" + head + "/" + tail + Root.path(Long.parseLong(last));
							remove = true;
						}

						if(last.contains("/"))
							remove = true;

						File file = new File(full);

						if(file.exists()) {
							JSONObject obj = new JSONObject(file(file));

							if(remove) {
								obj.put("id", hash(obj.getString("key")));
								obj.remove("key");
							}

							if(!obj.has("date"))
								obj.put("date", file.lastModified());

							writeObject(event, obj);
						}
						else {
							writeObject(event, new JSONObject());
						}
					}
				}
			}
			catch(Exception e) {
				e.printStackTrace();
				fail(3, event, full, rule, head, tail, last);
			}
		}

		private void write_last(Event event, String type, RandomAccessFile file, String poll, String last, int from, int size, boolean remove, String algo) throws Event, Exception {
			LinkedList list = new LinkedList();
			int length = (int) (file.length() > 8 * size ? 8 * size : file.length());
			long start = length + from * 8;

			if(start > file.length()) {
				start = file.length();
			}

			byte[] data = new byte[length];
			file.seek(file.length() - start);
			int read = file.read(data);
			ByteBuffer buffer = ByteBuffer.wrap(data); // TODO: add 0, read?

			for(int i = 0; i < size; i++) {
				if(buffer.remaining() > 0)
					list.addFirst(new Long(buffer.getLong()));
				else
					break;
			}

			print_list(event, type, list, poll, last, file.length() / 8, remove, algo);
		}

		private void print_list(Event event, String type, List list, String poll, String last, long total, boolean remove, String algo) throws Event, Exception {
			boolean secure = poll != null && last != null && last.matches("[0-9]+") && type.equals("user");
			String key = "";

			if(secure) {
				event.query().parse();

				String hash = event.string("hash");
				String salt = event.string("salt");

				if(Salt.salt.containsKey(salt)) {
					Salt.salt.remove(salt);
				}
				else {
					event.reply().code("400 Bad Request");
					event.output().print("<pre>Salt not found.</pre>");
					throw event;
				}

				JSONObject object = new JSONObject(file(home() + "/node/" + poll + "/id/" + Root.path(last, 3)));
				key = object.getString("key");

				String match = Deploy.hash(key + salt, algo);

				if(!hash.equals(match)) {
					event.reply().code("400 Bad Request");
					event.output().print("<pre>Auth did not match.</pre>");
					throw event;
				}
			}

			StringBuilder builder = new StringBuilder();
			builder.append("{\"total\": " + total + ", \"list\":[");

			Iterator it = list.iterator();

			while(it.hasNext()) {
				long id = ((Long) it.next()).longValue();

				if(type.equals("date")) {
					builder.append(id);
				}
				else {
					String open = home() + "/node/" + type + "/id" + Root.path(id);

					try {
						JSONObject obj = new JSONObject(file(open));

						if(remove) {
							obj.put("id", hash(obj.getString("key")));
							obj.remove("key");
						}

						//if(type.equals("user"))
						// obj.remove("pass");

						builder.append(obj.toString(4));
					}
					catch(Exception e) {
						System.out.println("File " + open + " not found.");
					}
				}

				if(it.hasNext())
					builder.append(",");
			}

			builder.append("]}");

			String result = builder.toString();

			if(secure) {
				event.reply().header("Hash", Deploy.hash(result + key, algo));
			}

			event.reply().type("application/json; charset=UTF-8");
			byte[] data = result.getBytes("UTF-8");
			Output out = event.reply().output(data.length);
			out.write(data);
		}

		private void fail(int spot, Event event, String path, String rule, String head, String tail, String last) throws Event, Exception {
			event.reply().code("404 Not Found");
			//event.output().print("<pre>" + toCase(rule) + " '" + event.query().path() + "' was not found on host " + local + ".</pre>");
			event.output().print(Event.warn(toCase(rule) + " '" + event.query().path() + "' was not found on host " + local + "."));

			JSONObject obj = new JSONObject("{\"spot\":\"" + spot + "\",\"path\":\"" + path + "\",\"rule\":\"" + rule + "\",\"head\":\"" + head + "\",\"tail\":\"" + tail + "\",\"last\":\"" + last + "\"}");
			System.out.println(obj.toString(4));
//System.out.println("YO");
			throw event;
		}

		public final static String toCase(String name) {
			return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
		}
	}

	public static class Link extends Service {
		public String path() {
			return "/link";
		}

		public void filter(Event event) throws Event, Exception {
			if(event.push()) {
				for(int i = 0; i < host.length; i++) {
					event.query().put(host[i], "");
				}

				Output out = event.output();
				out.println(event.query().string("result"));
				out.finish();
				out.flush();
			}
			else {
				event.query().parse();
				String ptype = event.string("ptype");
				String ctype = event.string("ctype");
				String pkey = event.string("pkey");
				String ckey = event.string("ckey");

				if(event.query().method() == Query.GET) {
					Output out = event.output();
					out.println("<style> input, select { font-family: monospace; } </style>");
					out.println("<pre>");
					out.println("<form action=\"link\" method=\"post\">");
					out.println(" Parent <input type=\"text\" size=\"16\" name=\"pkey\"> " + type(node_type, "ptype", "user") + "<br>");
					out.println(" Child <input type=\"text\" size=\"16\" name=\"ckey\"> " + type(link_type, "ctype", "data") + "<br>");
					out.println(" <input type=\"submit\" value=\"Link\">");
					out.println("</form>");
					out.println("</pre>");
					throw event;
				}
				else {
					JSONObject json = new JSONObject(
							"{\"parent\":{\"key\":\"" + pkey + "\",\"type\":\"" + ptype + "\"}," +
									"\"child\":{\"key\":\"" + ckey + "\",\"type\":\"" + ctype + "\"}}");
					event.query().put("json", json);
					link(json, event);
				}
			}
		}
	}

	/* Use this to build tree structured comments.
	 * Requires a "user" key with the users name in the child node json.
	 */
	public static class Tree extends Service {
		public String path() {
			return "/tree";
		}

		public void filter(Event event) throws Event, Exception {
			if(event.push()) {
				for(int i = 0; i < host.length; i++) {
					event.query().put(host[i], "");
				}

				meta((JSONObject) event.query().get("json"), null);

				Output out = event.output();
				out.println(event.query().string("result"));
				out.finish();
				out.flush();
			}
			else {
				event.query().parse();

				long node = event.big("node");
				String type = event.string("type");
				String user = event.string("user");
				boolean tear = event.query().bit("tear", false); // delete
				String path = event.string("path");
				String data = event.string("json");

				if(event.query().method() == Query.GET) {
					Output out = event.output();
					out.println("<style> input, select { font-family: monospace; } </style>");
					out.println("<pre>");
					out.println("<form action=\"meta\" method=\"post\">");
					out.println(" Node <input type=\"text\" size=\"16\" name=\"node\"> " + type(node_type, "type", "task") + "<br>");
					out.println(" User <input type=\"text\" size=\"16\" name=\"user\"><br>");
					out.println(" <input type=\"checkbox\" name=\"tear\"> Tear (Delete!)<br>");
					out.println(" <textarea rows=\"10\" cols=\"50\" name=\"json\">{}</textarea><br>");
					out.println(" Path <input type=\"text\" name=\"path\" value=\"\"><br>");
					out.println(" <input type=\"submit\" value=\"Meta\">");
					out.println("</form>");
					out.println("</pre>");
					throw event;
				}
				else {
					if(path.length() == 0)
						throw new Exception("Path '" + path + "' is too short.");

					if(path.split("/").length < 2)
						throw new Exception("Path '" + path + "' needs atleast one folder.");

					if(path.endsWith("/"))
						throw new Exception("Path '" + path + "' must not end with /.");

					if(!path.startsWith("/"))
						throw new Exception("Path '" + path + "' must begin with /.");

					JSONObject root = new JSONObject(file(home() + "/node/user/key" + Root.path(user)));

					String end = "/" + root.getString("name") + "/root";

					JSONObject file = new JSONObject(file(home() + "/node/" + type + "/id" + Root.path(node)));

					String ckey = file.getString("key");

					if(!path.endsWith(end)) {
						throw new Exception("Path '" + path + "' must end with user name.");
					}

					String name = Root.path(file.getString("user"));

					root = new JSONObject(file(home() + "/node/user/name" + name));

					String pkey = root.getString("key");

					File folder = new File(home() + "/meta/user/" + type + Root.path(pkey) + "/" + ckey + path.substring(0, path.length() - end.length()));

					if(!folder.exists()) {
						throw new Exception("Path '" + path + "' parent folder not found.");
					}

					JSONObject json = new JSONObject(
							"{\"parent\":{\"key\":\"" + pkey + "\",\"type\":\"user\"}," +
									"\"child\":{\"key\":\"" + ckey + "\",\"type\":\"" + type + "\"}," +
									"\"json\":" + data + ",\"tear\":" + tear + ",\"path\":\"" + path + "\"}");
					event.query().put("json", json);
					meta(json, event);
				}
			}
		}
	}

	public static class Meta extends Service {
		public String path() {
			return "/meta";
		}

		public void filter(Event event) throws Event, Exception {
			if(event.push()) {
				for(int i = 0; i < host.length; i++) {
					event.query().put(host[i], "");
				}

				meta((JSONObject) event.query().get("json"), null);

				Output out = event.output();
				out.println(event.query().string("result"));
				out.finish();
				out.flush();
			}
			else {
				event.query().parse();
				String ptype = event.string("ptype");
				String ctype = event.string("ctype");
				String pkey = event.string("pkey");
				String ckey = event.string("ckey");
				boolean echo = event.query().bit("echo", false);
				boolean tear = event.query().bit("tear", false); // delete
				String path = event.string("path");
				String data = event.string("json");

				if(path.length() > 0) {
					if(path.endsWith("/"))
						throw new Exception("Path '" + path + "' must not end with /.");

					if(!path.startsWith("/"))
						throw new Exception("Path '" + path + "' must begin with /.");
				}

				if(event.query().method() == Query.GET) {
					Output out = event.output();
					out.println("<style> input, select { font-family: monospace; } </style>");
					out.println("<pre>");
					out.println("<form action=\"meta\" method=\"post\">");
					out.println(" Parent <input type=\"text\" size=\"16\" name=\"pkey\"> " + type(node_type, "ptype", "user") + "<br>");
					out.println(" Child <input type=\"text\" size=\"16\" name=\"ckey\"> " + type(meta_type, "ctype", "data") + "<br>");
					out.println(" <input type=\"checkbox\" name=\"echo\"> Echo<br>");
					out.println(" <input type=\"checkbox\" name=\"tear\"" + (tear ? " checked" : "") + "> Tear (Delete!)<br>");
					out.println(" <textarea rows=\"10\" cols=\"50\" name=\"json\">{}</textarea><br>");
					out.println(" Path <input type=\"text\" name=\"path\" value=\"\"><br>");
					out.println(" <input type=\"submit\" value=\"Meta\">");
					out.println("</form>");
					out.println("</pre>");
					throw event;
				}
				else {
					JSONObject json = new JSONObject(
							"{\"parent\":{\"key\":\"" + pkey + "\",\"type\":\"" + ptype + "\"}," +
									"\"child\":{\"key\":\"" + ckey + "\",\"type\":\"" + ctype + "\"}," +
									"\"echo\":" + echo + ",\"tear\":" + tear +
									",\"path\":\"" + path + "\"}");

					if(data.length() > 0)
						json.put("json", new JSONObject(data));

					event.query().put("json", json);
					meta(json, event);
				}
			}
		}
	}

	public static class Salt extends Service {
		static HashMap salt = new HashMap();

		public String path() {
			return "/salt";
		}

		public void filter(Event event) throws Event, Exception {
			String salt = Event.random(4);

			while(this.salt.containsKey(salt)) {
				salt = Event.random(4);
			}

			this.salt.put(salt, null);
			event.output().print(salt);
		}
	}

	public static class Hash extends Service {
		public String path() {
			return "/hash";
		}

		public void filter(Event event) throws Event, Exception {
			event.query().parse();
			String key = event.string("key");
			Output out = event.output();

			if(key.length() == 0) {
				out.println("<style> input { font-family: monospace; } </style>");
				out.println("<pre>");
				out.println("<form action=\"hash\" method=\"get\">");
				out.println(" Key <input type=\"text\" size=\"16\" name=\"key\" value=\"" + key + "\"> <input type=\"submit\" value=\"Hash\">");
				out.println("</form>");
				out.println("</pre>");
			}
			else
				out.println("<pre>" + hash(key) + "</pre>");
		}
	}

	public static class Node extends Service {
		public String path() { return "/make:/node"; }
		public void filter(final Event event) throws Event, Exception {
			if(event.push()) {
							/* This stuff should be done in the read method on the Async.Work
							instead since here it manages to block wakeups from writing if many
							nodes have the same latency. Up to 3 nodes with similar latency works
							fine, and many more with varying latencies.
							 */

				JSONObject json = json(event);

																//System.err.println(json);

																int working = 0, complete = 0, collision = 0, failed = 0;
				long time = event.big("time");
				boolean trace = event.query().bit("trace", true);

				if(trace)
					event.output().println(" < " + event.string("wakeup") + " " + (System.currentTimeMillis() - time) + " ms.");

				for(int i = 0; i < host.length; i++) {
					if(!host[i].equals(local)) {
						String result = event.string(host[i]);
						String key = event.string("in_" + host[i] + "_key");

						if(trace)
							event.output().println(" " + host[i] + " " + result + " " + (result.equals("0") || result.equals("1") ? "(" + key + ") " : ""));

						/* "" = ready for write
						 * "2" = writing
						 * "1" = write completed successfully
						 * "0" = write conflict
						 * "-1" = write fail (remote error) // deprecated
						 * "-2" = resend
						 */
						if(result.equals("2"))
																												working++;
																								else if(result.equals("1"))
							complete++;
						else if(result.equals("0")) {
							event.query().put(host[i], "");
							if(json.getString("key").equals(key)) {
								if(trace)
									event.output().println(" collision match");

								collision++;
							}
							else {
								if(trace)
									event.output().println(" collision fail");

								//failed++;
							}
						}
						else if(result.length() > 0) {
							if(result.equals("-2"))
								event.query().put(host[i], "");
							if(result.equals("java.nio.channels.ClosedChannelException"))
								event.query().put(host[i], "-2");
							if(result.startsWith("Root$SortFail")) {
								event.output().print(result + "[" + local + "]");
								event.output().finish();
								event.output().flush();
								throw event;
								//throw new SortException(result.substring(result.indexOf(": ") + 2));
							}
							if(result.startsWith("java.")) {
								event.output().print(result + "[" + local + "]");
								event.output().finish();
								event.output().flush();
								throw event;
								//throw new Exception("Node [" + host[i] + "] failed: " + result);
							}

							failed++;
						}
					}
				}

				if(trace)
					event.output().flush();

				if(complete == host.length - 1) {
					boolean create = event.query().bit("create", true);

					store(json, event.query().string("type", "user"),
							event.query().string("sort", "key"),
							event.query().path().equals("/make") || create,
							((String) event.query().header("host")).equals("test.rupy.se"));

					for(int i = 0; i < host.length; i++) {
						event.query().put(host[i], "");
					}

					if(!trace)
						event.reply().type("application/json; charset=UTF-8");

					byte[] data = json.toString(4).getBytes("UTF-8");
					Output out = event.reply().output(data.length);

					out.write(data);
					out.finish();
					out.flush();
				}
				else if(failed > 0 || collision > 0) {
					if(collision > 0)
						for(int i = 0; i < host.length; i++) {
							String state = event.string(host[i]);
							if(state.equals("0") || state.equals("1") || state.equals("2")) {
								if(trace) {
									event.output().println(" invalidated " + host[i] + " " + state);
									event.output().flush();
								}

								event.query().put(host[i], "");
							}
						}

					//async(event, collision > 0);
					async(event);
				}

																//System.err.println("done");
												}
			else {
				event.hold();
				event.query().parse(10240);
				event.query().put("time", System.currentTimeMillis());

				boolean trace = event.query().bit("trace", true);
				boolean create = event.query().bit("create", true);

				if(trace)
					event.output().println("<pre>");

				if(event.query().path().equals("/node")) {
					if(event.query().method() == Query.GET) {
						boolean make = event.query().bit("make", true);

						int user = type(node_type, "user");

						Output out = event.output();
						out.println("<script>");
						out.println(" function toggle() {");
						out.println(" var make = document.getElementById('make').checked;");
						out.println(" document.getElementById('node').value = (make ? 'Make' : 'Edit');");
						out.println(" var select = document.getElementById('type');");
						out.println(" if(make) {");
						out.println(" select.remove(" + user + ");");
						out.println(" } else {");
						out.println(" var option = document.createElement('option');");
						out.println(" option.text = 'user';");
						out.println(" select.options.add(option, select.options[" + user + "]);");
						out.println(" }");
						out.println(" }");
						out.println("</script>");
						out.println("<style> input, select { font-family: monospace; } </style>");
						out.println("<pre>");
						out.println("<form action=\"node\" method=\"post\">");
						out.println(" <textarea rows=\"10\" cols=\"50\" name=\"json\">{}</textarea><br>");
						out.print(" Type " + type(node_type, "type", "data"));
						out.print(" <input id=\"make\" type=\"checkbox\" name=\"create\" onclick=\"toggle();\"/> Make");
						out.println(" <input type=\"checkbox\" name=\"trace\"> Info<br>");
						out.println(" Comma separated list of JSON keys to index;");
						out.println(" \"word\" key reserved for full word search");
						out.println(" (min. 4 characters). \"name\" key reserved");
						out.println(" for relational mapping with <i>/meta</i>.<br>");
						out.println(" Sort <input type=\"text\" name=\"sort\" value=\"key\"><br>");
						out.println(" <input id=\"node\" type=\"submit\" value=\"Edit\">");
						out.println("</form>");
						out.println("</pre>");

						if(make)
							out.println("<script>document.getElementById('make').checked = true; toggle();</script>");

						out.finish();
						out.flush();
						throw event;
					}
				}

				try {
					//async(event, event.query().path().equals("/make") || create);
					async(event);
				}
				catch(Exception e) {
					e.printStackTrace();
					event.output().print(e + "[" + local + "]");
					event.output().finish();
					event.output().flush();
					throw event;
				}
			}
		}

		private void async(Event event) throws Exception {
			boolean create = event.query().bit("create", true);
			if(event.query().path().equals("/make"))
				create = true;
			JSONObject json = Root.sync(event, event.query().string("type", "user"), event.query().string("sort", "key"), create);
			event.query().put("json", json);
		}
	}
}