--// =============================================================================
--// Chili DebugHandler
--// =============================================================================

--// todo:
--// * use the custom errorhandler for any widget- > chili interaction,
--//   atm if a widget calls mybutton:SetCaption() the widgethandler errorhandler is used,
--//   but this one uses pcall and so it can't generate a stacktrace, which makes debugging
--//   nearly impossible (cuz the error mostly occurs in a ver low level chili object as `Font`).
--//   So any :New{} should in thoery return a wrapped table to the widgets, I already tried
--//   to wrap the whole WG.Chili namespace (check the `SafeWrap` function beneath), but my tests
--//   ended in crashes in chili, cuz such wrapped tables ended in chili itself
--//   (chili should only use links and direct ones, but never wrapped tables)


DebugHandler = {}

--// =============================================================================
--// List of all chili object for easy debugging

--// DebugHandler.objectsOwnedByWidgets[widget][n] = owned_object
--// hint: because the per-widget table is a weaktable you still need to use pairs() to iterate it!
DebugHandler.objectsOwnedByWidgets = {}
setmetatable(DebugHandler.objectsOwnedByWidgets, {
	__mode = "k",
	__index = function(t, i)
		local st = {};
		setmetatable(st, {__mode = "v"});
		if (not i) then
			Spring.Log("Chili", "error", "WAAA", debug.traceback())
		end
		t[i] = st;
		return st;
	end,
})

--// holds all created chili objects (uses a weaktable to do so)
--// hint: because it is a weaktable you still need to use pairs() to iterate it!
DebugHandler.allObjects = {}
setmetatable(DebugHandler.allObjects, {
	__mode = "v",
})


--// =============================================================================
--// Chili ErrorHandler
--//
--// Chili is a framework, so many widgets move parts of their code into its
--// thread. If now a widget error happens, it could be shown as chili errors and
--// the widgethandler would unload chili instead of the causing widget.
--//
--// Now this errorhandler removes the widget itself (and its owned chili objects)
--// w/o crashing the whole framework.

DebugHandler.maxStackLength = 7
DebugHandler.maxChiliErrors = 5
DebugHandler.removeWidgets = false
local numChiliErrors = 0
local lastError = 0

local function ChiliErrorHandler(msg, ...)
	local control
	local _i = 2 --// 1 is this function, so skip it
	repeat
		if (not debug.getinfo(_i)) then
			break
		end

		local i = 1
		repeat
			local name, value = debug.getlocal(_i, i)
			if (not name) then
				break
			end
			if (name == "self") then
				if (value._widget ~= widget) then --// ignore controls owned by Chili Framework!
					--// ignore non-chili objects; TODO: implement a better way of checking whether something is a chili object
					if value.name and value.Dispose then
						control = value
					end
				end
			end

			i = i + 1
		until(control)

		_i = _i + 1
	until(control)


	if (control) then
		local w = control._widget
		if isindexable(w) then
			local wname = (w.whInfo and w.whInfo.name) or "unknown"
			Spring.Log("Chili", "error", ("in `%s`:%s : %s"):format(wname, control.name, msg))
			Spring.Log("Chili", "error", DebugHandler.Stacktrace())

			if DebugHandler.removeWidgets then
					--// this also unloads all owned chili objects
					Spring.Log("Chili", "error", "Removed widget: " .. wname)
					widgetHandler:RemoveWidget(w)
			else
					Spring.Log("Chili", "error", "In widget: " .. wname)
			end
		else
			Spring.Log("Chili", "error", ("in `%s` (couldn't detect the owner widget): %s"):format(control.name, msg))
			Spring.Log("Chili", "error", DebugHandler.Stacktrace())
			if DebugHandler.removeWidgets then
					control:Dispose()
			else
					Spring.Log("Chili", "error", "In control: " .. control.name)
			end
		end
	else
		--// error in Chili itself
		Spring.Log("Chili", "error", ("%s"):format(msg))
		Spring.Log("Chili", "error", DebugHandler.Stacktrace())

		if (os.clock() - lastError < 5) then
			numChiliErrors = numChiliErrors + 1
		else
			numChiliErrors = 1
		end
		lastError = os.clock()

		--// unload Chili to avoid error spam
		if (numChiliErrors >= DebugHandler.maxChiliErrors) then
			Spring.Log("Chili", "error", "Removed widget: " .. widget.whInfo.name)
			widgetHandler:RemoveWidget(widget)
		end
	end
end
DebugHandler.ChiliErrorHandler = ChiliErrorHandler


--// =============================================================================
--// Stacktrace

function DebugHandler.Stacktrace()
	local self_src = debug.getinfo(1).short_src

	local trace = {"stacktrace:"}
	local _i = 3
	repeat
		local info = debug.getinfo(_i)
		if (not info) then
			break
		end

		--// we remove any occurence of the debughandler in the stacktrace (it's useless for debugging)
		if (info.short_src ~= self_src)
			and(info.name ~= "SafeCall")
			and(info.name ~= "cdxpcall")
			and(info.name ~= "cdCreateList")
		then
			local curLine = info.currentline
			if (curLine < 0) then
				curLine = ""
			else
				curLine = ":" .. curLine
			end
			if (info.name) then
				if (info.name == "") then
					trace[#trace + 1] = ("%s%s: in [?]"):format(info.short_src, curLine)
				else
					trace[#trace + 1] = ("%s%s: in %s"):format(info.short_src, curLine, info.name)
				end
			else
				trace[#trace + 1] = ("%s%s"):format(info.short_src, curLine)
			end
		end

		_i = _i + 1
	until(false)

	if ((#trace-1) > DebugHandler.maxStackLength) then
		local i = 1 + math.ceil(DebugHandler.maxStackLength/2) + 1
		local tail = (DebugHandler.maxStackLength - i)
		local j = #trace - tail
		trace[i] = ("... (%i calls)"):format(#trace - (i + tail))
		for n = 1, tail do
			trace[i + n] = trace[j + n]
		end
		for i = 1 + DebugHandler.maxStackLength + 1, #trace do
			trace[i] = nil
		end
	end

	return table.concat(trace, "\n\t")
end


--// =============================================================================
--// Define some helper functions needed for xpcall

--// we do this so we can identify it in Stacktrace() (cd = chili debug)
local cdxpcall = xpcall

local sharedEnv = {_f = nil}
local setArgsFuncs = {}
local callFuncs = {}
do
	local function CreateNewHelpers(_, n)
		local t0 = {}
		local t1 = {}
		local t2 = {}
		for i = 1, n do
			t0[i] = "a" .. i
			t1[i] = "arg" .. i
			t2[i] = "a" .. i .. " = arg" .. i
		end
		local varargs = table.concat(t0, ", ")
		local arglist = table.concat(t1, ", ")
		local setargs = table.concat(t2, "; ")

		local src = ([[local %s;
	local setargs = function(%s) %s; end;
	local call = function() return _f(%s); end
				return setargs, call
		]]):format(varargs, arglist, setargs, varargs)

		local setargsfunc, callfunc = assert(loadstring(src))()
		setfenv(setargsfunc, sharedEnv)
		setfenv(callfunc, sharedEnv)
		rawset(setArgsFuncs, n, setargsfunc)
		rawset(callFuncs, n, callfunc)
		return setargsfunc
	end
	setmetatable(setArgsFuncs, {__index = CreateNewHelpers})
	setmetatable(callFuncs, {__index = CreateNewHelpers})
end


local function GetResults(status, ...)
	if (status) then
		return ...
	else
		return nil
	end
end


local function xpcall_va(func, ...)
	local n = select("#", ...)
	if (n == 0) then
		return GetResults(cdxpcall(func, ChiliErrorHandler))
	else
		sharedEnv._f = func
		setArgsFuncs[n](...)
		return GetResults(cdxpcall(callFuncs[n], ChiliErrorHandler))
	end
end


--// =============================================================================
--// Define Chili.SafeCall

DebugHandler.SafeCall = xpcall_va
SafeCall = xpcall_va --// external code should access it via Chili.SafeCall()


--// =============================================================================
--// Use our errorhandler in gl.CreateList

local orig_gl = gl
local cdCreateList = gl.CreateList
gl = {}
setmetatable(gl, {__index = function(t, i) t[i] = orig_gl[i] or false; return t[i]; end}) --// use metatable to copy table entries on-demand
gl.CreateList = function(...)
	return cdCreateList(xpcall_va, ...)
end



--// =============================================================================
--// Chili.DebugHandler.SafeWrap
--// usage:
--//   used for WG.Chili & screen0 to make any calling to them safe
--// how:
--//   it wraps a table, so any calling to a function in it (or ones of its subtables)
--//   uses automatically the Chili internal error handler
--// note:
--//   it doesn't auto wrap th return values of such functions (for performance reasons)


local function IndexWrappedTable(t, i)
	local v_orig = t.__orig[i]

	local v
	if (isindexable(v_orig)) then
		--Spring.Echo("wrap table", i)
		v = DebugHandler.SafeWrap(v_orig);
		t[i] = v;
	elseif (isfunc(v_orig)) then
		--Spring.Echo("wrap func", i)
		v = function(self, ...)
			if (isindexable(self)) and (self.__orig) then
				self = self.__orig
			end
			return DebugHandler.SafeWrap(xpcall_va(v_orig, self, ...));
		end;
		t[i] = v;
	else
		v = v_orig;
		--t[i] = v;  don't cache values (else they wouldn't update when they got in the original table)
	end
	return v;
end

local function HasSubFunctions(t)
	if (not istable(t)) then
		return true
	end
	for i, v in pairs(t) do
		if (isfunc(v)) then
			return true
		elseif (isindexable(v)) then
			local r = HasSubFunctions(v)
			if (r) then
				return true
			end
		end
	end
	return false
end

local function IndexWrappedLink(link, i)
	local t = link()
	local v_orig = t[i]
	--local t_wrap = getmetatable(link)._wrap

	local v = v_orig;
	if (isindexable(v_orig)) then
	--  Spring.Echo("wrap table", i, HasSubFunctions(v_orig))
	--  if (HasSubFunctions(v_orig)) then
	--    v = DebugHandler.SafeWrap(v_orig);
	--    t_wrap[i] = v;
	--  end
	elseif (isfunc(v_orig)) then
		--Spring.Echo("wrap func", i)
		v = function(self, ...)
			if (isindexable(self)) and (self.__orig) then
				self = self.__orig
			end
			return xpcall_va(v_orig, self, ...);
			--return DebugHandler.SafeWrap(xpcall_va(v_orig, self, ...));
		end;
		--t_wrap[i] = v;
	end
	return v;
end

local safewrap_meta = {
	__index = IndexWrappedTable
}

function DebugHandler.SafeWrap(t_orig, ...)
	if isindexable(t_orig) then
		local meta = getmetatable(t_orig)
		if (meta) and (meta._islink) then
			meta._wrap = {}
			meta[meta._wrap] = true
			meta.__index = IndexWrappedLink
			return t_orig
		else
			local t = {__orig = t_orig}
			setmetatable(t, safewrap_meta)
			return t
		end
	else
		return t_orig, ...
	end
end


--// =============================================================================
--// When a Widget gets removed destroy all its owned chili object, too

local origRemoveWidget = widgetHandler.RemoveWidget
widgetHandler.RemoveWidget = function(self, widget, ...)
	local ownedObjects = DebugHandler.objectsOwnedByWidgets[widget]
	for k, obj in pairs(ownedObjects) do
		obj:Dispose()
		ownedObjects[k] = nil
	end

	origRemoveWidget(self, widget, ...)
end


--// =============================================================================
--// Check the stack for any indication for an user widget
--//  so we can remove all owned chili object when the widget crashes/unloads
--//  (also useful for a lot of other debugging purposes)

function DebugHandler.GetWidgetOrigin(_i)
	local chiliwidget = widget
	_i = _i or 4 --// 1: = this function, 2: = RegisterObject, 3: = class:New{}, 4: = user widget?
	repeat
		--// CHECK STACK
		local info = debug.getinfo(_i, "f")
		if (not info) then
			return
		end

		--// CHECK ENVIROMENT
		if (info.func) then
			local _G = debug.getfenv(info.func)
			if (_G.widget) and (_G.widget~= chiliwidget) and (_G.widget.GetInfo) then
				--Spring.Echo("env widget found", _G.widget:GetInfo().name)
				return _G.widget
			end
		end

		--// CHECK LOCALS
		local i = 1
		repeat
			local name, value = debug.getlocal(_i, i)
			if (not name) then
				break
			end
			if (name == "widget") and (value.GetInfo) then
				--Spring.Echo("local widget found", value:GetInfo().name)
				return value
			else
				--// this code gets triggered when a chili control creates another one
				--// (the new control should then inherit the widget origin)
				local typ = type(value)
				if (typ == "table") or (typ == "metatable") or (typ == "userdata") then
					if (typ._widget) and (typ._widget.GetInfo) then
						--Spring.Echo("local2 widget found", typ._widget:GetInfo().name)
						return typ._widget
					end
				end
			end
			i = i + 1
		until(false)

		_i = _i + 1
	until(false)
end


--// =============================================================================
--// gets called by Chili.Object:New{}

function DebugHandler:RegisterObject(obj)
	local w = self.GetWidgetOrigin()
	if (w) then
		obj._widget = w
		local t = self.objectsOwnedByWidgets[w]
		t[#t + 1] = obj
	end
	local ta = self.allObjects
	ta[#ta + 1] = obj
	--// hint: both tables above are weaktables, so we don't need a UnregisterObject!
end

--// =============================================================================
