local _serialize, _serializeStrings, _serializeTable, _serializeObject
local encodedStrings
local buffer

local getmetatable = getmetatable
local math = math
local pairs = pairs
local rawget = rawget
local tostring = tostring
local type = type

function serialize(item, refs, depth)
    buffer = Buffer:new()
    
    Profiler.time("serialize", function()
        encodedStrings = {}
        _serialize(item, refs, depth)
        buffer:add('\n')
        encodedStrings = nil
        collectgarbage()
    end)
    
    return buffer
end

function _serialize(item, refs, depth)
    if refs and refs[item] then
        _serialize(refs[item], nil, depth)
        return
    end
    local itemtype = type(item)
    if itemtype == 'string' then
        _serializeString(item)
    elseif itemtype == 'number' then
        buffer:add(tostring(item))
    elseif itemtype == 'boolean' then
        buffer:add(tostring(item))
    elseif itemtype == 'userdata' then
        depth = depth or 0
        if item.archive then
            local typeName, properties, sortedProperties = item:archive(refs)
            _serializeObject(typeName, properties, sortedProperties, refs, depth)
        end
    elseif itemtype == 'table' then
        depth = depth or 0
        if getmetatable(item) and item.archive then
            local typeName, properties, sortedProperties = item:archive(refs)
            _serializeObject(typeName, properties, sortedProperties, refs, depth)
        else
            _serializeTable(item, nil, refs, depth)
        end
    else
        buffer:add('null')
    end
end

function _serializeString(item)
    local result = encodedStrings[item]
    if not result then
        result = json.encode(item)
        encodedStrings[item] = result
    end
    buffer:add(result)
end

function _serializeTable(item, itemKeys, refs, depth)
    local isArray = true
    local arrayLength = 0
    local isObject = true
    for k, v in pairs(item) do
        if (type(k) == 'number' and k == math.floor(k) and k > 0) then
            arrayLength = math.max(arrayLength, k)
        else
            isArray = false
        end
        if not (type(k) == 'string') then
            isObject = false
        end
    end
    local spaces = string.rep(' ', depth)
    local moreSpaces = '\n' .. spaces .. ' '
    if isArray then
        buffer:add('[')
        for index = 1, arrayLength do
            buffer:add(moreSpaces)
            _serialize(item[index], refs, depth + 1)
            if index < arrayLength then
                buffer:add(',')
            end
        end
        buffer:add('\n', spaces, ']')
    elseif isObject then
        buffer:add('{')
        local mt = getmetatable(item)
        local keys = itemKeys or sortedkeys(item)
        for index = 1, #keys do
            local key = keys[index]
            local value = rawget(item, key)
            if value ~= nil then
                buffer:add(moreSpaces)
                _serializeString(key)
                buffer:add(': ')
                _serialize(value, refs, depth + 1)
                if index < #keys then
                    buffer:add(',')
                end
            end
        end
        buffer:add('\n', spaces, '}')
    else
        warn('Table is neither array nor object.')
        buffer:add('null')
    end
end

function _serializeObject(typeName, properties, sortedProperties, refs, depth)
    if type(typeName) == 'string' then
        buffer:add('[')
        _serializeString(typeName)
        buffer:add(', ')
        if type(properties) == 'table' then
            _serializeTable(properties, sortedProperties, refs, depth)
        else
            _serialize(properties, refs, depth)
        end
        buffer:add(']')
    else
        _serialize(properties, refs, depth)
    end
end

return serialize
