package Tools;

import java.io.*;
import java.util.*;

/**
 * A tool for parsing options from the command line.
 * Nice because the same definition is used to determine how options
 * are parsed <em>and</em> generate a helpful usage message.
 *
 * Can be subclassed to declare the option types.
 */
public class Options {
	/**
	 * Does the program allow the user to ``make up'' option names?
	 * <p>Subclass may set this in defineOptions().
	 */
	public boolean allowExtraOptions = false;

	/**
	 * Does the program allow the user to append extra unnamed arguments?
	 * <p>Subclass may set this in defineOptions().
	 */
	public boolean allowExtraArguments = false;

	/**
	 * What is the program called, so usage() prints something meaningful?
	 * <p>Subclass may set this in defineOptions().
	 */
	public String programName="Program";		// used in usage() help display

	HashMap knownOptions = new HashMap();
						// maps known options to a type specifier
	Vector knownArguments = new Vector();
	int argsRequired = 0;
	Vector optionList = new Vector();
						// stored in order for usage() output

	boolean printUsage = true;

	public void suppressUsage() {
		printUsage = false;
	}

	/**
	 * Subclasses override this method to define the set of acceptable
	 * options and arguments. This method should call {@link
	 * defineOption()}, {@link defineArgument()} to set up the definitions. 
	 *
	 * Set <code>allowExtraOptions</code> and <code>allowExtraArguments</code>
	 * to control whether extra options and arguments (beyond those defined)
	 * are allowed. Set <code>programName</code> to determine how the
	 * program name appears in the usage() display.
	 */
	public void defineOptions() {
	}

	/**
	 * Describes an option or an argument.
	 */
	public class OptEntry {
		String name;
		boolean required;
			// Used only with arguments. Means the argument MUST appear
			// on the command line.
		String description;
		String defaultValue;
		String value;
		boolean defined;
			// as opposed to "the user just made it up."
		// Could have type info here

		OptEntry() {}
		OptEntry(String name, boolean required, String description,
			String defaultValue, boolean defined) {
			this.name = name;
			this.required = required;
			this.description = description;
			this.defaultValue = defaultValue;
			this.defined = defined;
		}
	}

	/**
	 * Define an option, a string (argVal) on the command line preceded
	 * by "-argName=argVal" or "-argName argVal".
	 */
	public void defineOption(String optName, String description,
		String defaultValue) {
		OptEntry oe = new OptEntry(optName, false, description,
			defaultValue, true);
		knownOptions.put(optName, oe);
		optionList.add(oe);
	}

	/**
	 * Define an argument (an input without an -optName tag)
	 */
	public void defineArgument(String argName,
		boolean required, String description, String defaultValue) {
		knownArguments.add(new OptEntry(argName, required, description,
			defaultValue, true));
		if (required) {
			argsRequired++;
			if (argsRequired<knownArguments.size()) {
				throw new RuntimeException(
					"Required argument appears after optional argument");
			}
		}
	}

	/**
	 * Define the name of this program, as it appears in the usage()
	 * display.
	 */
	public void defineProgram(String programName) {
		this.programName = programName;
	}

	/**
	 * Thrown to alert application that option parsing failed and the
	 * user should be notified. The default constructor handles this
	 * exception automatically so that most applications can ignore it.
	 */
	public static class OptionException extends Exception {}

	public void setOption(String optName, String optValue)
		throws OptionException {
		OptEntry oe = getOE(optName);
		if (oe==null) {
			if (allowExtraOptions) {
				oe = new OptEntry();
				oe.name = optName;
				knownOptions.put(optName, oe);
				// we don't add it to the optionList, of course,
				// because it's not an official option, and
				// shouldn't appear in usage().
			} else {
				optionError("undefined option "+optName);
			}
		}
		oe.value = optValue;
	}
	// TODO: setArgument

	// For callers that are building up options with calls to setOptions()
	public Options() {
		defineOptions();
	}

	public Options(String[] argParam) {
		defineOptions();
		try {
			int numArgs = 0;	// number of non-option arguments
			for (int i=0; i<argParam.length; i++) {
				if (argParam[i].charAt(0)=='-') {
					String optName;
					String optVal;
					int c = argParam[i].indexOf('=', 1);
					if (c>=0) {
						// -option=value
						optName = argParam[i].substring(1,c);
						optVal = argParam[i].substring(c+1);
					} else {
						// -option value
						optName = argParam[i].substring(1);
						if (i+1>=argParam.length) {
							optionError("option "+optName+" with no value");
						}
						optVal = argParam[++i];
					}
					setOption(optName, optVal);
				} else {
					OptEntry oe = getOE(numArgs);
					if (oe==null) {
						if (allowExtraArguments) {
							oe = new OptEntry();
							knownArguments.add(oe);
						} else {
							optionError("extraneous argument "+argParam[i]);
						}
					}
					oe.value = argParam[i];
					numArgs++;
				}
			}
			if (numArgs<argsRequired) {
				optionError("missing required argument "
					+getOE(numArgs).name);
			}
		} catch (OptionException ex) {
			System.exit(1);
			// TODO: let user pass in a closure explaining what to
			// do in the event of an OptionException.
		}
	}

	/**
	 * override this method to do something different that barf on
	 * stderr if options don't parse out correctly. When you're done,
	 * throw Options.OptionException to cause option parsing to stop.
	 */
	public void optionError(String error)
		throws OptionException {
		if (printUsage) {
			System.err.println(error);
			usage();
		}
		throw new OptionException();
	}

	OptEntry getOE(int argIndex) {
		if (argIndex<knownArguments.size()) {
			return (OptEntry) knownArguments.get(argIndex);
		}
		return null;
	}

	OptEntry getOE(String name) {
		if (knownOptions!=null) {
			OptEntry oe = (OptEntry) knownOptions.get(name);
			if (oe!=null) {
				return oe;
			}
		}
		for (int i=0; i<knownArguments.size(); i++) {
			OptEntry oe = (OptEntry) knownArguments.get(i);
			if (oe.name.equals(name)) {
				return oe;
			}
		}
		return null;
	}

	// methods to extract opt/argument info
	/**
	 * Get an argument or option by name
	 * 
	 * @returns null if the argument or option is not defined. 
	 */
	public String get(String name) {
		OptEntry oe = getOE(name);
		if (oe==null) {
			return null;
		}
		if (oe.value==null) {
			return oe.defaultValue;
		}
		return oe.value;
	}

	/**
	 * Get an argument or option by name, supplying a dynamic default
	 * value. Useful when an argument or option has a default value
	 * that isn't a static string, but can be computed by the time the
	 * argument is fetched.
	 */
	public String get(String name, String defaultValue) {
		String value = get(name);
		if (value==null) {
			return defaultValue;
		}
		return value;
	}

	public int getInt(String name) {
		return Integer.parseInt(get(name));
	}

	public boolean getBoolean(String name) {
		char first = get(name).charAt(0);
		return (first=='t' || first=='T');
	}

	/**
	 * Get an argument by position (0-indexed). Useful if you're being
	 * lazy and using this class directly, rather than subclassing it
	 * to give your arguments names.
	 *
	 * @returns null if there are not enough arguments for there to be
	 * one at <code>index</code>.
	 */
	public String getArg(int index) {
		return index<=knownArguments.size()
			? (String) knownArguments.get(index)
			: null;
	}

	/** Outputs usage info to stderr.
	*/
	public void usage() {
		System.err.print("Usage: "+programName);
		if (knownOptions==null
			|| knownOptions.keySet().size()>0) {
			System.err.print(" [options]");
		}

		boolean printedBracket = false;
		for (int i=0; i<knownArguments.size(); i++) {
			OptEntry oe = (OptEntry) knownArguments.get(i);
			if (!oe.defined) {
				// user made this option up
				continue;
			}
			String lbracket = "";
			if (i==argsRequired) {
				// beginning of optional arguments
				lbracket = "[";
				printedBracket = true;
			}
			System.err.print(" "+lbracket+oe.name);
		}
		if (allowExtraArguments) {
			System.err.print(" ...");
		}
		if (printedBracket) {
			System.err.print("]");
		}
		System.err.println();

		if (knownArguments.size()>0) {
			System.err.println("Arguments:");
			for (int i=0; i<knownArguments.size(); i++) {
				OptEntry oe = (OptEntry) knownArguments.get(i);
				if (!oe.defined) {
					// user made this arg up
					continue;
				}
				System.err.print("  "+pad(oe.name,16)
					+" "+oe.description);
				if (oe.defaultValue!=null) {
					System.err.print(" ["+oe.defaultValue+"]");
				}
				System.err.println();
			}
		}

		if (optionList.size()>0) {
			System.err.println("Options:");
			for (int i=0; i<optionList.size(); i++) {
				OptEntry oe = (OptEntry) optionList.get(i);
				if (!oe.defined) {
					// user made this arg up
					continue;
				}
				System.err.print("  -"+pad(oe.name,15)
					+" "+oe.description);
				if (oe.defaultValue!=null) {
					System.err.print(" ["+oe.defaultValue+"]");
				}
				System.err.println();
			}
		}
	}

	public String pad(String arg, int len) {
		StringBuffer b = new StringBuffer(arg);
		while (b.length()<len) {
			b.append(' ');
		}
		return b.toString();
	}

	public void dumpOptions() {
		dumpOptions(System.err);
	}

	public void dumpOptions(OutputStream out) {
		PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));

		if (knownArguments.size()>0) {
			for (int i=0; i<knownArguments.size(); i++) {
				String key = ((OptEntry) knownArguments.elementAt(i)).name;
				String val = get(key);
				if (val==null) {
					val = " (null)";
				}
				pw.println(key+"="+val);
			}
		}

		if (optionList.size()>0) {
			System.err.println("Options:");
			for (int i=0; i<optionList.size(); i++) {
				String key = ((OptEntry) optionList.elementAt(i)).name;
				String val = get(key);
				if (val==null) {
					val = " (null)";
				}
				pw.println("-"+key+"="+val);
			}
		}
		pw.flush();
	}
}
