------------------------------------------------------------------------------
-- Dump.lua
--
-- Contributed by Iriel, Esamynn and Kirov from DevTools v1.11
-- /dump Implementation
--
-- Globals: DevTools, SLASH_DEVTOOLSDUMP1, DevTools_Dump, DevTools_RunDump
-- Globals: DEVTOOLS_MAX_ENTRY_CUTOFF, DEVTOOLS_LONG_STRING_CUTOFF
-- Globals: DEVTOOLS_DEPTH_CUTOFF, DEVTOOLS_INDENT
-- Globals: DEVTOOLS_USE_TABLE_CACHE, DEVTOOLS_USE_FUNCTION_CACHE
-- Globals: DEVTOOLS_USE_USERDATA_CACHE
---------------------------------------------------------------------------

if not IsInGlobalEnvironment() then
	-- Don't want to load this file into the secure environment
	return;
end

local forceinsecure = forceinsecure;

local DT = {};

DEVTOOLS_MAX_ENTRY_CUTOFF = 30;    -- Maximum table entries shown
DEVTOOLS_LONG_STRING_CUTOFF = 200; -- Maximum string size shown
DEVTOOLS_DEPTH_CUTOFF = 10;        -- Maximum table depth
DEVTOOLS_USE_TABLE_CACHE = true;   -- Look up table names
DEVTOOLS_USE_FUNCTION_CACHE = true;-- Look up function names
DEVTOOLS_USE_USERDATA_CACHE = true;-- Look up userdata names
DEVTOOLS_INDENT='  ';              -- Indentation string

local DEVTOOLS_TYPE_COLOR="|cff88ff88";
local DEVTOOLS_TABLEREF_COLOR="|cffffcc00";
local DEVTOOLS_CUTOFF_COLOR="|cffff0000";
local DEVTOOLS_TABLEKEY_COLOR="|cff88ccff";

local FORMATS = {};
-- prefix type suffix
FORMATS["opaqueTypeVal"] = "%s" .. DEVTOOLS_TYPE_COLOR .. "<%s>|r%s";
-- prefix type name suffix
FORMATS["opaqueTypeValName"] = "%s" .. DEVTOOLS_TYPE_COLOR .. "<%s %s>|r%s";
-- type
FORMATS["opaqueTypeKey"] = "<%s>";
-- type name
FORMATS["opaqueTypeKeyName"] = "<%s %s>";
-- value
FORMATS["bracketTableKey"] = "[%s]";
-- prefix value
FORMATS["tableKeyAssignPrefix"] = DEVTOOLS_TABLEKEY_COLOR .. "%s%s|r=";
-- prefix cutoff
FORMATS["tableEntriesSkipped"] = "%s" .. DEVTOOLS_CUTOFF_COLOR .. "<skipped %s>|r";
-- prefix suffix
FORMATS["tableTooDeep"] = "%s" .. DEVTOOLS_CUTOFF_COLOR .. "<table (too deep)>|r%s";
-- prefix value suffix
FORMATS["simpleValue"] = "%s%s%s";
-- prefix tablename suffix
FORMATS["tableReference"] = "%s" .. DEVTOOLS_TABLEREF_COLOR .. "%s|r%s";

-- Grab a copy various oft-used functions
local rawget = rawget;
local type = type;
local string_len = string.len;
local string_sub = string.sub;
local string_gsub = string.gsub;
local string_format = string.format;
local string_match = string.match;
local table_insert = table.insert;
local secureexecuterange = secureexecuterange;

local messageHandlers = {};

local function CallCallback(index, callback, msg)
	callback(msg);
end

local function WriteMessage(msg)
	secureexecuterange(messageHandlers, CallCallback, msg);
end

local function DevTools_Write(self, msg)
	WriteMessage(msg);
end

function DevTools_AddMessageHandler(callback)
	table_insert(messageHandlers, callback);
end

local function prepSimple(val, context)
	local valType = type(val);
	if (valType == "nil")  then
		return "nil";
	elseif (valType == "number") then
		return val;
	elseif (valType == "boolean") then
		if (val) then
			return "true";
		else
			return "false";
		end
	elseif (valType == "string") then
		local l = string_len(val);
		if ((l > DEVTOOLS_LONG_STRING_CUTOFF) and
			(DEVTOOLS_LONG_STRING_CUTOFF > 0)) then
			local more = l - DEVTOOLS_LONG_STRING_CUTOFF;
			val = string_sub(val, 1, DEVTOOLS_LONG_STRING_CUTOFF);
			return string_gsub(string_format("%q...+%s",val,more),"[|]", "||");
		else
			return string_gsub(string_format("%q",val),"[|]", "||");
		end
	elseif (valType == "function") then
		local fName = context:GetFunctionName(val);
		if (fName) then
			return string_format(FORMATS.opaqueTypeKeyName, valType, fName);
		else
			return string_format(FORMATS.opaqueTypeKey, valType);
		end
	elseif (valType == "userdata") then
		local uName = context:GetUserdataName(val);
		if (uName) then
			return string_format(FORMATS.opaqueTypeKeyName, valType, uName);
		else
			return string_format(FORMATS.opaqueTypeKey, valType);
		end
	elseif (valType == 'table') then
		local tName = context:GetTableName(val);
		if (tName) then
			return string_format(FORMATS.opaqueTypeKeyName, valType, tName);
		else
			return string_format(FORMATS.opaqueTypeKey, valType);
		end
	end
	error("Bad type '" .. valType .. "' to prepSimple");
end

local function prepSimpleKey(val, context)
	local valType = type(val);
	if (valType == "string") then
		local l = string_len(val);
		if ((l <= DEVTOOLS_LONG_STRING_CUTOFF) or
			(DEVTOOLS_LONG_STRING_CUTOFF <= 0)) then
			if (string_match(val, "^[a-zA-Z_][a-zA-Z0-9_]*$")) then
				return val;
			end
		end
	end
	return string_format(FORMATS.bracketTableKey, prepSimple(val, context));
end

local function DevTools_InitFunctionCache(context)
	local ret = {};

	for _,k in ipairs(DT.functionSymbols) do
		local v = getglobal(k);
		if (type(v) == 'function') then
			ret[v] = '[' .. k .. ']';
		end
	end

	for k,v in pairs(getfenv(0)) do
		if (type(v) == 'function') then
			if (not ret[v]) then
				ret[v] = '[' .. k .. ']';
			end
		end
	end

	return ret;
end

local function DevTools_InitUserdataCache(context)
	local ret = {};

	for _,k in ipairs(DT.userdataSymbols) do
		local v = getglobal(k);
		if (type(v) == 'table') then
			local u = rawget(v,0);
			if (type(u) == 'userdata') then
				ret[u] = k .. '[0]';
			end
		end
	end

	for k,v in pairs(getfenv(0)) do
		if (type(v) == 'table') then
			local u = rawget(v, 0);
			if (type(u) == 'userdata') then
				if (not ret[u]) then
					ret[u] = k .. '[0]';
				end
			end
		end
	end

	return ret;
end

local function DevTools_Cache_Nil(self, value, newName)
	return nil;
end

local function DevTools_Cache_Function(self, value, newName)
	if (not self.fCache) then
		self.fCache = DevTools_InitFunctionCache(self);
	end
	local name = self.fCache[value];
	if ((not name) and newName) then
		self.fCache[value] = newName;
	end
	return name;
end

local function DevTools_Cache_Userdata(self, value, newName)
	if (not self.uCache) then
		self.uCache = DevTools_InitUserdataCache(self);
	end
	local name = self.uCache[value];
	if ((not name) and newName) then
		self.uCache[value] = newName;
	end
	return name;
end

local function DevTools_Cache_Table(self, value, newName)
	if (not self.tCache) then
		self.tCache = {};
	end
	local name = self.tCache[value];
	if ((not name) and newName) then
		self.tCache[value] = newName;
	end
	return name;
end

local DevTools_DumpValue;

local function DevTools_DumpTableContents(val, prefix, firstPrefix, context)
	local showCount = 0;
	local oldDepth = context.depth;
	local oldKey = context.key;

	-- Use this to set the cache name
	context:GetTableName(val, oldKey or 'value');

	local iter = pairs(val);
	local nextK, nextV = iter(val, nil);

	while (nextK) do
		local k,v = nextK, nextV;
		nextK, nextV = iter(val, k);

		showCount = showCount + 1;
		if ((showCount <= DEVTOOLS_MAX_ENTRY_CUTOFF) or
			(DEVTOOLS_MAX_ENTRY_CUTOFF <= 0)) then
			local prepKey = prepSimpleKey(k, context);
			if (oldKey == nil) then
				context.key = prepKey;
			elseif (string_sub(prepKey, 1, 1) == "[") then
				context.key = oldKey .. prepKey
			else
				context.key = oldKey .. "." .. prepKey
			end
			context.depth = oldDepth + 1;

			local rp = string_format(FORMATS.tableKeyAssignPrefix, firstPrefix,
									 prepKey);
			firstPrefix = prefix;
			DevTools_DumpValue(v, prefix, rp,
							   (nextK and ",") or '',
							   context);
		end
	end
	local cutoff = showCount - DEVTOOLS_MAX_ENTRY_CUTOFF;
	if ((cutoff > 0) and (DEVTOOLS_MAX_ENTRY_CUTOFF > 0)) then
		context:Write(string_format(FORMATS.tableEntriesSkipped,firstPrefix,
									cutoff));
	end
	context.key = oldKey;
	context.depth = oldDepth;
	return (showCount > 0)
end

-- Return the specified value
function DevTools_DumpValue(val, prefix, firstPrefix, suffix, context)
	local valType = type(val);

	if (valType == "userdata") then
		local uName = context:GetUserdataName(val, 'value');
		if (uName) then
			context:Write(string_format(FORMATS.opaqueTypeValName,
										firstPrefix, valType, uName, suffix));
		else
			context:Write(string_format(FORMATS.opaqueTypeVal,
										firstPrefix, valType, suffix));
		end
		return;
	elseif (valType == "function") then
		local fName = context:GetFunctionName(val, 'value');
		if (fName) then
			context:Write(string_format(FORMATS.opaqueTypeValName,
										firstPrefix, valType, fName, suffix));
		else
			context:Write(string_format(FORMATS.opaqueTypeVal,
										firstPrefix, valType, suffix));
		end
		return;
	elseif (valType ~= "table")  then
		context:Write(string_format(FORMATS.simpleValue,
									firstPrefix,prepSimple(val, context),
									suffix));
		return;
	end

	local cacheName = context:GetTableName(val);
	if (cacheName) then
		context:Write(string_format(FORMATS.tableReference,
									firstPrefix, cacheName, suffix));
		return;
	end

	if ((context.depth >= DEVTOOLS_DEPTH_CUTOFF) and
		(DEVTOOLS_DEPTH_CUTOFF > 0)) then
		context:Write(string_format(FORMATS.tableTooDeep,
									firstPrefix, suffix));
		return;
	end

	firstPrefix = firstPrefix .. "{";
	local oldPrefix = prefix;
	prefix = prefix .. DEVTOOLS_INDENT;

	context:Write(firstPrefix);
	firstPrefix = prefix;
	local anyContents = DevTools_DumpTableContents(val, prefix, firstPrefix,
												   context);
	context:Write(oldPrefix .. "}" .. suffix);
end

local function Pick_Cache_Function(func, setting)
	if (setting) then
		return func;
	else
		return DevTools_Cache_Nil;
	end
end

function DevTools_RunDump(value, context)
	local prefix = "";
	local firstPrefix = prefix;

	local valType = type(value);
	if (type(value) == 'table') then
		local any =
			DevTools_DumpTableContents(value, prefix, firstPrefix, context);
		if (context.Result) then
			return context:Result();
		end
		if (not any) then
			context:Write("empty result");
		end
		return;
	end

	DevTools_DumpValue(value, '', '', '', context);
	if (context.Result) then
		return context:Result();
	end
end

-- Dump the specified list of value
function DevTools_Dump(value, startKey)
	local context = {
		depth = 0,
		key = startKey,
	};

	context.GetTableName = Pick_Cache_Function(DevTools_Cache_Table,
											   DEVTOOLS_USE_TABLE_CACHE);
	context.GetFunctionName = Pick_Cache_Function(DevTools_Cache_Function,
												  DEVTOOLS_USE_FUNCTION_CACHE);
	context.GetUserdataName = Pick_Cache_Function(DevTools_Cache_Userdata,
												  DEVTOOLS_USE_USERDATA_CACHE);
	context.Write = DevTools_Write;

	DevTools_RunDump(value, context);
end

function DevTools_DumpCommand(msg, editBox)
	forceinsecure();
	if (string_match(msg,"^[A-Za-z_][A-Za-z0-9_]*$")) then
		WriteMessage("Dump: " .. msg);
		local val = _G[msg];
		local tmp = {};
		if (val == nil) then
			local key = string_format(FORMATS.tableKeyAssignPrefix,
									  '', prepSimpleKey(msg, {}));
			WriteMessage(key .. "nil,");
		else
			tmp[msg] = val;
		end
		DevTools_Dump(tmp);
		return;
	end

	WriteMessage("Dump: value=" .. msg);
	local func,err = loadstring("return " .. msg);
	if (not func) then
		WriteMessage("Dump: ERROR: " .. err);
	else
		DevTools_Dump({ func() }, "value");
	end
end

DT.functionSymbols = {};
DT.userdataSymbols = {};

local funcSyms = DT.functionSymbols;
local userSyms = DT.userdataSymbols;

for k,v in pairs(getfenv(0)) do
	if (type(v) == 'function') then
		table.insert(funcSyms, k);
	elseif (type(v) == 'table') then
		if (type(rawget(v,0)) == 'userdata') then
			table.insert(userSyms, k);
		end
	end
end

