local function escape_char(c)
	return ("\\%03d"):format(c:byte())
end

local function table_len(t)
	for i = 1, #t do
		if t[i] == nil then
			return i-1
		end
	end
	return #t
end

function pprint(...)
	print(ptostring(...))
end

local function key_sort(alpha, bravo)
	local type_alpha, type_bravo = type(alpha), type(bravo)
	if type_alpha ~= type_bravo then
		return type_alpha < type_bravo
	end
	
	if type_alpha == "string" then
		return alpha:lower() < bravo:lower()
	elseif type_alpha == "number" then
		return alpha < bravo
	elseif type_alpha == "table" then
		return tostring(alpha) < tostring(bravo)
	else
		return false
	end
end

local first_ptostring = true
function ptostring(...)
	local t = {}
	for i = 1, select('#', ...) do
		if i > 1 then
			t[#t+1] = ",\t"
		end
		local v = select(i, ...)
		if type(v) == "string" then
			t[#t+1] = (("%q"):format(v):gsub("[\001-\031\128-\255]", escape_char))
		elseif type(v) == "table" then
			t[#t+1] = "{ "
			local keys = {}
			for a in pairs(v) do
				keys[#keys+1] = a
			end
			table.sort(keys, key_sort)
			local first = true
			for _,a in ipairs(keys) do
				local b = v[a]
				if first then
					first = nil
				else
					t[#t+1] = ", "
				end
				if type(a) ~= "number" or a < 1 or a > table_len(v) then
					if type(a) == "string" and a:match("^[a-zA-Z_][a-zA-Z_0-9]*$") then
						t[#t+1] = a
						t[#t+1] = " = "
					else
						t[#t+1] = "["
						t[#t+1] = ptostring(a)
						t[#t+1] = "] = "
					end
				end
				t[#t+1] = ptostring(b)
			end
			if first then
				t[#t] = "{}"
			else
				t[#t+1] = " }"
			end
		else
			t[#t+1] = tostring(v)
		end
	end
	return table.concat(t)
end

local function is_equal(alpha, bravo)
	if type(alpha) ~= type(bravo) then
		return false
	end
	
	if type(alpha) == "number" then
		return alpha == bravo or tostring(alpha) == tostring(bravo) or math.abs(alpha - bravo) < 1e-15
	elseif type(alpha) ~= "table" then
		return alpha == bravo
	end
	
	local num = 0
	for k,v in pairs(alpha) do
		num = num + 1
		if not is_equal(v, bravo[k]) then
			return false
		end
	end
	
	for k,v in pairs(bravo) do
		num = num - 1
	end
	if num ~= 0 then
		return false
	end
	return true
end

function geterrorhandler()
	return function(err)
		print(debug.traceback(err, 2))
		os.exit()
	end
end

function assert_equal(alpha, bravo)
	if not is_equal(alpha, bravo) then
		error(("Assertion failed: %s == %s"):format(ptostring(alpha), ptostring(bravo)), 2)
	end
end

local function CreateFontString(parent, name, layer)
	local fs = {
		[0] = newproxy(), -- fake userdata
	}
	function fs:GetObjectType()
		return "FontString"
	end
	local text
	function fs:SetText(x)
		text = x
	end
	function fs:GetText()
		return text
	end
	local alpha = 1
	function fs:SetAlpha(a)
		alpha = a
	end
	function fs:GetAlpha()
		return alpha
	end
	return fs
end
local frames = {}
local frameRegisteredEvents = {}
local ALL_EVENTS = newproxy()
function CreateFrame(frameType, ...)
	local frame = {
		[0] = newproxy(), -- fake userdata
	}
	frames[frame] = true
	function frame:GetObjectType()
		return frameType
	end
	function frame:GetFrameType()
		return frameType
	end
	local scripts = {}
	function frame:SetScript(script, func)
		scripts[script] = func
	end
	function frame:GetScript(script)
		return scripts[script]
	end
	local events = {}
	frameRegisteredEvents[frame] = events
	function frame:RegisterEvent(event)
		events[event] = true
	end
	function frame:UnregisterEvent(event)
		events[event] = nil
	end
	function frame:UnregisterAllEvents()
		for event in pairs(events) do
			events[event] = nil
		end
	end
	function frame:RegisterAllEvents()
		events[ALL_EVENTS] = true
	end
	function frame:CreateFontString(...)
		return CreateFontString(frame, ...)
	end
	return frame
end

local currentTime = 1e5 -- initial time
function GetTime()
	return currentTime
end

function FireOnUpdate(elapsed)
	if not elapsed then
		elapsed = 1
	end
	currentTime = currentTime + elapsed
	for frame in pairs(frames) do
		local OnUpdate = frame:GetScript("OnUpdate")
		if OnUpdate then
			OnUpdate(frame, elapsed)
		end
	end
end

function FireEvent(event, ...)
	for frame in pairs(frames) do
		if frameRegisteredEvents[frame][event] or frameRegisteredEvents[frame][ALL_EVENTS] then
			local OnEvent = frame:GetScript("OnEvent")
			if OnEvent then
				OnEvent(frame, event, ...)
			end
		end
	end
end

local function sorted(t, sort_func)
	local tab = { unpack(t) }
	table.sort(tab, sort_func)
	return tab
end

CurseProfiler_DEBUG = true

dofile("CurseProfiler.lua")

assert_equal(CurseProfiler.base64encode(""), "")
assert_equal(CurseProfiler.base64encode("Hey"), "SGV5")
assert_equal(CurseProfiler.base64encode("Hello, how are you doing today?"), "SGVsbG8sIGhvdyBhcmUgeW91IGRvaW5nIHRvZGF5Pw==")
local s = ''
for i = 0, 255 do
	s = s .. string.char(i)
end
assert_equal(CurseProfiler.base64encode(s), "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==")
s = '!' .. s
assert_equal(CurseProfiler.base64encode(s), "IQABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyMnKy8zNzs/Q0dLT1NXW19jZ2tvc3d7f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/v8=")
s = '!' .. s
assert_equal(CurseProfiler.base64encode(s), "ISEAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4CBgoOEhYaHiImKi4yNjo+QkZKTlJWWl5iZmpucnZ6foKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u7y9vr/AwcLDxMXGx8jJysvMzc7P0NHS09TV1tfY2drb3N3e3+Dh4uPk5ebn6Onq6+zt7u/w8fLz9PX29/j5+vv8/f7/")
s = '!' .. s
assert_equal(CurseProfiler.base64encode(s), "ISEhAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==")

for i = 0, 255 do
	assert_equal(CurseProfiler.base64decode(CurseProfiler.base64encode(string.char(i))), string.char(i))
end

for i = 1, 500 do
	local s = ''
	for j = 1, i do
		s = s .. string.char(math.random(0, 255))
	end
	assert_equal(CurseProfiler.base64decode(CurseProfiler.base64encode(s)), s)
end

assert_equal(sorted({ "charlie", "alpha", "bravo" }, CurseProfiler.set_sort), { "alpha", "bravo", "charlie" })
assert_equal(sorted({ 124, 754, -533 }, CurseProfiler.set_sort), { -533, 124, 754 })
assert_equal(sorted({ false, true, false, true }, CurseProfiler.set_sort), { false, false, true, true })
assert_equal(sorted({ "alpha", 1234, true, { 'alpha' }, { alpha = true }, { alpha = 'bravo' } }, CurseProfiler.set_sort), { true, { alpha = "bravo" }, { "alpha" }, 1234, { alpha = true }, "alpha" })

assert_equal(sorted({ { "charlie" }, { "alpha" }, { "bravo" } }, CurseProfiler.set_sort), {{ "alpha" }, { "bravo" }, { "charlie" }})

assert_equal(sorted({ { charlie = true }, { alpha = true }, { bravo = true } }, CurseProfiler.set_sort), { { alpha = true }, { bravo = true }, { charlie = true } })

assert_equal(sorted({ { charlie = true }, { alpha = true }, { alpha = true, bravo = true, charlie = true}, { bravo = true } }, CurseProfiler.set_sort), { { alpha = true }, { bravo = true }, { charlie = true }, { alpha = true, bravo = true, charlie = true } })

local INT = CurseProfiler.makeSerializedInteger
local ITEM = CurseProfiler.makeSerializedItem
local TUPLE = CurseProfiler.makeSerializedTuple

assert_equal(CurseProfiler.serialize("Hello"), "\005\000Hello")
assert_equal(CurseProfiler.serialize(TUPLE {"Hello"}), "\005\000Hello")
assert_equal(CurseProfiler.serialize("Überfantastisch"), "\016\000Überfantastisch")
assert_equal(CurseProfiler.serialize("\000\000\000"), "\003\000\000\000\000")
assert_equal(CurseProfiler.serialize(true), "\001")
assert_equal(CurseProfiler.serialize(false), "\000")
assert_equal(CurseProfiler.serialize(INT("Byte", 0)), "\000")
assert_equal(CurseProfiler.serialize(INT("Byte", 150)), "\150")
assert_equal(CurseProfiler.serialize(INT("UInt16", 1000)), "\232\003")
assert_equal(CurseProfiler.serialize(INT("UInt16", 30000)), "\048\117")
assert_equal(CurseProfiler.serialize(INT("Int16", -1000)), "\024\252")
assert_equal(CurseProfiler.serialize(INT("Int32", -2000000000)), "\000\108\202\136")
assert_equal(CurseProfiler.serialize(INT("Int64", -5000000000000000000)), "\000\000\012\187\125\110\156\186")
assert_equal(CurseProfiler.serialize({ "Hello", "Friend" }), "\002\000\005\000Hello\006\000Friend")
assert_equal(CurseProfiler.serialize({ Hello = true, Friend = true }), "\002\000\006\000Friend\005\000Hello")
assert_equal(CurseProfiler.serialize({ Charlie = "Delta", Alpha = "Bravo" }), "\002\000\005\000Alpha\005\000Bravo\007\000Charlie\005\000Delta")
assert_equal(CurseProfiler.serialize(ITEM("item:28484:1503:0:2946:2945:0:0:210677200")), "\068\111\223\005\000\000\130\011\129\011\000\000\000\000\208\173\142\012")

assert_equal({CurseProfiler.deserialize("string", "\005\000Hello\006\000Friend", 1)}, { 8, "Hello"})
assert_equal({CurseProfiler.deserialize("Byte", "\005\000Hello\006\000Friend", 1)}, { 2, 5 })
assert_equal({CurseProfiler.deserialize("Byte", "\005\185Hello\006\000Friend", 2)}, { 3, 185 })
assert_equal({CurseProfiler.deserialize("SByte", "\005\185Hello\006\000Friend", 2)}, { 3, -71 })
assert_equal({CurseProfiler.deserialize("UInt16", "\005\001Hello\006\000Friend", 1)}, { 3, 261 })
assert_equal({CurseProfiler.deserialize("UInt64", "\000\000\232\137\004\035\199\138", 1)}, { 9, 10000000000000000000 })
assert_equal({CurseProfiler.deserialize("Int64", "\000\000\012\187\125\110\156\186", 1)}, { 9, -5000000000000000000 })
assert_equal({CurseProfiler.deserialize("boolean", "\000\000\012\187\125\110\156\186", 1)}, { 2, false })
assert_equal({CurseProfiler.deserialize("boolean", "\001\000\012\187\125\110\156\186", 1)}, { 2, true })
assert_equal({CurseProfiler.deserialize("list", "\001\000\012\187\125\110\156\186", 1)}, { 3, 1 })
