local _G = _G
local CurseProfiler = {}
_G.CurseProfiler = CurseProfiler

-- Note: real version is the highest among this version and all modules
CurseProfiler.version = "2.0 2008-10-15T21:01:04Z"

local newproxy = _G.newproxy
local type = _G.type
local error = _G.error
local tonumber = _G.tonumber
local ipairs = _G.ipairs
local pairs = _G.pairs
local xpcall = _G.xpcall
local select = _G.select
local tostring = _G.tostring
local next = _G.next
local unpack = _G.unpack
local table_sort = _G.table.sort
local table_concat = _G.table.concat
local string_char = _G.string.char
local math_floor = _G.math.floor

local modules = {}
local frame = _G.CreateFrame("Frame", "CurseProfilerFrame")
CurseProfiler.frame = frame

local call__data = {}
local function call()
	return call__data[1](unpack(call__data, 2, call__data.n))
end

local errors = {}
--[===[@debug@
CurseProfiler.errors = errors
--@end-debug@]===]

local function errorhandler(err)
	local eventData = _G.event
	if eventData then
		eventData = eventData .. "("
		for i = 1, 9 do
			local arg = _G["arg" .. i]
			if arg == nil then
				break
			end
			if i > 1 then
				eventData = eventData .. ", "
			end
			if type(arg) == "string" then
				arg = ("%q"):format(arg)
			else
				arg = tostring(arg)
			end
			eventData = eventData .. arg
		end
		eventData = eventData .. ")"
	end
	errors[#errors+1] = (err or '') .. "\n" .. tostring(CurseProfiler.version or '') .. "\n" .. (_G.date("!%Y-%m-%dT%H:%M:%S") or '') .. "\n" .. (_G.UnitName("player") or '') .. "\n" .. (_G.GetRealmName() or '') .. "\n" .. (select(2, _G.UnitClass("player")) or '') .. "\n" .. (select(2, _G.UnitRace("player")) or '') .. "\n" .. (_G.GetRealZoneText() or '') .. "\n" .. (eventData or '') .. "\n" .. (_G.debugstack() or '')
end

frame:SetScript("OnEvent", function(this, event, ...)
	call__data[3] = event
	local n = select('#', ...)
	call__data.n = n+3
	for i = 1, n do
		call__data[i+3] = select(i, ...)
	end
	for _, module in pairs(modules) do
		if module[event] then
			call__data[1] = module[event]
			call__data[2] = module
			xpcall(call, errorhandler)
		end
	end
	if CurseProfiler[event] then
		call__data[1] = CurseProfiler[event]
		call__data[2] = CurseProfiler
		xpcall(call, errorhandler)
	end
	for k,v in pairs(call__data) do
		call__data[k] = nil
	end
end)
frame:RegisterEvent("ADDON_LOADED")

local nextGC = false
frame:SetScript("OnUpdate", function(this, elapsed)
	call__data[3] = elapsed
	call__data.n = 3
	for _, module in pairs(modules) do
		if module.OnUpdate then
			call__data[1] = module.OnUpdate
			call__data[2] = module
			xpcall(call, errorhandler)
		end
	end
	if CurseProfiler.OnUpdate then
		call__data[1] = CurseProfiler.OnUpdate
		call__data[2] = CurseProfiler
		xpcall(call, errorhandler)
	end
	for k,v in pairs(call__data) do
		call__data[k] = nil
	end
	if nextGC and nextGC < GetTime() and not InCombatLockdown() then
		nextGC = false
		collectgarbage('collect')
	end
end)

CurseProfiler.internalVersion = 0

local getRealLocale
do
	local locales = {
		["enUS"] = true,
		["enGB"] = true,
		["frFR"] = true,
		["deDE"] = true,
		["zhCN"] = true,
		["zhTW"] = true,
		["koKR"] = true,
		["esES"] = true,
		["esMX"] = true,
	}
	local _realLocale
	function getRealLocale()
		if _realLocale then
			return _realLocale
		end
		for locale in pairs(locales) do
			if _G["LOCALE_" .. locale] then
				_realLocale = locale
				return locale
			end
		end
		_realLocale = "xxXX"
		return "xxXX"
	end
end

local version = tonumber("20081015210104")
local CurseProfiler_DEBUG = _G.CurseProfiler_DEBUG or false
_G.CurseProfiler_DEBUG = nil
if version then
	CurseProfiler.internalVersion = version
else
	CurseProfiler.version = "2.0 Development"
	CurseProfiler.internalVersion = 0
	CurseProfiler_DEBUG = true
end

local serializing = false
local tmp = {}
function CurseProfiler:NewModule(name)
	local tab = {}
	modules[name] = tab
	self[name] = tab
	
	return tab
end

local enumRegistry = {
	class = {
		WARRIOR = 1,
		PALADIN = 2,
		HUNTER = 3,
		ROGUE = 4,
		PRIEST = 5,
		DEATHKNIGHT = 6,
		SHAMAN = 7,
		MAGE = 8,
		WARLOCK = 9,
		DRUID = 11
	},
	race = {
		Human = 1,
		Orc = 2,
		Dwarf = 3,
		NightElf = 4,
		Scourge = 5,
		Tauren = 6,
		Gnome = 7,
		Troll = 8,
		BloodElf = 10,
		Draenei = 11,
	},
	objectType = {
		item = 1,
		gameobject = 2,
		npc = 3,
	},
}

local zones = {}
local factions = {}

local integers = {}
local integerNumBytes = {}
local tuples = {}
local rawStrings = {}
local rawTableLength = {}
local items = {}
local enums = {}
local enumTypes = {}
local function clearSerializationData()
	integers = {}
	integerNumBytes = {}
	tuples = {}
	rawStrings = {}
	rawTableLength = {}
	items = {}
	enums = {}
	enumTypes = {}
end

local function getDataType(data)
	local type_data = type(data)
	if type_data == "userdata" then
	 	if integers[data] then
			return "integer"
		elseif tuples[data] then
			return "tuple-" .. getDataType(tuples[data])
		elseif rawStrings[data] then
			return "raw"
		elseif items[data] then
			return "item"
		elseif enums[data] then
			return "enum-" .. enumTypes[data]
		elseif zones[data] then
			return "zone"
		elseif factions[data] then
			return "faction"
		end
	elseif type_data ~= "table" then
		return type_data
	end
	local n = #data
	local isList = true
	local isSet = true
	for k, v in pairs(data) do
		if isSet and v ~= true then
			isSet = false
		end
		if isList then
			if type(k) ~= "number" or k < 1 or k > n then
				isList = false
			end
		end
		if not isList and not isSet then
			break
		end
	end	
	if isSet then
		return "set"
	elseif isList then
		for i = 1, n do
			if data[i] == nil then
				return "dict"
			end
		end
		return "list"
	else
		return "dict"
	end
end

local intFuncs = {}
function CurseProfiler.makeSerializedInteger(kind, integer)
	if not serializing then
		error("Cannot serialize outside of serializing path", 2)
	end
	local min, max, numBytes
	if kind == "Byte" then
		min = 0
		max = 2^8 - 1
		numBytes = 1
	elseif kind == "Int16" then
		min = -2^15
		max = 2^15 - 1
		numBytes = 2
	elseif kind == "UInt16" then
		min = 0
		max = 2^16 - 1
		numBytes = 2
	elseif kind == "Int32" then
		min = -2^31
		max = 2^31 - 1
		numBytes = 4
	elseif kind == "Int64" then
		min = -2^63
		max = 2^63 - 1
		numBytes = 8
	else
		error(("Bad integer type %s"):format(tostring(kind)), 2)
	end
	local p = newproxy()
	if type(integer) ~= "number" then
		error(("Bad type %s (%s)"):format(tostring(type(integer)), tostring(integer)), 2)
	elseif math_floor(integer) ~= integer then
		error(("%s is not an integer."):format(tostring(integer)), 2)
	end
	if integer >= max+1 or integer <= min-1 then
		error(("%s does not fit into a %s [%s, %s]"):format(integer, kind, min, max), 2)
	end
	integers[p] = integer
	integerNumBytes[p] = numBytes
	return p
end
local INT = CurseProfiler.makeSerializedInteger

function CurseProfiler.makeSerializedItem(itemString)
	if not serializing then
		error("Cannot serialize outside of serializing path", 2)
	end
	-- item:itemId:enchantId:jewelId1:jewelId2:jewelId3:jewelId4:suffixId:uniqueId
	if itemString then
		local itemId, enchantId, jewelId1, jewelId2, jewelId3, jewelId4, suffixId, uniqueId = itemString:match("item:(%d+):(%d+):(%d+):(%d+):(%d+):(%d+):(%-?%d+):(%-?%d+)")
		if not itemId then
			error(("%q is not a real itemString"):format(itemString))
		end
		
		local p = newproxy()
		local item = {
			itemId+0,
			enchantId+0,
			jewelId1+0,
			jewelId2+0,
			jewelId3+0,
			jewelId4+0,
			suffixId+0,
			uniqueId+0,
		}
		items[p] = item
		
		if item[1] < 0 or item[1] >= 2^16 then
			error(("%q's itemID is not within [%d, %d)"):format(itemString, 0, 2^16), 2)
		elseif item[2] < 0 or item[2] >= 2^16 then
			error(("%q's enchantID is not within [%d, %d)"):format(itemString, 0, 2^16), 2)
		elseif item[3] < 0 or item[3] >= 2^16 then
			error(("%q's jewelID1 is not within [%d, %d)"):format(itemString, 0, 2^16), 2)
		elseif item[4] < 0 or item[4] >= 2^16 then
			error(("%q's jewelID2 is not within [%d, %d)"):format(itemString, 0, 2^16), 2)
		elseif item[5] < 0 or item[5] >= 2^16 then
			error(("%q's jewelID3 is not within [%d, %d)"):format(itemString, 0, 2^16), 2)
		elseif item[6] < 0 or item[6] >= 2^16 then
			error(("%q's jewelID4 is not within [%d, %d)"):format(itemString, 0, 2^16), 2)
		elseif item[7] < -2^15 or item[7] >= 2^15 then
			error(("%q's suffixID is not within [%d, %d)"):format(itemString, -2^15, 2^15), 2)
		elseif item[8] < -2^31 or item[8] >= 2^31 then
			error(("%q's uniqueID is not within [%d, %d)"):format(itemString, -2^31, 2^31), 2)
		end
		
		return p
	else	
		local p = newproxy()
		items[p] = {
			0,
			0,
			0,
			0,
			0,
			0,
			0,
			0,
		}
		return p
	end
end
local ITEM = CurseProfiler.makeSerializedItem

function CurseProfiler.makeSerializedTuple(tab)
	if not serializing then
		error("Cannot serialize outside of serializing path", 2)
	end
	local p = newproxy()
	tuples[p] = tab
	return p
end
local TUPLE = CurseProfiler.makeSerializedTuple

function CurseProfiler.makeRaw(str, tableLength)
	if not serializing then
		error("Cannot serialize outside of serializing path", 2)
	end
	local p = newproxy()
	rawStrings[p] = str
	rawTableLength[p] = tableLength or 1
	return p
end
local RAW = CurseProfiler.makeRaw

function CurseProfiler.makeSerializedEnum(enumType, value)
	if not serializing then
		error("Cannot serialize outside of serializing path", 2)
	end
	local enumRegistry_enumType = enumRegistry[enumType]
	if not enumRegistry_enumType then
		error(("No known enum type: %q"):format(tostring(enumType)), 2)
	end
	if value ~= '' and not enumRegistry_enumType[value] then
		error(("Unknown value %q for enum type %q"):format(tostring(value), tostring(enumType)), 2)
	end
	local p = newproxy()
	enums[p] = value
	enumTypes[p] = enumType
	return p
end
local ENUM = CurseProfiler.makeSerializedEnum

local function set_sort(alpha, bravo)
	local alpha_dataType = getDataType(alpha)
	local bravo_dataType = getDataType(bravo)
	if alpha_dataType ~= bravo_dataType then
		return alpha_dataType < bravo_dataType
	end
	if alpha_dataType:match("^tuple%-") then
		alpha = tuples[alpha]
		bravo = tuples[bravo]
		alpha_dataType = getDataType(alpha)
		bravo_dataType = getDataType(bravo)
	end
	if alpha_dataType:match("^enum%-") then
		local enumType = enumTypes[alpha]
		local enumRegistry_enumType = enumRegistry[enumType]
		return (enumRegistry_enumType[enums[alpha]] or 0) < (enumRegistry_enumType[enums[bravo]] or 0)
	elseif alpha_dataType == "string" then
		return alpha < bravo
	elseif alpha_dataType == "number" then
		return alpha < bravo
	elseif alpha_dataType == "integer" then
		return integers[alpha] < integers[bravo]
	elseif alpha_dataType == "list" or alpha_dataType == "item" then
		if alpha_dataType == "item" then
			alpha = items[alpha]
			bravo = items[bravo]
		end
		if #alpha ~= #bravo then
			return #alpha < #bravo
		end
		
		for i, alpha_v in ipairs(alpha) do
			local bravo_v = bravo[i]
			if set_sort(alpha_v, bravo_v) then
				return true
			elseif set_sort(bravo_v, alpha_v) then
				return false
			end
		end
	elseif alpha_dataType == "dict" or alpha_dataType == "set" then
		local num = 0
		for k in pairs(alpha) do
			num = num + 1
		end
		for k in pairs(bravo) do
			num = num - 1
		end
		if num ~= 0 then
			return num < 0
		end
		
		local bravo_k = nil
		for k, v in pairs(alpha) do
			bravo_k = next(bravo, bravo_k)
			if set_sort(k, bravo_k) then
				return true
			elseif set_sort(bravo_k, k) then
				return false
			end
			local bravo_v = bravo[bravo_k]
			if set_sort(v, bravo_v) then
				return true
			elseif set_sort(bravo_v, v) then
				return false
			end
		end
	end
	return tostring(alpha) < tostring(bravo)
end

local function getTableSize(data)
	data = tuples[data] or data
	local dataType = getDataType(data)
	if dataType == "set" then
		local num = 0
		for k in pairs(data) do
			local dataType = getDataType(k)
			if rawTableLength[k] then
				num = num + rawTableLength[k]
			else
				num = num + 1
			end
		end
		return num
	elseif dataType == "list" then
		local num = 0
		for i, v in ipairs(data) do
			if rawTableLength[v] then
				num = num + rawTableLength[v]
			else
				num = num + 1
			end
		end
		return num
	elseif dataType == "dict" then
		local num = 0
		for k, v in pairs(data) do
			if rawTableLength[v] then
				num = num + rawTableLength[v]
			else
				num = num + 1
			end
		end
		return num
	end
	return rawTableLength[data] or 1
end

local function serialize(data, t)
	local madeT = not t
	local isTuple = tuples[data]
	if isTuple then
		data = isTuple
	end
	local dataType = getDataType(data)
	if madeT then
		t = {}
	end
	if dataType == "raw" then
		t[#t+1] = rawStrings[data]
	elseif dataType:match("^enum%-") then
		local enumType = enumTypes[data]
		local enumValue = enums[data]
		t[#t+1] = string_char(enumRegistry[enumType][enumValue] or 0)
	elseif dataType == "integer" then
		local num = integers[data]
		for i = 1, integerNumBytes[data] do
			local c = num % 256
			t[#t+1] = string_char(c)
			num = (num - c) / 256
		end
	elseif dataType == "item" then
		data = items[data]
		serialize(INT("UInt16", data[1]), t)
		serialize(INT("UInt16", data[2]), t)
		serialize(INT("UInt16", data[3]), t)
		serialize(INT("UInt16", data[4]), t)
		serialize(INT("UInt16", data[5]), t)
		serialize(INT("UInt16", data[6]), t)
		serialize(INT("Int16", data[7]), t)
		serialize(INT("Int32", data[8]), t)
	elseif dataType == "boolean" then
		t[#t+1] = data and string_char(1) or string_char(0)
	elseif dataType == "zone" then
		serialize(INT("Byte", zones[data] or 0), t)
	elseif dataType == "faction" then
		serialize(INT("UInt16", factions[data] or 0), t)
	elseif dataType == "string" then
		serialize(INT("UInt16", #data), t)
		t[#t+1] = data
	elseif dataType == "list" then
		if not isTuple then
			serialize(INT("UInt16", getTableSize(data)), t)
		end
		for i, v in ipairs(data) do
			serialize(v, t)
		end
	elseif dataType == "set" then
		if not isTuple then
			serialize(INT("UInt16", getTableSize(data)), t)
		end
		local keys = {}
		for k in pairs(data) do
			keys[#keys+1] = k
		end
		table_sort(keys, set_sort)
		for _, k in ipairs(keys) do
			serialize(k, t)
		end
	elseif dataType == "dict" then
		if not isTuple then
			serialize(INT("UInt16", getTableSize(data)), t)
		end
		local keys = {}
		for k in pairs(data) do
			keys[#keys+1] = k
		end
		table_sort(keys, set_sort)
		for _, k in ipairs(keys) do
			local v = data[k]
			serialize(k, t)
			serialize(v, t)
		end
	else
		error(("Unknown data type: %q (%s)"):format(dataType, tostring(data)), 2)
	end
	if madeT then
		return table_concat(t)
	end
end

local function deserialize(expected, text, position, dontCare)
	dontCare = false
	local signed, isByte = expected:match("^(S?)(Byte)$")
	local unsigned, bits = expected:match("^(U?)Int(%d%d+)$")
	if isByte then
		unsigned = signed ~= "S" and "U" or ""
		bits = 8
	end
	if bits then
		bits = bits+0
		unsigned = unsigned == 'U'
		if dontCare then
			return position + bits/8
		else
			local num = 0
			for i = 0, bits/8 - 1 do
				num = num + text:byte(position + i) * 256^i
			end
			if not unsigned and num > 2^(bits-1) then
				num = num - 2^bits
			end
			return position + bits/8, num
		end
	elseif expected == "boolean" then
		if dontCare then
			return position+1
		else
			return position+1, text:byte(position) == 1
		end
	elseif expected == "string" then
		local pos, num = deserialize("UInt16", text, position)
		if dontCare then
			return pos + num
		end
		local str = text:sub(pos, pos + num - 1)
		return pos + num, str
	elseif expected == "zone" then
		if dontCare then
			return position+1
		end
		local pos, num = deserialize("Byte", text, position)
		return pos, zones[num] or ''
	elseif expected == "faction" then
		if dontCare then
			return position+2
		end
		local pos, num = deserialize("UInt16", text, position)
		return pos, factions[num] or ''
	elseif expected == "list" or expected == "set" or expected == "dict" then
		local pos, num = deserialize("UInt16", text, position)
		return pos, num
	elseif expected == "item" then
		if dontCare then
			return position + 18
		end
		local itemId, enchantId, jewelId1, jewelId2, jewelId3, jewelId4, suffixId, uniqueId
		position, itemId = deserialize("UInt16", text, position)
		position, enchantId = deserialize("UInt16", text, position)
		position, jewelId1 = deserialize("UInt16", text, position)
		position, jewelId2 = deserialize("UInt16", text, position)
		position, jewelId3 = deserialize("UInt16", text, position)
		position, jewelId4 = deserialize("UInt16", text, position)
		position, suffixId = deserialize("Int16", text, position)
		position, uniqueId = deserialize("Int32", text, position)
		return position, ("item:%d:%d:%d:%d:%d:%d:%d:%d"):format(itemId, enchantId, jewelId1, jewelId2, jewelId3, jewelId4, suffixId, uniqueId)
	elseif expected:match("^enum%-") then
		local enumType = expected:sub(6)
		if not enumRegistry[enumType] then
			error(("Unknown enum type %q"):format(tostring(enumType)), 2)
		end
		if dontCare then
			return position + 1
		end
		local position, num = deserialize("Byte", text, position)
		if num == 0 then
			return position, ''
		end
		for name, value in pairs(enumRegistry[enumType]) do
			if num == value then
				return position, name
			end
		end
		error(("Unknown enum value %d for type %q"):format(num, enumType), 2)
	else
		error(("Unknown expected type %q"):format(tostring(expected)), 2)
	end
end
CurseProfiler.deserialize = deserialize

local base64encode, base64decode
do
	local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
	local A_byte = ("A"):byte()
	local Z_byte = ("Z"):byte()
	local a_byte = ("a"):byte()
	local z_byte = ("z"):byte()
	local zero_byte = ("0"):byte()
	local nine_byte = ("9"):byte()
	local plus_byte = ("+"):byte()
	local slash_byte = ("/"):byte()
	local function getNum(byte)
		if byte >= A_byte and byte <= Z_byte then
			return byte - A_byte
		elseif byte >= a_byte and byte <= z_byte then
			return byte - a_byte + 26
		elseif byte >= zero_byte and byte <= nine_byte then
			return byte - zero_byte + 52
		elseif byte == plus_byte then
			return 62
		elseif byte == slash_byte then
			return 63
		end
	end
	local t = {}
	function base64encode(text)
		for i = 1, #text, 3 do
			local a, b, c = text:byte(i, i+2)
			local nilNum = 0
			if not b then
				nilNum = 2
				b = 0
				c = 0
			elseif not c then
				nilNum = 1
				c = 0
			end
			local num = a * 2^16 + b * 2^8 + c
			
			local d = num % 2^6
			num = (num - d) / 2^6
			
			local c = num % 2^6
			num = (num - c) / 2^6
			
			local b = num % 2^6
			num = (num - b) / 2^6
			
			local a = num % 2^6
			
			t[#t+1] = chars:sub(a+1, a+1)
			t[#t+1] = chars:sub(b+1, b+1)
			if nilNum >= 2 then
				t[#t+1] = "="
			else
				t[#t+1] = chars:sub(c+1, c+1)
			end
			if nilNum >= 1 then
				t[#t+1] = "="
			else
				t[#t+1] = chars:sub(d+1, d+1)
			end
		end
		local s = table_concat(t)
		for i = 1, #t do
			t[i] = nil
		end
		return s
	end
	function base64decode(text)
		for i = 1, #text, 4 do
			local a, b, c, d = text:byte(i, i+3)
			a, b, c, d = getNum(a), getNum(b), getNum(c), getNum(d)
			
			local nilNum = 0
			if not c then
				nilNum = 2
				c = 0
				d = 0
			elseif not d then
				nilNum = 1
				d = 0
			end
			
			local num = a * 2^18 + b * 2^12 + c * 2^6 + d
			
			local c = num % 2^8
			num = (num - c) / 2^8
			
			local b = num % 2^8
			num = (num - b) / 2^8
			
			local a = num % 2^8
			num = (num - a) / 2^8
			
			t[#t+1] = string_char(a)
			if nilNum < 2 then
				t[#t+1] = string_char(b)
			end
			if nilNum < 1 then
				t[#t+1] = string_char(c)
			end
		end
		local s = table_concat(t)
		for i = 1, #t do
			t[i] = nil
		end
		return s
	end
end
if CurseProfiler_DEBUG then
	CurseProfiler.set_sort = set_sort
	CurseProfiler.serialize = serialize
	CurseProfiler.base64encode = base64encode
	CurseProfiler.base64decode = base64decode
end

local function startDeserialization(CurseProfilerDB)
	CurseProfilerDB = base64decode(CurseProfilerDB:sub(5))
	
	local pos, val = deserialize("Int64", CurseProfilerDB, 1)
	if val ~= CurseProfiler.internalVersion then
		-- don't deserialize old versions' code
		return
	end
	pos, val = deserialize("string", CurseProfilerDB, pos)
	if val ~= _G.GetCVar('realmlist') then
		-- changing servers, wipe data
		return
	end
	pos, val = deserialize("string", CurseProfilerDB, pos)
	if val ~= getRealLocale() then
		-- changing locales, wipe data
		return
	end
	pos, val = deserialize("string", CurseProfilerDB, pos)
	if val ~= _G.GetBuildInfo() then
		-- different WoW version, wipe data
		return
	end
	pos, val = deserialize("Int16", CurseProfilerDB, pos)
	if val ~= select(2, _G.GetBuildInfo())+0 then
		-- different WoW version, wipe data
		return
	end
	pos, val = deserialize("list", CurseProfilerDB, pos)
	for i = 1, val do
		local err
		pos, err = deserialize("string", CurseProfilerDB, pos)
		errors[#errors+1] = err
	end
	if val > 0 then
		return
	end
	pos, val = deserialize("list", CurseProfilerDB, pos)
	for i = 1, val do
		local zone
		pos, zone = deserialize("string", CurseProfilerDB, pos)
		zones[i] = zone
		zones[zone] = i
	end
	pos, val = deserialize("list", CurseProfilerDB, pos)
	for i = 1, val do
		local faction
		pos, faction = deserialize("string", CurseProfilerDB, pos)
		factions[i] = faction
		factions[faction] = i
	end


	local names = {}
	for name in pairs(modules) do
		names[#names+1] = name
	end
	table_sort(names)
	for _, name in ipairs(names) do
		local module = modules[name]
		pos = module:OnDeserialize(CurseProfilerDB, pos)
		if not pos then
			-- better stop here, error occurred
			errors[#errors+1] = ("Premature deserialize exist with module %s"):format(name)
			return
		end
	end
end

local old_CurseProfilerDB

local expectedModules = { GameObjects = true, NPCs = true, PlayerProfile = true, Quests = true }
function CurseProfiler:ADDON_LOADED()
	self.ADDON_LOADED = nil
	_G.CurseProfilerDebugDB = nil
	
	local CurseProfilerDB = _G.CurseProfilerDB
	old_CurseProfilerDB = CurseProfilerDB
	_G.CurseProfilerDB = nil
	
	for k in pairs(CurseProfiler) do
		if type(k) == "string" and k:upper() == k then
			frame:RegisterEvent(k)
		end
	end
	
	local good = true
	for k in pairs(expectedModules) do
		if not modules[k] then
			good = false
			_G.geterrorhandler()(("CurseProfiler cannot find module %s"):format(k))
		end
	end
	for k, v in pairs(modules) do
		if not expectedModules[k] then
			good = false
			_G.geterrorhandler()(("CurseProfiler found unexpected module %s"):format(k))
		else
			for event in pairs(v) do
				if type(event) == "string" and event:upper() == event then
					frame:RegisterEvent(event)
				end
			end
		end
	end
	if not good then
		-- clean out CurseProfiler's footprint
		frame:UnregisterAllEvents()
		frame:SetScript("OnEvent", nil)
		frame:SetScript("OnUpdate", nil)
		for k in pairs(CurseProfiler) do
			CurseProfiler[k] = nil
		end
		CurseProfiler = nil
		_G.CurseProfiler = nil
		return
	end
	
	if CurseProfilerDB then
		call__data[1] = startDeserialization
		call__data[2] = CurseProfilerDB
		xpcall(call, errorhandler)
		for k,v in pairs(call__data) do
			call__data[k] = nil
		end
		CurseProfilerDB = nil
	end
	for name, module in pairs(modules) do
		module.OnDeserialize = nil
	end
end

function CurseProfiler:PLAYER_LOGIN()
	self.PLAYER_LOGIN = nil
	
	_G.collectgarbage('collect')
end

local fix
if CurseProfiler_DEBUG then
	function fix(data, parent)
		local dataType = getDataType(data)
		if dataType == "integer" then
			local length = integerNumBytes[data]
			local type
			if length == 8 then
				type = "Byte"
			elseif length == -8 then
				type = "SByte"
			elseif length < 0 then
				type = "Int" .. (-length*8)
			else
				type = "UInt" .. (length*8)
			end
			return "@" .. type .. "[[" .. tostring(integers[data]) .. "]]"
		elseif dataType == "number" then
			return "@number[[" .. data .. "]]"
		elseif dataType == "zone" then
			return "@zone[[" .. (zones[zones[data]] or '') .. "]]"
		elseif dataType == "faction" then
			return "@faction[[" .. (factions[factions[data]] or '') .. "]]"
		elseif dataType == "raw" then
			return "@raw[[" .. rawStrings[data] .. "]]"
		elseif dataType:match("^enum%-") then
			return "@enum-" .. enumTypes[data] .. "[[" .. enums[data] .. "]]"
		elseif dataType == "set" or dataType == "tuple-set" then
			local t = {}
			if dataType == "tuple-set" then
				data = tuples[data]
				t["@tuple"] = true
			end
			for k in pairs(data) do
				t[#t+1] = fix(k)
			end
			table_sort(t, set_sort)
			return t
		elseif dataType == "dict" or dataType == "tuple-dict" then
			local t = {}
			if dataType == "tuple-dict" then
				data = tuples[data]
				t["@tuple"] = true
			end
			for k,v in pairs(data) do
				t[fix(k)] = fix(v)
			end
			return t
		elseif dataType == "list" or dataType == "tuple-list" then	
			local t = {}
			if dataType == "tuple-list" then
				data = tuples[data]
				t["@tuple"] = true
			end
			for i,v in ipairs(data) do
				t[i] = fix(v)
			end
			return t
		elseif dataType == "item" then
			local data = items[data]
			return "@item[[item:" .. data[1] .. ":" .. data[2] .. ":" .. data[3] .. ":" .. data[4] .. ":" .. data[5] .. ":" .. data[6] .. ":" .. data[7] .. ":" .. data[8] .. "]]"
		elseif dataType == "string" then
			return "@string[[" .. data .. "]]"
		elseif dataType == "boolean" then
			return "@boolean[[" .. tostring(data) .. "]]"
		end
		return data
	end
end

local function safe_call(func, ...)
	if type(func) ~= "function" then
		return "bad function"
	end
	return select(2, pcall(func, ...))
end

local serializeTime = 0
local saved = false
local function OnSerialize()
	if serializeTime + 10 > GetTime() then
		return
	end
	serializeTime = GetTime()
	if _G.UnitHealthMax("player") == 0 then
		if not saved then
			_G.CurseProfilerDB = old_CurseProfilerDB
		end
		return
	end
	serializing = true
	saved = true
	local data = {}
	data[#data+1] = INT("Int64", tonumber(CurseProfiler.internalVersion) or 0)
	data[#data+1] = tostring(safe_call(_G.GetCVar, 'realmlist'))
	data[#data+1] = tostring(safe_call(getRealLocale) or 'xxXX')
	data[#data+1] = tostring(safe_call(_G.GetBuildInfo))
	data[#data+1] = INT("Int16", tonumber(select(2, safe_call(_G.GetBuildInfo)) or 0) or 0)
	local names = {}
	for name in pairs(modules) do
		names[#names+1] = name
	end
	table_sort(names)
	local tmp = {}
	for _, name in pairs(names) do
		local module = modules[name]
		call__data[1] = module.OnSerialize
		call__data[2] = module
		local success, ret = xpcall(call, errorhandler)
		for k,v in pairs(call__data) do
			call__data[k] = nil
		end
		if success then
			tmp[#tmp+1] = ret
		else
			break
		end
	end
	data[#data+1] = errors
	
	if #errors == 0 then
		local zoneEnum = {}
		for i, v in ipairs(zones) do
			zoneEnum[#zoneEnum+1] = v
		end
		data[#data+1] = zoneEnum

		local factionEnum = {}
		for i, v in ipairs(factions) do
			factionEnum[#factionEnum+1] = v
		end
		data[#data+1] = factionEnum
		
		for i,v in ipairs(tmp) do
			data[#data+1] = v
		end
	end
	tmp = nil
	
	_G.CurseProfilerDB = tostring(safe_call(getRealLocale) or 'xxXX') .. base64encode(serialize(TUPLE(data)))
	if CurseProfiler_DEBUG then
		_G.CurseProfilerDebugDB = fix(data)
	end
	serializing = false
	data = nil
	clearSerializationData()
	nextGC = GetTime() + 20
end

function CurseProfiler:MIRROR_TIMER_STOP(event, arg1)
	if arg1 == "EXHAUSTION" and not UnitIsDeadOrGhost("player") and UnitHealth("player") > UnitHealthMax("player")/2 then
		OnSerialize()
	end
end

function CurseProfiler:PLAYER_CAMPING()
	OnSerialize()
end

function CurseProfiler:PLAYER_QUITING()
	OnSerialize()
end

function CurseProfiler:LOGOUT_CANCEL()
	OnSerialize()
end

function CurseProfiler:PLAYER_LOGOUT()
	OnSerialize()
end

function CurseProfiler.getShortID(text)
	if type(text) ~= "string" then
		return 0
	end
    return tonumber(text:match("[a-z]+:(%-?%d+)"))
end

function CurseProfiler.getZoneID(zone)
	if type(zone) ~= "string" then
		error(("Expected a string, got %q"):format(type(zone)), 2)
	end
	
	local p = newproxy()
	
	if zone == '' then
		zones[p] = 0
		return p
	end
	
	if zones[zone] then
		zones[p] = zones[zone]
		return p
	end
	
	zones[zone] = #zones+1
	zones[#zones+1] = zone
	
	zones[p] = #zones
	return p
end

function CurseProfiler.getFactionID(faction)
	if type(faction) ~= "string" then
		error(("Expected a string, got %q"):format(type(faction)), 2)
	end
	
	local p = newproxy()
	
	if faction == '' then
		factions[p] = 0
		return p
	end
	
	if factions[faction] then
		factions[p] = factions[faction]
		return p
	end
	
	factions[faction] = #factions+1
	factions[#factions+1] = faction
	
	factions[p] = #factions
	return p
end
