-- It is also possible to use mw.dumpObject but the result from this
-- module is clearer and is close to valid Lua source.
-- The main purpose is to allow easy inspection of Wikidata items.
-- Preview the following in a sandbox to see entity Q833639 as a Lua table:
-- 
-- Preview the following to dump a built-in table:
-- 

local Collection -- a table to hold items
Collection = {
	add = function 
		if item ~= nil then
			self.n = self.n + 1
			self[self.n] = item
		end
	end,
	join = function 
		return table.concat
	end,
	remove = function 
		if self.n > 0 and (pos == nil or (0 < pos and pos <= self.n)) then
			self.n = self.n - 1
			return table.remove
		end
	end,
	sort = function 
		table.sort
	end,
	new = function 
		return setmetatable
	end
Collection.__index = Collection

local function pre_block(text)
	-- Pre tags returned by a module do not act like wikitext .
	return '\n'
end

local function make_tabstr(indent)
	-- Return a string to generate one level of indent.
	if indent == 'tab' then
		-- Tabs do not work well in a browser edit window, but can force them.
		return '\t'
	end
	indent = tonumber(indent)
	if not (type(indent) == 'number' and 1 <= indent and indent <= 32) then
		indent = 4
	end
	return string.rep
end

local function _dumphtml
	-- Return a pretty-text formatted dump of an html string.
	-- This assumes clean html, for example, tag "".
	if type(html) ~= 'string' then
		return ''
	end
	local selfClosingTags = { -- from mw.html.lua
		area = true,
		base = true,
		br = true,
		col = true,
		command = true,
		embed = true,
		hr = true,
		img = true,
		input = true,
		keygen = true,
		link = true,
		meta = true,
		param = true,
		source = true,
		track = true,
		wbr = true,
	local tabstr = make_tabstr(tabwidth)
	local function indent_pad
		-- Return a string with an indent to match depth.
		if depth > 0 then
			return '\n' .. string.rep
		end
		return isfirst and '' or '\n'
	end
	local function extract
		-- Dump more of html into table result and return new pos.
		local has_child
		while pos <= len do
			local s, e = html:find
			if s then
				if s > pos then
					table.insert(result, html:sub)
				end
				if html:sub == '/' then
					-- A closing tag.
					local tag = html:match('^([a-zA-Z0-9]+)>', s+2) or 'NOTAG'
					if tag == currenttag then
						local indent = has_child and indent_pad(depth - 1) or ''
						table.insert
					end
					return e + 1
				end
				local tag = html:match or 'NOTAG'
				if html:sub == '/' or selfClosingTags[tag] then
					-- A self-closing tag.
					table.insert(result, html:sub)
					pos = e + 1
				else
					-- An opening tag.
					table.insert(result, indent_pad .. html:sub)
					pos = extract
					has_child = true
				end
			else
				table.insert(result, html:sub(pos))
				break
			end
		end
		return len + 1
	end
	local result = {}
	html = html:gsub('>%s+<'):gsub
	extract
	return pre_block(table.concat(result))
end

local function dumphtml(frame)
	local args = frame.args
	local pargs = frame:getParent.args
	local text = args[1] or pargs[1]
	local indent = args.indent or pargs.indent
	return _dumphtml
end

local function quoted(str)
	return (string.format:gsub)
end

local function iterkeys
	-- Return an iterator over the keys of var (which should be a table).
	-- The keys are sorted with numbered keys first, then other types.
	-- The iterator returns key, repr where key is the actual key, and
	-- repr is its representation: a number for the ipairs keys, or
	-- a string, including for number keys above the table length.
	if type(var) ~= 'table' then
		return function return nil end
	end
	local nums = {}
	local results = Collection.new
	for i, _ in ipairs(var) do
		nums[i] = true
		results:add
	end
	local keys = Collection.new
	for k, _ in pairs(var) do
		if not nums[k] then
			keys:add(k)
		end
	end
	local autoname = control.autoname
	keys:sort(function 
			local ta, tb = type(a), type(b)
			if ta == tb then
				if ta == 'number' or ta == 'string' then
					return a < b
				end
				if ta == 'boolean' then
					return b and not a
				end
				return autoname(a) < autoname(b)
			end
			if ta == 'number' then
				return true
			elseif tb == 'number' then
				return false
			else
				return ta < tb
			end
		end)
	for _, k in ipairs(keys) do
		local repr
		local tk = type(k)
		if tk == 'number' then
			repr = '[' .. k .. ']'
		elseif tk == 'string' then
			if k:match('^[%a_][%w_]*$') then
				repr = k
			else
				repr = '[' .. quoted(k) .. ']'
			end
		elseif tk == 'boolean' then
			repr = '[' .. tostring(k) .. ']'
		else
			repr = autoname(k)
			control.needed[repr] = true
		end
		results:add
	end
	local last = 0
	return function 
		if last < results.n then
			last = last + 1
			return unpack(results[last])
		end
	end
end

local function vardump
	-- Update items in control with results from dumping a variable.
	local function put
		options = options or {}
		local indent = options.indent or depth
		local comma = (options.kind == 'open' or indent == 0) and '' or ','
		control.items:add({
			key = (type(vname) == 'string' and options.kind ~= 'close') and vname or nil,
			value = value .. comma,
			depth = indent,
			note = options.note
	end
	if var == nil then
		put('nil')
	elseif type(var) == 'string' then
		put(quoted(var))
	elseif type(var) == 'table' then
		local this = control.autoname(var)
		if depth >= control.limitdepth then
			put(this)
		elseif parents and parents[this] then
			control.needed[this] = true
			if self == this then
				put
				control.needed['self'] = true
			else
				put
				control.needed['repeat'] = true
			end
		else
			parents = parents or {}
			parents[this] = true
			self = this
			put
			local mt = getmetatable(var)
			if mt then
				vardump
			end
			local maxsize = control.items.n + control.limititems
			for key, keyrep in iterkeys do
				if control.items.n > maxsize then
					put('...more...')
					break
				end
				vardump
			end
			put
		end
	elseif type(var) == 'boolean' or type(var) == 'number' then
		put(tostring(var))
	else -- function (or userdata or thread)
		put(control.autoname(var))
	end
end

local function dumper
	-- Return a string representing var in almost-correct Lua syntax.
	-- There is no newline at the end of the result.
	local onames = {}
	local tcounts = {}
	local function autoname(var)
		-- Return a string that is a unique name for var, given it is not
		-- a number or string.
		if not onames[var] then
			local name = type(var)
			tcounts[name] = (tcounts[name] or 0) + 1
			onames[var] = name .. '_' .. tcounts[name]
		end
		return onames[var]
	end
	local control = {
		autoname = autoname,
		limititems = limititems or 10000,
		limitdepth = limitdepth or 50,
		items = Collection.new,
		needed = {},
	vardump(var, tostring(vname or 'variable'), 0, control)
	local tabstr = make_tabstr(tabwidth)
	local lines = Collection.new
	for i, v in ipairs(control.items) do
		local indent = string.rep
		local note = v.note
		if note and control.needed[note] then
			note = ' -- ' .. note
		else
			note = ''
		end
		local k = v.key and (v.key .. ' = ') or ''
		lines:add(indent .. k .. v.value .. note)
	end
	local raw = lines:join('\n')
	return wantraw and raw or pre_block(raw)
end

local function dump_testcase(frame)
	local item
	if type(frame) == 'table' then
		item = frame.args[1]
	else
		item = frame
	end
	if item == 'G' or item == '_G' then
		return dumper
	end
	local fruit = { 'apple', 'banana', [0] = 'zero', [{'anon'}] = 'anon' }
	local testcase = {
		[100] = 'one hundred',
		[99] = 'ninety nine',
		[0.5] = 'one half',
		[-1] = 'negative one',
		'one',
		'two',
		[' '] = 'space',
		['1 –◆— z'] = 'unicode',
		alpha = 'aaa',
		beta = 'bbb',
		c = 123,
		data = {
			dumper = dumper,
			[dumper] = 'dumper',
			'three',
			'four',
			T = true,
			[true] = 'T',
			alpha2 = 'aaa2',
			beta2 = 'bbb2',
			F = false,
			[false] = 'F',
			c2 = 1234,
			data2 = {
				'five',
				'six',
				alpha3 = 'aaa3',
				beta3 = 'bbb3',
				c3 = 12345,
				fruit = fruit,
				[fruit] = 'fruit',
		z = 'zoo',
	testcase.testcase = testcase
	testcase.data.me = testcase.data
	testcase.data.data2.me = testcase
	testcase.data.data2.fruit.back = testcase.data
	setmetatable(testcase.data, {
		__index = function  return type(key) == 'string' and #key or nil end,
		__tostring = function (self) return tostring(#self) end,
	if item == 'return table' then
		return testcase
	end
	return dumper
end

local function execute(frame)
	-- Return a dump of the result from executing .
	-- In general that is not possible in Scribunto so this has built-in code
	-- to parse some expressions of interest.
	-- The primary aim is to test the result of calling a Wikidata function
	-- while previewing an edit in an article.
	-- Examples of EXPRESSION:
	-- mw.wikibase.getEntityIdForCurrentPage
	-- mw.wikibase.getBestStatements
	-- mw.wikibase.getBestStatements -- also accepted
	-- mw.wikibase.getEntity:getDescription('de')
	-- mw.wikibase.getEntity('Q868'):getDescription('de')
	-- getEntityObject is an alias for getEntity.
	-- Using the following gives an "out of memory" error presumably because
	-- the result is a table with a metatable that dump repeatedly expands.
	-- mw.title.getCurrentTitle
	local function params
		local p = { first }
		for item in :gmatch('(%S.-)%s*,') do
			-- Remove any quotes around each parameter because it is already a string.
			local _, s = item:match([[^%s*(['"])(.*)%1%s*$]])
			table.insert(p, s or tonumber(item) or item)
		end
		return unpack(p)
	end
	local expression = frame.args[1] or ''
	local text = expression:match('^%s*mw(%..-)%s*$')
	if not text then
		return 'Expression not recognized: "' .. expression .. '"'
	end
	-- Look for a supported expression of form 'mw.a.b(c):d.e(f)'.
	local entity
	local object = mw
	local item, ptext, rest = text:match('^%.wikibase%.(%w+)%s*%((.*)%):(.*)$')
	if item == 'getEntity' or item == 'getEntityObject' then
		entity = mw.wikibase.getEntity(params(ptext))
		if not entity then
			return 'No entity found for (' .. ptext .. ')'
		end
		object = entity
		text = '.' .. rest -- treat ':' as '.'
	end
	local upto = 1
	for i1, item, i2 in text:gmatch('%.(%w+)') do
		if i1 == upto and type(object) == 'table' then
			object = object[item]
		else
			object = nil
		end
		if object == nil then
			return 'Invalid item "' .. item .. '"'
		end
		if type(object) == 'function' then
			if text:sub == '' then
				object = object
				i2 = i2 + 2
			end
		end
		upto = i2
	end
	local parm = text:sub(upto):match('^%((.*)%)%s*$')
	if parm then
		object = object(params)
	end
	return dumper
end

local function dumpargs(frame)
	-- Return text dump of frame.args.
	-- → display ref strip marker
	local control = {
		autoname = function (var) return tostring(var) end, -- should not be called since keys should be numbers or strings
	local lines = Collection.new
	for key, keyrep in iterkeys do
		lines:add(keyrep .. ' = ')
	end
	return lines:join('\n')
end

local function parameters(frame)
	-- Return text dump of args and parent args from frame.
	-- This is for debugging a module to show what parameters it received.
	local control = {
		autoname = function (var) return tostring(var) end, -- should not be called since keys should be numbers or strings
	local lines = Collection.new
	lines:add('')
	for _, f in ipairs do
		lines:add('' .. f:getTitle .. '')
		for key, keyrep in iterkeys do
			lines:add('&nbsp;&nbsp;' .. mw.text.nowiki(keyrep .. '=' .. f.args[key]))
		end
	end
	lines:add('')
	return lines:join('\n')
end

local function wikidata(frame)
	local item = frame.args[1]
	if item then
		local id = item:match('^%s*([PQ]%d+)%s*$')
		if id then
			local entity = mw.wikibase.getEntity(id)
			return dumper
		end
	end
	return 'Parameter should be a Wikidata identifier such as P2386 or Q833639'
end

local builtins = {
	-- Handle preview of wikitext like 
	-- where TEXT is a built-in value that can be dumped.
	__index = function 
		local result
		local function caller
			return result
		end
		if type(key) == 'string' then
			local title = key:match('^%s*[\'"]?(.*%.tab)[\'"]?%s*$')
			if title then
				-- Assume structured data from Commons at c:Data:.
				if title:match('^[Dd]ata:') then
					title = title:sub(6)
				end
				local data = mw.ext.data.get(title) -- false if page does not exist
				result = dumper
			end
		end
		result = result or ('UNKNOWN: ' .. tostring(key))
		return caller
	end

return setmetatable({
	args = dumpargs,
	_dump = dumper,
	_dumphtml = _dumphtml,
	dumphtml = dumphtml,
	execute = execute,
	parameters = parameters,
	testcase = dump_testcase,
	wikidata = wikidata,