-- tables
local _G = _G
local string, io, debug, coroutine = string, io, debug, coroutine

-- functions
local tostring, print, require = tostring, print, require
local next, assert = next, assert
local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs
local error = error

assert(debug, "debug table must be available at this point")

local io_open = io.open
local string_gmatch = string.gmatch
local string_sub = string.sub
local table_concat = table.concat

local _M = {
	max_tb_output_len = 70	-- controls the maximum length of the 'stringified' table before cutting with ' (more...)'
}

-- this tables should be weak so the elements in them won't become uncollectable
local m_known_tables = { [_G] = "_G (global table)" }
local function add_known_module(name, desc)
	local ok, mod = pcall(require, name)
	if ok then
		m_known_tables[mod] = desc
	end
end

add_known_module("string", "string module")
add_known_module("io", "io module")
add_known_module("os", "os module")
add_known_module("table", "table module")
add_known_module("math", "math module")
add_known_module("package", "package module")
add_known_module("debug", "debug module")
add_known_module("coroutine", "coroutine module")

-- lua5.2
add_known_module("bit32", "bit32 module")
-- luajit
add_known_module("bit", "bit module")
add_known_module("jit", "jit module")


local m_user_known_tables = {}

local m_known_functions = {}
for _, name in ipairs{
	-- Lua 5.2, 5.1
	"assert",
	"collectgarbage",
	"dofile",
	"error",
	"getmetatable",
	"ipairs",
	"load",
	"loadfile",
	"next",
	"pairs",
	"pcall",
	"print",
	"rawequal",
	"rawget",
	"rawlen",
	"rawset",
	"require",
	"select",
	"setmetatable",
	"tonumber",
	"tostring",
	"type",
	"xpcall",

	-- Lua 5.1
	"gcinfo",
	"getfenv",
	"loadstring",
	"module",
	"newproxy",
	"setfenv",
	"unpack",
	-- TODO: add table.* etc functions
} do
	if _G[name] then
		m_known_functions[_G[name]] = name
	end
end



local m_user_known_functions = {}

local function safe_tostring (value)
	local ok, err = pcall(tostring, value)
	if ok then return err else return ("<failed to get printable value>: '%s'"):format(err) end
end

-- Private:
-- Parses a line, looking for possible function definitions (in a very naïve way)
-- Returns '(anonymous)' if no function name was found in the line
local function ParseLine(line)
	assert(type(line) == "string")
	--print(line)
	local match = line:match("^%s*function%s+(%w+)")
	if match then
		--print("+++++++++++++function", match)
		return match
	end
	match = line:match("^%s*local%s+function%s+(%w+)")
	if match then
		--print("++++++++++++local", match)
		return match
	end
	match = line:match("^%s*local%s+(%w+)%s+=%s+function")
	if match then
		--print("++++++++++++local func", match)
		return match
	end
	match = line:match("%s*function%s*%(")	-- this is an anonymous function
	if match then
		--print("+++++++++++++function2", match)
		return "(anonymous)"
	end
	return "(anonymous)"
end

-- Private:
-- Tries to guess a function's name when the debug info structure does not have it.
-- It parses either the file or the string where the function is defined.
-- Returns '?' if the line where the function is defined is not found
local function GuessFunctionName(info)
	--print("guessing function name")
	if type(info.source) == "string" and info.source:sub(1,1) == "@" then
		local file, err = io_open(info.source:sub(2), "r")
		if not file then
			print("file not found: "..tostring(err))	-- whoops!
			return "?"
		end
		local line
		for i = 1, info.linedefined do
			line = file:read("*l")
		end
		if not line then
			print("line not found")	-- whoops!
			return "?"
		end
		return ParseLine(line)
	else
		local line
		local lineNumber = 0
		for l in string_gmatch(info.source, "([^\n]+)\n-") do
			lineNumber = lineNumber + 1
			if lineNumber == info.linedefined then
				line = l
				break
			end
		end
		if not line then
			print("line not found")	-- whoops!
			return "?"
		end
		return ParseLine(line)
	end
end

---
-- Dumper instances are used to analyze stacks and collect its information.
--
local Dumper = {}

Dumper.new = function(thread)
	local t = { lines = {} }
	for k,v in pairs(Dumper) do t[k] = v end

	t.dumping_same_thread = (thread == coroutine.running())

	-- if a thread was supplied, bind it to debug.info and debug.get
	-- we also need to skip this additional level we are introducing in the callstack (only if we are running
	-- in the same thread we're inspecting)
	if type(thread) == "thread" then
		t.getinfo = function(level, what)
			if t.dumping_same_thread and type(level) == "number" then
				level = level + 1
			end
			return debug.getinfo(thread, level, what)
		end
		t.getlocal = function(level, loc)
			if t.dumping_same_thread then
				level = level + 1
			end
			return debug.getlocal(thread, level, loc)
		end
	else
		t.getinfo = debug.getinfo
		t.getlocal = debug.getlocal
	end

	return t
end

-- helpers for collecting strings to be used when assembling the final trace
function Dumper:add (text)
	self.lines[#self.lines + 1] = text
end
function Dumper:add_f (fmt, ...)
	self:add(fmt:format(...))
end
function Dumper:concat_lines ()
	return table_concat(self.lines)
end

---
-- Private:
-- Iterates over the local variables of a given function.
--
-- @param level The stack level where the function is.
--
function Dumper:DumpLocals (level)
	local prefix = "\t "
	local i = 1

	if self.dumping_same_thread then
		level = level + 1
	end

	local name, value = self.getlocal(level, i)
	if not name then
		return
	end
	self:add("\tLocal variables:\r\n")
	while name do
		if type(value) == "number" then
			self:add_f("%s%s = number: %g\r\n", prefix, name, value)
		elseif type(value) == "boolean" then
			self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value))
		elseif type(value) == "string" then
			self:add_f("%s%s = string: %q\r\n", prefix, name, value)
		elseif type(value) == "userdata" then
			self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value))
		elseif type(value) == "nil" then
			self:add_f("%s%s = nil\r\n", prefix, name)
		elseif type(value) == "table" then
			if m_known_tables[value] then
				self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value])
			elseif m_user_known_tables[value] then
				self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value])
			else
				local txt = "{"
				for k,v in pairs(value) do
					txt = txt..safe_tostring(k)..":"..safe_tostring(v)
					if #txt > _M.max_tb_output_len then
						txt = txt.." (more...)"
						break
					end
					if next(value, k) then txt = txt..", " end
				end
				self:add_f("%s%s = %s  %s\r\n", prefix, name, safe_tostring(value), txt.."}")
			end
		elseif type(value) == "function" then
			local info = self.getinfo(value, "nS")
			local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value]
			if info.what == "C" then
				self:add_f("%s%s = C %s\r\n", prefix, name, (fun_name and ("function: " .. fun_name) or tostring(value)))
			else
				local source = info.short_src
				if source:sub(2,7) == "string" then
					source = source:sub(9)
				end
				--for k,v in pairs(info) do print(k,v) end
				fun_name = fun_name or GuessFunctionName(info)
				self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name, info.linedefined, source)
			end
		elseif type(value) == "thread" then
			self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value))
		end
		i = i + 1
		name, value = self.getlocal(level, i)
	end
end


---
-- Public:
-- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc.
-- This function is suitable to be used as an error handler with pcall or xpcall
--
-- @param thread An optional thread whose stack is to be inspected (defaul is the current thread)
-- @param message An optional error string or object.
-- @param level An optional number telling at which level to start the traceback (default is 1)
--
-- Returns a string with the stack trace and a string with the original error.
--
function _M.stacktrace(thread, message, level)
	if type(thread) ~= "thread" then
		-- shift parameters left
		thread, message, level = nil, thread, message
	end

	thread = thread or coroutine.running()

	level = level or 1

	local dumper = Dumper.new(thread)

	local original_error

	if type(message) == "table" then
		dumper:add("an error object {\r\n")
		local first = true
		for k,v in pairs(message) do
			if first then
				dumper:add("  ")
				first = false
			else
				dumper:add(",\r\n  ")
			end
			dumper:add(safe_tostring(k))
			dumper:add(": ")
			dumper:add(safe_tostring(v))
		end
		dumper:add("\r\n}")
		original_error = dumper:concat_lines()
	elseif type(message) == "string" then
		dumper:add(message)
		original_error = message
	end

	dumper:add("\r\n")
	dumper:add[[
Stack Traceback
===============
]]
	--print(error_message)

	local level_to_show = level
	if dumper.dumping_same_thread then level = level + 1 end

	local info = dumper.getinfo(level, "nSlf")
	while info do
		if info.what == "main" then
			if string_sub(info.source, 1, 1) == "@" then
				dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show, string_sub(info.source, 2), info.currentline)
			else
				dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.short_src, info.currentline)
			end
		elseif info.what == "C" then
			--print(info.namewhat, info.name)
			--for k,v in pairs(info) do print(k,v, type(v)) end
			local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or tostring(info.func)
			dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name)
			--dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value)))
		elseif info.what == "tail" then
			--print("tail")
			--for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name)
			dumper:add_f("(%d) tail call\r\n", level_to_show)
			dumper:DumpLocals(level)
		elseif info.what == "Lua" then
			local source = info.short_src
			local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name
			if source:sub(2, 7) == "string" then
				source = source:sub(9)
			end
			local was_guessed = false
			if not function_name or function_name == "?" then
				--for k,v in pairs(info) do print(k,v, type(v)) end
				function_name = GuessFunctionName(info)
				was_guessed = true
			end
			-- test if we have a file name
			local function_type = (info.namewhat == "") and "function" or info.namewhat
			if info.source and info.source:sub(1, 1) == "@" then
				dumper:add_f("(%d) Lua %s '%s' at file '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
			elseif info.source and info.source:sub(1,1) == '#' then
				dumper:add_f("(%d) Lua %s '%s' at template '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
			else
				dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type, function_name, info.currentline, source)
			end
			dumper:DumpLocals(level)
		else
			dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what)
		end

		level = level + 1
		level_to_show = level_to_show + 1
		info = dumper.getinfo(level, "nSlf")
	end

	return dumper:concat_lines(), original_error
end

--
-- Adds a table to the list of known tables
function _M.add_known_table(tab, description)
	if m_known_tables[tab] then
		error("Cannot override an already known table")
	end
	m_user_known_tables[tab] = description
end

--
-- Adds a function to the list of known functions
function _M.add_known_function(fun, description)
	if m_known_functions[fun] then
		error("Cannot override an already known function")
	end
	m_user_known_functions[fun] = description
end

return _M
