-- lx-bundle:true

--- Bounded structured value inspection for LuaPad results
-- @module plugins.luapad.dump

local dump = {}

local DEFAULT_MAX_DEPTH = 5
local DEFAULT_MAX_ITEMS = 100
local DEFAULT_MAX_STRING_LENGTH = 1000

local key_type_order = {
	number = 1,
	string = 2,
	boolean = 3,
	table = 4,
	["function"] = 5,
	userdata = 6,
	thread = 7,
}

--- Convert one value to text without trusting its metatable
-- @param value any source value
-- @return string text
function dump.safe_tostring(value)
	local converted, text = pcall(tostring, value)

	if converted then
		return text
	end

	return string.format("<%s: tostring failed>", type(value))
end

--- Return a bounded quoted string for one result value
-- @param value string source string
-- @param maximum_length number maximum retained bytes
-- @return string text
local function format_string(value, maximum_length)
	if #value <= maximum_length then
		return string.format("%q", value)
	end

	return string.format("%q", value:sub(1, maximum_length)) .. "…"
end

--- Return one compact value description for table keys
-- @param value any key value
-- @param options table normalized inspection options
-- @return string text
local function format_key_value(value, options)
	local value_type = type(value)

	if value_type == "string" then
		return format_string(value, options.max_string_length)
	elseif value_type == "number" or value_type == "boolean" or value_type == "nil" then
		return tostring(value)
	end

	return dump.safe_tostring(value)
end

--- Return a readable table-child label for one key
-- @param key any table key
-- @param options table normalized inspection options
-- @return string label
local function format_key_label(key, options)
	if type(key) == "string" and key:match("^[%a_][%w_]*$") then
		return key
	end

	return "[" .. format_key_value(key, options) .. "]"
end

--- Compare table keys by type and then by their stable display text
-- @param left any first key
-- @param right any second key
-- @param options table normalized inspection options
-- @return boolean before
local function compare_keys(left, right, options)
	local left_type = type(left)
	local right_type = type(right)
	local left_order = key_type_order[left_type] or 100
	local right_order = key_type_order[right_type] or 100

	if left_order ~= right_order then
		return left_order < right_order
	end

	if left_type == "number" then
		return left < right
	end

	return format_key_value(left, options) < format_key_value(right, options)
end

--- Inspect one value into a bounded tree
-- @param value any source value
-- @param state table shared inspection state
-- @param depth number current table depth
-- @return table node
local function inspect_value(value, state, depth)
	local value_type = type(value)

	if value_type == "nil" then
		return { kind = "nil", text = "nil" }
	elseif value_type == "string" then
		return {
			kind = "string",
			text = format_string(value, state.options.max_string_length),
		}
	elseif value_type == "number" or value_type == "boolean" then
		return { kind = value_type, text = tostring(value) }
	elseif value_type ~= "table" then
		return {
			kind = value_type,
			text = dump.safe_tostring(value),
		}
	end

	local existing_node = state.seen[value]

	if existing_node then
		return {
			kind = "reference",
			target = existing_node,
		}
	end

	local node = {
		kind = "table",
		children = {},
	}
	state.seen[value] = node

	if depth >= state.options.max_depth then
		node.children[1] = {
			label = "…",
			node = {
				kind = "limit",
				text = "maximum depth reached",
			},
		}

		return node
	end

	local keys = {}
	local truncated = false
	local key = next(value)

	while key ~= nil do
		if #keys >= state.options.max_items then
			truncated = true
			break
		end

		keys[#keys + 1] = key
		key = next(value, key)
	end

	table.sort(keys, function(left, right)
		return compare_keys(left, right, state.options)
	end)

	for index = 1, #keys do
		local child_key = keys[index]
		node.children[#node.children + 1] = {
			label = format_key_label(child_key, state.options),
			node = inspect_value(rawget(value, child_key), state, depth + 1),
		}
	end

	if state.options.include_metatable then
		local metatable = getmetatable(value)

		if metatable ~= nil then
			node.children[#node.children + 1] = {
				label = "<metatable>",
				node = inspect_value(metatable, state, depth + 1),
			}
		end
	end

	if truncated then
		node.children[#node.children + 1] = {
			label = "…",
			node = {
				kind = "limit",
				text = "more items",
			},
		}
	end

	return node
end

--- Build one structured, cycle-safe value tree
-- @param value any source value
-- @param options table|nil inspection limits
-- @return table node
function dump.inspect(value, options)
	options = type(options) == "table" and options or {}
	local normalized_options = {
		max_depth = math.max(0, math.floor(tonumber(options.max_depth) or DEFAULT_MAX_DEPTH)),
		max_items = math.max(0, math.floor(tonumber(options.max_items) or DEFAULT_MAX_ITEMS)),
		max_string_length = math.max(
			0,
			math.floor(tonumber(options.max_string_length) or DEFAULT_MAX_STRING_LENGTH)
		),
		include_metatable = options.include_metatable == true,
	}
	local state = {
		options = normalized_options,
		seen = {},
	}

	return inspect_value(value, state, 0)
end

return dump
