local fs = require("filesystem")
local serialization = {}

-- Important: pretty formatting will allow presenting non-serializable values
-- but may generate output that cannot be unserialized back.
function serialization.serialize(value, pretty)
    local kw = {
        ["and"] = true,
        ["break"] = true,
        ["do"] = true,
        ["else"] = true,
        ["elseif"] = true,
        ["end"] = true,
        ["false"] = true,
        ["for"] = true,
        ["function"] = true,
        ["goto"] = true,
        ["if"] = true,
        ["in"] = true,
        ["local"] = true,
        ["nil"] = true,
        ["not"] = true,
        ["or"] = true,
        ["repeat"] = true,
        ["return"] = true,
        ["then"] = true,
        ["true"] = true,
        ["until"] = true,
        ["while"] = true
    }
    local id = "^[%a_][%w_]*$"
    local ts = {}
    local result_pack = {}
    local function recurse(current_value, depth)
        local t = type(current_value)
        if t == "number" then
            if current_value ~= current_value then
                table.insert(result_pack, "0/0")
            elseif current_value == math.huge then
                table.insert(result_pack, "math.huge")
            elseif current_value == -math.huge then
                table.insert(result_pack, "-math.huge")
            else
                table.insert(result_pack, tostring(current_value))
            end
        elseif t == "string" then
            table.insert(result_pack, (string.format("%q", current_value):gsub("\\\n", "\\n")))
        elseif
            t == "nil" or t == "boolean" or pretty and (t ~= "table" or (getmetatable(current_value) or {}).__tostring)
         then
            table.insert(result_pack, tostring(current_value))
        elseif t == "table" then
            if ts[current_value] then
                if pretty then
                    table.insert(result_pack, "recursion")
                    return
                else
                    error("tables with cycles are not supported")
                end
            end
            ts[current_value] = true
            local f
            if pretty then
                local ks, sks, oks = {}, {}, {}
                for k in pairs(current_value) do
                    if type(k) == "number" then
                        table.insert(ks, k)
                    elseif type(k) == "string" then
                        table.insert(sks, k)
                    else
                        table.insert(oks, k)
                    end
                end
                table.sort(ks)
                table.sort(sks)
                for _, k in ipairs(sks) do
                    table.insert(ks, k)
                end
                for _, k in ipairs(oks) do
                    table.insert(ks, k)
                end
                local n = 0
                f =
                    table.pack(
                    function()
                        n = n + 1
                        local k = ks[n]
                        if k ~= nil then
                            return k, current_value[k]
                        else
                            return nil
                        end
                    end
                )
            else
                f = table.pack(pairs(current_value))
            end
            local i = 1
            local first = true
            table.insert(result_pack, "{")
            for k, v in table.unpack(f) do
                if not first then
                    table.insert(result_pack, ",")
                    if pretty then
                        table.insert(result_pack, "\n" .. string.rep(" ", depth))
                    end
                end
                first = nil
                local tk = type(k)
                if tk == "number" and k == i then
                    i = i + 1
                    recurse(v, depth + 1)
                else
                    if tk == "string" and not kw[k] and string.match(k, id) then
                        table.insert(result_pack, k)
                    else
                        table.insert(result_pack, "[")
                        recurse(k, depth + 1)
                        table.insert(result_pack, "]")
                    end
                    table.insert(result_pack, "=")
                    recurse(v, depth + 1)
                end
            end
            ts[current_value] = nil -- allow writing same table more than once
            table.insert(result_pack, "}")
        else
            error("unsupported type: " .. t)
        end
    end
    recurse(value, 1)
    local result = table.concat(result_pack)
    if pretty then
        local limit = type(pretty) == "number" and pretty or 10
        local truncate = 0
        while limit > 0 and truncate do
            truncate = string.find(result, "\n", truncate + 1, true)
            limit = limit - 1
        end
        if truncate then
            return result:sub(1, truncate) .. "..."
        end
    end
    return result
end

function serialization.unserialize(data)
    checkArg(1, data, "string")

    local result, reason = load("return " .. data, "=unserialize", nil, {math = {huge = math.huge}})
    if not result then
        return nil, reason
    end
    
    local ok, output = pcall(result)
    if not ok then
        return nil, output
    end
    
    if type(output) == "table" then
        return output
    end
    return nil, "type error, input data is not a table"
end

function serialization.load(path)
    checkArg(1, path, "string")
    
    local content, err = fs.readFile(path)
    if not content then return nil, err end

    return serialization.unserialize(content)
end

function serialization.save(path, data)
    checkArg(1, path, "string")
    checkArg(2, data, "table")
    return fs.writeFile(path, serialization.serialize(data))
end

serialization.unloadable = true
return serialization