package net.sourceforge.phpdt.externaltools.model;

/**********************************************************************
 Copyright (c) 2002 IBM Corp. and others. All rights reserved.
 This file is made available under the terms of the Common Public License v1.0
 which accompanies this distribution, and is available at
 http://www.eclipse.org/legal/cpl-v10.html
 �
 Contributors:
 **********************************************************************/

import java.util.ArrayList;

import net.sourceforge.phpdt.externaltools.internal.model.ExternalToolsModelMessages;
import net.sourceforge.phpdt.externaltools.internal.registry.ArgumentVariable;
import net.sourceforge.phpdt.externaltools.internal.registry.ArgumentVariableRegistry;
import net.sourceforge.phpdt.externaltools.internal.registry.PathLocationVariable;
import net.sourceforge.phpdt.externaltools.internal.registry.PathLocationVariableRegistry;
import net.sourceforge.phpdt.externaltools.variable.ExpandVariableContext;
import net.sourceforge.phpeclipse.externaltools.ExternalToolsPlugin;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.MultiStatus;

/**
 * General utility class dealing with external tools
 */
public final class ToolUtil {
	/**
	 * Argument parsing constants
	 */
	private static final char ARG_DELIMITER = ' '; //$NON-NLS-1$

	private static final char ARG_DBL_QUOTE = '"'; //$NON-NLS-1$

	/**
	 * Variable tag indentifiers
	 */
	private static final char VAR_TAG_START_CHAR1 = '$'; //$NON-NLS-1$

	private static final char VAR_TAG_START_CHAR2 = '{'; //$NON-NLS-1$

	private static final char VAR_TAG_END_CHAR1 = '}'; //$NON-NLS-1$

	private static final String VAR_TAG_START = "${"; //$NON-NLS-1$

	private static final String VAR_TAG_END = "}"; //$NON-NLS-1$

	private static final String VAR_TAG_SEP = ":"; //$NON-NLS-1$

	/**
	 * No instances allowed
	 */
	private ToolUtil() {
		super();
	}

	/**
	 * Builds a variable tag that will be auto-expanded before the tool is run.
	 * 
	 * @param varName
	 *            the name of a known variable (one of the VAR_* constants for
	 *            instance)
	 * @param varArgument
	 *            an optional argument for the variable, <code>null</code> if
	 *            none
	 */
	public static String buildVariableTag(String varName, String varArgument) {
		StringBuffer buf = new StringBuffer();
		buildVariableTag(varName, varArgument, buf);
		return buf.toString();
	}

	/**
	 * Builds a variable tag that will be auto-expanded before the tool is run.
	 * 
	 * @param varName
	 *            the name of a known variable (one of the VAR_* constants for
	 *            instance)
	 * @param varArgument
	 *            an optional argument for the variable, <code>null</code> if
	 *            none
	 * @param buffer
	 *            the buffer to write the constructed variable tag
	 */
	public static void buildVariableTag(String varName, String varArgument,
			StringBuffer buffer) {
		buffer.append(VAR_TAG_START);
		buffer.append(varName);
		if (varArgument != null && varArgument.length() > 0) {
			buffer.append(VAR_TAG_SEP);
			buffer.append(varArgument);
		}
		buffer.append(VAR_TAG_END);
	}

	/**
	 * Expands all the variables found in an individual argument text.
	 * 
	 * @param argument
	 *            one of the argument text in the list of arguments
	 * @param context
	 *            the context to use for expanding variables
	 * @param status
	 *            multi status to report any problems expanding variables
	 * @return the argument text with all variables expanded, or
	 *         <code>null</code> if not possible
	 */
	public static String expandArgument(String argument,
			ExpandVariableContext context, MultiStatus status) {
		StringBuffer buffer = new StringBuffer();

		int start = 0;
		while (true) {
			VariableDefinition varDef = extractVariableTag(argument, start);

			// No more variables found...
			if (varDef.start == -1) {
				if (start == 0)
					buffer.append(argument);
				else
					buffer.append(argument.substring(start));
				break;
			}

			// Invalid variable format
			if (varDef.end == -1 || varDef.name == null
					|| varDef.name.length() == 0) {
				String msg = ExternalToolsModelMessages
						.getString("ToolUtil.argumentVarFormatWrong"); //$NON-NLS-1$
				status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
				return null;
			}

			// Copy text between start and variable.
			if (varDef.start > start)
				buffer.append(argument.substring(start, varDef.start));
			start = varDef.end;

			// Lookup the variable if it exist
			ArgumentVariableRegistry registry;
			registry = ExternalToolsPlugin.getDefault()
					.getArgumentVariableRegistry();
			ArgumentVariable variable = registry
					.getArgumentVariable(varDef.name);
			if (variable == null) {
				String msg = ExternalToolsModelMessages
						.format(
								"ToolUtil.argumentVarMissing", new Object[] { varDef.name }); //$NON-NLS-1$
				status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
				return null;
			}

			// Expand the variable as text if possible
			String text = variable.getExpander().getText(varDef.name,
					varDef.argument, context);
			if (text == null) {
				String msg = ExternalToolsModelMessages
						.format(
								"ToolUtil.argumentVarExpandFailed", new Object[] { varDef.name }); //$NON-NLS-1$
				status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
				return null;
			}
			buffer.append(text);
		}

		return buffer.toString();
	}

	/**
	 * Returns a list of individual arguments where all variables have been
	 * expanded.
	 * 
	 * @param arguments
	 *            the arguments with leading and trailing spaces already
	 *            removed.
	 * @param context
	 *            the context used to expand the variable(s)
	 * @param status
	 *            multi status to report any problems expanding variables
	 * @return the list of individual arguments where some elements in the list
	 *         maybe <code>null</code> if problems expanding variable(s).
	 */
	public static String[] expandArguments(String arguments,
			ExpandVariableContext context, MultiStatus status) {
		if (arguments == null || arguments.length() == 0)
			return new String[0];

		String[] argList = parseArgumentsIntoList(arguments);
		for (int i = 0; i < argList.length; i++)
			argList[i] = expandArgument(argList[i], context, status);

		return argList;
	}

	/**
	 * Returns the expanded directory location if represented by a directory
	 * variable. Otherwise, the directory location given is return unless an
	 * unknown variable was detected.
	 * 
	 * @param dirLocation
	 *            a directory location either as a path or a variable with
	 *            leading and trailing spaces already removed.
	 * @param context
	 *            the context used to expand the variable
	 * @param status
	 *            multi status to report any problems expanding variables
	 * @return the directory location as a string or <code>null</code> if not
	 *         possible
	 */
	public static String expandDirectoryLocation(String dirLocation,
			ExpandVariableContext context, MultiStatus status) {
		if (dirLocation == null || dirLocation.length() == 0)
			return ""; //$NON-NLS-1$

		VariableDefinition varDef = extractVariableTag(dirLocation, 0);
		// Return if no variable found
		if (varDef.start < 0)
			return dirLocation;

		// Disallow text before/after variable
		if (varDef.start != 0
				|| (varDef.end < dirLocation.length() && varDef.end != -1)) {
			String msg = ExternalToolsModelMessages
					.getString("ToolUtil.dirLocVarBetweenText"); //$NON-NLS-1$
			status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
			return null;
		}

		// Invalid variable format
		if (varDef.name == null || varDef.name.length() == 0
				|| varDef.end == -1) {
			String msg = ExternalToolsModelMessages
					.getString("ToolUtil.dirLocVarFormatWrong"); //$NON-NLS-1$
			status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
			return null;
		}

		// Lookup the variable if it exist
		PathLocationVariableRegistry registry;
		registry = ExternalToolsPlugin.getDefault()
				.getDirectoryLocationVariableRegistry();
		PathLocationVariable variable = registry
				.getPathLocationVariable(varDef.name);
		if (variable == null) {
			String msg = ExternalToolsModelMessages.format(
					"ToolUtil.dirLocVarMissing", new Object[] { varDef.name }); //$NON-NLS-1$
			status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
			return null;
		}

		// Expand the variable into a IPath if possible
		IPath path = variable.getExpander().getPath(varDef.name,
				varDef.argument, context);
		if (path == null) {
			String msg = ExternalToolsModelMessages
					.format(
							"ToolUtil.dirLocVarExpandFailed", new Object[] { varDef.name }); //$NON-NLS-1$
			status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
			return null;
		}

		return path.toOSString();
	}

	/**
	 * Returns the expanded file location if represented by a file variable.
	 * Otherwise, the file location given is return unless an unknown variable
	 * was detected.
	 * 
	 * @param fileLocation
	 *            a file location either as a path or a variable with leading
	 *            and trailing spaces already removed.
	 * @param context
	 *            the context used to expand the variable
	 * @param status
	 *            multi status to report any problems expanding variables
	 * @return the file location as a string or <code>null</code> if not
	 *         possible
	 */
	public static String expandFileLocation(String fileLocation,
			ExpandVariableContext context, MultiStatus status) {
		if (fileLocation == null || fileLocation.length() == 0)
			return ""; //$NON-NLS-1$

		VariableDefinition varDef = extractVariableTag(fileLocation, 0);
		// Return if no variable found
		if (varDef.start < 0)
			return fileLocation;

		// Disallow text before/after variable
		if (varDef.start != 0
				|| (varDef.end < fileLocation.length() && varDef.end != -1)) {
			String msg = ExternalToolsModelMessages
					.getString("ToolUtil.fileLocVarBetweenText"); //$NON-NLS-1$
			status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
			return null;
		}

		// Invalid variable format
		if (varDef.name == null || varDef.name.length() == 0
				|| varDef.end == -1) {
			String msg = ExternalToolsModelMessages
					.getString("ToolUtil.fileLocVarFormatWrong"); //$NON-NLS-1$
			status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
			return null;
		}

		// Lookup the variable if it exist
		PathLocationVariableRegistry registry;
		registry = ExternalToolsPlugin.getDefault()
				.getFileLocationVariableRegistry();
		PathLocationVariable variable = registry
				.getPathLocationVariable(varDef.name);
		if (variable == null) {
			String msg = ExternalToolsModelMessages.format(
					"ToolUtil.fileLocVarMissing", new Object[] { varDef.name }); //$NON-NLS-1$
			status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
			return null;
		}

		// Expand the variable into a IPath if possible
		IPath path = variable.getExpander().getPath(varDef.name,
				varDef.argument, context);
		if (path == null) {
			String msg = ExternalToolsModelMessages
					.format(
							"The variable {0} with argument {1} could not be expanded to a valid path.",
							new Object[] { varDef.name, varDef.argument });
			status.merge(ExternalToolsPlugin.newErrorStatus(msg, null));
			return null;
		}

		return path.toString();
	}

	/**
	 * Extracts from the source text the variable tag's name and argument.
	 * 
	 * @param text
	 *            the source text to parse for a variable tag
	 * @param start
	 *            the index in the string to start the search
	 * @return the variable definition
	 */
	public static VariableDefinition extractVariableTag(String text, int start) {
		VariableDefinition varDef = new VariableDefinition();

		varDef.start = text.indexOf(VAR_TAG_START, start);
		if (varDef.start < 0)
			return varDef;
		start = varDef.start + VAR_TAG_START.length();

		int end = text.indexOf(VAR_TAG_END, start);
		if (end < 0)
			return varDef;
		varDef.end = end + VAR_TAG_END.length();
		if (end == start)
			return varDef;

		int mid = text.indexOf(VAR_TAG_SEP, start);
		if (mid < 0 || mid > end) {
			varDef.name = text.substring(start, end);
		} else {
			if (mid > start)
				varDef.name = text.substring(start, mid);
			mid = mid + VAR_TAG_SEP.length();
			if (mid < end)
				varDef.argument = text.substring(mid, end);
		}

		return varDef;
	}

	/**
	 * Parses the argument text into an array of individual arguments using the
	 * space character as the delimiter. An individual argument containing
	 * spaces must have a double quote (") at the start and end. Two double
	 * quotes together is taken to mean an embedded double quote in the argument
	 * text. Variables are treated as a single unit and therefore spaces and
	 * double quotes inside a variable are copied as is and not parsed.
	 * 
	 * @param arguments
	 *            the arguments as one string
	 * @return the array of arguments
	 */
	public static String[] parseArgumentsIntoList(String arguments) {
		if (arguments == null || arguments.length() == 0)
			return new String[0];

		ArrayList list = new ArrayList(10);
		boolean inQuotes = false;
		boolean inVar = false;
		int start = 0;
		int end = arguments.length();
		StringBuffer buffer = new StringBuffer(end);

		while (start < end) {
			char ch = arguments.charAt(start);
			start++;

			switch (ch) {
			case ARG_DELIMITER:
				if (inQuotes || inVar) {
					buffer.append(ch);
				} else {
					if (buffer.length() > 0) {
						list.add(buffer.toString());
						buffer.setLength(0);
					}
				}
				break;

			case ARG_DBL_QUOTE:
				if (inVar) {
					buffer.append(ch);
				} else {
					if (start < end) {
						if (arguments.charAt(start) == ARG_DBL_QUOTE) {
							// Two quotes together represents one quote
							buffer.append(ch);
							start++;
						} else {
							inQuotes = !inQuotes;
						}
					} else {
						// A lone quote at the end, just drop it.
						inQuotes = false;
					}
				}
				break;

			case VAR_TAG_START_CHAR1:
				buffer.append(ch);
				if (!inVar && start < end) {
					if (arguments.charAt(start) == VAR_TAG_START_CHAR2) {
						buffer.append(VAR_TAG_START_CHAR2);
						inVar = true;
						start++;
					}
				}
				break;

			case VAR_TAG_END_CHAR1:
				buffer.append(ch);
				inVar = false;
				break;

			default:
				buffer.append(ch);
				break;
			}

		}

		if (buffer.length() > 0)
			list.add(buffer.toString());

		String[] results = new String[list.size()];
		list.toArray(results);
		return results;
	}

	/**
	 * Structure to represent a variable definition within a source string.
	 */
	public static final class VariableDefinition {
		/**
		 * Index in the source text where the variable started or
		 * <code>-1</code> if no valid variable start tag identifier found.
		 */
		public int start = -1;

		/**
		 * Index in the source text of the character following the end of the
		 * variable or <code>-1</code> if no valid variable end tag found.
		 */
		public int end = -1;

		/**
		 * The variable's name found in the source text, or <code>null</code>
		 * if no valid variable found.
		 */
		public String name = null;

		/**
		 * The variable's argument found in the source text, or
		 * <code>null</code> if no valid variable found or if the variable did
		 * not specify an argument
		 */
		public String argument = null;

		/**
		 * Create an initialized variable definition.
		 */
		private VariableDefinition() {
			super();
		}

		/**
		 * Create an initialized variable definition.
		 */
		private VariableDefinition(int start, int end) {
			super();
			this.start = start;
			this.end = end;
		}
	}
}
