/*
 * Options.java - This file is part of the Jakstab project.
 * Copyright 2007-2015 Johannes Kinder <jk@jakstab.org>
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, see <http://www.gnu.org/licenses/>.
 */
package org.jakstab;

import java.io.File;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;

import org.jakstab.util.Logger;

/**
 * Parses and holds command line options.
 * 
 * @author Johannes Kinder
 */
public class Options {

	private static final Logger logger = Logger.getLogger(Options.class);

	private final static int lineLength = 80;
	private final static int indentation = 22;

	
	public static final String jakstabHome;
	static {
		// Get path of Jakstab's directory from VM
		String classFileName = Options.class.getResource("/org/jakstab/Options.class").getPath();
		if (classFileName.startsWith("file:")) 
			classFileName = classFileName.substring(5);
		classFileName = classFileName.replace("%20", " ");
		jakstabHome = (new File(classFileName)).getParentFile().getParentFile().getParentFile().getParent();
	}
	
	private static Map<String,JOption<?>> options = new TreeMap<String,JOption<?>>(new Comparator<String>() {
		public int compare(String s1, String s2) {
			if (s1.length() != 2 && s2.length() == 2) return 1;
			if (s1.length() == 2 && s2.length() != 2) return -1;
			else return s1.compareTo(s2);
		}
	});
	
	static void addOption(JOption<?> o) {
		String name = o.getName();
		if (options.containsKey(name)) {
			logger.fatal("Option " + name + " already present!");
			System.exit(1);
		} else {
			options.put(name, o);
		}
	}
	

	
	public static String mainFilename = null;
	public static List<String> moduleFilenames = new LinkedList<String>();

	public static String arguments;

	public static JOption<String> sslFilename = JOption.create("ssl", "file", jakstabHome + "/ssl/pentium.ssl", "Use <file> instead of pentium.ssl.");
	public static JOption<Long> startAddress = JOption.create("a", "address", -1L, "Start analysis at given virtual address.");
	public static JOption<Boolean> wdm = JOption.create("wdm", "WDM mode, export main function as DriverMain.");
	public static JOption<Boolean> allEdges = JOption.create("all-edges", "Generate a true over-approximation and add edges to all possible addresses when over-approximating a jump (very slow!).");
	public static JOption<Boolean> dumpStates = JOption.create("s", "Output all reached states after analysis.");	
	public static JOption<Boolean> outputLocationsWithMostStates = JOption.create("toplocs", "Output the 10 locations with the highest state count.");
	public static JOption<Boolean> failFast = JOption.create("fail-fast", "Stop when unsound assumptions are necessary to continue.");
	public static JOption<Boolean> debug = JOption.create("debug", "Stop on failed assertions or weak updates to the complete stack or all store regions.");
	public static JOption<Boolean> asmTrace = JOption.create("asm-trace", "Output any error trace as a list of assembly instructions instead of IL statements.");
	public static JOption<Boolean> errorTrace = JOption.create("error-trace", "Build an abstract error trace for failed assertions and debug stops.");
	public static JOption<Boolean> backward = JOption.create("backward", "Perform secondary cpa as a backward analysis.");
	public static JOption<Boolean> background = JOption.create("b", "Background mode, i.e., disable shutdown hook on enter.");
	public static JOption<Boolean> graphML = JOption.create("graphML", "Produce graphML output instead of GraphViz .dot files.");
	public static JOption<Boolean> noGraphs = JOption.create("no-graphs", "Do not generate output graphs");
	public static JOption<Boolean> heuristicEntryPoints = JOption.create("h", "Use heuristics to determine additional procedures and add pseudo-calls to include them in disassembly.");
	public static JOption<Boolean> ignoreWeakUpdates = JOption.create("ignore-weak-updates", "Do not perform weak store updates (unsound).");
	public static JOption<Boolean> initHeapToBot = JOption.create("bot-heap", "Initialize heap cells to BOT to force strong updates.");
	public static JOption<Boolean> summarizeRep = JOption.create("summarize-rep", "Use summarizing transformer for string instructions.");
	public static JOption<Boolean> basicBlocks = JOption.create("basicblocks", "Build CFA from basic-blocks instead of single statements.");
	public static JOption<Integer> simplifyVCFG = JOption.create("simplifyVCFG", "l", 1, "In VPC-CFG reconstruction, simplify the reconstructed graph using (0) nothing (1) DCE (2) DCE + Expression Substitution");
	public static JOption<Integer> verbosity = JOption.create("v", "level", 3, "Set verbosity to value. Default is 3.");
	public static JOption<Integer> timeout = JOption.create("timeout", "t", -1, "Set timeout in seconds for the analysis.");
	public static JOption<Integer> procedureAbstraction = JOption.create("procedures", "n", 0, "Level of procedure assumptions: " +
			"0: Pessimistic: No assumptions, treat calls and returns as jumps (default). " + 
			"1: Semi-optimistic: Abstract unknown calls according to ABI contract. " + 
			"2: Optimistic: Abstract all calls to ABI contract (fastest).");
	public static JOption<Integer> getProcAddress = JOption.create("getprocaddress", "n", 2, "How to resolve GetProcAddress: 0: Always succeed, 1: Split success/fail, 2: Merge success/fail (default)");

	private static AnalysisManager mgr = AnalysisManager.getInstance();
	public static JOption<String> cpas = JOption.create("cpa", "{" + mgr.getShorthandsString() + "}", "x", "Configure which analyses to use for control flow reconstruction.");
	public static JOption<String> secondaryCPAs = JOption.create("cpa2", "{" + mgr.getShorthandsString() + "}", "", "Secondary analyses to be performed after the initial CFG reconstruction and dead code elimination are completed.");
	public static JOption<String> procedureGraph = JOption.create("procedure-graph", "p", "", "Generate intraprocedural CFG for procedure with give name (requires symbols)");
	
	/**
	 * Handle command line options.
	 * 
	 * @param args
	 */
	public static void parseOptions(String args[]) {
		
		// Pre-load analyses so that they can register their options
		AnalysisManager.getInstance();
		
		StringBuilder argStringBuilder = new StringBuilder();
		for (int i = 0; i < args.length - 1; i++) {
			argStringBuilder.append(args[i]).append(" ");
		}
		if (args.length > 0)
			argStringBuilder.append(args[args.length - 1]);
		arguments = argStringBuilder.toString();
		
		for (int i = 0; i < args.length; i++) {
			String arg = args[i];
			// Dash (-) arguments
			if (arg.startsWith("-")) {
				
				JOption<?> opt = options.get(arg);
				if (opt != null) {
					if (opt.getDefaultValue() instanceof Boolean) {
						opt.setValue(Boolean.TRUE);
					} else if (opt.getDefaultValue() instanceof Integer) {
						opt.setValue(Integer.parseInt(args[++i]));
					} else if (opt.getDefaultValue() instanceof Long) {
						arg = args[++i];
						if (arg.startsWith("0x")) 
							opt.setValue(Long.parseLong(arg.substring(2), 16));
						else
							opt.setValue(Long.parseLong(arg));
					} else if (opt.getDefaultValue() instanceof String) {
						opt.setValue(args[++i]);
					} else {
						assert false : "Unhandled Option type " + opt.getDefaultValue().getClass().getSimpleName();
					}
				}			
				// Arguments which require arguments
				else if (arg.equals("-m")) {
					mainFilename = args[++i];
				} else {
					logger.fatal("Invalid command line argument: " + arg);
					logger.fatal("");
					Options.printOptions();
					System.exit(1);
				}
			} // arguments w/o dash
			else {
				moduleFilenames.add(arg);
			}
		}
		
		if (mainFilename == null) {
			logger.fatal("No main file specified!");
			logger.fatal("");
			Options.printOptions();
			System.exit(1);
		}
		
	}
	
	public static void printOptions() {
		logger.fatal("Usage: jakstab [options] -m mainfile [ modules... ]");
		logger.fatal("");
		logger.fatal("Options:");
		
		for (JOption<?> o : options.values()) {
			StringBuilder os = new StringBuilder(lineLength);
			os.append("  ").append(o.getName());
			if (o.getParamName() != null)
				os.append(' ').append(o.getParamName());
			os.append(' ');

			printWithIndentedLineWrap(os, o.getDescription());
			
			// Special treatment for CPAs option
			if (o.equals(cpas)) {
				String shorthands = mgr.getShorthandsString();
				for (int i=0; i<shorthands.length(); i++) {
					Character cpa = shorthands.charAt(i);
					
					os = new StringBuilder(lineLength);
					os.append("        ").append(cpa);
					
					printWithIndentedLineWrap(os, mgr.getName(cpa) + ": " + mgr.getDescription(cpa));
				}
			}
			
		}
	}
	
	private static void printWithIndentedLineWrap(StringBuilder os, String s) {
		StringTokenizer st = new StringTokenizer(s, " ");
		String nextWord = st.nextToken();
		printLine: while (true) {
			// We are at the first or a new line
			while (os.length() < indentation) {
				os.append(' ');
			}
			os.append(nextWord);

			// Output text until end of line
			while (true) {
				if (!st.hasMoreTokens())
					break printLine;
				nextWord = st.nextToken();
				
				if (os.length() + 1 + nextWord.length() <= lineLength) {
					os.append(" ").append(nextWord);
				} else {
					logger.fatal(os.toString());
					os = new StringBuilder(lineLength);
					// Start new line
					continue printLine;
				}
			}
		}
		logger.fatal(os.toString());
	}

}
