if (typeof(JavaScriptStringEncode) == "undefined") throw new Error("visualize() requires JavaScriptStringEncode()");
function visualize(xValue, sPadding, iDepth) {
	if (typeof(sPadding) !== "string") sPadding = "";
	if (typeof(iDepth) !== "number") iDepth = 2;
	switch (typeof(xValue)) {
		case "undefined": return "undefined";
		case "boolean":   return xValue.toString();
		case "string":
			return (String(xValue).length >= 80 ? 
				"(" + String(xValue).length + " chars) " : ""
			) + "\"" + JavaScriptStringEncode(xValue) + "\"";
		case "number":
			// Inifinity, NaN, floats, small integers and negative numbers
			// are displayed as is:
			if (
				!isFinite(xValue) || isNaN(xValue) ||
				xValue != parseInt(xValue) || xValue < 10
			) {
				return xValue.toString();
			}
			// Otherwise we'll display as a decimal and hexadecimal number:
			return xValue.toString() + "  (0x" + xValue.toString(16).toUpperCase() + ")";
		case "function":
			var sFunction = xValue.toString();
			if (iDepth == 2) return sFunction.replace(/\r?\n/g, "$&" + sPadding);
			else return JavaScriptStringEncode(sFunction);
		case "object":
			if (xValue == null) return "null";
			var sConstructor = null;
			try { sConstructor = xValue.constructor.toString(); }
			catch (e) { sConstructor = ""; }
			var sObjectName = "Unknown";
			var oName = sConstructor.match(/^\r?\n?function\s+(\w+)\s*\([\s\S]*$/);
			if (oName) switch (oName[1]) {
				case "Boolean": return "Boolean(" + visualize(xValue.valueOf(), sPadding, iDepth) + ")";
				case "Date":    return "Date(" + visualize(xValue.toString(), sPadding, iDepth) + ")";
				case "Number":  return "Number(" + visualize(xValue.valueOf(), sPadding, iDepth) + ")";
				case "String":  return "String(" + visualize(xValue.valueOf(), sPadding, iDepth) + ")";
				case "RegExp":
					var sFlags = "";
					if (xValue.global) sFlags += "g";
					if (xValue.ignoreCase) sFlags += "i";
					if (xValue.multiline) sFlags += "m";
					return "RegExp(/" + xValue.source + "/" + sFlags + ")";
				case "Array":
					sObjectName = "Array";
					break;
				default:
					sObjectName = "Object " + oName[1];
			} else try {
				oName = xValue.toString().match(/^\[(?:object )?(.*)\]$/);
				if (oName) sObjectName = "Object [" + oName[1] + "]";
			} catch (e) { }
			if (iDepth == 0) return sObjectName + " \u2026";
			var asValues = [];
			var iChildCount = 0;
			try {
				for (var i in xValue) iChildCount++;
			} catch (e) {
				return "object unknown";
			}
			var iChildCount2 = 0;
			for (var i in xValue) {
				// Integers are displayed as numbers, anything else as a string
				var sIndex = (parseInt(i).toString() == i ?
					i: visualize(i)
				) +  ": ";
				// A child object that is the same as its parent is displayed as "=> self",
				// Anything else is "visualized":
				var sValue = null;
				var sChildHeadHeader = " " + (iChildCount2 + 1 == iChildCount ?
					"\u2514" : "\u251C") + "\u2500 ";
				// pad for the index:
				var sChildBodyHeader = " " + (iChildCount2 + 1 == iChildCount ?
					" " : "\u2502") + "  ";
				while (sChildBodyHeader.length < sIndex.length + 4) sChildBodyHeader+= " ";
				var bSelf = false, bUnreadable = false;
				try {
					bSelf = xValue[i] === xValue;
				} catch (e) {
					bUnreadable = true;
				}
				if (bUnreadable) {
					sValue = "\u2758 unreadable";
				} else if (bSelf) {
					sValue = "\u21D2 self";
				} else {
					sValue = visualize(xValue[i], sPadding + sChildBodyHeader, iDepth - 1);
				}
				asValues.push(sPadding + sChildHeadHeader + sIndex + sValue);
				iChildCount2++;
			}
			return sObjectName + "\r\n" +
				asValues.join("\r\n");
		default:
			try { return typeof(xValue) + " " + new String(xValue); }
			catch(e) { return "unknown"; }
	}
}
