/*
 * Copyright (c) 2007, intarsys consulting GmbH
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * - Neither the name of intarsys nor the names of its contributors may be used
 *   to endorse or promote products derived from this software without specific
 *   prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package de.intarsys.tools.functor;

import java.awt.geom.Point2D;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import de.intarsys.tools.enumeration.EnumItem;
import de.intarsys.tools.enumeration.EnumMeta;
import de.intarsys.tools.locator.FileLocator;
import de.intarsys.tools.locator.ILocator;
import de.intarsys.tools.locator.ILocatorFactory;
import de.intarsys.tools.reflect.ClassTools;
import de.intarsys.tools.string.Converter;
import de.intarsys.tools.string.StringTools;

/**
 * Tool class to ease handling of arguments.
 * 
 */
public class ArgTools {

	static private int nesting = 0;

	public static final IFunctor toString = new IFunctor() {

		public Object perform(IFunctorCall call)
				throws FunctorInvocationException {
			Args args = (Args) call.getReceiver();
			StringBuilder sb = new StringBuilder();
			if (args.isNamed()) {
				for (Iterator it = args.names().iterator(); it.hasNext();) {
					String name = (String) it.next();
					sb.append(name);
					sb.append(" = ");
					sb.append(args.get(name));
					sb.append("\n");
				}
			} else {
				for (int i = 0; i < args.size(); i++) {
					sb.append(i);
					sb.append(" = ");
					sb.append(args.get(i));
					sb.append("\n");
				}
			}
			return sb.toString();
		}

	};

	private static Set visited;

	protected static ILocator createLocator(Object optionValue,
			ILocator defaultValue, ILocatorFactory factory) {
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof ILocator) {
			return (ILocator) optionValue;
		}
		if (optionValue instanceof File) {
			try {
				return factory.createLocator(((File) optionValue)
						.getAbsolutePath());
			} catch (IOException e) {
				return defaultValue;
			}
		}
		if (optionValue instanceof String) {
			if (StringTools.isEmpty((String) optionValue)) {
				return defaultValue;
			}
			try {
				return factory.createLocator((String) optionValue);
			} catch (IOException e) {
				return defaultValue;
			}
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as an {@link IArgs} instance. If
	 * the argument value is not provided or not convertible,
	 * <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link IArgs}, {@link String}, {@link Map}
	 * and {@link List}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as an {@link IArgs}
	 *         instance.
	 */
	public static IArgs getArgs(IArgs args, String name, IArgs defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof IArgs) {
			return (IArgs) optionValue;
		}
		if (optionValue instanceof String) {
			optionValue = Converter.asMap((String) optionValue);
		}
		if (optionValue instanceof Map) {
			return new Args((Map) optionValue);
		}
		if (optionValue instanceof List) {
			return new Args((List) optionValue);
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a boolean. If the argument
	 * value is not provided or not convertible, <code>defaultValue</code>is
	 * returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Boolean}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a <code>boolean</code>
	 */
	public static boolean getBool(IArgs args, String name, boolean defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof Boolean) {
			return ((Boolean) optionValue).booleanValue();
		}
		if (optionValue instanceof String) {
			String optionString = (String) optionValue;
			return Converter.asBoolean(optionString, defaultValue);
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a byte. If the argument value
	 * is not provided or not convertible, <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Number}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a byte.
	 */
	public static byte getByte(IArgs args, String name, byte defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object value = args.get(name);
		if (value instanceof Number) {
			return ((Number) value).byteValue();
		}
		if (value instanceof String) {
			try {
				return Byte.parseByte((String) value);
			} catch (NumberFormatException e) {
				return defaultValue;
			}
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a char. If the argument value
	 * is not provided or not convertible, <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Character}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a char.
	 */
	public static char getChar(IArgs args, String name, char defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object value = args.get(name);
		if (value instanceof Character) {
			return ((Character) value).charValue();
		}
		if (value instanceof String) {
			String valueString = (String) value;
			if (valueString.length() > 0) {
				return valueString.charAt(0);
			}
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a char[]. If the argument
	 * value is not provided or not convertible, <code>defaultValue</code>is
	 * returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link String}, char[]. <b>Unlike the other
	 * conversion methods, this one throws an IllegalArgumentException, if the
	 * value is not of type <code>String</code> or <code>char[]</code>.</b>
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @exception IllegalArgumentException
	 *                if value is not of type <code>String</code> or
	 *                <code>char[]</code>
	 * @return The argument value at <code>name</code> as a {@link String}.
	 */
	public static char[] getCharArray(IArgs args, String name,
			char[] defaultValue) throws IllegalArgumentException {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof char[]) {
			return (char[]) optionValue;
		}
		if (optionValue instanceof String) {
			return ((String) optionValue).toCharArray();
		}
		throw new IllegalArgumentException("argument '" + name //$NON-NLS-1$
				+ "' must be of type string or char[]"); //$NON-NLS-1$
	}

	/**
	 * The argument value at <code>name</code> as a {@link Class}. If the
	 * argument value is not provided or not convertible,
	 * <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Boolean}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a {@link Class}.
	 */
	public static Class getClass(IArgs args, String name, Class defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof Class) {
			return (Class) optionValue;
		}
		if (optionValue instanceof String) {
			String optionString = (String) optionValue;
			try {
				return ClassTools.createClass(optionString, Object.class, null);
			} catch (Exception e) {
				return defaultValue;
			}
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a {@link Date}. If the
	 * argument value is not provided or not convertible,
	 * <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Date}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a {@link Date}.
	 */
	public static Date getDate(IArgs args, String name, Date defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof Date) {
			return (Date) optionValue;
		}
		if (optionValue instanceof String) {
			String optionString = (String) optionValue;
			try {
				return DateFormat.getInstance().parse(optionString);
			} catch (ParseException e) {
				return defaultValue;
			}
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a {@link EnumItem}. If the
	 * argument value is not provided or not convertible, the enumeration
	 * default value is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link EnumItem}, {@link String}.
	 * 
	 * @param args
	 * @param meta
	 * @param name
	 * @return The argument value at <code>name</code> as a {@link EnumItem}.
	 */
	public static <T extends EnumItem> T getEnumItem(IArgs args,
			EnumMeta<T> meta, String name) {
		if (args == null) {
			return meta.getDefault();
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return meta.getDefault();
		}
		if (optionValue instanceof EnumItem) {
			return (T) optionValue;
		}
		if (optionValue instanceof String) {
			String optionString = (String) optionValue;
			return meta.getItemOrDefault(optionString);
		}
		return meta.getDefault();
	}

	/**
	 * The argument value at <code>name</code> as a {@link EnumItem}. If the
	 * argument value is not provided or not convertible, the enumeration item
	 * with the id <code>defaultValuee</code> is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link EnumItem}, {@link String}.
	 * 
	 * @param args
	 * @param meta
	 * @param name
	 * @return The argument value at <code>name</code> as a {@link EnumItem}.
	 */
	public static <T extends EnumItem> T getEnumItem(IArgs args,
			EnumMeta<T> meta, String name, String defaultValue) {
		if (args == null) {
			return meta.getItemOrDefault(defaultValue);
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return meta.getItemOrDefault(defaultValue);
		}
		if (optionValue instanceof EnumItem) {
			return (T) optionValue;
		}
		if (optionValue instanceof String) {
			String optionString = (String) optionValue;
			return meta.getItemOrDefault(optionString);
		}
		return meta.getItemOrDefault(defaultValue);
	}

	/**
	 * The argument value at <code>name</code> as a {@link File}. If the
	 * argument value is not provided or not convertible,
	 * <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link File}, {@link String},
	 * {@link ILocator}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a {@link Date}.
	 */
	public static File getFile(IArgs args, String name, File defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object value = args.get(name);
		if (value instanceof File) {
			return (File) value;
		}
		if (value instanceof String) {
			return new File((String) value);
		}
		if (value instanceof FileLocator) {
			return ((FileLocator) value).getFile();
		}
		if (value instanceof ILocator) {
			return new File(((ILocator) value).getFullName());
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a float. If the argument value
	 * is not provided or not convertible, <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Number}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a float.
	 */
	public static float getFloat(IArgs args, String name, float defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object value = args.get(name);
		if (value instanceof Number) {
			return ((Number) value).floatValue();
		}
		if (value instanceof String) {
			String stringValue = (String) value;
			if (stringValue.indexOf("%") != -1) { //$NON-NLS-1$
				try {
					Number result = NumberFormat.getPercentInstance().parse(
							stringValue);
					return result.floatValue();
				} catch (ParseException e) {
					// todo log warning
					return defaultValue;
				}
			}
			try {
				return Float.parseFloat(stringValue);
			} catch (NumberFormatException e) {
				// todo log warning
				return defaultValue;
			}
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a int. If the argument value
	 * is not provided or not convertible, <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Number}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a int.
	 */
	public static int getInt(IArgs args, String name, int defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object value = args.get(name);
		if (value instanceof Number) {
			return ((Number) value).intValue();
		}
		if (value instanceof String) {
			try {
				return Integer.parseInt((String) value);
			} catch (NumberFormatException e) {
				return defaultValue;
			}
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a {@link ILocator}. If the
	 * argument value is not provided or not convertible,
	 * <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link ILocator}, {@link String},
	 * {@link File}
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @param factory
	 * @return The argument value at <code>name</code> as a {@link ILocator}.
	 */
	public static ILocator getLocator(IArgs args, String name,
			ILocator defaultValue, ILocatorFactory factory) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		return createLocator(optionValue, defaultValue, factory);
	}

	/**
	 * The argument value at <code>name</code> as a List<ILocator>. If the
	 * argument value is not provided, <code>null</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are {@link Collection} of {@link ILocator}, {@link String} and
	 * {@link File}.
	 * 
	 * @param args
	 * @param name
	 * @param factory
	 * @return The argument value at <code>name</code> as a {@link List}.
	 */
	public static List<ILocator> getLocators(IArgs args, String name,
			ILocatorFactory factory) {
		if (args == null) {
			return null;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return null;
		}
		List<ILocator> locators = new ArrayList<ILocator>();
		if (optionValue instanceof Collection) {
			for (Iterator i = ((Collection) optionValue).iterator(); i
					.hasNext();) {
				Object candidate = i.next();
				ILocator locator = createLocator(candidate, null, factory);
				if (locator != null) {
					locators.add(locator);
				}
			}
		} else {
			ILocator locator = createLocator(optionValue, null, factory);
			if (locator != null) {
				locators.add(locator);
			}
		}
		return locators;
	}

	/**
	 * The argument value at <code>name</code> as a {@link Map}. If the argument
	 * value is not provided or not convertible, <code>defaultValue</code>is
	 * returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Map}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a {@link Map}.
	 */
	public static Map getMap(IArgs args, String name, Map defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof Map) {
			return (Map) optionValue;
		}
		if (optionValue instanceof String) {
			return Converter.asMap((String) optionValue);
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a {@link Object}. If the
	 * argument value is not provided or not convertible,
	 * <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Object}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a {@link Object}.
	 */
	public static Object getObject(IArgs args, String name, Object defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		return optionValue;
	}

	/**
	 * The argument value at <code>name</code> as a {@link Point2D}. If the
	 * argument value is not provided or not convertible,
	 * <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link Point2D}, {@link String}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a {@link Point2D}.
	 */
	public static Point2D getPoint(IArgs args, String name, Point2D defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof Point2D) {
			return (Point2D) optionValue;
		}
		if (optionValue instanceof String) {
			String optionString = (String) optionValue;
			String[] coords = optionString.split("[x*@]"); //$NON-NLS-1$
			if ((coords == null) || (coords.length != 2)) {
				return defaultValue;
			}
			try {
				float x = Float.parseFloat(coords[0]);
				float y = Float.parseFloat(coords[1]);
				return new Point2D.Float(x, y);
			} catch (NumberFormatException e) {
				return defaultValue;
			}
		}
		return defaultValue;
	}

	/**
	 * The argument value at <code>name</code> as a {@link String}. If the
	 * argument value is not provided or not convertible,
	 * <code>defaultValue</code>is returned.
	 * <p>
	 * This method performs the necessary casts and conversions. Supported input
	 * types are <code>null</code>, {@link String}, {@link Object}.
	 * 
	 * @param args
	 * @param name
	 * @param defaultValue
	 * @return The argument value at <code>name</code> as a {@link String}.
	 */
	public static String getString(IArgs args, String name, String defaultValue) {
		if (args == null) {
			return defaultValue;
		}
		Object optionValue = args.get(name);
		if (optionValue == null) {
			return defaultValue;
		}
		if (optionValue instanceof String) {
			return (String) optionValue;
		}
		if (optionValue instanceof char[]) {
			return new String((char[]) optionValue);
		}
		return String.valueOf(optionValue);
	}

	/**
	 * Create a new argument name from <code>name</code> by prefixing with
	 * <code>prefix</code>.
	 * 
	 * @param prefix
	 * @param name
	 * @return The new argument name.
	 */
	public static String prefix(String prefix, String name) {
		if (name == null) {
			return null;
		}
		if ((prefix == null) || (prefix.length() == 0)) {
			return name;
		}
		return prefix + Character.toUpperCase(name.charAt(0))
				+ name.substring(1);
	}

	/**
	 * Cast or convert <code>value</code> to an {@link IArgs}.
	 * 
	 * @param value
	 * @return The {@link IArgs} created from <code>value</code>.
	 */
	public static IArgs toArgs(Object value) {
		if (value instanceof IArgs) {
			return (IArgs) value;
		}
		if (value instanceof String) {
			value = Converter.asMap((String) value);
		}
		if (value instanceof Map) {
			return new Args((Map) value);
		}
		if (value instanceof List) {
			return new Args((List) value);
		}
		return Args.EMPTY;
	}

	/**
	 * Convert the <code>args</code> to a {@link List}.
	 * 
	 * @param args
	 * @return The {@link List} representation of the <code>args</code>
	 */
	public static List toList(IArgs args) {
		List result = new ArrayList();
		int i = args.size();
		while (i > 0) {
			i--;
			result.add(args.get(i));
		}
		return result;
	}

	/**
	 * Convert the <code>args</code> to a {@link Map}.
	 * 
	 * @param args
	 * @return The {@link Map} representation of the <code>args</code>
	 */
	public static Map toMap(IArgs args) {
		Map result = new HashMap();
		if (args != null) {
			for (Iterator it = args.names().iterator(); it.hasNext();) {
				String name = (String) it.next();
				result.put(name, args.get(name));
			}
		}
		return result;
	}

	/**
	 * Create a printable {@link String} for <code>args</code>.
	 * 
	 * @param args
	 * @param prefix
	 * @return
	 */
	synchronized public static String toString(IArgs args, String prefix) {
		if (visited == null) {
			visited = new HashSet();
			nesting = 0;
		}
		if (visited.contains(args)) {
			return "...recursive...";
		}
		if (nesting == 4) {
			return "...nested to deeply...";
		}
		visited.add(args);
		nesting++;
		try {
			StringBuilder sb = new StringBuilder();
			if (args.isNamed()) {
				// flat ones....
				for (Iterator it = args.names().iterator(); it.hasNext();) {
					String name = (String) it.next();
					Object value = args.get(name);
					if (!(value instanceof IArgs)) {
						toStringPlain(prefix, sb, name, value);
					}
				}
				// nested ones...
				for (Iterator it = args.names().iterator(); it.hasNext();) {
					String name = (String) it.next();
					Object value = args.get(name);
					if (value instanceof IArgs) {
						toStringArgs(prefix, sb, name, (IArgs) value);
					}
				}
			} else {
				for (int i = 0; i < args.size(); i++) {
					Object value = args.get(i);
					if (value instanceof IArgs) {
						toStringArgs(prefix, sb, "" + i, (IArgs) value);
					} else {
						toStringPlain(prefix, sb, "" + i, value);
					}
				}
			}
			return sb.toString();
		} finally {
			nesting--;
			if (nesting == 0) {
				visited = null;
			}
		}
	}

	protected static void toStringArgs(String prefix, StringBuilder sb,
			String name, IArgs value) {
		for (int i = 1; i < nesting; i++) {
			sb.append("   ");
		}
		sb.append(name);
		sb.append(" = ");
		sb.append("{");
		sb.append("\n");
		sb.append(toString(value, prefix));
		sb.append("\n");
		for (int i = 1; i < nesting; i++) {
			sb.append("   ");
		}
		sb.append("}");
		sb.append("\n");
	}

	protected static void toStringPlain(String prefix, StringBuilder sb,
			String name, Object value) {
		for (int i = 1; i < nesting; i++) {
			sb.append("   ");
		}
		sb.append(name);
		sb.append(" = ");
		sb.append(StringTools.safeString(value));
		sb.append("\n");
	}
}
