package timingexp;

import Tools.*;
import java.util.*;

/**
 * GenerateTestCases is the master test harness for the timings
 * in the Measurement chapter of my thesis.
 * It produces all permutations of variables in several
 * dimensions, then kicks out the cases we can't or don't want to test
 * (not applicable, unimplemented, or too slow). The resulting list is
 * indexed by "test case number," so we can specify an integer to skip
 * over some preceding number of tests.<p>
 *
 * The Overview for this manual tells how to reproduce specific
 * experiments from the dissertation.
 */
public class GenerateTestCases {
	public static void main(String[] args) {
		(new GenerateTestCases()).dmain(args);
	}

	public void dmain(String[] args) {
		try {
			Options opts = new Options(args) {
				public void defineOptions() {
				programName = "GenerateTestCases";
				defineOption("runTests",
					"don't just describe them, run them", "false");
				defineOption("printTests",
					"describe each generated test", "true");
				defineOption("skipToTest",
					"skip over tests with a smaller number", "0");
				defineOption("stopAtTest",
					"run this test last", "-1");
				defineOption("testList",
					"comma-separated list of tests to run", null);
				defineOption("expBase",
					"base experiment number", "0");
				defineOption("mode",
					"which set of axes to explore", null);
				}
			};

			String[] logcases = new String[] {
				"000100", "000114", "000131", "000151", "000173",
				"000199", "000229", "000263", "000301", "000346",
				"000398", "000457", "000524", "000602", "000691",
				"000794", "000912", "001047", "001202", "001380",
				"001584", "001819", "002089", "002398", "002754",
				"003162", "003630", "004168", "004786", "005495",
				"006309", "007244", "008317", "009549", "010964",
				"012589", "014454", "016595", "019054", "021877",
				"025118", "028840", "033113", "038018", "043651",
				"050118", "057543", "066069", "075857", "087096",
				"100000" };
			String[] smalllog = new String[] {
				"000100", "000199", "000398", "000794", "001584",
				"003162", "006309", "012589", "025118", "050118",
				"100000" };
			String[] tinylog = new String[] {
				"000100", "000398", "001584",
				"006309", "025118", "100000" };
			String[] smalllin = new String[] {
				"000100", "010090", "020080", "030070", "040060",
				"050050", "060040", "070030", "080020", "090010",
				"100000" };
			String[] rmilogcases = new String[] {
				"000100", "000114", "000131", "000151", "000173",
				"000199", "000229", "000263", "000301", "000346",
				"000398", "000457", "000524", "000602", "000691",
				"000794", "000912", "001047", "001202", "001380",
				"001584", "001819", "002089", "002398", "002754",
				"003162", "003630", "004168", "004786", "005495",
				"006309", "007244", "008317", "009549", "010964",
				"012589", "014454", "016595", "019054", "021877",
				"025118", "028840", "033113", "038018", "043651",
				"050118", "057543" };
			String[] rmismalllog = new String[] {
				"000100", "000199", "000398", "000794", "001584",
				"003162", "006309", "012589", "025118", "050118" };

	
			// Describe the option dimensions
			// The minor axis is last, so we'll iterate all cases of it
			// before moving on to a more major (earlier) axis.
			String mode = opts.get("mode");
			if ("snowflake-authz".equals(mode)) {
				axis("useSnowflake", new String[] {"true"});
				axis("fileLength", smalllog);
				axis("socket", new String[] {"plain"});
				axis("server", new String[] {"simple"});
				axis("protocol", new String[] {"1.0"});
				axis("numberOfConnections", new int[] {10});
				axis("requestsPerConnection",
					new int[] {1});
				axis("useMacs", new String[] {"true", "false"});
				axis("identicalRequests", new String[] {"true", "false"});
				axis("authenticateServer", new String[] {"false"});
				axis("signFiles", new String[] {"false"});
				axis("cacheSigns", new String[] {"false"});
			} else if ("snowflake-authzlin".equals(mode)) {
				axis("useSnowflake", new String[] {"true"});
				axis("fileLength", smalllin);
				axis("socket", new String[] {"plain"});
				axis("server", new String[] {"simple"});
				axis("protocol", new String[] {"1.0"});
				axis("numberOfConnections", new int[] {10});
				axis("requestsPerConnection",
					new int[] {1});
				axis("useMacs", new String[] {"true", "false"});
				axis("identicalRequests", new String[] {"true", "false"});
				axis("authenticateServer", new String[] {"false"});
				axis("signFiles", new String[] {"false"});
				axis("cacheSigns", new String[] {"false"});
			} else if ("snowflake-signs".equals(mode)) {
				axis("useSnowflake", new String[] {"true"});
				axis("fileLength", tinylog);
				axis("socket", new String[] {"plain"});
				axis("server", new String[] {"simple"});
				axis("protocol", new String[] {"1.0"});
				axis("numberOfConnections", new int[] {10});
				axis("requestsPerConnection",
					new int[] {1});
				axis("useMacs", new String[] {"true"});
				axis("identicalRequests", new String[] {"true"});
				axis("authenticateServer", new String[] {"true", "false"});
				axis("signFiles", new String[] {"true", "false"});
				axis("cacheSigns", new String[] {"true", "false"});
			} else if ("snowflake-weird".equals(mode)) {
				// the weird case where the curve isn't linear. Explore it more.
				axis("useSnowflake", new String[] {"true"});
				axis("fileLength", logcases);
				axis("socket", new String[] {"plain"});
				axis("server", new String[] {"simple"});
				axis("protocol", new String[] {"1.0"});
				axis("numberOfConnections", new int[] {10});
				axis("requestsPerConnection",
					new int[] {1});
				axis("useMacs", new String[] {"true"});
				axis("identicalRequests", new String[] {"true"});
				axis("authenticateServer", new String[] {"false"});
				axis("signFiles", new String[] {"true"});
				axis("cacheSigns", new String[] {"true"});
			} else if ("apache".equals(mode)) {
				axis("useSnowflake", new String[] {"false"});
				axis("server", new String[] {"apache","simple","Jetty"});
				axis("fileLength", smalllog);
				axis("socket", new String[] {"plain"});
				axis("protocol", new String[] {"1.0"});
				axis("numberOfConnections", new int[] {20});
				axis("requestsPerConnection",
					new int[] {1});
				axis("cacheContext", new String[] {"true"});
				axis("cacheSessions", new String[] {"true"});
			} else if ("SSL".equals(mode)) {
				axis("useSnowflake", new String[] {"false"});
				axis("server", new String[] {"apache","simple","Jetty"});
				axis("fileLength", tinylog);
				axis("socket", new String[] {"SSL"});
				axis("protocol", new String[] {"1.0"});
				axis("numberOfConnections", new int[] {10});
				axis("requestsPerConnection",
					new int[] {1});
				axis("cacheContext", new String[] {"true","false"});
				axis("cacheSessions", new String[] {"true","false"});
			} else if ("sslbw11".equals(mode)) {
				axis("cacheContext", new String[] {"true","false"});
				axis("cacheSessions", new String[] {"true","false"});
				axis("useSnowflake", new String[] {"false"});
				axis("server", new String[] {"apache","simple","Jetty"});
				axis("fileLength", tinylog);
				axis("socket", new String[] {"SSL"});
				axis("protocol", new String[] {"1.1"});
				axis("numberOfConnections", new int[] {10});
				axis("requestsPerConnection",
					new int[] {1});
			} else if ("SSLreq".equals(mode)) {
				axis("requestsPerConnection",
					new int[] {1,2,5,10,20});
				axis("numberOfConnections", new int[] {10});
				axis("useSnowflake", new String[] {"false"});
				axis("server", new String[] {"apache","Jetty"});
				axis("fileLength", new String[] {"000100","006309"});
				axis("socket", new String[] {"SSL"});
				axis("protocol", new String[] {"1.1"});
				axis("cacheContext", new String[] {"true"});
				axis("cacheSessions", new String[] {"true"});
			} else if ("11req".equals(mode)) {
				axis("useSnowflake", new String[] {"false"});
				axis("server", new String[] {"apache","Jetty"});
				axis("fileLength", new String[] {"000100","006309"});
				axis("socket", new String[] {"plain"});
				axis("protocol", new String[] {"1.1"});
				axis("numberOfConnections", new int[] {10});
				axis("requestsPerConnection",
					new int[] {1,2,5,10,20});
				axis("cacheContext", new String[] {"true"});
				axis("cacheSessions", new String[] {"true"});
			} else if ("SSLsetup".equals(mode)) {
				axis("useSnowflake", new String[] {"false"});
				axis("server", new String[] {"Jetty"});
				axis("fileLength", new String[] {"000100"});
				axis("socket", new String[] {"SSL"});
				axis("protocol", new String[] {"1.1"});
				axis("numberOfConnections", new int[] {8});
				// axis("requestsPerConnection",
				// 		new int[] {1,2,5,10,20});
				axis("requestsPerConnection",
					new int[] {1,2,5,7,10,13,15,17,20});
				axis("cacheContext", new String[] {"true","false"});
				axis("cacheSessions", new String[] {"true","false"});
			} else if ("sslreq3".equals(mode)) {
				axis("requestsPerConnection",
					new int[] {1,2,5,10,15,20});
				axis("numberOfConnections", new int[] {10});
				axis("useSnowflake", new String[] {"false"});
				axis("server", new String[] {"apache","Jetty"});
				axis("fileLength", new String[] {"000100"});
				axis("socket", new String[] {"SSL"});
				axis("protocol", new String[] {"1.1"});
				axis("cacheContext", new String[] {"true"});
				axis("cacheSessions", new String[] {"true"});
			} else if ("rmibw".equals(mode)) {
				axis("experimentType", "RMIExp");
				// axis("fileLength", rmilogcases);
				axis("fileLength", smalllog);
				axis("numberOfConnections", new int[] {100});
				axis("registryService", new String[] {
					"TestRMIServer0",
					// "TestRMIServer1",	// SSL
					"TestRMIServer2",
					"TestRMIServer3" });
				axis("requestsPerConnection", new int[] {1});
			} else if ("rmisetup".equals(mode)) {
				axis("experimentType", "RMIExp");
				axis("fileLength", tinylog);
				axis("numberOfConnections", new int[] {15});
				axis("registryService", new String[] { "TestRMIServer3" });
				axis("requestsPerConnection", new int[] {1});
				axis("clientCachesProofs", new String[] {"false", "true"});
				axis("serverCachesProofs", new String[] {"false", "true"});
			} else if ("rmisetup2".equals(mode)) {
				axis("experimentType", "RMIExp");
				axis("fileLength", tinylog);
				axis("numberOfConnections", new int[] {15});
				axis("registryService", new String[] {
					"TestRMIServer2", "TestRMIServer3" });
				axis("requestsPerConnection", new int[] {1});
				axis("clientCachesProofs", new String[] {"false", "true"});
				axis("serverCachesProofs", new String[] {"false", "true"});
				axis("cacheRMIConnections", new String[] {"false", "true"});
				axis("borrowingAllowed", new String[] {"false", "true"});
			} else {
				throw new RuntimeException("undefined mode");
			}
			// axis("requestsPerConnection",
				// new int[] {1,2,5,10,20,50,100,200,500,1000});
				// new int[] {1,5,10,50,100,500,1000});
			// axis("useSnowflake", new String[] {"true", "false"});
			// axis("fileLength", new int[] {100,1000,10000,100000});
			// axis("numberOfConnections", new int[] {1,5,10,50,100,500,1000});
			// axis("cacheContext", new String[] {"true", "false"});
	
	//		for (int i=0; i<axes.size(); i++) {
	//			System.out.println(axes.get(i));
	//		}
	
			// Generate the x1*x2*x3*...xn cases
			Vector cases = generateCases(0);
	
			System.out.println("Generated "+cases.size()+" cases.");
	
			// Filter cases
			// return 'true' for invalid cases
			// f0: several variables are meaningless in the Snowflake case,
			// so don't replicate them.
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"useSnowflake","true")
					&& (fEq(v,"socket", "SSL")
						|| fEq(v,"protocol", "1.1")
						|| !fEq(v,"server", "simple"))
						);
			}});
			// f1: don't suck too much data in a single connection
			filter(new Filter() { boolean f(Vector v) {
				return (fInt(v,"requestsPerConnection")
					*fInt(v,"numberOfConnections") > 200);
			}});
			// f2: don't make too many SSL connections
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"socket", "SSL")
					&& fInt(v,"numberOfConnections")>100);
			}});
			// f3: 1.0 protocol can't have more than one request per connection
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"protocol","1.0")
					&& fInt(v,"requestsPerConnection")>1);
			}});
			// f4: simple server can't handle 1.1 protocol
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"protocol","1.1")
					&& fEq(v,"server", "simple"));
			}});
			// f6: if cacheContext is false, cacheSessions=true is meaningless
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"cacheContext","false"))
					&& (fEq(v,"cacheSessions","true"));
			}});
			// f7: if identicalRequests==true, results don't depend on useMacs
			// (except for time saved initially by not asking for a MAC.)
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"identicalRequests","true"))
					&& (fEq(v,"useMacs","false"));
			}});
			// f8: if signFiles=false, results don't depend on cacheSigns.
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"signFiles","false"))
					&& (fEq(v,"cacheSigns","true"));
			}});
			// f9: if authenticateServer is true, signFiles must be true;
			// otherwise we're measuring an uninteresting exceptional
			// condition.
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"authenticateServer","true"))
					&& (fEq(v,"signFiles","false"));
			}});
			// Snowflake/RMI/ssh: if server caches proofs, client doesn't
			// have to, so client=false, server=true is a duplicate of
			// client=true, server=true
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"clientCachesProofs","false"))
					&& (fEq(v,"serverCachesProofs","true"));
			}});
			// if we caching RMI connections, borrowing has no effect since
			// it can only happen in the warm-up period.
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"cacheRMIConnections","true"))
					&& (fEq(v,"borrowingAllowed","false"));
			}});
			// Don't look at cases where something is happening both in
			// Snowflake caching params and in SSH/RMI params
			filter(new Filter() { boolean f(Vector v) {
				return
					(   (fEq(v,"cacheRMIConnections","false"))
					 || (fEq(v,"borrowingAllowed","false")))
				&&	(   (fEq(v,"clientCachesProofs","false"))
					 || (fEq(v,"serverCachesProofs","false")));
			}});
			// TestRMIServer2 doesn't care about Snowflake parameters
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"registryService","TestRMIServer2"))
					&& (fEq(v,"clientCachesProofs","false"));
			}});
			filter(new Filter() { boolean f(Vector v) {
				return (fEq(v,"registryService","TestRMIServer2"))
					&& (fEq(v,"serverCachesProofs","false"));
			}});

			cases = filterCases(cases);
			System.out.println("Filtered to "+cases.size()+" cases.");
	
			// generate port numbers and experiment identifiers
			cases = generateAux(cases, opts.getInt("expBase"));
	
			Vector experimentList = new Vector();
			if (opts.get("testList")!=null) {
				StringTokenizer t =
					new StringTokenizer(opts.get("testList"),",");
				while (t.hasMoreTokens()) {
					experimentList.add(new Integer(t.nextToken()));
				}
			} else {
				int stopAt = opts.getInt("stopAtTest");
				if (stopAt<0) {
					stopAt = cases.size()-1;
				}
				for (int i=opts.getInt("skipToTest"); i<=stopAt; i++) {
					experimentList.add(new Integer(i));
				}
			}

			// Print the cases out for inspection
			Iterator iter = experimentList.iterator();
			while (iter.hasNext()) {
				int i = ((Integer) iter.next()).intValue();

				Vector icase = (Vector) cases.get(i);
				if (opts.getBoolean("printTests")) {
					printCase(icase);
				}
				if (opts.getBoolean("runTests")) {
					Experiment exp;
					String expType = getOneOpt(icase, "experimentType");
					if (expType==null || expType.equals("HttpExp")) {
						exp = new HttpExp();
					} else if (expType.equals("RMIExp")) {
						exp = new RMIExp();
					} else {
						throw new RuntimeException("Undefined experiment type "
							+expType);
					}
					Options caseOpts = makeOpts(exp, icase);
					// caseOpts.dumpOptions();
					// memReport("before");
					exp.runExperiment(caseOpts);
					// memReport("after");
				}
			}
			// printCases(cases);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	void memReport(String desc) {
		System.gc();
		Runtime rt = Runtime.getRuntime();
		System.out.println(desc+": "
			+(rt.totalMemory()-rt.freeMemory())/1024+"K");
	}

	Vector filters = new Vector();

	void filter(Filter f) {
		filters.add(f);
	}

	Vector filterCases(Vector cases) {
		Vector outv = new Vector();
		for (int i=0; i<cases.size(); i++) {
			Vector icase = (Vector) cases.get(i);
			if (i%1000==0) {
				// System.out.println("filtered "+i+" cases");
			}
			boolean discard = false;
			for (int f=0; f<filters.size(); f++) {
				Filter filter = (Filter) filters.get(f);
				if (filter.f(icase)) {
					discard = true;
					break;
				}
			}
			if (!discard) {
				outv.add(icase);
			}
		}
		return outv;
	}

	static abstract class Filter {
		abstract boolean f(Vector v);
	}

	static String fStr(Vector v, String name) {
		for (int i=0; i<v.size(); i++) {
			KeyValue kv = (KeyValue) v.get(i);
			if (kv.key.equals(name)) {
				return (String) kv.value;
			}
		}
		// throw new RuntimeException("no key "+name);
		return "";	// Allow filters to run for undefined keys
	}

	static boolean fEq(Vector v, String name, String value) {
		return fStr(v,name).equals(value);
	}

	static int fInt(Vector v, String name) {
		for (int i=0; i<v.size(); i++) {
			KeyValue kv = (KeyValue) v.get(i);
			if (kv.key.equals(name)) {
				return ((Integer) kv.value).intValue();
			}
		}
		throw new RuntimeException("no key "+name);
	}

	Vector generateAux(Vector cases, int expBase) {
		for (int i=0; i<cases.size(); i++) {
			Vector icase = (Vector) cases.get(i);
			int port;
			// generate port number
			if (fEq(icase, "useSnowflake", "true")) {
				port = 8041;
			} else if (fEq(icase, "server", "apache")) {
				if (fEq(icase, "socket", "plain")) {
					port = 80;
				} else {
					port = 443;
				}
			} else {
				port = 8000;
				if (fEq(icase, "socket", "plain")) {
					port += 80;
				} else {
					port += 43;
				}
				if (fEq(icase, "server", "Jetty")) {
					port += 200;
				} else {
					port += 100;
				}
			}
			icase.add(new KeyValue("port", new Integer(port)));
			// generate file name
			String filename = "/timing/data-"+fStr(icase, "fileLength")+".txt";
			if (fEq(icase, "useSnowflake", "true")) {
				filename = "/files"+filename;
			}
			icase.add(new KeyValue("uri", filename));
			icase.add(new KeyValue("experimentID", new Integer(expBase+i)));
		}
		return cases;
	}

	Vector generateCases(int axis) {
		if (axis==axes.size()-1) {
			KeyValue kv = (KeyValue) axes.get(axis);
			Object[] values = (Object[]) kv.value;
			Vector outv = new Vector();
			for (int i=0; i<values.length; i++) {
				Vector p = new Vector();
				p.add(new KeyValue(kv.key, values[i]));
				outv.add(p);
			}
			return outv;
		}
		Vector out = new Vector();	 		// will be a vector of vectors
		KeyValue cross = (KeyValue) axes.get(axis);
		Object[] values = (Object[]) cross.value;
											// each element is an Object
		Vector in = generateCases(axis+1);
			// each element is a Vector of KeyValues
		for (int elti=0; elti<values.length; elti++) {
			Object o = values[elti];
			KeyValue kv = new KeyValue(cross.key, o);
			for (int ini=0; ini<in.size(); ini++) {
				Vector v = (Vector) in.get(ini);
				Vector outv = new Vector(v);
				outv.add(kv);
				out.add(outv);
			}
		}
		return out;
	}

	void printCases(Vector cases) {
		System.out.println("There are "+cases.size()+" cases:");
		for (int i=0; i<cases.size(); i++) {
			Vector icase = (Vector) cases.get(i);
			printCase(icase);
		}
	}

	void printCase(Vector icase) {
		int experimentID = fInt(icase, "experimentID");
		for (int elt=0; elt<icase.size(); elt++) {
			KeyValue kv = (KeyValue) icase.get(elt);
			System.out.println("EXP "+experimentID+" "+kv.key+" "+kv.value);
		}
	}

	String getOneOpt(Vector icase, String optName) {
		for (int elt=0; elt<icase.size(); elt++) {
			KeyValue kv = (KeyValue) icase.get(elt);
			if (kv.key.equals(optName)) {
				return kv.value.toString();
			}
		}
		return null;
	}

	Options makeOpts(Experiment exp, Vector icase) {
		Options opts = exp.optionsFactory();
		opts.suppressUsage();
		for (int elt=0; elt<icase.size(); elt++) {
			KeyValue kv = (KeyValue) icase.get(elt);
			try {
				opts.setOption(kv.key, kv.value.toString());
			} catch (Options.OptionException ex) {
				// undefined option? no problem, ignore it
			}
		}
		return opts;
	}

	Vector axes = new Vector();
	static class KeyValue {
		String key;
		Object value;
		KeyValue(String key, Object value) {
			this.key = key;
			this.value = value;
		}
		public String toString() {
			return "-"+key+"="+value;
		}
	}

	// trivial axis
	public void axis(String optName, Object value) {
		axis(optName, new Object[] { value });
	}

	public void axis(String optName, Object[] values) {
		axes.add(new KeyValue(optName, values));
	}

	public void axis(String optName, int[] values) {
		Integer[] oValues = new Integer[values.length];
		for (int i=0; i<oValues.length; i++) {
			oValues[i] = new Integer(values[i]);
		}
		axis(optName, oValues);
	}
}
