// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.core.cli;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.pipeline.common.PipelineConstants;
import org.openstreetmap.osmosis.core.pipeline.common.TaskConfiguration;


/**
 * Parses command line arguments into a form that can be consumed by the rest of
 * the application.
 * 
 * @author Brett Henderson
 */
public class CommandLineParser {
	
	private static final String GLOBAL_ARGUMENT_PREFIX = "-";
	private static final String TASK_ARGUMENT_PREFIX = "--";
	private static final String OPTION_QUIET_SHORT = "q";
	private static final String OPTION_QUIET_LONG = "quiet";
	private static final String OPTION_VERBOSE_SHORT = "v";
	private static final String OPTION_VERBOSE_LONG = "verbose";
	private static final String OPTION_PLUGIN_SHORT = "p";
	private static final String OPTION_PLUGIN_LONG = "plugin";
	
	
	/**
	 * The index into the LOG_LEVELS array for the default log level.
	 */
	private static final int DEFAULT_LOG_LEVEL_INDEX = 3;
	
	
	private List<TaskConfiguration> taskConfigList;
	private int quietValue;
	private int verboseValue;
	private List<String> plugins;
	
	
	/**
	 * Creates a new instance.
	 */
	public CommandLineParser() {
		taskConfigList = new ArrayList<TaskConfiguration>();
		
		quietValue = 0;
		verboseValue = 0;
		plugins = new ArrayList<String>();
	}
	
	
	/**
	 * Parses the command line arguments.
	 * 
	 * @param programArgs
	 *            The arguments.
	 */
	public void parse(String[] programArgs) {
		List<GlobalOptionConfiguration> globalOptions;
		
		// Create the global options list.
		globalOptions = new ArrayList<GlobalOptionConfiguration>();
		
		// Process the command line arguments to build all nodes in the pipeline.
		for (int i = 0; i < programArgs.length;) {
			String arg;
			
			arg = programArgs[i];
			
			if (arg.indexOf(TASK_ARGUMENT_PREFIX) == 0) {
				i = parseTask(programArgs, i);
			} else if (arg.indexOf(GLOBAL_ARGUMENT_PREFIX) == 0) {
				i = parseGlobalOption(globalOptions, programArgs, i);
			} else {
				throw new OsmosisRuntimeException("Expected argument " + (i + 1) + " to be an option or task name.");
			}
		}
		
		// Process the global options.
		for (GlobalOptionConfiguration globalOption : globalOptions) {
			if (isArgumentForOption(OPTION_QUIET_SHORT, OPTION_QUIET_LONG, globalOption.name)) {
				quietValue = parseOptionIntegerWithDefault(globalOption, 0) + 1;
			} else if (isArgumentForOption(OPTION_VERBOSE_SHORT, OPTION_VERBOSE_LONG, globalOption.name)) {
				verboseValue = parseOptionIntegerWithDefault(globalOption, 0) + 1;
			} else if (isArgumentForOption(OPTION_PLUGIN_SHORT, OPTION_PLUGIN_LONG, globalOption.name)) {
				plugins.add(parseOptionString(globalOption));
			} else {
				throw new OsmosisRuntimeException("Argument " + (globalOption.offset + 1)
						+ " specifies an unrecognised option \"" + GLOBAL_ARGUMENT_PREFIX + globalOption.name
						+ "\".");
			}
		}
	}
	
	
	/**
	 * Checks if the current command line argument is for the specified option.
	 * 
	 * @param shortOptionName
	 *            The short name of the option to check for.
	 * @param longOptionName
	 *            The long name of the option to check for.
	 * @param argument
	 *            The command line argument without the option prefix.
	 * @return True if the argument is for the specified option.
	 */
	private boolean isArgumentForOption(String shortOptionName, String longOptionName, String argument) {
		return shortOptionName.equals(argument) || longOptionName.equals(argument);
	}
	
	
	/**
	 * Determines if an argument is a parameter to the current option/task or
	 * the start of another option/task.
	 * 
	 * @param argument
	 *            The argument.
	 * @return True if this is an option parameter.
	 */
	private boolean isOptionParameter(String argument) {
		if (argument.length() >= GLOBAL_ARGUMENT_PREFIX.length()) {
			if (argument.substring(0, GLOBAL_ARGUMENT_PREFIX.length()).equals(GLOBAL_ARGUMENT_PREFIX)) {
				return false;
			}
		}
		
		if (argument.length() >= TASK_ARGUMENT_PREFIX.length()) {
			if (argument.substring(0, TASK_ARGUMENT_PREFIX.length()).equals(TASK_ARGUMENT_PREFIX)) {
				return false;
			}
		}
		
		return true;
	}
	
	
	/**
	 * Parses a command line option into an integer. If none is specified, zero
	 * will be returned.
	 * 
	 * @param globalOption
	 *            The global option to be parsed.
	 * @return The integer value.
	 */
	private int parseOptionIntegerWithDefault(GlobalOptionConfiguration globalOption, int defaultValue) {
		int result;
		
		// If no parameters are available, we use the default value.
		if (globalOption.parameters.size() <= 0) {
			return defaultValue;
		}
		
		// An integer option may only have one parameter.
		if (globalOption.parameters.size() > 1) {
			throw new OsmosisRuntimeException(
					"Expected argument " + (globalOption.offset + 1) + " to have no more than one parameter.");
		}
		
		// Parse the option.
		try {
			result = Integer.parseInt(globalOption.parameters.get(0));
			
		} catch (NumberFormatException e) {
			throw new OsmosisRuntimeException(
					"Expected argument " + (globalOption.offset + 2) + " to contain an integer value.");
		}
		
		return result;
	}
	
	
	/**
	 * Parses a command line option into an string.
	 * 
	 * @param globalOption
	 *            The global option to be parsed.
	 */
	private String parseOptionString(GlobalOptionConfiguration globalOption) {
		// A string option must have one parameter.
		if (globalOption.parameters.size() != 1) {
			throw new OsmosisRuntimeException(
					"Expected argument " + (globalOption.offset + 1) + " to have one parameter.");
		}
		
		return globalOption.parameters.get(0);
	}
	
	
	/**
	 * Parses the details of a single option.
	 * 
	 * @param globalOptions
	 *            The list of global options being parsed, the new option will
	 *            be stored into this object.
	 * @param programArgs
	 *            The command line arguments passed to this application.
	 * @param offset
	 *            The current offset through the command line arguments.
	 * @return The new offset through the command line arguments.
	 */
	private int parseGlobalOption(List<GlobalOptionConfiguration> globalOptions, String[] programArgs, int offset) {
		int i;
		String argument;
		GlobalOptionConfiguration globalOption;
		
		i = offset;
		argument = programArgs[i++].substring(1);
		
		globalOption = new GlobalOptionConfiguration();
		globalOption.name = argument;
		globalOption.offset = offset;
		
		// Loop until the next option or task is reached and add the arguments as parameters to the option.
		while ((i < programArgs.length) && isOptionParameter(programArgs[i])) {
			globalOption.parameters.add(programArgs[i++]);
		}
		
		// Add the fully populated global option object to the list.
		globalOptions.add(globalOption);
		
		return i;
	}
	
	
	/**
	 * Parses the details of a single task and creates a task information object.
	 * 
	 * @param programArgs
	 *            The command line arguments passed to this application.
	 * @param offset
	 *            The current offset through the command line arguments.
	 * @return The new offset through the command line arguments.
	 */
	private int parseTask(String[] programArgs, int offset) {
		int i;
		String taskType;
		Map<String, String> taskArgs;
		Map<String, String> pipeArgs;
		String taskId;
		int defaultArgIndex;
		String defaultArg;
		
		i = offset;
		
		// Extract the task type from the current argument.
		taskType = programArgs[i++].substring(TASK_ARGUMENT_PREFIX.length());
		
		// Build up a list of task and pipe arguments.
		taskArgs = new HashMap<String, String>();
		pipeArgs = new HashMap<String, String>();
		defaultArg = null;
		defaultArgIndex = -1;
		while (i < programArgs.length) {
			String arg;
			int equalsIndex;
			String argName;
			String argValue;
			
			arg = programArgs[i];
			
			if (arg.indexOf(TASK_ARGUMENT_PREFIX) == 0) {
				break;
			}
			
			equalsIndex = arg.indexOf("=");
			
			// If an equals sign exists this is a named argument, otherwise it is a default argument.
			if (equalsIndex >= 0) {
				// Check if the name component of the argument exists.
				if (equalsIndex == 0) {
					throw new OsmosisRuntimeException(
							"Argument " + (i + 1) + " doesn't contain a name before the '=' (ie. name=value).");
				}
				
				// Check if the value component of the argument exists.
				if (equalsIndex >= (arg.length() - 1)) {
					throw new OsmosisRuntimeException(
							"Argument " + (i + 1) + " doesn't contain a value after the '=' (ie. name=value).");
				}
				
				// Split the argument into name and value.
				argName = arg.substring(0, equalsIndex);
				argValue = arg.substring(equalsIndex + 1);
				
				// Add pipeline arguments to pipeArgs, all other arguments to taskArgs.
				// A pipeline arg is inPipe, inPipe.x, outPipe or outPipe.x.
				if (
						PipelineConstants.IN_PIPE_ARGUMENT_PREFIX.equals(argName)
						|| argName.indexOf(PipelineConstants.IN_PIPE_ARGUMENT_PREFIX + ".") == 0
						|| PipelineConstants.OUT_PIPE_ARGUMENT_PREFIX.equals(argName)
						|| argName.indexOf(PipelineConstants.OUT_PIPE_ARGUMENT_PREFIX + ".") == 0) {
					pipeArgs.put(argName, argValue);
				} else {
					taskArgs.put(argName, argValue);
				}
				
			} else {
				if (defaultArgIndex >= 0) {
					throw new OsmosisRuntimeException(
							"Only one default (un-named) argument can exist per task.  Arguments "
							+ (i + 1) + " and " + (defaultArgIndex + 1) + " have no name.");
				}
				
				defaultArg = arg;
				defaultArgIndex = i;
			}
			
			i++;
		}
		
		// Build a unique task id.
		taskId = (taskConfigList.size() + 1) + "-" + taskType;
		
		// Create a new task information object and add it to the list.
		taskConfigList.add(
			new TaskConfiguration(taskId, taskType, pipeArgs, taskArgs, defaultArg)
		);
		
		return i;
	}
	
	
	/**
	 * The list of task information objects.
	 * 
	 * @return The taskInfoList.
	 */
	public List<TaskConfiguration> getTaskInfoList() {
		return taskConfigList;
	}
	
	
	/**
	 * Gets the level of logging required. This is a number that can be used to
	 * access a log level from the LogLevels class.
	 * 
	 * @return The index of the log level to be used.
	 */
	public int getLogLevelIndex() {
		return DEFAULT_LOG_LEVEL_INDEX + verboseValue - quietValue;
	}
	
	
	/**
	 * Returns the plugins to be loaded.
	 * 
	 * @return The list of plugin class names.
	 */
	public List<String> getPlugins() {
		return plugins;
	}
	
	
	/**
	 * A data storage class holding information relating to a global option
	 * during parsing.
	 */
	private class GlobalOptionConfiguration {
		/**
		 * The name of the option.
		 */
		public String name;
		/**
		 * The parameters for the option.
		 */
		public List<String> parameters;
		/**
		 * The command line argument offset of this global option.
		 */
		public int offset;
		
		
		/**
		 * Creates a new instance.
		 */
		GlobalOptionConfiguration() {
			parameters = new ArrayList<String>();
		}
	}
}
