
local char, byte = string.char, string.byte

local air_cid = minetest.get_content_id("air")

local function encode_uint16(int)
	local a, b = int % 0x100, int / 0x100
	return char(a, b)
end

local function decode_uint16(str, ofs)
	ofs = ofs or 1
	local a = byte(str, ofs)
    local b = byte(str, ofs + 1)
	return a + b * 0x100
end

-- nodeid -> name
local nodeid_name_mapping = {}

function pick_and_place.serialize(pos1, pos2)
    local manip = minetest.get_voxel_manip()
	local e1, e2 = manip:read_from_map(pos1, pos2)
	local area = VoxelArea:new({MinEdge=e1, MaxEdge=e2})

    local node_data = manip:get_data()
	local param2 = manip:get_param2_data()

    local mapdata = {}
    local metadata = {}

    -- nodeid -> true
    local nodeids = {}

    for z=pos1.z,pos2.z do
    for x=pos1.x,pos2.x do
    for y=pos1.y,pos2.y do
        local i = area:index(x,y,z)
        nodeids[node_data[i]] = true
        table.insert(mapdata, encode_uint16(node_data[i]))
        table.insert(mapdata, char(param2[i]))
    end
    end
    end

    -- id -> name
    local nodeid_mapping = {}

    for nodeid in pairs(nodeids) do
        local name = nodeid_name_mapping[nodeid]
        if not name then
            name = minetest.get_name_from_content_id(nodeid)
            nodeid_name_mapping[nodeid] = name
        end

        nodeid_mapping[nodeid] = name
    end

    -- store metadata
    local nodes_with_meta = minetest.find_nodes_with_meta(pos1, pos2)
    for _, pos in ipairs(nodes_with_meta) do
        local rel_pos = vector.subtract(pos, pos1)
        local meta = minetest.get_meta(pos)
        local meta_table = meta:to_table()

        -- Convert metadata item stacks to item strings
        for _, invlist in pairs(meta_table.inventory) do
            for index = 1, #invlist do
                local itemstack = invlist[index]
                if itemstack.to_string then
                    invlist[index] = itemstack:to_string()
                end
            end
        end

        metadata[minetest.pos_to_string(rel_pos)] = meta_table
    end

    local data = {
        version = 1,
        mapdata = table.concat(mapdata),
        metadata = metadata,
        nodeid_mapping = nodeid_mapping,
        size = vector.add(vector.subtract(pos2, pos1), 1)
    }

    local serialized_data = minetest.serialize(data)
    local compressed_data = minetest.compress(serialized_data, "deflate")
    local encoded_data = minetest.encode_base64(compressed_data)

    return encoded_data
end

-- name -> nodeid
local name_nodeid_mapping = {}

function pick_and_place.deserialize(pos1, encoded_data)
    local compressed_data = minetest.decode_base64(encoded_data)
    local serialized_data = minetest.decompress(compressed_data, "deflate")
    local data = minetest.deserialize(serialized_data)

    if data.version ~= 1 then
        return false, "invalid version: " .. (data.version or "nil")
    end

    local pos2 = vector.add(pos1, vector.subtract(data.size, 1))

    local manip = minetest.get_voxel_manip()
	local e1, e2 = manip:read_from_map(pos1, pos2)
	local area = VoxelArea:new({MinEdge=e1, MaxEdge=e2})

    local node_data = manip:get_data()
	local param2 = manip:get_param2_data()

    -- foreign_nodeid -> local_nodeid
    local localized_id_mapping = {}

    for foreign_nodeid, name in pairs(data.nodeid_mapping) do
        local local_nodeid = name_nodeid_mapping[name]
        if not local_nodeid then
            local_nodeid = minetest.get_content_id(name)
            name_nodeid_mapping[name] = local_nodeid
        end

        localized_id_mapping[foreign_nodeid] = local_nodeid
    end

    local j = 1
    for z=pos1.z,pos2.z do
    for x=pos1.x,pos2.x do
    for y=pos1.y,pos2.y do
        local i = area:index(x,y,z)
        local foreign_nodeid = decode_uint16(data.mapdata, j)

        -- localize nodeid mapping
        local local_nodeid = localized_id_mapping[foreign_nodeid]

        if local_nodeid ~= air_cid then
            node_data[i] = local_nodeid
            param2[i] = byte(data.mapdata, j+2)
        end

        j = j + 3
    end
    end
    end

    -- metadata
    for pos_str, meta_table in pairs(data.metadata) do
        local pos = minetest.string_to_pos(pos_str)
        local abs_pos = vector.add(pos1, pos)
        local meta = minetest.get_meta(abs_pos)
        meta:from_table(meta_table)
    end

    manip:set_data(node_data)
    manip:set_param2_data(param2)
    manip:write_to_map()

    return true
end
