-- by "Yevano", tweaked due to issues with signed integer handling
--[[
    BLODS - Binary Lua Object (De)Serialization
]]

--[[
    Save on table access.
]]
local pairs       = pairs
local type        = type
local loadstring  = loadstring
local mathabs     = math.abs
local mathfloor   = math.floor
local mathfrexp   = math.frexp
local mathmodf    = math.modf
local mathpow     = math.pow
local stringbyte  = string.byte
local stringchar  = string.char
local stringdump  = string.dump
local stringsub   = string.sub
local tableconcat = table.concat

--[[
    Float conversions. Modified from http://snippets.luacode.org/snippets/IEEE_float_conversion_144.
]]
local function double2str(value)
    local s=value<0 and 1 or 0
    if mathabs(value)==1/0 then
        return (s==1 and "\0\0\0\0\0\0\240\255" or "\0\0\0\0\0\0\240\127")
    end
    if value~=value then
        return "\170\170\170\170\170\170\250\255"
    end
    local fr,exp=mathfrexp(mathabs(value))
    fr,exp=fr*2,exp-1
    exp=exp+1023
    return tableconcat({stringchar(mathfloor(fr*2^52)%256),
    stringchar(mathfloor(fr*2^44)%256),
    stringchar(mathfloor(fr*2^36)%256),
    stringchar(mathfloor(fr*2^28)%256),
    stringchar(mathfloor(fr*2^20)%256),
    stringchar(mathfloor(fr*2^12)%256),
    stringchar(mathfloor(fr*2^4)%16+mathfloor(exp)%16*16),
    stringchar(mathfloor(exp/2^4)%128+128*s)})
end

local function str2double(str)
    local fr=stringbyte(str, 1)/2^52+stringbyte(str, 2)/2^44+stringbyte(str, 3)/2^36+stringbyte(str, 4)/2^28+stringbyte(str, 5)/2^20+stringbyte(str, 6)/2^12+(stringbyte(str, 7)%16)/2^4+1
    local exp=(stringbyte(str, 8)%128)*16+mathfloor(str:byte(7)/16)-1023
    local s=mathfloor(stringbyte(str, 8)/128)
    if exp==1024 then
        return fr==1 and (1-2*s)/0 or 0/0
    end
    return (1-2*s)*fr*2^exp
end

--[[
    Integer conversions. Taken from http://lua-users.org/wiki/ReadWriteFormat.
    Modified to support signed ints.
]]

local function signedstringtonumber(str)
  local function _b2n(exp, num, digit, ...)
    if not digit then return num end
    return _b2n(exp*256, num + digit*exp, ...)
  end
  return _b2n(256, stringbyte(str, 1, -1)) - mathpow(2, #str * 8 - 1)
end

local function stringtonumber(str)
    local function _b2n(exp, num, digit, ...)
      if not digit then return num end
      return _b2n(exp*256, num + digit*exp, ...)
    end
    return _b2n(256, stringbyte(str, 1, -1))
end

local function numbertobytes(num, width)
    local function _n2b(width, num, rem)
        rem = rem * 256
        if width == 0 then return rem end
        return rem, _n2b(width-1, mathmodf(num/256))
    end
    return stringchar(_n2b(width-1, mathmodf((num)/256)))
end

local function log2(x)
	return math.log10(x) / math.log10(2)
end

--[[
    (De)Serialization for Lua types.
]]

local function intWidth(int)
    local out = math.ceil((log2(int) + 1) / 8)
	if out == math.huge or out == -math.huge then return 1 end
	return out
end

local types = {
    boolean = "b",
    double = "d",
    posinteger = "p",
    neginteger = "n",
    string = "s",
    table = "t",
    ["function"] = "f",
    ["nil"] = "_"
}

local serialization = { }
local deserialization = { }

function serialization.boolean(obj)
    return obj and "\1" or "\0"
end

function serialization.double(obj)
    return double2str(obj)
end

function serialization.integer(obj)
    local width = intWidth(obj)
    return stringchar(width) .. numbertobytes(obj, width)
end

function serialization.string(obj)
    local len = #obj
    local width = intWidth(len)
    return tableconcat({ stringchar(width), numbertobytes(len, width), obj })
end

serialization["function"] = function(obj)
    local ok, s = pcall(stringdump, obj)
	if not ok then return "_" end
    return numbertobytes(#s, 4) .. s
end

function deserialization.b(idx, ser)
    local ret = stringsub(ser[1], idx, idx) == "\1"
    return ret, idx + 1
end

function deserialization.d(idx, ser)
    local ret = str2double(stringsub(ser[1], idx, idx + 8))
    return ret, idx + 8
end

function deserialization.p(idx, ser)
    local width = stringtonumber(stringsub(ser[1], idx, idx))
    local ret = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
    return ret, idx + width + 1
end

function deserialization.n(idx, ser)
    local width = stringtonumber(stringsub(ser[1], idx, idx))
    local ret = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
    return -ret, idx + width + 1
end

function deserialization.s(idx, ser)
    local width = stringtonumber(stringsub(ser[1], idx, idx))
    local len = stringtonumber(stringsub(ser[1], idx + 1, idx + width))
    local ret = stringsub(ser[1], idx + width + 1, idx + width + len)
    return ret, idx + width + len + 1
end

function deserialization.f(idx, ser)
    local len = stringtonumber(stringsub(ser[1], idx, idx + 3))
    local ret = loadstring(stringsub(ser[1], idx + 4, idx + len + 3))
    return ret, idx + len + 4
end

function deserialization._(idx, ser)
    return nil, idx
end

local function yield()
	os.queueEvent ""
	os.pullEvent ""
end

if not os.queueEvent then yield = function() end end

function serialize(obj)
    -- State vars.
    local ntables = 1
    local tables = { }
    local tableIDs = { }
    local tableSerial = { }

    -- Internal recursive function.
    local function serialize(obj)
		--yield()
        local t = type(obj)
        if t == "table" then
            local len = #obj

            if tables[obj] then
                -- We already serialized this table. Just return the id.
                return tableIDs[obj]
            end

            -- Insert table info.
            local id = ntables
            tables[obj] = true
            local width = intWidth(ntables)
            local ser = "t" .. numbertobytes(width, 1) .. numbertobytes(ntables, width)
            tableIDs[obj] = ser

            -- Important to increment here so tables inside this one don't use the same id.
            ntables = ntables + 1

            -- Serialize the table.
            local serialConcat = { }

            -- Array part.
            for i = 1, len do
                if obj[i] == nil then
                    len = i - 1
                    break
                end
                serialConcat[#serialConcat + 1] = serialize(obj[i])
            end
            serialConcat[#serialConcat + 1] = "\0"

            -- Table part.
            for k, v in pairs(obj) do
                if type(k) ~= "number" or ((k > len or k < 1) or mathfloor(k) ~= k) then
                    -- For each pair, serialize both the key and the value.
                    local idx = #serialConcat
                    serialConcat[idx + 1] = serialize(k)
                    serialConcat[idx + 2] = serialize(v)
                end
            end
            serialConcat[#serialConcat + 1] = "\0"

            -- tableconcat is way faster than normal concatenation using .. when dealing with lots of strings.
            -- Add this serialization to the table of serialized tables for quick access and later more concatenation.
            tableSerial[id] = tableconcat(serialConcat)
            return ser
        else
            -- Do serialization on a non-recursive type.
            if t == "number" then
                -- Space optimization can be done for ints, so serialize them differently from doubles.
                -- OSMARKS EDIT: handle sign in type and not actual serialization
                if mathfloor(obj) == obj then
                    local intval = serialization.integer(math.abs(obj))
                    local typespec = "p"
                    if obj < 0 then typespec = "n" end
                    return typespec .. intval
                end
                return "d" .. serialization.double(obj)
            end
            local ser = types[t]
            return obj == nil and ser or ser .. serialization[t](obj)
        end
    end

    -- Either serialize for a table or for a non-recursive type.
    local ser = serialize(obj)
    if type(obj) == "table" then
        return tableconcat({ "t", tableconcat(tableSerial) })
    end
    return ser
end

function deserialize(ser)
    local idx = 1
    local tables = { { } }
    local serref = { ser }

    local function getchar()
        local ret = stringsub(serref[1], idx, idx)
        return ret ~= "" and ret or nil
    end

    local function deserializeValue()
		--yield()
        local t = getchar()
        idx = idx + 1
        if t == "t" then
            -- Get table id.
            local width = stringtonumber(getchar())
            idx = idx + 1
            local id = stringtonumber(stringsub(serref[1], idx, idx + width - 1))
            idx = idx + width

            -- Create an empty table as a placeholder.
            if not tables[id] then
                tables[id] = { }
            end

            return tables[id]
        else
            local ret
            ret, idx = deserialization[t](idx, serref)
            return ret
        end
    end

    -- Either deserialize for a table or for a non-recursive type.
    local i = 1
    if getchar() == "t" then
        idx = idx + 1
        while getchar() do
            if not tables[i] then tables[i] = { } end
            local curtbl = tables[i]

            -- Array part.
            while getchar() ~= "\0" do
                curtbl[#curtbl + 1] = deserializeValue()
            end

            -- Table part.
            idx = idx + 1
            while getchar() ~= "\0" do
                curtbl[deserializeValue()] = deserializeValue()
            end

            i = i + 1
            idx = idx + 1
        end
        return tables[1]
    end
    return deserializeValue()
end

return { serialize = serialize, deserialize = deserialize }