-- minetest provides its own built-in serializer mechanisms.
-- however, these leave much to be desired: the first works
-- by converting values to *lua source code* and 'deserializes'
-- these values by executing it in a mildly sandboxed env.
-- it's LSON basically. the other serializer is JSON. clearly
-- we need something better. if this was lua 5.3, we'd just
-- use string.pack, but alas, they had to use luajit, which
-- is stuck on an ancient version because it no longer exists.
-- if we built against moonjit, we could use pack, but then
-- that would raise compat issues for other users who might
-- want to use these mods. so we need to write our own.
--
-- good news is, it's very easy to do better than both the
-- minetest people and the clowns at PUC-Rio. (if only we had
-- general purpose bitops, it would be even easier)

local m = {
	err = {
		unmarshalled = {
			exp = 'the bytes passed are not a marshalled data structure';
		};
		corrupt = {
			exp = 'the marshalled data is corrupt';
		};
		domain = {
			exp = 'a value is outside the allowed domain';
		};
	};
}
m.wrong = function(e)
	for k,v in pairs(m.err) do
		if v == e then return k end
	end
	return false
end;
local proto = {
	header = string.char(0xFE,0x99)
}

local intcoder = function(bits,signed)
	local bytes = math.ceil(bits / 8)
	local max = 2 ^ bits
	local spoint = math.floor(max/2)
	return { 
		enc = function(obj)
			local val = math.abs(obj)
			local str = ''
			if signed then
				local max = math.floor(max / 2)
				if (obj > max) or (obj < (0-(max+1))) then
					return m.err.domain end
				if obj < 0 then val = val + spoint end
				-- e.g. for 8bit: 0x80 == -1; 0xFF = -128
			else
				if val > max then return m.err.domain end
			end
			for i=1,bytes do
				local n = math.fmod(val, 0x100)
				str = str .. string.char(n)
				val = math.floor(val / 0x100)
			end
			return str
		end;
		dec = function(str)
			local val = 0
			for i = 0, bytes-1 do
				local b = string.byte(str,bytes - i)
				val = (val * 0x100) + b
			end
			if signed then
				if val > spoint then val = 0 - (val - spoint) end
			end
			return val, string.sub(str, 1 + bytes)
		end;
	}
end

local blobcoder = function(bitlen)
	return {
		enc = function(obj)
			obj = obj or ''
			return intcoder(bitlen,false).enc(string.len(obj)) .. obj
		end;
		dec = function(str)
			local len, ns = intcoder(bitlen,false).dec(str)
			local val = string.sub(ns, 1, len)
			return val, string.sub(ns, len + 1)
		end;
	}
end
local arycoder = function(bitlen,t)
	return {
		enc = function(obj)
			local sz = (obj and #obj) or 0
			local str = intcoder(bitlen,false).enc(sz)
			for i=1,sz do
				str = str .. t.enc(obj[i])
			end
			return str
		end;
		dec = function(str)
			local ct, body = intcoder(bitlen,false).dec(str)
			local obj = {}
			for i=1,ct do
				obj[i], body = t.dec(body)
			end
			return obj, body
		end;
	}
end
m.t = {
	u8  = intcoder( 8,false),  s8 = intcoder( 8,true);
	u16 = intcoder(16,false), s16 = intcoder(16,true);
	u32 = intcoder(32,false), s32 = intcoder(32,true);
	u64 = intcoder(64,false), s64 = intcoder(64,true);
		-- technically this is sort of just a 52-bit integer
		-- type since while the logic could handle 64-bit ints 
		-- with no problem, lua 5.2 only has doubles, so we 
		-- only have 52 bits of mantissa to work with. but in
		-- lua 5.3 this will work as a full i64
	str = blobcoder(16,false);
	text = blobcoder(32,false);

}

m.transcoder = function(format)
	local keys = {}
	for k in pairs(format) do keys[#keys + 1] = k end
	table.sort(keys)
	local encoder = function(struct)
		struct = struct or {}
		local str = proto.header -- identify the marshalling format
		-- via a magic number so we can gracefully detect marshalled
		-- data, and possibly handle breaking upgrades
		for i=1,#keys do
			local fld = keys[i]
			local ty = format[fld]
			local code = ty.enc(struct[fld])
			-- propagate error conditions
			if m.wrong(code) then return code end
			str = str .. code
		end
		return str
	end
	local decoder = function(s)
		local obj = {}
		if string.sub(s,1,2) ~= proto.header then
			return m.err.unmarshalled end

		local str = string.sub(s,3)
		for i=1,#keys do
			local fld = keys[i]
			local ty = format[fld]
			local code, newstr = ty.dec(str)
			if m.wrong(code) then return code end
			obj[fld], str = code, newstr
		end
		return obj, str
	end
	return encoder, decoder
end
m.g = {
	blob = blobcoder;
	int = intcoder;
	array = arycoder;
	struct = function(format)
		-- okay, this is a bit of an abuse and a little
		-- inefficient, because this is the raw transcoder
		-- function, meaning it stores nested copies of
		-- the protocol header. i can't be arsed to care
		local encoder, decoder = m.transcoder(format)
		return { enc = encoder, dec = decoder };
	end;
}
-- example:
--- local pack, unpack = m.transcoder {
--- 	version = m.t.u16;
--- 	name = m.t.str;
--- 	time = m.t.u64;
--- 	blob = m.t.text;
--- }

return m
