--- Lua module to serialize values as Lua code.
-- From: https://github.com/fab13n/metalua/blob/no-dll/src/lib/serialize.lua
-- License: MIT
-- @copyright 2006-2997 Fabien Fleutot <metalua@gmail.com>
-- @author Fabien Fleutot <metalua@gmail.com>
-- @author ShadowNinja <shadowninja@minetest.net>
--------------------------------------------------------------------------------

--- Serialize an object into a source code string. This string, when passed as
-- an argument to deserialize(), returns an object structurally identical to
-- the original one.  The following are currently supported:
--   * Booleans, numbers, strings, and nil.
--   * Functions; uses interpreter-dependent (and sometimes platform-dependent) bytecode!
--   * Tables; they can cantain multiple references and can be recursive, but metatables aren't saved.
-- This works in two phases:
--   1. Recursively find and record multiple references and recursion.
--   2. Recursively dump the value into a string.
-- @param x Value to serialize (nil is allowed).
-- @return load()able string containing the value.
function core.serialize(x)
	local local_index  = 1  -- Top index of the "_" local table in the dump
	-- table->nil/1/2 set of tables seen.
	-- nil = not seen, 1 = seen once, 2 = seen multiple times.
	local seen = {}

	-- nest_points are places where a table appears within itself, directly
	-- or not.  For instance, all of these chunks create nest points in
	-- table x: "x = {}; x[x] = 1", "x = {}; x[1] = x",
	-- "x = {}; x[1] = {y = {x}}".
	-- To handle those, two tables are used by mark_nest_point:
	-- * nested - Transient set of tables being currently traversed.
	--   Used for detecting nested tables.
	-- * nest_points - parent->{key=value, ...} table cantaining the nested
	--   keys and values in the parent.  They're all dumped after all the
	--   other table operations have been performed.
	--
	-- mark_nest_point(p, k, v) fills nest_points with information required
	-- to remember that key/value (k, v) creates a nest point  in table
	-- parent. It also marks "parent" and the nested item(s) as occuring
	-- multiple times, since several references to it will be required in
	-- order to patch the nest points.
	local nest_points  = {}
	local nested = {}
	local function mark_nest_point(parent, k, v)
		local nk, nv = nested[k], nested[v]
		local np = nest_points[parent]
		if not np then
			np = {}
			nest_points[parent] = np
		end
		np[k] = v
		seen[parent] = 2
		if nk then seen[k] = 2 end
		if nv then seen[v] = 2 end
	end

	-- First phase, list the tables and functions which appear more than
	-- once in x.
	local function mark_multiple_occurences(x)
		local tp = type(x)
		if tp ~= "table" and tp ~= "function" then
			-- No identity (comparison is done by value, not by instance)
			return
		end
		if seen[x] == 1 then
			seen[x] = 2
		elseif seen[x] ~= 2 then
			seen[x] = 1
		end

		if tp == "table" then
			nested[x] = true
			for k, v in pairs(x) do
				if nested[k] or nested[v] then
					mark_nest_point(x, k, v)
				else
					mark_multiple_occurences(k)
					mark_multiple_occurences(v)
				end
			end
			nested[x] = nil
		end
	end

	local dumped     = {}  -- object->varname set
	local local_defs = {}  -- Dumped local definitions as source code lines

	-- Mutually recursive local functions:
	local dump_val, dump_or_ref_val

	-- If x occurs multiple times, dump the local variable rather than
	-- the value. If it's the first time it's dumped, also dump the
	-- content in local_defs.
	function dump_or_ref_val(x)
		if seen[x] ~= 2 then
			return dump_val(x)
		end
		local var = dumped[x]
		if var then  -- Already referenced
			return var
		end
		-- First occurence, create and register reference
		local val = dump_val(x)
		local i = local_index
		local_index = local_index + 1
		var = "_["..i.."]"
		local_defs[#local_defs + 1] = var.." = "..val
		dumped[x] = var
		return var
	end

	-- Second phase.  Dump the object; subparts occuring multiple times
	-- are dumped in local variables which can be referenced multiple
	-- times.  Care is taken to dump local vars in a sensible order.
	function dump_val(x)
		local  tp = type(x)
		if     x  == nil        then return "nil"
		elseif tp == "string"   then return string.format("%q", x)
		elseif tp == "boolean"  then return x and "true" or "false"
		elseif tp == "function" then
			return string.format("loadstring(%q)", string.dump(x))
		elseif tp == "number"   then
			-- Serialize integers with string.format to prevent
			-- scientific notation, which doesn't preserve
			-- precision and breaks things like node position
			-- hashes.  Serialize floats normally.
			if math.floor(x) == x then
				return string.format("%d", x)
			else
				return tostring(x)
			end
		elseif tp == "table" then
			local vals = {}
			local idx_dumped = {}
			local np = nest_points[x]
			for i, v in ipairs(x) do
				if not np or not np[i] then
					vals[#vals + 1] = dump_or_ref_val(v)
				end
				idx_dumped[i] = true
			end
			for k, v in pairs(x) do
				if (not np or not np[k]) and
						not idx_dumped[k] then
					vals[#vals + 1] = "["..dump_or_ref_val(k).."] = "
						..dump_or_ref_val(v)
				end
			end
			return "{"..table.concat(vals, ", ").."}"
		else
			error("Can't serialize data of type "..tp)
		end
	end

	local function dump_nest_points()
		for parent, vals in pairs(nest_points) do
			for k, v in pairs(vals) do
				local_defs[#local_defs + 1] = dump_or_ref_val(parent)
					.."["..dump_or_ref_val(k).."] = "
					..dump_or_ref_val(v)
			end
		end
	end

	mark_multiple_occurences(x)
	local top_level = dump_or_ref_val(x)
	dump_nest_points()

	if next(local_defs) then
		return "local _ = {}\n"
			..table.concat(local_defs, "\n")
			.."\nreturn "..top_level
	else
		return "return "..top_level
	end
end

-- Deserialization

local env = {
	loadstring = loadstring,
}

local safe_env = {
	loadstring = function() end,
}

function core.deserialize(str, safe)
	if type(str) ~= "string" then
		return nil, "Cannot deserialize type '"..type(str)
			.."'. Argument must be a string."
	end
	if str:byte(1) == 0x1B then
		return nil, "Bytecode prohibited"
	end
	local f, err = loadstring(str)
	if not f then return nil, err end
	setfenv(f, safe and safe_env or env)

	local good, data = pcall(f)
	if good then
		return data
	else
		return nil, data
	end
end


-- Unit tests
local test_in = {cat={sound="nyan", speed=400}, dog={sound="woof"}}
local test_out = core.deserialize(core.serialize(test_in))

assert(test_in.cat.sound == test_out.cat.sound)
assert(test_in.cat.speed == test_out.cat.speed)
assert(test_in.dog.sound == test_out.dog.sound)

test_in = {escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"}
test_out = core.deserialize(core.serialize(test_in))
assert(test_in.escape_chars == test_out.escape_chars)
assert(test_in.non_european == test_out.non_european)
