package aQute.bnd.osgi;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.Formatter;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

import aQute.bnd.osgi.Processor.FileLine;
import aQute.bnd.version.MavenVersion;
import aQute.bnd.version.Version;
import aQute.bnd.version.VersionRange;
import aQute.lib.base64.Base64;
import aQute.lib.collections.ExtList;
import aQute.lib.collections.SortedList;
import aQute.lib.filter.ExtendedFilter;
import aQute.lib.filter.Get;
import aQute.lib.hex.Hex;
import aQute.lib.io.IO;
import aQute.lib.utf8properties.UTF8Properties;
import aQute.libg.glob.Glob;
import aQute.service.reporter.Reporter.SetLocation;

/**
 * Provide a macro processor. This processor can replace variables in strings
 * based on a properties and a domain. The domain can implement functions that
 * start with a "_" and take args[], the names of these functions are available
 * as functions in the macro processor (without the _). Macros can nest to any
 * depth but may not contain loops. Add POSIX macros: ${#parameter} String
 * length. ${parameter%word} Remove smallest suffix pattern. ${parameter%%word}
 * Remove largest suffix pattern. ${parameter#word} Remove smallest prefix
 * pattern. ${parameter##word} Remove largest prefix pattern.
 */
public class Macro {
	final static String		NULLVALUE		= "c29e43048791e250dfd5723e7b8aa048df802c9262cfa8fbc4475b2e392a8ad2";
	final static String		LITERALVALUE	= "017a3ddbfc0fcd27bcdb2590cdb713a379ae59ef";
	final static Pattern	NUMERIC_P		= Pattern.compile("[-+]?(\\d*\\.?\\d+|\\d+\\.)(e[-+]?[0-9]+)?");
	final static Pattern	PRINTF_P		= Pattern.compile(
			"%(?:(\\d+)\\$)?(-|\\+|0|\\(|,|\\^|#| )*(\\d*)?(?:\\.(\\d+))?(a|A|b|B|h|H|d|f|c|s|x|X|u|o|z|Z|e|E|g|G|p|n|b|B|%)");
	Processor				domain;
	Object					targets[];
	boolean					flattening;
	String					profile;
	private boolean			nosystem;
	ScriptEngine			engine			= null;
	ScriptContext			context			= null;
	Bindings				bindings		= null;
	StringWriter			stdout			= new StringWriter();
	StringWriter			stderr			= new StringWriter();

	public Macro(Processor domain, Object... targets) {
		this.domain = domain;
		this.targets = targets;
		if (targets != null) {
			for (Object o : targets) {
				assert o != null;
			}
		}
	}

	public String process(String line, Processor source) {
		return process(line, new Link(source, null, line));
	}

	String process(String line, Link link) {
		StringBuilder sb = new StringBuilder();
		process(line, 0, '\u0000', '\u0000', sb, link);
		return sb.toString();
	}

	int process(CharSequence org, int index, char begin, char end, StringBuilder result, Link link) {
		if (org == null) { // treat null like empty string
			return index;
		}
		StringBuilder line = new StringBuilder(org);
		int nesting = 1;

		StringBuilder variable = new StringBuilder();

		outer: while (index < line.length()) {
			char c1 = line.charAt(index++);
			if (c1 == end) {
				if (--nesting == 0) {
					result.append(replace(variable.toString(), link, begin, end));
					return index;
				}
			} else if (c1 == begin)
				nesting++;
			else if (c1 == '\\' && index < line.length() - 1 && line.charAt(index) == '$') {
				// remove the escape backslash and interpret the dollar
				// as a
				// literal
				index++;
				variable.append('$');
				continue outer;
			} else if (c1 == '$' && index < line.length() - 2) {
				char c2 = line.charAt(index);
				char terminator = getTerminator(c2);
				if (terminator != 0) {
					index = process(line, index + 1, c2, terminator, variable, link);
					continue outer;
				}
			} else if (c1 == '.' && index < line.length() && line.charAt(index) == '/') {
				// Found the sequence ./
				if (index == 1 || Character.isWhitespace(line.charAt(index - 2))) {
					// make sure it is preceded by whitespace or starts at begin
					index++;
					variable.append(domain.getBase().getAbsolutePath());
					variable.append('/');
					continue outer;
				}
			}
			variable.append(c1);
		}
		result.append(variable);
		return index;
	}

	public static char getTerminator(char c) {
		switch (c) {
			case '(' :
				return ')';
			case '[' :
				return ']';
			case '{' :
				return '}';
			case '<' :
				return '>';
			case '\u00ab' : // Guillemet double << >>
				return '\u00bb';
			case '\u2039' : // Guillemet single
				return '\u203a';
		}
		return 0;
	}

	protected String getMacro(String key, Link link) {
		return getMacro(key, link, '{', '}');
	}

	private String getMacro(String key, Link link, char begin, char end) {
		if (link != null && link.contains(key))
			return "${infinite:" + link.toString() + "}";

		if (key != null) {
			key = key.trim();
			if (key.length() > 0) {
				Processor source = domain;
				String value = null;

				if (key.indexOf(';') < 0) {
					Instruction ins = new Instruction(key);
					if (!ins.isLiteral()) {
						SortedList<String> sortedList = SortedList.fromIterator(domain.iterator());
						StringBuilder sb = new StringBuilder();
						String del = "";
						for (String k : sortedList) {
							if (ins.matches(k)) {
								String v = replace(k, new Link(source, link, key), begin, end);
								if (v != null) {
									sb.append(del);
									del = ",";
									sb.append(v);
								}
							}
						}
						return sb.toString();
					}
				}
				while (value == null && source != null) {
					value = source.getProperties().getProperty(key);
					source = source.getParent();
				}

				if (value != null)
					return process(value, new Link(source, link, key));

				value = doCommands(key, link);
				if (value != null) {
					if (value == NULLVALUE)
						return null;
					if (value == LITERALVALUE)
						return LITERALVALUE;
					return process(value, new Link(source, link, key));
				}

				if (key != null && key.trim().length() > 0) {
					value = System.getProperty(key);
					if (value != null)
						return value;
				}

				if (key != null && key.indexOf(';') >= 0) {
					String parts[] = key.split(";");
					if (parts.length > 1) {
						if (parts.length >= 16) {
							domain.error("too many arguments for template: %s, max is 16", key);
						}

						String template = domain.getProperties().getProperty(parts[0]);
						if (template != null) {
							domain = new Processor(domain);
							for (int i = 0; i < 16; i++) {
								domain.setProperty("" + i, i < parts.length ? parts[i] : "null");
							}
							ExtList<String> args = new ExtList<String>(parts);
							args.remove(0);
							domain.setProperty("#", args.join());
							try {
								value = process(template, new Link(domain, link, key));
								if (value != null)
									return value;
							} finally {
								domain = domain.getParent();
							}
						}
					}
				}
			} else {
				domain.warning("Found empty macro key");
			}
		} else {
			domain.warning("Found null macro key");
		}

		// Prevent recursion, but try to get a profiled variable
		if (key != null && !key.startsWith("[") && !key.equals(Constants.PROFILE)) {
			if (profile == null)
				profile = domain.get(Constants.PROFILE);
			if (profile != null) {
				String replace = getMacro("[" + profile + "]" + key, link, begin, end);
				if (replace != null)
					return replace;
			}
		}
		return null;
	}

	public String replace(String key, Link link) {
		return replace(key, link, '{', '}');
	}

	private String replace(String key, Link link, char begin, char end) {
		String value = getMacro(key, link, begin, end);
		if (value != LITERALVALUE) {
			if (value != null)
				return value;
			if (!flattening && !key.startsWith("@"))
				domain.warning("No translation found for macro: %s", key);
		}
		return "$" + begin + key + end;
	}

	/**
	 * Parse the key as a command. A command consist of parameters separated by
	 * ':'.
	 */
	static Pattern commands = Pattern.compile("(?<!\\\\);");

	@SuppressWarnings("resource")
	private String doCommands(String key, Link source) {
		String[] args = commands.split(key);
		if (args == null || args.length == 0)
			return null;

		for (int i = 0; i < args.length; i++)
			if (args[i].indexOf('\\') >= 0)
				args[i] = args[i].replaceAll("\\\\;", ";");

		if (args[0].startsWith("^")) {
			String varname = args[0].substring(1).trim();

			Processor parent = source.start.getParent();
			if (parent != null)
				return parent.getProperty(varname);
			return null;
		}

		Processor rover = domain;
		while (rover != null) {
			String result = doCommand(rover, args[0], args);
			if (result != null)
				return result;

			rover = rover.getParent();
		}

		for (int i = 0; targets != null && i < targets.length; i++) {
			String result = doCommand(targets[i], args[0], args);
			if (result != null)
				return result;
		}

		return doCommand(this, args[0], args);
	}

	private String doCommand(Object target, String method, String[] args) {
		if (target == null)
			; // System.err.println("Huh? Target should never be null " +
		// domain);
		else {
			// Assume macro names do not start with '-'
			if (method.startsWith("-")) {
				return null;
			}

			String part = method.replaceAll("-", "_");
			for (int i = 0; i < part.length(); i++) {
				if (!Character.isJavaIdentifierPart(part.charAt(i)))
					return null;
			}

			String cname = "_" + part;
			try {
				Method m = target.getClass().getMethod(cname, new Class[] {
						String[].class
				});
				Object result = m.invoke(target, new Object[] {
						args
				});
				return result == null ? NULLVALUE : result.toString();
			} catch (NoSuchMethodException e) {
				return null;
			} catch (InvocationTargetException e) {
				if (e.getCause() instanceof IllegalArgumentException) {
					domain.error("%s, for cmd: %s, arguments; %s", e.getCause().getMessage(), method,
							Arrays.toString(args));
				} else {
					domain.warning("Exception in replace: %s", e.getCause());
				}
				return NULLVALUE;
			} catch (Exception e) {
				domain.warning("Exception in replace: %s method=%s", e, method);
				return NULLVALUE;
			}
		}
		return null;
	}

	/**
	 * Return a unique list where the duplicates are removed.
	 */
	static String _uniqHelp = "${uniq;<list> ...}";

	public String _uniq(String args[]) {
		verifyCommand(args, _uniqHelp, null, 1, Integer.MAX_VALUE);
		Set<String> set = new LinkedHashSet<String>();
		for (int i = 1; i < args.length; i++) {
			Processor.split(args[i], set);
		}
		return Processor.join(set, ",");
	}

	public String _pathseparator(String args[]) {
		return File.pathSeparator;
	}

	public String _separator(String args[]) {
		return File.separator;
	}

	public String _filter(String args[]) {
		return filter(args, false);
	}

	public String _select(String args[]) {
		return filter(args, false);
	}

	public String _filterout(String args[]) {
		return filter(args, true);

	}

	public String _reject(String args[]) {
		return filter(args, true);

	}

	static String _filterHelp = "${%s;<list>;<regex>}";

	String filter(String[] args, boolean include) {
		verifyCommand(args, String.format(_filterHelp, args[0]), null, 3, 3);

		Collection<String> list = toCollection(args[1]);
		Pattern pattern = Pattern.compile(args[2]);

		for (Iterator<String> i = list.iterator(); i.hasNext();) {
			if (pattern.matcher(i.next()).matches() == include)
				i.remove();
		}
		return Processor.join(list);
	}

	ArrayList<String> toCollection(String arg) {
		return new ArrayList<String>(Processor.split(arg));
	}

	static String _sortHelp = "${sort;<list>...}";

	public String _sort(String args[]) {
		verifyCommand(args, _sortHelp, null, 2, Integer.MAX_VALUE);

		List<String> result = new ArrayList<String>();
		for (int i = 1; i < args.length; i++) {
			Processor.split(args[i], result);
		}
		Collections.sort(result);
		return Processor.join(result);
	}

	static String _nsortHelp = "${nsort;<list>...}";

	public String _nsort(String args[]) {
		verifyCommand(args, _nsortHelp, null, 2, Integer.MAX_VALUE);

		ExtList<String> result = new ExtList<String>();
		for (int i = 1; i < args.length; i++) {
			result.addAll(ExtList.from(args[i]));
		}
		Collections.sort(result, new Comparator<String>() {

			public int compare(String a, String b) {
				while (a.startsWith("0"))
					a = a.substring(1);

				while (b.startsWith("0"))
					b = b.substring(1);

				if (a.length() == b.length())
					return a.compareTo(b);
				else if (a.length() > b.length())
					return 1;
				else
					return -1;

			}
		});
		return result.join();
	}

	static String _joinHelp = "${join;<list>...}";

	public String _join(String args[]) {

		verifyCommand(args, _joinHelp, null, 1, Integer.MAX_VALUE);

		List<String> result = new ArrayList<String>();
		for (int i = 1; i < args.length; i++) {
			Processor.split(args[i], result);
		}
		return Processor.join(result);
	}

	static String _sjoinHelp = "${sjoin;<separator>;<list>...}";

	public String _sjoin(String args[]) throws Exception {
		verifyCommand(args, _sjoinHelp, null, 2, Integer.MAX_VALUE);

		List<String> result = new ArrayList<String>();
		for (int i = 2; i < args.length; i++) {
			Processor.split(args[i], result);
		}
		return Processor.join(args[1], result);
	}

	static String _ifHelp = "${if;<condition>;<iftrue> [;<iffalse>] } condition is either a filter expression or truthy";

	public String _if(String args[]) throws Exception {
		verifyCommand(args, _ifHelp, null, 2, 4);
		String condition = args[1];
		if (isTruthy(condition))
			return args.length > 2 ? args[2] : "true";

		if (args.length > 3)
			return args[3];
		return "";
	}

	public boolean isTruthy(String condition) throws Exception {
		if (condition == null)
			return false;

		condition = condition.trim();

		if (condition.startsWith("(") && condition.endsWith(")")) {
			return doCondition(condition);
		}

		return !condition.equalsIgnoreCase("false") && !condition.equals("0") && !condition.equals("0.0")
				&& condition.length() != 0;
	}

	public final static String _nowHelp = "${now;pattern|'long'}, returns current time";

	public Object _now(String args[]) {
		verifyCommand(args, _nowHelp, null, 1, 2);
		Date now = new Date();

		if (args.length == 2) {
			if ("long".equals(args[1]))
				return now.getTime();

			DateFormat df = new SimpleDateFormat(args[1], Locale.US);
			df.setTimeZone(TimeZone.getTimeZone("UTC"));
			return df.format(now);
		}
		return new Date();
	}

	public final static String _fmodifiedHelp = "${fmodified;<list of filenames>...}, return latest modification date";

	public String _fmodified(String args[]) throws Exception {
		verifyCommand(args, _fmodifiedHelp, null, 2, Integer.MAX_VALUE);

		long time = 0;
		Collection<String> names = new ArrayList<String>();
		for (int i = 1; i < args.length; i++) {
			Processor.split(args[i], names);
		}
		for (String name : names) {
			File f = new File(name);
			if (f.exists() && f.lastModified() > time)
				time = f.lastModified();
		}
		return "" + time;
	}

	public String _long2date(String args[]) {
		try {
			return new Date(Long.parseLong(args[1])).toString();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return "not a valid long";
	}

	public String _literal(String args[]) {
		if (args.length != 2)
			throw new RuntimeException("Need a value for the ${literal;<value>} macro");
		return "${" + args[1] + "}";
	}

	public String _def(String args[]) {
		verifyCommand(args, "${def;<name>[;<value>]}, get the property or a default value if unset", null, 2, 3);

		return domain.getProperty(args[1], args.length == 3 ? args[2] : "");
	}

	/**
	 * replace ; <list> ; regex ; replace
	 *
	 * @param args
	 */
	public String _replace(String args[]) {
		if (args.length < 4 || args.length > 5) {
			domain.warning("Invalid nr of arguments to replace %s", Arrays.asList(args));
			return null;
		}

		String middle = ", ";
		if (args.length > 4)
			middle = args[4];

		String list[] = args[1].split("\\s*,\\s*");
		StringBuilder sb = new StringBuilder();
		String del = "";
		for (int i = 0; i < list.length; i++) {
			String element = list[i].trim();
			if (!element.equals("")) {
				sb.append(del);
				sb.append(element.replaceAll(args[2], args[3]));
				del = middle;
			}
		}

		return sb.toString();
	}

	public String _warning(String args[]) throws Exception {
		for (int i = 1; i < args.length; i++) {
			SetLocation warning = domain.warning("%s", process(args[i]));
			FileLine header = domain.getHeader(Pattern.compile(".*"), Pattern.compile("\\$\\{warning;"));
			if (header != null)
				header.set(warning);
		}
		return "";
	}

	public String _error(String args[]) throws Exception {
		for (int i = 1; i < args.length; i++) {
			SetLocation error = domain.error("%s", process(args[i]));
			FileLine header = domain.getHeader(Pattern.compile(".*"), Pattern.compile("\\$\\{error;"));
			if (header != null)
				header.set(error);
		}
		return "";
	}

	/**
	 * toclassname ; <path>.class ( , <path>.class ) *
	 */
	static String _toclassnameHelp = "${classname;<list of class names>}, convert class paths to FQN class names ";

	public String _toclassname(String args[]) {
		verifyCommand(args, _toclassnameHelp, null, 2, 2);
		Collection<String> paths = Processor.split(args[1]);

		List<String> names = new ArrayList<String>(paths.size());
		for (String path : paths) {
			if (path.endsWith(".class")) {
				String name = path.substring(0, path.length() - 6).replace('/', '.');
				names.add(name);
			} else if (path.endsWith(".java")) {
				String name = path.substring(0, path.length() - 5).replace('/', '.');
				names.add(name);
			} else {
				domain.warning("in toclassname, %s is not a class path because it does not end in .class", args[1]);
			}
		}
		return Processor.join(names, ",");
	}

	/**
	 * toclassname ; <path>.class ( , <path>.class ) *
	 */

	static String _toclasspathHelp = "${toclasspath;<list>[;boolean]}, convert a list of class names to paths";

	public String _toclasspath(String args[]) {
		verifyCommand(args, _toclasspathHelp, null, 2, 3);
		boolean cl = true;
		if (args.length > 2)
			cl = Boolean.valueOf(args[2]);

		Collection<String> names = Processor.split(args[1]);
		Collection<String> paths = new ArrayList<String>(names.size());
		for (String name : names) {
			String path = name.replace('.', '/') + (cl ? ".class" : "");
			paths.add(path);
		}
		return Processor.join(paths, ",");
	}

	public String _dir(String args[]) {
		if (args.length < 2) {
			domain.warning("Need at least one file name for ${dir;...}");
			return null;
		}
		String del = "";
		StringBuilder sb = new StringBuilder();
		for (int i = 1; i < args.length; i++) {
			File f = domain.getFile(args[i]);
			if (f.exists() && f.getParentFile().exists()) {
				sb.append(del);
				sb.append(f.getParentFile().getAbsolutePath());
				del = ",";
			}
		}
		return sb.toString();

	}

	public String _basename(String args[]) {
		if (args.length < 2) {
			domain.warning("Need at least one file name for ${basename;...}");
			return null;
		}
		String del = "";
		StringBuilder sb = new StringBuilder();
		for (int i = 1; i < args.length; i++) {
			File f = domain.getFile(args[i]);
			if (f.exists() && f.getParentFile().exists()) {
				sb.append(del);
				sb.append(f.getName());
				del = ",";
			}
		}
		return sb.toString();

	}

	public String _isfile(String args[]) {
		if (args.length < 2) {
			domain.warning("Need at least one file name for ${isfile;...}");
			return null;
		}
		boolean isfile = true;
		for (int i = 1; i < args.length; i++) {
			File f = new File(args[i]).getAbsoluteFile();
			isfile &= f.isFile();
		}
		return isfile ? "true" : "false";

	}

	public String _isdir(String args[]) {
		// if (args.length < 2) {
		// domain.warning("Need at least one file name for ${isdir;...}");
		// return null;
		// }
		boolean isdir = true;
		// If no dirs provided, return false
		if (args.length < 2) {
			isdir = false;
		}
		for (int i = 1; i < args.length; i++) {
			File f = new File(args[i]).getAbsoluteFile();
			isdir &= f.isDirectory();
		}
		return isdir ? "true" : "false";

	}

	public String _tstamp(String args[]) {
		String format = "yyyyMMddHHmm";
		long now = System.currentTimeMillis();
		TimeZone tz = TimeZone.getTimeZone("UTC");

		if (args.length > 1) {
			format = args[1];
		}
		if (args.length > 2) {
			tz = TimeZone.getTimeZone(args[2]);
		}
		if (args.length > 3) {
			now = Long.parseLong(args[3]);
		}
		if (args.length > 4) {
			domain.warning("Too many arguments for tstamp: %s", Arrays.toString(args));
		}

		SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
		sdf.setTimeZone(tz);
		String tstamp = domain.getProperty(Constants.TSTAMP);
		if (tstamp != null)
			try {
				now = Long.parseLong(tstamp);
			} catch (NumberFormatException e) {
				// ignore, just use current time
			}
		return sdf.format(new Date(now));
	}

	/**
	 * Wildcard a directory. The lists can contain Instruction that are matched
	 * against the given directory ${lsr;<dir>;<list>(;<list>)*} ${lsa;<dir>;
	 * <list>(;<list>)*}
	 *
	 * @author aqute
	 */

	public String _lsr(String args[]) {
		return ls(args, true);
	}

	public String _lsa(String args[]) {
		return ls(args, false);
	}

	String ls(String args[], boolean relative) {
		if (args.length < 2)
			throw new IllegalArgumentException("the ${ls} macro must at least have a directory as parameter");

		File dir = domain.getFile(args[1]);
		if (!dir.isAbsolute())
			throw new IllegalArgumentException("the ${ls} macro directory parameter is not absolute: " + dir);

		if (!dir.exists())
			throw new IllegalArgumentException("the ${ls} macro directory parameter does not exist: " + dir);

		if (!dir.isDirectory())
			throw new IllegalArgumentException(
					"the ${ls} macro directory parameter points to a file instead of a directory: " + dir);

		Collection<File> files = new ArrayList<File>(new SortedList<File>(dir.listFiles()));

		for (int i = 2; i < args.length; i++) {
			Instructions filters = new Instructions(args[i]);
			files = filters.select(files, true);
		}

		List<String> result = new ArrayList<String>();
		for (File file : files)
			result.add(relative ? file.getName() : file.getAbsolutePath().replace(File.separatorChar, '/'));

		return Processor.join(result, ",");
	}

	public String _currenttime(String args[]) {
		return Long.toString(System.currentTimeMillis());
	}

	/**
	 * Modify a version to set a version policy. The policy is a mask that is
	 * mapped to a version.
	 *
	 * <pre>
	 *  + increment - decrement = maintain s only
	 * pos=3 (qualifier). If qualifer == SNAPSHOT, return m.m.m-SNAPSHOT else
	 * m.m.m.q s only pos=3 (qualifier). If qualifer == SNAPSHOT, return
	 * m.m.m-SNAPSHOT else m.m.m &tilde; discard ==+ = maintain major, minor,
	 * increment micro, discard qualifier &tilde;&tilde;&tilde;= = just get the
	 * qualifier version=&quot;[${version;==;${&#x40;}},${version;=+;${&#x40;}})&quot;
	 * </pre>
	 */
	final static String		MASK_STRING			= "[\\-+=~0123456789]{0,3}[=~]?";
	final static Pattern	MASK				= Pattern.compile(MASK_STRING);
	final static String		_versionHelp		= "${version;<mask>;<version>}, modify a version\n"
			+ "<mask> ::= [ M [ M [ M [ MQ ]]]\n" + "M ::= '+' | '-' | MQ\n" + "MQ ::= '~' | '='";
	final static Pattern	_versionPattern[]	= new Pattern[] {
			null, null, MASK, Verifier.VERSION
	};

	public String _versionmask(String args[]) {
		return _version(args);
	}

	public String _version(String args[]) {
		verifyCommand(args, _versionHelp, null, 2, 3);

		String mask = args[1];

		Version version = null;
		if (args.length >= 3) {
			if (isLocalTarget(args[2]))
				return LITERALVALUE;

			version = Version.parseVersion(args[2]);
		}

		return version(version, mask);
	}

	String version(Version version, String mask) {
		if (version == null) {
			String v = domain.getProperty("@");
			if (v == null) {
				return LITERALVALUE;
			}
			version = new Version(v);
		}

		StringBuilder sb = new StringBuilder();
		String del = "";

		for (int i = 0; i < mask.length(); i++) {
			char c = mask.charAt(i);
			String result = null;
			if (c != '~') {
				if (i == 3) {
					result = version.getQualifier();
					MavenVersion mv = new MavenVersion(version);
					if (c == 'S') {
						// we have a request for a Maven snapshot
						if (mv.isSnapshot())
							return sb.append("-SNAPSHOT").toString();
					} else if (c == 's') {
						// we have a request for a Maven snapshot
						if (mv.isSnapshot())
							return sb.append("-SNAPSHOT").toString();
						else
							return sb.toString();
					}
				} else if (Character.isDigit(c)) {
					// Handle masks like +00, =+0
					result = String.valueOf(c);
				} else {
					int x = version.get(i);
					switch (c) {
						case '+' :
							x++;
							break;
						case '-' :
							x--;
							break;
						case '=' :
							break;
					}
					result = Integer.toString(x);
				}
				if (result != null) {
					sb.append(del);
					del = ".";
					sb.append(result);
				}
			}
		}
		return sb.toString();
	}

	/**
	 * Schortcut for version policy
	 *
	 * <pre>
	 *  -provide-policy : ${policy;[==,=+)}
	 * -consume-policy : ${policy;[==,+)}
	 * </pre>
	 */

	static Pattern	RANGE_MASK		= Pattern.compile("(\\[|\\()(" + MASK_STRING + "),(" + MASK_STRING + ")(\\]|\\))");
	static String	_rangeHelp		= "${range;<mask>[;<version>]}, range for version, if version not specified lookup ${@}\n"
			+ "<mask> ::= [ M [ M [ M [ MQ ]]]\n" + "M ::= '+' | '-' | MQ\n" + "MQ ::= '~' | '='";
	static Pattern	_rangePattern[]	= new Pattern[] {
			null, RANGE_MASK
	};

	public String _range(String args[]) {
		verifyCommand(args, _rangeHelp, _rangePattern, 2, 3);
		Version version = null;
		if (args.length >= 3) {
			String string = args[2];
			if (isLocalTarget(string))
				return LITERALVALUE;

			version = new Version(string);
		} else {
			String v = domain.getProperty("@");
			if (v == null)
				return LITERALVALUE;

			version = new Version(v);
		}
		String spec = args[1];

		Matcher m = RANGE_MASK.matcher(spec);
		m.matches();
		String floor = m.group(1);
		String floorMask = m.group(2);
		String ceilingMask = m.group(3);
		String ceiling = m.group(4);

		String left = version(version, floorMask);
		String right = version(version, ceilingMask);
		StringBuilder sb = new StringBuilder();
		sb.append(floor);
		sb.append(left);
		sb.append(",");
		sb.append(right);
		sb.append(ceiling);

		String s = sb.toString();
		VersionRange vr = new VersionRange(s);
		if (!(vr.includes(vr.getHigh()) || vr.includes(vr.getLow()))) {
			domain.error("${range} macro created an invalid range %s from %s and mask %s", s, version, spec);
		}
		return sb.toString();
	}

	boolean isLocalTarget(String string) {
		return string.matches("\\$(\\{@\\}|\\[@\\]|\\(@\\)|<@>|«@»|‹@›)");
	}

	/**
	 * System command. Execute a command and insert the result.
	 */
	public String system_internal(boolean allowFail, String args[]) throws Exception {
		if (nosystem)
			throw new RuntimeException("Macros in this mode cannot excute system commands");

		verifyCommand(args,
				"${" + (allowFail ? "system-allow-fail" : "system") + ";<command>[;<in>]}, execute a system command",
				null, 2, 3);
		String command = args[1];
		String input = null;

		if (args.length > 2) {
			input = args[2];
		}

		if (File.separatorChar == '\\')
			command = "cmd /c \"" + command + "\"";

		Process process = Runtime.getRuntime().exec(command, null, domain.getBase());
		if (input != null) {
			process.getOutputStream().write(input.getBytes(UTF_8));
		}
		process.getOutputStream().close();

		String s = IO.collect(process.getInputStream(), UTF_8);
		int exitValue = process.waitFor();

		if (exitValue != 0) {
			if (!allowFail) {
				domain.error("System command %s failed with exit code %d", command, exitValue);
			} else {
				domain.warning("System command %s failed with exit code %d (allowed)", command, exitValue);

			}
			return null;
		}

		return s.trim();
	}

	public String _system(String args[]) throws Exception {
		return system_internal(false, args);
	}

	public String _system_allow_fail(String args[]) throws Exception {
		String result = "";
		try {
			result = system_internal(true, args);
			return result == null ? "" : result;
		} catch (Throwable t) {
			/* ignore */
			return "";
		}
	}

	public String _env(String args[]) {
		verifyCommand(args, "${env;<name>[;alternative]}, get the environment variable", null, 2, 3);

		try {
			String ret = System.getenv(args[1]);
			if (ret != null)
				return ret;

			if (args.length > 2)
				return args[2];

		} catch (Throwable t) {
			// ignore
		}
		return "";
	}

	/**
	 * Get the contents of a file.
	 *
	 * @throws IOException
	 */

	public String _cat(String args[]) throws IOException {
		verifyCommand(args, "${cat;<in>}, get the content of a file", null, 2, 2);
		File f = domain.getFile(args[1]);
		if (f.isFile()) {
			return IO.collect(f).replaceAll("\\\\", "\\\\\\\\");
		} else if (f.isDirectory()) {
			return Arrays.toString(f.list());
		} else {
			try {
				URL url = new URL(args[1]);
				return IO.collect(url, UTF_8);
			} catch (MalformedURLException mfue) {
				// Ignore here
			}
			return null;
		}
	}

	/**
	 * Get the Base64 encoding of a file.
	 *
	 * @throws IOException
	 */
	public String _base64(String... args) throws IOException {
		verifyCommand(args, "${base64;<file>[;fileSizeLimit]}, get the Base64 encoding of a file", null, 2, 3);

		File file = domain.getFile(args[1]);
		long maxLength = 100_000;
		if (args.length > 2)
			maxLength = Long.parseLong(args[2]);

		if (file.length() > maxLength)
			throw new IllegalArgumentException(
					"Maximum file size (" + maxLength + ") for base64 macro exceeded for file " + file);

		return Base64.encodeBase64(file);
	}

	/**
	 * Get a digest of a file.
	 *
	 * @throws NoSuchAlgorithmException
	 * @throws IOException
	 */
	public String _digest(String... args) throws NoSuchAlgorithmException, IOException {
		verifyCommand(args, "${digest;<algo>;<in>}, get a digest (e.g. MD5, SHA-256) of a file", null, 3, 3);

		MessageDigest digester = MessageDigest.getInstance(args[1]);
		File f = domain.getFile(args[2]);
		IO.copy(f, digester);
		byte[] digest = digester.digest();
		return Hex.toHexString(digest);
	}

	public static void verifyCommand(String args[], String help, Pattern[] patterns, int low, int high) {
		String message = "";
		if (args.length > high) {
			message = "too many arguments";
		} else if (args.length < low) {
			message = "too few arguments";
		} else {
			for (int i = 0; patterns != null && i < patterns.length && i < args.length; i++) {
				if (patterns[i] != null) {
					Matcher m = patterns[i].matcher(args[i]);
					if (!m.matches())
						message += String.format("Argument %s (%s) does not match %s%n", i, args[i],
								patterns[i].pattern());
				}
			}
		}
		if (message.length() != 0) {
			StringBuilder sb = new StringBuilder();
			String del = "${";
			for (String arg : args) {
				sb.append(del);
				sb.append(arg);
				del = ";";
			}
			sb.append("}, is not understood. ");
			sb.append(message);
			throw new IllegalArgumentException(sb.toString());
		}
	}

	// Helper class to track expansion of variables
	// on the stack.
	static class Link {
		Link		previous;
		String		key;
		Processor	start;

		public Link(Processor start, Link previous, String key) {
			this.previous = previous;
			this.key = key;
			this.start = start;
		}

		public boolean contains(String key) {
			if (this.key.equals(key))
				return true;

			if (previous == null)
				return false;

			return previous.contains(key);
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			String del = "[";
			for (Link r = this; r != null; r = r.previous) {
				sb.append(del);
				sb.append(r.key);
				del = ",";
			}
			sb.append("]");
			return sb.toString();
		}
	}

	/**
	 * Take all the properties and translate them to actual values. This method
	 * takes the set properties and traverse them over all entries, including
	 * the default properties for that properties. The values no longer contain
	 * macros.
	 * <p>
	 * There are some rules
	 * <ul>
	 * <li>Property names starting with an underscore ('_') are ignored. These
	 * are reserved for properties that cause an unwanted side effect when
	 * expanded unnecessary
	 * <li>Property names starting with a minus sign ('-') are not expanded to
	 * maintain readability
	 * </ul>
	 *
	 * @return A new Properties with the flattened values
	 */
	public Properties getFlattenedProperties() {
		return getFlattenedProperties(true);
	}

	/**
	 * Take all the properties and translate them to actual values. This method
	 * takes the set properties and traverse them over all entries, including
	 * the default properties for that properties. The values no longer contain
	 * macros.
	 * <p>
	 * Property names starting with an underscore ('_') are ignored. These are
	 * reserved for properties that cause an unwanted side effect when expanded
	 * unnecessary
	 *
	 * @return A new Properties with the flattened values
	 */
	public Properties getFlattenedProperties(boolean ignoreInstructions) {
		// Some macros only work in a lower processor, so we
		// do not report unknown macros while flattening
		flattening = true;
		try {
			Properties flattened = new UTF8Properties();
			Properties source = domain.getProperties();
			for (Enumeration< ? > e = source.propertyNames(); e.hasMoreElements();) {
				String key = (String) e.nextElement();
				if (!key.startsWith("_")) {
					String value = source.getProperty(key);
					if (value == null) {
						Object raw = source.get(key);
						domain.warning("Key '%s' has a non-String value: %s:%s", key,
								raw == null ? "" : raw.getClass().getName(), raw);
					} else {
						if (ignoreInstructions && key.startsWith("-"))
							flattened.put(key, value);
						else
							flattened.put(key, process(value));
					}
				}
			}
			return flattened;
		} finally {
			flattening = false;
		}
	}

	public final static String _fileHelp = "${file;<base>;<paths>...}, create correct OS dependent path";

	public String _osfile(String args[]) {
		verifyCommand(args, _fileHelp, null, 3, 3);
		File base = new File(args[1]);
		File f = Processor.getFile(base, args[2]);
		return f.getAbsolutePath();
	}

	public String _path(String args[]) {
		List<String> list = new ArrayList<String>();
		for (int i = 1; i < args.length; i++) {
			list.addAll(Processor.split(args[i]));
		}
		return Processor.join(list, File.pathSeparator);
	}

	public final static String _sizeHelp = "${size;<collection>;...}, count the number of elements (of all collections combined)";

	public int _size(String args[]) {
		verifyCommand(args, _sizeHelp, null, 1, 16);
		int size = 0;
		for (int i = 1; i < args.length; i++) {
			ExtList<String> l = ExtList.from(args[i]);
			size += l.size();
		}
		return size;
	}

	public static Properties getParent(Properties p) {
		try {
			Field f = Properties.class.getDeclaredField("defaults");
			f.setAccessible(true);
			return (Properties) f.get(p);
		} catch (Exception e) {
			Field[] fields = Properties.class.getFields();
			System.err.println(Arrays.toString(fields));
			return null;
		}
	}

	public String process(String line) {
		return process(line, domain);
	}

	public boolean isNosystem() {
		return nosystem;
	}

	public boolean setNosystem(boolean nosystem) {
		boolean tmp = this.nosystem;
		this.nosystem = nosystem;
		return tmp;
	}

	public String _unescape(String args[]) {
		StringBuilder sb = new StringBuilder();
		for (int i = 1; i < args.length; i++) {
			sb.append(args[i]);
		}

		for (int j = 0; j < sb.length() - 1; j++) {
			if (sb.charAt(j) == '\\') {
				switch (sb.charAt(j + 1)) {

					case 'n' :
						sb.replace(j, j + 2, "\n");
						break;

					case 'r' :
						sb.replace(j, j + 2, "\r");
						break;

					case 'b' :
						sb.replace(j, j + 2, "\b");
						break;

					case 'f' :
						sb.replace(j, j + 2, "\f");
						break;

					case 't' :
						sb.replace(j, j + 2, "\t");
						break;

					default :
						break;
				}
			}
		}
		return sb.toString();
	}

	static String _startswith = "${startswith;<string>;<prefix>}";

	public String _startswith(String args[]) throws Exception {
		verifyCommand(args, _startswith, null, 3, 3);
		if (args[1].startsWith(args[2]))
			return args[1];
		else
			return "";
	}

	static String _endswith = "${endswith;<string>;<suffix>}";

	public String _endswith(String args[]) throws Exception {
		verifyCommand(args, _endswith, null, 3, 3);
		if (args[1].endsWith(args[2]))
			return args[1];
		else
			return "";
	}

	static String _extension = "${extension;<string>}";

	public String _extension(String args[]) throws Exception {
		verifyCommand(args, _extension, null, 2, 2);
		String name = args[1];
		int n = name.indexOf('.');
		if (n < 0)
			return "";
		return name.substring(n + 1);
	}

	static String _stem = "${stem;<string>}";

	public String _stem(String args[]) throws Exception {
		verifyCommand(args, _stem, null, 2, 2);
		String name = args[1];
		int n = name.indexOf('.');
		if (n < 0)
			return name;
		return name.substring(0, n);
	}

	static String _substring = "${substring;<string>;<start>[;<end>]}";

	public String _substring(String args[]) throws Exception {
		verifyCommand(args, _substring, null, 3, 4);

		String string = args[1];
		int start = Integer.parseInt(args[2].equals("") ? "0" : args[2]);
		int end = string.length();

		if (args.length > 3) {
			end = Integer.parseInt(args[3]);
			if (end < 0)
				end = string.length() + end;
		}

		if (start < 0)
			start = string.length() + start;

		if (start > end) {
			int t = start;
			start = end;
			end = t;
		}

		return string.substring(start, end);
	}

	static String	_rand	= "${rand;[<min>[;<end>]]}";
	static Random	random	= new Random();

	public long _rand(String args[]) throws Exception {
		verifyCommand(args, _rand, null, 2, 3);

		int min = 0;
		int max = 100;
		if (args.length > 1) {
			max = Integer.parseInt(args[1]);
			if (args.length > 2) {
				min = Integer.parseInt(args[2]);
			}
		}
		int diff = max - min;

		double d = random.nextDouble() * diff + min;
		return Math.round(d);
	}

	static String _length = "${length;<string>}";

	public int _length(String args[]) throws Exception {
		verifyCommand(args, _length, null, 1, 2);
		if (args.length == 1)
			return 0;

		return args[1].length();
	}

	static String _get = "${get;<index>;<list>}";

	public String _get(String args[]) throws Exception {
		verifyCommand(args, _get, null, 3, 3);

		int index = Integer.parseInt(args[1]);
		List<String> list = toList(args, 2, 3);
		if (index < 0)
			index = list.size() + index;
		return list.get(index);
	}

	static String _sublist = "${sublist;<start>;<end>[;<list>...]}";

	public String _sublist(String args[]) throws Exception {
		verifyCommand(args, _sublist, null, 4, Integer.MAX_VALUE);

		int start = Integer.parseInt(args[1]);
		int end = Integer.parseInt(args[2]);
		ExtList<String> list = toList(args, 3, args.length);

		if (start < 0)
			start = list.size() + start + 1;

		if (end < 0)
			end = list.size() + end + 1;

		if (start > end) {
			int t = start;
			start = end;
			end = t;
		}

		return Processor.join(list.subList(start, end));
	}

	private ExtList<String> toList(String[] args, int i, int j) {
		ExtList<String> list = new ExtList<String>();
		for (; i < j; i++) {
			Processor.split(args[i], list);
		}
		return list;
	}

	static String _first = "${first;<list>[;<list>...]}";

	public String _first(String args[]) throws Exception {
		verifyCommand(args, _first, null, 1, Integer.MAX_VALUE);

		List<String> list = toList(args, 1, args.length);
		if (list.isEmpty())
			return "";

		return list.get(0);
	}

	static String _last = "${last;<list>[;<list>...]}";

	public String _last(String args[]) throws Exception {
		verifyCommand(args, _last, null, 1, Integer.MAX_VALUE);

		List<String> list = toList(args, 1, args.length);
		if (list.isEmpty())
			return "";

		return list.get(list.size() - 1);
	}

	static String _max = "${max;<list>[;<list>...]}";

	public String _max(String args[]) throws Exception {
		verifyCommand(args, _max, null, 2, Integer.MAX_VALUE);

		List<String> list = toList(args, 1, args.length);
		String a = null;

		for (String s : list) {
			if (a == null || a.compareTo(s) < 0)
				a = s;
		}
		if (a == null)
			return "";

		return a;
	}

	static String _min = "${min;<list>[;<list>...]}";

	public String _min(String args[]) throws Exception {
		verifyCommand(args, _min, null, 2, Integer.MAX_VALUE);

		List<String> list = toList(args, 1, args.length);
		String a = null;

		for (String s : list) {
			if (a == null || a.compareTo(s) > 0)
				a = s;
		}
		if (a == null)
			return "";

		return a;
	}

	static String _nmax = "${nmax;<list>[;<list>...]}";

	public String _nmax(String args[]) throws Exception {
		verifyCommand(args, _nmax, null, 2, Integer.MAX_VALUE);

		List<String> list = toList(args, 1, args.length);
		double d = Double.NaN;

		for (String s : list) {
			double v = Double.parseDouble(s);
			if (Double.isNaN(d) || v > d)
				d = v;
		}
		return toString(d);
	}

	static String _nmin = "${nmin;<list>[;<list>...]}";

	public String _nmin(String args[]) throws Exception {
		verifyCommand(args, _nmin, null, 2, Integer.MAX_VALUE);

		List<String> list = toList(args, 1, args.length);
		double d = Double.NaN;

		for (String s : list) {
			double v = Double.parseDouble(s);
			if (Double.isNaN(d) || v < d)
				d = v;
		}
		return toString(d);
	}

	static String _sum = "${sum;<list>[;<list>...]}";

	public String _sum(String args[]) throws Exception {
		verifyCommand(args, _sum, null, 2, Integer.MAX_VALUE);

		List<String> list = toList(args, 1, args.length);
		double d = 0;

		for (String s : list) {
			double v = Double.parseDouble(s);
			d += v;
		}
		return toString(d);
	}

	static String _average = "${average;<list>[;<list>...]}";

	public String _average(String args[]) throws Exception {
		verifyCommand(args, _sum, null, 2, Integer.MAX_VALUE);

		List<String> list = toList(args, 1, args.length);
		if (list.isEmpty())
			throw new IllegalArgumentException("No members in list to calculate average");

		double d = 0;

		for (String s : list) {
			double v = Double.parseDouble(s);
			d += v;
		}
		return toString(d / list.size());
	}

	static String _reverse = "${reverse;<list>[;<list>...]}";

	public String _reverse(String args[]) throws Exception {
		verifyCommand(args, _reverse, null, 2, Integer.MAX_VALUE);

		ExtList<String> list = toList(args, 1, args.length);
		Collections.reverse(list);
		return Processor.join(list);
	}

	static String _indexof = "${indexof;<value>;<list>[;<list>...]}";

	public int _indexof(String args[]) throws Exception {
		verifyCommand(args, _indexof, null, 3, Integer.MAX_VALUE);

		String value = args[1];
		ExtList<String> list = toList(args, 2, args.length);
		return list.indexOf(value);
	}

	static String _lastindexof = "${lastindexof;<value>;<list>[;<list>...]}";

	public int _lastindexof(String args[]) throws Exception {
		verifyCommand(args, _indexof, null, 3, Integer.MAX_VALUE);

		String value = args[1];
		ExtList<String> list = toList(args, 1, args.length);
		return list.lastIndexOf(value);
	}

	static String _find = "${find;<target>;<searched>}";

	public int _find(String args[]) throws Exception {
		verifyCommand(args, _find, null, 3, 3);

		return args[1].indexOf(args[2]);
	}

	static String _findlast = "${findlast;<find>;<target>}";

	public int _findlast(String args[]) throws Exception {
		verifyCommand(args, _findlast, null, 3, 3);

		return args[2].lastIndexOf(args[1]);
	}

	static String _split = "${split;<regex>[;<target>...]}";

	public String _split(String args[]) throws Exception {
		verifyCommand(args, _split, null, 2, Integer.MAX_VALUE);

		List<String> collected = new ArrayList<String>();
		for (int n = 2; n < args.length; n++) {
			String value = args[n];
			String[] split = value.split(args[1]);
			for (String s : split)
				if (!s.isEmpty())
					collected.add(s);
		}
		return Processor.join(collected);
	}

	static String _js = "${js [;<js expr>...]}";

	public Object _js(String args[]) throws Exception {
		verifyCommand(args, _js, null, 2, Integer.MAX_VALUE);

		StringBuilder sb = new StringBuilder();

		for (int i = 1; i < args.length; i++)
			sb.append(args[i]).append(';');

		if (context == null) {
			synchronized (this) {
				if (engine == null)
					engine = new ScriptEngineManager().getEngineByName("javascript");
			}
			context = engine.getContext();
			bindings = context.getBindings(ScriptContext.ENGINE_SCOPE);
			bindings.put("domain", domain);
			String javascript = domain.mergeProperties("javascript", ";");
			if (javascript != null && javascript.length() > 0) {
				engine.eval(javascript, context);
			}
			context.setErrorWriter(stderr);
			context.setWriter(stdout);
		}
		Object eval = engine.eval(sb.toString(), context);
		StringBuffer buffer = stdout.getBuffer();
		if (buffer.length() > 0) {
			domain.error("Executing js: %s: %s", sb, buffer);
			buffer.setLength(0);
		}

		if (eval != null) {
			return toString(eval);
		}

		String out = stdout.toString();
		stdout.getBuffer().setLength(0);
		return out;
	}

	private String toString(Object eval) {
		if (eval == null)
			return "null";

		if (eval instanceof Double || eval instanceof Float) {
			String v = eval.toString();
			if (v.endsWith(".0"))
				return v.substring(0, v.length() - 2);
		}
		return eval.toString();

	}

	static String _toupper = "${toupper;<target>}";

	public String _toupper(String args[]) throws Exception {
		verifyCommand(args, _tolower, null, 2, 2);

		return args[1].toUpperCase();
	}

	static String _tolower = "${tolower;<target>}";

	public String _tolower(String args[]) throws Exception {
		verifyCommand(args, _tolower, null, 2, 2);

		return args[1].toLowerCase();
	}

	static String _compare = "${compare;<astring>;<bstring>}";

	public int _compare(String args[]) throws Exception {
		verifyCommand(args, _compare, null, 3, 3);
		int n = args[1].compareTo(args[2]);
		if (n == 0)
			return 0;

		return n > 0 ? 1 : -1;
	}

	static String _ncompare = "${ncompare;<anumber>;<bnumber>}";

	public int _ncompare(String args[]) throws Exception {
		verifyCommand(args, _ncompare, null, 3, 3);
		double a = Double.parseDouble(args[1]);
		double b = Double.parseDouble(args[2]);
		if (a > b)
			return 1;
		if (a < b)
			return -1;
		return 0;
	}

	static String _matches = "${matches;<target>;<regex>}";

	public boolean _matches(String args[]) throws Exception {
		verifyCommand(args, _matches, null, 3, 3);

		return args[1].matches(args[2]);
	}

	static String _subst = "${subst;<target>;<regex>[;<replace>[;count]]}";

	public StringBuffer _subst(String args[]) throws Exception {
		verifyCommand(args, _subst, null, 4, 5);

		Pattern p = Pattern.compile(args[2]);
		Matcher matcher = p.matcher(args[1]);
		String replace = "";
		int count = Integer.MAX_VALUE;

		if (args.length > 3) {
			replace = args[3];
		}

		if (args.length > 4) {
			count = Integer.parseInt(args[4]);
		}

		StringBuffer sb = new StringBuffer();

		for (int i = 0; i < count; i++) {
			if (matcher.find()) {
				matcher.appendReplacement(sb, replace);
			} else
				break;
		}
		matcher.appendTail(sb);
		return sb;
	}

	static String _trim = "${trim;<target>}";

	public String _trim(String args[]) throws Exception {
		verifyCommand(args, _trim, null, 2, 2);

		return args[1].trim();
	}

	static String _format = "${format;<format>[;args...]}";

	public String _format(String args[]) throws Exception {
		verifyCommand(args, _format, null, 2, Integer.MAX_VALUE);

		Object[] args2 = new Object[args.length + 10];

		Matcher m = PRINTF_P.matcher(args[1]);
		int n = 2;
		while (n < args.length && m.find()) {
			char conversion = m.group(5).charAt(0);
			switch (conversion) {
				// d|f|c|s|h|n|x|X|u|o|z|Z|e|E|g|G|p|\n|%)");
				case 'd' :
				case 'u' :
				case 'o' :
				case 'x' :
				case 'X' :
				case 'z' :
				case 'Z' :
					args2[n - 2] = Long.parseLong(args[n]);
					n++;
					break;

				case 'f' :
				case 'e' :
				case 'E' :
				case 'g' :
				case 'G' :
				case 'a' :
				case 'A' :
					args2[n - 2] = Double.parseDouble(args[n]);
					n++;
					break;

				case 'c' :
					if (args[n].length() != 1)
						throw new IllegalArgumentException("Character expected but found '" + args[n] + "'");
					args2[n - 2] = args[n].charAt(0);
					n++;
					break;

				case 'b' :
					String v = args[n].toLowerCase();
					if (v == null || v.equals("false") || v.isEmpty()
							|| (NUMERIC_P.matcher(v).matches() && Double.parseDouble(v) == 0.0D))
						args2[n - 2] = false;
					else
						args2[n - 2] = false;
					n++;
					break;

				case 's' :
				case 'h' :
				case 'H' :
				case 'p' :
					args2[n - 2] = args[n];
					n++;
					break;

				case 't' :
				case 'T' :
					String dt = args[n];

					if (NUMERIC_P.matcher(dt).matches()) {
						args2[n - 2] = Long.parseLong(dt);
					} else {
						DateFormat df;
						switch (args[n].length()) {
							case 6 :
								df = new SimpleDateFormat("yyMMdd", Locale.US);
								break;

							case 8 :
								df = new SimpleDateFormat("yyyyMMdd", Locale.US);
								break;

							case 12 :
								df = new SimpleDateFormat("yyyyMMddHHmm", Locale.US);
								break;

							case 14 :
								df = new SimpleDateFormat("yyyyMMddHHmmss", Locale.US);
								break;
							case 19 :
								df = new SimpleDateFormat("yyyyMMddHHmmss.SSSZ", Locale.US);
								break;

							default :
								throw new IllegalArgumentException("Unknown dateformat " + args[n]);
						}
						df.setTimeZone(TimeZone.getTimeZone("UTC"));
						args2[n - 2] = df.parse(args[n]);
					}
					break;

				case 'n' :
				case '%' :
					break;
			}
		}

		try (Formatter f = new Formatter()) {
			f.format(args[1], args2);
			return f.toString();
		}
	}

	static String _isempty = "${isempty;[<target>...]}";

	public boolean _isempty(String args[]) throws Exception {
		verifyCommand(args, _isempty, null, 1, Integer.MAX_VALUE);

		for (int i = 1; i < args.length; i++)
			if (!args[i].trim().isEmpty())
				return false;

		return true;
	}

	static String _isnumber = "${isnumber[;<target>...]}";

	public boolean _isnumber(String args[]) throws Exception {
		verifyCommand(args, _isnumber, null, 2, Integer.MAX_VALUE);

		for (int i = 1; i < args.length; i++)
			if (!NUMERIC_P.matcher(args[i]).matches())
				return false;

		return true;
	}

	static String _is = "${is;<a>;<b>}";

	public boolean _is(String args[]) throws Exception {
		verifyCommand(args, _is, null, 3, Integer.MAX_VALUE);
		String a = args[1];

		for (int i = 2; i < args.length; i++)
			if (!a.equals(args[i]))
				return false;

		return true;
	}

	/**
	 * Map a value from a list to a new value
	 */

	static String _map = "${map;<macro>[;<list>...]}";

	public String _map(String args[]) throws Exception {
		verifyCommand(args, _map, null, 2, Integer.MAX_VALUE);
		String macro = args[1];
		List<String> list = toList(args, 2, args.length);
		List<String> result = new ArrayList<String>();

		for (String s : list) {
			String invoc = process("${" + macro + ";" + s + "}");
			result.add(invoc);
		}

		return Processor.join(result);
	}

	/**
	 * Map a value from a list to a new value, providing the value and the index
	 */

	static String _foreach = "${foreach;<macro>[;<list>...]}";

	public String _foreach(String args[]) throws Exception {
		verifyCommand(args, _foreach, null, 2, Integer.MAX_VALUE);
		String macro = args[1];
		List<String> list = toList(args, 2, args.length);
		List<String> result = new ArrayList<String>();

		int n = 0;
		for (String s : list) {
			String invoc = process("${" + macro + ";" + s + ";" + n++ + "}");
			result.add(invoc);
		}

		return Processor.join(result);
	}

	/**
	 * Take a list and convert this to the argumets
	 */

	static String _apply = "${apply;<macro>[;<list>...]}";

	public String _apply(String args[]) throws Exception {
		verifyCommand(args, _apply, null, 2, Integer.MAX_VALUE);
		String macro = args[1];
		List<String> list = toList(args, 2, args.length);
		List<String> result = new ArrayList<String>();

		StringBuilder sb = new StringBuilder();
		sb.append("${").append(macro);
		for (String s : list) {
			sb.append(";").append(s);
		}
		sb.append("}");

		return process(sb.toString());
	}

	/**
	 * Format bytes
	 */
	public String _bytes(String[] args) {
		try (Formatter sb = new Formatter()) {
			for (int i = 0; i < args.length; i++) {
				long l = Long.parseLong(args[1]);
				bytes(sb, l, 0, new String[] {
						"b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb", "Bb", "Geopbyte"
				});
			}
			return sb.toString();
		}
	}

	private void bytes(Formatter sb, double l, int i, String[] strings) {
		if (l > 1024 && i < strings.length - 1) {
			bytes(sb, l / 1024, i + 1, strings);
			return;
		}
		l = Math.round(l * 10) / 10;
		sb.format("%s %s", l, strings[i]);
	}

	static String _globHelp = "${glob;<globexp>} (turn it into a regular expression)";

	public String _glob(String[] args) {
		verifyCommand(args, _globHelp, null, 2, 2);
		String glob = args[1];
		boolean negate = false;
		if (glob.startsWith("!")) {
			glob = glob.substring(1);
			negate = true;
		}

		Pattern pattern = Glob.toPattern(glob);
		if (negate)
			return "(?!" + pattern.pattern() + ")";
		else
			return pattern.pattern();
	}

	public boolean doCondition(String arg) throws Exception {
		ExtendedFilter f = new ExtendedFilter(arg);
		return f.match(new Get() {

			@Override
			public Object get(String key) throws Exception {
				if (key.endsWith("[]")) {
					key = key.substring(0, key.length() - 2);
					return toCollection(domain.getProperty(key));
				} else

					return domain.getProperty(key);
			}

		});
	}

}
