/*******************************************************************************
 * Copyright (c) 2005, 2013 Intel Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Intel Corporation - Initial API and implementation
 * Andrew Gvozdev (Quoin Inc.)
 *******************************************************************************/
package org.eclipse.cdt.utils.cdtvariables;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.cdt.core.cdtvariables.CdtVariableException;
import org.eclipse.cdt.core.cdtvariables.ICdtVariable;
import org.eclipse.cdt.core.cdtvariables.ICdtVariableStatus;
import org.eclipse.cdt.internal.core.cdtvariables.CdtMacroSupplier;

/**
 * Utility class to resolve macro and variable references. Provides fixture to parse ${macro}
 * expressions and replace macros with actual values using {@link IVariableSubstitutor}.
 * 
 * @since 3.0
 */
public class CdtVariableResolver {
	/** @since 5.5 */
	public static final String VAR_CONFIG_NAME = CdtMacroSupplier.VAR_CONFIG_NAME;
	/** @since 5.5 */
	public static final String VAR_CONFIG_DESCRIPTION = CdtMacroSupplier.VAR_CONFIG_DESCRIPTION;
	/** @since 5.5 */
	public static final String VAR_PROJ_NAME = CdtMacroSupplier.VAR_PROJ_NAME;
	/** @since 5.5 */
	public static final String VAR_PROJ_DIR_PATH = CdtMacroSupplier.VAR_PROJ_DIR_PATH;
	/** @since 5.5 */
	public static final String VAR_WORKSPACE_DIR_PATH = CdtMacroSupplier.VAR_WORKSPACE_DIR_PATH;
	/** @since 5.5 */
	public static final String VAR_DIRECTORY_DELIMITER = CdtMacroSupplier.VAR_DIRECTORY_DELIMITER;
	/** @since 5.5 */
	public static final String VAR_PATH_DELIMITER = CdtMacroSupplier.VAR_PATH_DELIMITER;
	/** @since 5.5 */
	public static final String VAR_ECLIPSE_VERSION = CdtMacroSupplier.VAR_ECLIPSE_VERSION;
	/** @since 5.5 */
	public static final String VAR_CDT_VERSION = CdtMacroSupplier.VAR_CDT_VERSION;
	/** @since 5.5 */
	public static final String VAR_HOST_OS_NAME = CdtMacroSupplier.VAR_HOST_OS_NAME;
	/** @since 5.5 */
	public static final String VAR_HOST_ARCH_NAME = CdtMacroSupplier.VAR_HOST_ARCH_NAME;
	/** @since 5.5 */
	public static final String VAR_OS_TYPE = CdtMacroSupplier.VAR_OS_TYPE;
	/** @since 5.5 */
	public static final String VAR_ARCH_TYPE = CdtMacroSupplier.VAR_ARCH_TYPE;

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

	public  static final String VARIABLE_PREFIX = "${";	//$NON-NLS-1$
	private static final String VARIABLE_PREFIX_MASKED = "$\1";	//$NON-NLS-1$
	public  static final char VARIABLE_SUFFIX = '}';
	private static final char VARIABLE_SUFFIX_MASKED = '\2';
	public  static final char VARIABLE_ESCAPE_CHAR = '\\';
	private static final char VARIABLE_ESCAPE_CHAR_MASKED = '\3';

	// Regular expression fragments
	private static final String RE_VPREFIX = "\\$\\{"; //$NON-NLS-1$
	private static final String RE_VSUFFIX = "\\}"; //$NON-NLS-1$
	private static final String RE_VNAME = "[^${}]*"; //$NON-NLS-1$
	private static final String RE_BSLASH = "[\\\\]"; // *one* backslash //$NON-NLS-1$
	
	/**
	 * Converts list of strings to one string using given string as delimiter,
	 * i.e -> "string1:string2:string3"
	 * 
	 * @param value - list of strings to convert.
	 * @param listDelimiter - delimiter.
	 * @return all strings from the list separated with given delimiter.
	 */
	static public String convertStringListToString(String value[], String listDelimiter) {
		
		if(value == null || value.length == 0)
			return EMPTY_STRING;
		
		StringBuffer buffer = new StringBuffer();
		for(int i = 0; i < value.length; i++){
			buffer.append(value[i]);
			if(listDelimiter != null && !EMPTY_STRING.equals(listDelimiter) && i < value.length -1 )
				buffer.append(listDelimiter);
		}
		return buffer.toString();
	}

	/**
	 * Resolves macros of kind ${Macro} in the given string by calling the macro substitutor
	 * for each macro reference found. Macros can be inside one another like
	 * ${workspace_loc:/${ProjName}/} but resolved just once. No recursive or concatenated
	 * macro names are allowed. It is possible to prevent macro from expanding using backslash \$.
	 * 
	 * @param string - macro expression.
	 * @param substitutor - macro resolution provider to retrieve macro values.
	 * @return resolved string
	 * 
	 * @throws CdtVariableException if substitutor can't handle the macro and returns null or throws.
	 */
	static public String resolveToString(String string, IVariableSubstitutor substitutor)
			throws CdtVariableException{
		if (string==null) {
			return EMPTY_STRING;
		}

		final Pattern pattern = Pattern.compile(".*?("+RE_BSLASH+"*)("+RE_VPREFIX+"("+RE_VNAME+")"+RE_VSUFFIX+").*"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$

		StringBuffer buffer = new StringBuffer(string);
		int limit=string.length();
		for (Matcher matcher = pattern.matcher(buffer);matcher.matches();matcher = pattern.matcher(buffer)) {
			String bSlashes=matcher.group(1);
			String macro=matcher.group(2);
			String name=matcher.group(3);
			String resolved = name.length()>0 ? substitutor.resolveToString(name) : EMPTY_STRING;
			if (resolved==null) {
				throw new CdtVariableException(ICdtVariableStatus.TYPE_MACRO_UNDEFINED, null, string, name);
			}
			if (limit-- < 0) {
				// to prevent incidental looping
				throw new CdtVariableException(ICdtVariableStatus.TYPE_ERROR, name, matcher.group(0), resolved);
			}

			int nBSlashes=bSlashes.length();
			if ((nBSlashes & 1)==1) {
				// if odd number of backslashes in front of "${...}" do not expand macro
				resolved = macro;
			}
			// Only one expansion is allowed, so hide any text interfering with macro syntax
			resolved=resolved.replace(VARIABLE_PREFIX, VARIABLE_PREFIX_MASKED);
			resolved=resolved.replace(VARIABLE_SUFFIX, VARIABLE_SUFFIX_MASKED);
			buffer.replace(matcher.start(2), matcher.end(2), resolved);
			// collapse and hide backslashes  \\\\${Macro} -> \\MacroValue or \\\\\${Macro} -> \\${Macro} 
			buffer.replace(matcher.start(1), matcher.end(1),
				bSlashes.substring(0, nBSlashes/2).replace(VARIABLE_ESCAPE_CHAR, VARIABLE_ESCAPE_CHAR_MASKED));
		}
		String result = buffer.toString();

		// take hidden data back
		result=result.replace(VARIABLE_PREFIX_MASKED, VARIABLE_PREFIX);
		result=result.replace(VARIABLE_SUFFIX_MASKED, VARIABLE_SUFFIX);
		result=result.replace(VARIABLE_ESCAPE_CHAR_MASKED, VARIABLE_ESCAPE_CHAR);

		return result;
	}

	/**
	 * finds the macro references in the given string and calls the macro substitutor for each macro found
	 * this could be used for obtaining the list of macros referenced in the given string, etc.
	 * 
	 * @param string
	 * @param substitutor
	 * @throws CdtVariableException
	 * 
	 * @deprecated Use {@link #resolveToString} which would do full nested expansion.
	 */
	@Deprecated
	static public void checkVariables(String string, IVariableSubstitutor substitutor)
			throws CdtVariableException{
		resolveToString(string, substitutor);
	}

	/**
	 * Resolves array of macros using {@code substitutor} to pull macro's list of values.
	 * Note that each macro of input array can in turn provide list of values and
	 * the resulting array combines all of them.
	 * 
	 * @param values - input array of macros.
	 * @param substitutor - macro resolution provider to retrieve macro values.
	 * @param ignoreErrors - if {@code true} then exceptions are caught and ignored.
	 * @return array of resolved values.
	 * @throws CdtVariableException if substitutor throws {@link CdtVariableException}
	 *         and {@code ignoreErrors}={@code null}.
	 */
	static public String[] resolveStringListValues(String values[], IVariableSubstitutor substitutor, boolean ignoreErrors) 
						throws CdtVariableException {
		String result[] = null;
		if(values == null || values.length == 0)
			result = values;
		else if(values.length == 1)
			try {
				result = CdtVariableResolver.resolveToStringList(values[0], substitutor);
			} catch (CdtVariableException e) {
				if(!ignoreErrors)
					throw e;
			}
		else {	
			List<String> list = new ArrayList<String>();
			for (String value : values) {
				String resolved[];
				try {
					resolved = CdtVariableResolver.resolveToStringList(value, substitutor);
					if(resolved != null && resolved.length > 0)
						list.addAll(Arrays.asList(resolved));
				} catch (CdtVariableException e) {
					if(!ignoreErrors)
						throw e;
				}
			}

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

	/**
	 * Resolves macro ${ListMacro} in the given String to the String-list using substitutor
	 * to pull macro's list of values. If the provided string is not exactly a single macro
	 * it is treated as macro expression and result is put into the first element of resulting array.
	 * 
	 * @param string - input string.
	 * @param substitutor - macro resolution provider to retrieve macro values.
	 * @return array of resolved values.
	 * @throws CdtVariableException if substitutor can't handle the macro and returns null or throws.
	 */
	static public String[] resolveToStringList(String string, IVariableSubstitutor substitutor)
			throws CdtVariableException{
		
		StringBuffer buffer = new StringBuffer(string);
		final Pattern pattern = Pattern.compile("^"+RE_VPREFIX+"("+RE_VNAME+")"+RE_VSUFFIX+"$"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		Matcher matcher = pattern.matcher(buffer);
		if (matcher.matches()) {
			String name=matcher.group(1);
			if (name.equals(EMPTY_STRING)) {
				return new String[0];
			}
			String[] result = substitutor.resolveToStringList(name);
			if (result==null) {
				throw new CdtVariableException(ICdtVariableStatus.TYPE_MACRO_UNDEFINED, null, string, name);
			}
			return result;
		}
		return new String[] {resolveToString(string, substitutor)};
	}

	/**
	 * Test for String-list type of macro.
	 *
	 * @param macroType - type of tested macro.
	 * @return {@code true} if the given macro is a String-list macro.
	 */
	public static boolean isStringListVariable(int macroType){
		switch(macroType){
		case ICdtVariable.VALUE_TEXT_LIST:
		case ICdtVariable.VALUE_PATH_FILE_LIST:
		case ICdtVariable.VALUE_PATH_DIR_LIST:
		case ICdtVariable.VALUE_PATH_ANY_LIST:
			return true;
		default:
			return false;
		}
	}
	
	/**
	 * Checks the macros integrity for the given context. If test fails {@link CdtVariableException}
	 * is thrown.
	 * 
	 * @param info - context information to acquire list of available macros.
	 * @param substitutor - macro resolution provider to retrieve macro values.
	 * @throws CdtVariableException propagated up if {@code substitutor} throws.
	 */
	public static void checkIntegrity(
			IVariableContextInfo info,
			IVariableSubstitutor substitutor) throws CdtVariableException{
		
		if(info != null){
			ICdtVariable macros[] = SupplierBasedCdtVariableManager.getVariables(info,true);
			if(macros != null){
				for (ICdtVariable macro : macros) {
					if(isStringListVariable(macro.getValueType()))
						substitutor.resolveToStringList(macro.getName());
					else
						substitutor.resolveToString(macro.getName());
				}
			}
		}
	}
	
	/**
	 * Constructs a macro reference given the macro name
	 * e.g. if the "macro1" name is passed, returns "${macro1}"
	 * 
	 * @param name - macro name.
	 * @return macro variable in form "${macro}"
	 */
	public static String createVariableReference(String name){
		return VARIABLE_PREFIX + name + VARIABLE_SUFFIX;
	}
	
}
