Quantcast
--[[-------------------------------------------------------------------------
  Copyright (c) 2006-2007, Dongle Development Team
  All rights reserved.

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are
  met:

      * Redistributions of source code must retain the above copyright
        notice, this list of conditions and the following disclaimer.
      * Redistributions in binary form must reproduce the above
        copyright notice, this list of conditions and the following
        disclaimer in the documentation and/or other materials provided
        with the distribution.
      * Neither the name of the Dongle Development Team nor the names of
        its contributors may be used to endorse or promote products derived
        from this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---------------------------------------------------------------------------]]
local major = "DongleStub"
local minor = tonumber(string.match("$Revision: 313 $", "(%d+)") or 1)

local g = getfenv(0)

if not g.DongleStub or g.DongleStub:IsNewerVersion(major, minor) then
	local lib = setmetatable({}, {
		__call = function(t,k)
			if type(t.versions) == "table" and t.versions[k] then
				return t.versions[k].instance
			else
				error("Cannot find a library with name '"..tostring(k).."'", 2)
			end
		end
	})

	function lib:IsNewerVersion(major, minor)
		local versionData = self.versions and self.versions[major]

		-- If DongleStub versions have differing major version names
		-- such as DongleStub-Beta0 and DongleStub-1.0-RC2 then a second
		-- instance will be loaded, with older logic.  This code attempts
		-- to compensate for that by matching the major version against
		-- "^DongleStub", and handling the version check correctly.

		if major:match("^DongleStub") then
			local oldmajor,oldminor = self:GetVersion()
			if self.versions and self.versions[oldmajor] then
				return minor > oldminor
			else
				return true
			end
		end

		if not versionData then return true end
		local oldmajor,oldminor = versionData.instance:GetVersion()
		return minor > oldminor
	end

	local function NilCopyTable(src, dest)
		for k,v in pairs(dest) do dest[k] = nil end
		for k,v in pairs(src) do dest[k] = v end
	end

	function lib:Register(newInstance, activate, deactivate)
		assert(type(newInstance.GetVersion) == "function",
			"Attempt to register a library with DongleStub that does not have a 'GetVersion' method.")

		local major,minor = newInstance:GetVersion()
		assert(type(major) == "string",
			"Attempt to register a library with DongleStub that does not have a proper major version.")
		assert(type(minor) == "number",
			"Attempt to register a library with DongleStub that does not have a proper minor version.")

		-- Generate a log of all library registrations
		if not self.log then self.log = {} end
		table.insert(self.log, string.format("Register: %s, %s", major, minor))

		if not self:IsNewerVersion(major, minor) then return false end
		if not self.versions then self.versions = {} end

		local versionData = self.versions[major]
		if not versionData then
			-- New major version
			versionData = {
				["instance"] = newInstance,
				["deactivate"] = deactivate,
			}

			self.versions[major] = versionData
			if type(activate) == "function" then
				table.insert(self.log, string.format("Activate: %s, %s", major, minor))
				activate(newInstance)
			end
			return newInstance
		end

		local oldDeactivate = versionData.deactivate
		local oldInstance = versionData.instance

		versionData.deactivate = deactivate

		local skipCopy
		if type(activate) == "function" then
			table.insert(self.log, string.format("Activate: %s, %s", major, minor))
			skipCopy = activate(newInstance, oldInstance)
		end

		-- Deactivate the old libary if necessary
		if type(oldDeactivate) == "function" then
			local major, minor = oldInstance:GetVersion()
			table.insert(self.log, string.format("Deactivate: %s, %s", major, minor))
			oldDeactivate(oldInstance, newInstance)
		end

		-- Re-use the old table, and discard the new one
		if not skipCopy then
			NilCopyTable(newInstance, oldInstance)
		end
		return oldInstance
	end

	function lib:GetVersion() return major,minor end

	local function Activate(new, old)
		-- This code ensures that we'll move the versions table even
		-- if the major version names are different, in the case of
		-- DongleStub
		if not old then old = g.DongleStub end

		if old then
			new.versions = old.versions
			new.log = old.log
		end
		g.DongleStub = new
	end

	-- Actually trigger libary activation here
	local stub = g.DongleStub or lib
	lib = stub:Register(lib, Activate)
end

--[[-------------------------------------------------------------------------
  Begin Library Implementation
---------------------------------------------------------------------------]]

local major = "Dongle-1.1"
local minor = tonumber(string.match("$Revision: 647 $", "(%d+)") or 1)

assert(DongleStub, string.format("%s requires DongleStub.", major))

if not DongleStub:IsNewerVersion(major, minor) then return end

local Dongle = {}
local methods = {
	"RegisterEvent", "UnregisterEvent", "UnregisterAllEvents", "IsEventRegistered",
	"RegisterMessage", "UnregisterMessage", "UnregisterAllMessages", "TriggerMessage", "IsMessageRegistered",
	"ScheduleTimer", "ScheduleRepeatingTimer", "CancelTimer", "IsTimerScheduled",
	"EnableDebug", "IsDebugEnabled", "Print", "PrintF", "Debug", "DebugF", "Echo", "EchoF",
	"InitializeDB",
	"InitializeSlashCommand",
	"NewModule", "HasModule", "IterateModules",
}

local registry = {}
local lookup = {}
local loadqueue = {}
local loadorder = {}
local events = {}
local databases = {}
local commands = {}
local messages = {}
local timers = {}
local heap = {}

local frame

--[[-------------------------------------------------------------------------
	Message Localization
---------------------------------------------------------------------------]]

local L = {
	["ADDMESSAGE_REQUIRED"] = "The frame you specify must have an 'AddMessage' method.",
	["ALREADY_REGISTERED"] = "A Dongle with the name '%s' is already registered.",
	["BAD_ARGUMENT"] = "bad argument #%d to '%s' (%s expected, got %s)",
	["BAD_ARGUMENT_DB"] = "bad argument #%d to '%s' (DongleDB expected)",
	["CANNOT_DELETE_ACTIVE_PROFILE"] = "You cannot delete your active profile. Change profiles, then attempt to delete.",
	["DELETE_NONEXISTANT_PROFILE"] = "You cannot delete a non-existant profile.",
	["MUST_CALLFROM_DBOBJECT"] = "You must call '%s' from a Dongle database object.",
	["MUST_CALLFROM_REGISTERED"] = "You must call '%s' from a registered Dongle.",
	["MUST_CALLFROM_SLASH"] = "You must call '%s' from a Dongle slash command object.",
	["PROFILE_DOES_NOT_EXIST"] = "Profile '%s' doesn't exist.",
	["REPLACE_DEFAULTS"] = "You are attempting to register defaults with a database that already contains defaults.",
	["SAME_SOURCE_DEST"] = "Source/Destination profile cannot be the same profile.",
	["EVENT_REGISTER_SPECIAL"] = "You cannot register for the '%s' event. Use the '%s' method instead.",
	["Unknown"] = "Unknown",
	["INJECTDB_USAGE"] = "Usage: DongleCmd:InjectDBCommands(db, ['copy', 'delete', 'list', 'reset', 'set'])",
	["DBSLASH_PROFILE_COPY_DESC"] = "profile copy <name> - Copies profile <name> into your current profile.",
	["DBSLASH_PROFILE_COPY_PATTERN"] = "^profile copy (.+)$",
	["DBSLASH_PROFILE_DELETE_DESC"] = "profile delete <name> - Deletes the profile <name>.",
	["DBSLASH_PROFILE_DELETE_PATTERN"] = "^profile delete (.+)$",
	["DBSLASH_PROFILE_LIST_DESC"] = "profile list - Lists all valid profiles.",
	["DBSLASH_PROFILE_LIST_PATTERN"] = "^profile list$",
	["DBSLASH_PROFILE_RESET_DESC"] = "profile reset - Resets the current profile.",
	["DBSLASH_PROFILE_RESET_PATTERN"] = "^profile reset$",
	["DBSLASH_PROFILE_SET_DESC"] = "profile set <name> - Sets the current profile to <name>.",
	["DBSLASH_PROFILE_SET_PATTERN"] = "^profile set (.+)$",
	["DBSLASH_PROFILE_LIST_OUT"] = "Profile List:",
}

--[[-------------------------------------------------------------------------
	Utility functions for Dongle use
---------------------------------------------------------------------------]]

local function assert(level,condition,message)
	if not condition then
		error(message,level)
	end
end

local function argcheck(value, num, ...)
	if type(num) ~= "number" then
		error(L["BAD_ARGUMENT"]:format(2, "argcheck", "number", type(num)), 1)
	end

	for i=1,select("#", ...) do
		if type(value) == select(i, ...) then return end
	end

	local types = strjoin(", ", ...)
	local name = string.match(debugstack(2,2,0), ": in function [`<](.-)['>]")
	error(L["BAD_ARGUMENT"]:format(num, name, types, type(value)), 3)
end

local function safecall(func,...)
	local success,err = pcall(func,...)
	if not success then
		geterrorhandler()(err)
	end
end

--[[-------------------------------------------------------------------------
	Dongle constructor, and DongleModule system
---------------------------------------------------------------------------]]

function Dongle:New(name, obj)
	argcheck(name, 2, "string")
	argcheck(obj, 3, "table", "nil")

	if not obj then
		obj = {}
	end

	if registry[name] then
		error(string.format(L["ALREADY_REGISTERED"], name))
	end

	local reg = {["obj"] = obj, ["name"] = name}

	registry[name] = reg
	lookup[obj] = reg
	lookup[name] = reg

	for k,v in pairs(methods) do
		obj[v] = self[v]
	end

	-- Add this Dongle to the end of the queue
	table.insert(loadqueue, obj)
	return obj,name
end

function Dongle:NewModule(name, obj)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "NewModule"))
	argcheck(name, 2, "string")
	argcheck(obj, 3, "table", "nil")

	obj,name = Dongle:New(name, obj)

	if not reg.modules then reg.modules = {} end
	reg.modules[obj] = obj
	reg.modules[name] = obj

	return obj,name
end

function Dongle:HasModule(module)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "HasModule"))
	argcheck(module, 2, "string", "table")

	return reg.modules and reg.modules[module]
end

local function ModuleIterator(t, name)
	if not t then return end
	local obj
	repeat
		name,obj = next(t, name)
	until type(name) == "string" or not name

	return name,obj
end

function Dongle:IterateModules()
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "IterateModules"))

	return ModuleIterator, reg.modules
end

--[[-------------------------------------------------------------------------
	Event registration system
---------------------------------------------------------------------------]]

local function OnEvent(frame, event, ...)
	local eventTbl = events[event]
	if eventTbl then
		for obj,func in pairs(eventTbl) do
			if type(func) == "string" then
				if type(obj[func]) == "function" then
					safecall(obj[func], obj, event, ...)
				end
			else
				safecall(func, event, ...)
			end
		end
	end
end

local specialEvents = {
	["PLAYER_LOGIN"] = "Enable",
	["PLAYER_LOGOUT"] = "Disable",
}

function Dongle:RegisterEvent(event, func)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "RegisterEvent"))
	argcheck(event, 2, "string")
	argcheck(func, 3, "string", "function", "nil")

	local special = (self ~= Dongle) and specialEvents[event]
	if special then
		error(string.format(L["EVENT_REGISTER_SPECIAL"], event, special), 3)
	end

	-- Name the method the same as the event if necessary
	if not func then func = event end

	if not events[event] then
		events[event] = {}
		frame:RegisterEvent(event)
	end
	events[event][self] = func
end

function Dongle:UnregisterEvent(event)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "UnregisterEvent"))
	argcheck(event, 2, "string")

	local tbl = events[event]
	if tbl then
		tbl[self] = nil
		if not next(tbl) then
			events[event] = nil
			frame:UnregisterEvent(event)
		end
	end
end

function Dongle:UnregisterAllEvents()
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "UnregisterAllEvents"))

	for event,tbl in pairs(events) do
		tbl[self] = nil
		if not next(tbl) then
			events[event] = nil
			frame:UnregisterEvent(event)
		end
	end
end

function Dongle:IsEventRegistered(event)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "IsEventRegistered"))
	argcheck(event, 2, "string")

	local tbl = events[event]
	return tbl
end

--[[-------------------------------------------------------------------------
	Inter-Addon Messaging System
---------------------------------------------------------------------------]]

function Dongle:RegisterMessage(msg, func)
	argcheck(self, 1, "table")
	argcheck(msg, 2, "string")
	argcheck(func, 3, "string", "function", "nil")

	-- Name the method the same as the message if necessary
	if not func then func = msg end

	if not messages[msg] then
		messages[msg] = {}
	end
	messages[msg][self] = func
end

function Dongle:UnregisterMessage(msg)
	argcheck(self, 1, "table")
	argcheck(msg, 2, "string")

	local tbl = messages[msg]
	if tbl then
		tbl[self] = nil
		if not next(tbl) then
			messages[msg] = nil
		end
	end
end

function Dongle:UnregisterAllMessages()
	argcheck(self, 1, "table")

	for msg,tbl in pairs(messages) do
		tbl[self] = nil
		if not next(tbl) then
			messages[msg] = nil
		end
	end
end

function Dongle:TriggerMessage(msg, ...)
	argcheck(self, 1, "table")
	argcheck(msg, 2, "string")
	local msgTbl = messages[msg]
	if not msgTbl then return end

	for obj,func in pairs(msgTbl) do
		if type(func) == "string" then
			if type(obj[func]) == "function" then
				safecall(obj[func], obj, msg, ...)
			end
		else
			safecall(func, msg, ...)
		end
	end
end

function Dongle:IsMessageRegistered(msg)
	argcheck(self, 1, "table")
	argcheck(msg, 2, "string")

	local tbl = messages[msg]
	return tbl[self]
end

--[[-------------------------------------------------------------------------
	Timer System
---------------------------------------------------------------------------]]

local function HeapSwap(i1, i2)
	heap[i1], heap[i2] = heap[i2], heap[i1]
end

local function HeapBubbleUp(index)
	while index > 1 do
		local parentIndex = math.floor(index / 2)
		if heap[index].timeToFire < heap[parentIndex].timeToFire then
			HeapSwap(index, parentIndex)
			index = parentIndex
		else
			break
		end
	end
end

local function HeapBubbleDown(index)
	while 2 * index <= heap.lastIndex do
		local leftIndex = 2 * index
		local rightIndex = leftIndex + 1
		local current = heap[index]
		local leftChild = heap[leftIndex]
		local rightChild = heap[rightIndex]

		if not rightChild then
			if leftChild.timeToFire < current.timeToFire then
				HeapSwap(index, leftIndex)
				index = leftIndex
			else
				break
			end
		else
			if leftChild.timeToFire < current.timeToFire or
			   rightChild.timeToFire < current.timeToFire then
				if leftChild.timeToFire < rightChild.timeToFire then
					HeapSwap(index, leftIndex)
					index = leftIndex
				else
					HeapSwap(index, rightIndex)
					index = rightIndex
				end
			else
				break
			end
		end
	end
end

local function OnUpdate(frame, elapsed)
	local schedule = heap[1]
	while schedule and schedule.timeToFire < GetTime() do
		if schedule.cancelled then
			HeapSwap(1, heap.lastIndex)
			heap[heap.lastIndex] = nil
			heap.lastIndex = heap.lastIndex - 1
			HeapBubbleDown(1)
		else
			if schedule.args then
				safecall(schedule.func, schedule.name, unpack(schedule.args))
			else
				safecall(schedule.func, schedule.name)
			end

			if schedule.repeating then
				schedule.timeToFire = schedule.timeToFire + schedule.repeating
				HeapBubbleDown(1)
			else
				HeapSwap(1, heap.lastIndex)
				heap[heap.lastIndex] = nil
				heap.lastIndex = heap.lastIndex - 1
				HeapBubbleDown(1)
				timers[schedule.name] = nil
			end
		end
		schedule = heap[1]
	end
	if not schedule then frame:Hide() end
end

function Dongle:ScheduleTimer(name, func, delay, ...)
	argcheck(self, 1, "table")
	argcheck(name, 2, "string")
	argcheck(func, 3, "function")
	argcheck(delay, 4, "number")

	if Dongle:IsTimerScheduled(name) then
		Dongle:CancelTimer(name)
	end

	local schedule = {}
	timers[name] = schedule
	schedule.timeToFire = GetTime() + delay
	schedule.func = func
	schedule.name = name
	if select('#', ...) ~= 0 then
		schedule.args = { ... }
	end

	if heap.lastIndex then
		heap.lastIndex = heap.lastIndex + 1
	else
		heap.lastIndex = 1
	end
	heap[heap.lastIndex] = schedule
	HeapBubbleUp(heap.lastIndex)
	if not frame:IsShown() then
		frame:Show()
	end
end

function Dongle:ScheduleRepeatingTimer(name, func, delay, ...)
	Dongle:ScheduleTimer(name, func, delay, ...)
	timers[name].repeating = delay
end

function Dongle:IsTimerScheduled(name)
	argcheck(self, 1, "table")
	argcheck(name, 2, "string")
	local schedule = timers[name]
	if schedule then
		return true, schedule.timeToFire - GetTime()
	else
		return false
	end
end

function Dongle:CancelTimer(name)
	argcheck(self, 1, "table")
	argcheck(name, 2, "string")
	local schedule = timers[name]
	if not schedule then return end
	schedule.cancelled = true
	timers[name] = nil
end

--[[-------------------------------------------------------------------------
	Debug and Print utility functions
---------------------------------------------------------------------------]]

function Dongle:EnableDebug(level, frame)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EnableDebug"))
	argcheck(level, 2, "number", "nil")
	argcheck(frame, 3, "table", "nil")

	assert(3, type(frame) == "nil" or type(frame.AddMessage) == "function", L["ADDMESSAGE_REQUIRED"])
	reg.debugFrame = frame or ChatFrame1
	reg.debugLevel = level
end

function Dongle:IsDebugEnabled()
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EnableDebug"))

	return reg.debugLevel, reg.debugFrame
end

local function argsToStrings(a1, ...)
	if select("#", ...) > 0 then
		return tostring(a1), argsToStrings(...)
	else
		return tostring(a1)
	end
end

local function printHelp(obj, method, header, frame, msg, ...)
	local reg = lookup[obj]
	assert(4, reg, string.format(L["MUST_CALLFROM_REGISTERED"], method))

	local name = reg.name

	if header then
		msg = "|cFF33FF99"..name.."|r: "..tostring(msg)
	end

	if select("#", ...) > 0 then
		msg = string.join(", ", msg, argsToStrings(...))
	end

	frame:AddMessage(msg)
end

local function printFHelp(obj, method, header, frame, msg, ...)
	local reg = lookup[obj]
	assert(4, reg, string.format(L["MUST_CALLFROM_REGISTERED"], method))

	local name = reg.name
	local success,txt

	if header then
		msg = "|cFF33FF99%s|r: " .. msg
		success,txt = pcall(string.format, msg, name, ...)
	else
		success,txt = pcall(string.format, msg, ...)
	end

	if success then
		frame:AddMessage(txt)
	else
		error(string.gsub(txt, "'%?'", string.format("'%s'", method)), 3)
	end
end

function Dongle:Print(msg, ...)
	local reg = lookup[self]
	assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Print"))
	argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata")
	return printHelp(self, "Print", true, DEFAULT_CHAT_FRAME, msg, ...)
end

function Dongle:PrintF(msg, ...)
	local reg = lookup[self]
	assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "PrintF"))
	argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata")
	return printFHelp(self, "PrintF", true, DEFAULT_CHAT_FRAME, msg, ...)
end

function Dongle:Echo(msg, ...)
	local reg = lookup[self]
	assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Echo"))
	argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata")
	return printHelp(self, "Echo", false, DEFAULT_CHAT_FRAME, msg, ...)
end

function Dongle:EchoF(msg, ...)
	local reg = lookup[self]
	assert(1, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "EchoF"))
	argcheck(msg, 2, "number", "string", "boolean", "table", "function", "thread", "userdata")
	return printFHelp(self, "EchoF", false, DEFAULT_CHAT_FRAME, msg, ...)
end

function Dongle:Debug(level, ...)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "Debug"))
	argcheck(level, 2, "number")

	if reg.debugLevel and level <= reg.debugLevel then
		printHelp(self, "Debug", true, reg.debugFrame, ...)
	end
end

function Dongle:DebugF(level, ...)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "DebugF"))
	argcheck(level, 2, "number")

	if reg.debugLevel and level <= reg.debugLevel then
		printFHelp(self, "DebugF", true, reg.debugFrame, ...)
	end
end

--[[-------------------------------------------------------------------------
	Database System
---------------------------------------------------------------------------]]

local dbMethods = {
	"RegisterDefaults", "SetProfile", "GetProfiles", "DeleteProfile", "CopyProfile",
	"GetCurrentProfile", "ResetProfile", "ResetDB",
	"RegisterNamespace",
}

local function copyTable(src)
	local dest = {}
	for k,v in pairs(src) do
		if type(k) == "table" then
			k = copyTable(k)
		end
		if type(v) == "table" then
			v = copyTable(v)
		end
		dest[k] = v
	end
	return dest
end

local function copyDefaults(dest, src, force)
	for k,v in pairs(src) do
		if k == "*" then
			if type(v) == "table" then
				-- Values are tables, need some magic here
				local mt = {
					__cache = {},
					__index = function(t,k)
						local mt = getmetatable(dest)
						local cache = rawget(mt, "__cache")
						local tbl = rawget(cache, k)
						if not tbl then
							local parent = t
							local parentkey = k
							tbl = copyTable(v)
							rawset(cache, k, tbl)
							local mt = getmetatable(tbl)
							if not mt then
								mt = {}
								setmetatable(tbl, mt)
							end
							local newindex = function(t,k,v)
								rawset(parent, parentkey, t)
								rawset(t, k, v)
							end
							rawset(mt, "__newindex", newindex)
						end
						return tbl
					end,
				}
				setmetatable(dest, mt)
				-- Now need to set the metatable on any child tables
				for dkey,dval in pairs(dest) do
					copyDefaults(dval, v)
				end
			else
				-- Values are not tables, so this is just a simple return
				local mt = {__index = function() return v end}
				setmetatable(dest, mt)
			end
		elseif type(v) == "table" then
			if not dest[k] then dest[k] = {} end
			copyDefaults(dest[k], v, force)
		else
			if (dest[k] == nil) or force then
				dest[k] = v
			end
		end
	end
end

local function removeDefaults(db, defaults)
	if not db then return end
	for k,v in pairs(defaults) do
		if k == "*" and type(v) == "table" then
			-- check for any defaults that have been changed
			local mt = getmetatable(db)
			local cache = rawget(mt, "__cache")

			for cacheKey,cacheValue in pairs(cache) do
				removeDefaults(cacheValue, v)
				if next(cacheValue) ~= nil then
					-- Something's changed
					rawset(db, cacheKey, cacheValue)
				end
			end
			-- Now loop through all the actual k,v pairs and remove
			for key,value in pairs(db) do
				removeDefaults(value, v)
			end
		elseif type(v) == "table" and db[k] then
			removeDefaults(db[k], v)
			if not next(db[k]) then
				db[k] = nil
			end
		else
			if db[k] == defaults[k] then
				db[k] = nil
			end
		end
	end
end

local function initSection(db, section, svstore, key, defaults)
	local sv = rawget(db, "sv")

	local tableCreated
	if not sv[svstore] then sv[svstore] = {} end
	if not sv[svstore][key] then
		sv[svstore][key] = {}
		tableCreated = true
	end

	local tbl = sv[svstore][key]

	if defaults then
		copyDefaults(tbl, defaults)
	end
	rawset(db, section, tbl)

	return tableCreated, tbl
end

local dbmt = {
	__index = function(t, section)
		local keys = rawget(t, "keys")
		local key = keys[section]
		if key then
			local defaultTbl = rawget(t, "defaults")
			local defaults = defaultTbl and defaultTbl[section]

			if section == "profile" then
				local new = initSection(t, section, "profiles", key, defaults)
				if new then
					Dongle:TriggerMessage("DONGLE_PROFILE_CREATED", t, rawget(t, "parent"), rawget(t, "sv_name"), key)
				end
			elseif section == "profiles" then
				local sv = rawget(t, "sv")
				if not sv.profiles then sv.profiles = {} end
				rawset(t, "profiles", sv.profiles)
			elseif section == "global" then
				local sv = rawget(t, "sv")
				if not sv.global then sv.global = {} end
				if defaults then
					copyDefaults(sv.global, defaults)
				end
				rawset(t, section, sv.global)
			else
				initSection(t, section, section, key, defaults)
			end
		end

		return rawget(t, section)
	end
}

local function initdb(parent, name, defaults, defaultProfile, olddb)
	-- This allows us to use an arbitrary table as base instead of saved variable name
	local sv
	if type(name) == "string" then
		sv = getglobal(name)
		if not sv then
			sv = {}
			setglobal(name, sv)
		end
	elseif type(name) == "table" then
		sv = name
	end

	-- Generate the database keys for each section
	local char = string.format("%s - %s", UnitName("player"), GetRealmName())
	local realm = GetRealmName()
	local class = select(2, UnitClass("player"))
	local race = select(2, UnitRace("player"))
	local faction = UnitFactionGroup("player")
	local factionrealm = string.format("%s - %s", faction, realm)

	-- Make a container for profile keys
	if not sv.profileKeys then sv.profileKeys = {} end

	-- Try to get the profile selected from the char db
	local profileKey = sv.profileKeys[char] or defaultProfile or char
	sv.profileKeys[char] = profileKey

	local keyTbl= {
		["char"] = char,
		["realm"] = realm,
		["class"] = class,
		["race"] = race,
		["faction"] = faction,
		["factionrealm"] = factionrealm,
		["global"] = true,
		["profile"] = profileKey,
		["profiles"] = true, -- Don't create until we need
	}

	-- If we've been passed an old database, clear it out
	if olddb then
		for k,v in pairs(olddb) do olddb[k] = nil end
	end

	-- Give this database the metatable so it initializes dynamically
	local db = setmetatable(olddb or {}, dbmt)

	-- Copy methods locally
	for idx,method in pairs(dbMethods) do
		db[method] = Dongle[method]
	end

	-- Set some properties in the object we're returning
	db.profiles = sv.profiles
	db.keys = keyTbl
	db.sv = sv
	db.sv_name = name
	db.defaults = defaults
	db.parent = parent

	databases[db] = true

	return db
end

function Dongle:InitializeDB(name, defaults, defaultProfile)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "InitializeDB"))
	argcheck(name, 2, "string", "table")
	argcheck(defaults, 3, "table", "nil")
	argcheck(defaultProfile, 4, "string", "nil")

	return initdb(self, name, defaults, defaultProfile)
end

-- This function operates on a Dongle DB object
function Dongle.RegisterDefaults(db, defaults)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "RegisterDefaults"))
	assert(3, db.defaults == nil, L["REPLACE_DEFAUTS"])
	argcheck(defaults, 2, "table")

	for section,key in pairs(db.keys) do
		if defaults[section] and rawget(db, section) then
			copyDefaults(db[section], defaults[section])
		end
	end

	db.defaults = defaults
end

function Dongle:ClearDBDefaults()
	for db in pairs(databases) do
		local defaults = db.defaults
		local sv = db.sv

		if db and defaults then
			for section,key in pairs(db.keys) do
				if defaults[section] and rawget(db, section) then
					removeDefaults(db[section], defaults[section])
				end
			end

			for section,key in pairs(db.keys) do
				local tbl = rawget(db, section)
				if tbl and not next(tbl) then
					if sv[section] then
						if type(key) == "string" then
							sv[section][key] = nil
						else
							sv[section] = nil
						end
					end
				end
			end
		end
	end
end

function Dongle.SetProfile(db, name)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "SetProfile"))
	argcheck(name, 2, "string")

	local old = db.profile
	local defaults = db.defaults and db.defaults.profile

	if defaults then
		-- Remove the defaults from the old profile
		removeDefaults(old, defaults)
	end

	db.profile = nil
	db.keys["profile"] = name
	db.sv.profileKeys[db.keys.char] = name

	Dongle:TriggerMessage("DONGLE_PROFILE_CHANGED", db, db.parent, db.sv_name, db.keys.profile)
end

function Dongle.GetProfiles(db, tbl)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "GetProfiles"))
	argcheck(t, 2, "table", "nil")

	-- Clear the container table
	if tbl then
		for k,v in pairs(tbl) do tbl[k] = nil end
	else
		tbl = {}
	end

	local i = 0
	for profileKey in pairs(db.profiles) do
		i = i + 1
		tbl[i] = profileKey
	end

	-- Add the current profile, if it hasn't been created yet
	if rawget(db, "profile") == nil then
		i = i + 1
		tbl[i] = db.keys.profile
	end

	return tbl, i
end

function Dongle.GetCurrentProfile(db)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "GetCurrentProfile"))
	return db.keys.profile
end

function Dongle.DeleteProfile(db, name)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "DeleteProfile"))
	argcheck(name, 2, "string")

	if db.keys.profile == name then
		error(L["CANNOT_DELETE_ACTIVE_PROFILE"], 2)
	end

	assert(type(db.sv.profiles[name]) == "table", L["DELETE_NONEXISTANT_PROFILE"])

	db.sv.profiles[name] = nil
	Dongle:TriggerMessage("DONGLE_PROFILE_DELETED", db, db.parent, db.sv_name, name)
end

function Dongle.CopyProfile(db, name)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "CopyProfile"))
	argcheck(name, 2, "string")

	assert(3, db.keys.profile ~= name, L["SAME_SOURCE_DEST"])
	assert(3, type(db.sv.profiles[name]) == "table", string.format(L["PROFILE_DOES_NOT_EXIST"], name))

	local profile = db.profile
	local source = db.sv.profiles[name]

	copyDefaults(profile, source, true)
	Dongle:TriggerMessage("DONGLE_PROFILE_COPIED", db, db.parent, db.sv_name, name, db.keys.profile)
end

function Dongle.ResetProfile(db)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "ResetProfile"))

	local profile = db.profile

	for k,v in pairs(profile) do
		profile[k] = nil
	end

	local defaults = db.defaults and db.defaults.profile
	if defaults then
		copyDefaults(profile, defaults)
	end
	Dongle:TriggerMessage("DONGLE_PROFILE_RESET", db, db.parent, db.sv_name, db.keys.profile)
end


function Dongle.ResetDB(db, defaultProfile)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "ResetDB"))
		argcheck(defaultProfile, 2, "nil", "string")

	local sv = db.sv
	for k,v in pairs(sv) do
		sv[k] = nil
	end

	local parent = db.parent

	initdb(parent, db.sv_name, db.defaults, defaultProfile, db)
	Dongle:TriggerMessage("DONGLE_DATABASE_RESET", db, parent, db.sv_name, db.keys.profile)
	Dongle:TriggerMessage("DONGLE_PROFILE_CHANGED", db, db.parent, db.sv_name, db.keys.profile)
	return db
end

function Dongle.RegisterNamespace(db, name, defaults)
	assert(3, databases[db], string.format(L["MUST_CALLFROM_DBOBJECT"], "RegisterNamespace"))
	argcheck(name, 2, "string")
	argcheck(defaults, 3, "nil", "table")

	local sv = db.sv
	if not sv.namespaces then sv.namespaces = {} end
	if not sv.namespaces[name] then
		sv.namespaces[name] = {}
	end

	local newDB = initdb(db, sv.namespaces[name], defaults, db.keys.profile)
	-- Remove the :SetProfile method from newDB
	newDB.SetProfile = nil

	if not db.children then db.children = {} end
	table.insert(db.children, newDB)
	return newDB
end

--[[-------------------------------------------------------------------------
	Slash Command System
---------------------------------------------------------------------------]]

local slashCmdMethods = {
	"InjectDBCommands",
	"RegisterSlashHandler",
	"PrintUsage",
}

local function OnSlashCommand(cmd, cmd_line)
	if cmd.patterns then
		for idx,tbl in pairs(cmd.patterns) do
			local pattern = tbl.pattern
			if string.match(cmd_line, pattern) then
				local handler = tbl.handler
				if type(tbl.handler) == "string" then
					local obj
					-- Look in the command object before we look at the parent object
					if cmd[handler] then obj = cmd end
					if cmd.parent[handler] then obj = cmd.parent end
					if obj then
						obj[handler](obj, string.match(cmd_line, pattern))
					end
				else
					handler(string.match(cmd_line, pattern))
				end
				return
			end
		end
	end
	cmd:PrintUsage()
end

function Dongle:InitializeSlashCommand(desc, name, ...)
	local reg = lookup[self]
	assert(3, reg, string.format(L["MUST_CALLFROM_REGISTERED"], "InitializeSlashCommand"))
	argcheck(desc, 2, "string")
	argcheck(name, 3, "string")
	argcheck(select(1, ...), 4, "string")
	for i = 2,select("#", ...) do
		argcheck(select(i, ...), i+2, "string")
	end

	local cmd = {}
	cmd.desc = desc
	cmd.name = name
	cmd.parent = self
	cmd.slashes = { ... }
	for idx,method in pairs(slashCmdMethods) do
		cmd[method] = Dongle[method]
	end

	local genv = getfenv(0)

	for i = 1,select("#", ...) do
		genv["SLASH_"..name..tostring(i)] = "/"..select(i, ...)
	end

	genv.SlashCmdList[name] = function(...) OnSlashCommand(cmd, ...) end

	commands[cmd] = true

	return cmd
end

function Dongle.RegisterSlashHandler(cmd, desc, pattern, handler)
	assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "RegisterSlashHandler"))

	argcheck(desc, 2, "string")
	argcheck(pattern, 3, "string")
	argcheck(handler, 4, "function", "string")

	if not cmd.patterns then
		cmd.patterns = {}
	end

	table.insert(cmd.patterns, {
		["desc"] = desc,
		["handler"] = handler,
		["pattern"] = pattern,
	})
end

function Dongle.PrintUsage(cmd)
	assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "PrintUsage"))
	local parent = cmd.parent

	parent:Echo(cmd.desc.."\n".."/"..table.concat(cmd.slashes, ", /")..":\n")
	if cmd.patterns then
		for idx,tbl in ipairs(cmd.patterns) do
			parent:Echo(" - " .. tbl.desc)
		end
	end
end

local dbcommands = {
	["copy"] = {
		L["DBSLASH_PROFILE_COPY_DESC"],
		L["DBSLASH_PROFILE_COPY_PATTERN"],
		"CopyProfile",
	},
	["delete"] = {
		L["DBSLASH_PROFILE_DELETE_DESC"],
		L["DBSLASH_PROFILE_DELETE_PATTERN"],
		"DeleteProfile",
	},
	["list"] = {
		L["DBSLASH_PROFILE_LIST_DESC"],
		L["DBSLASH_PROFILE_LIST_PATTERN"],
	},
	["reset"] = {
		L["DBSLASH_PROFILE_RESET_DESC"],
		L["DBSLASH_PROFILE_RESET_PATTERN"],
		"ResetProfile",
	},
	["set"] = {
		L["DBSLASH_PROFILE_SET_DESC"],
		L["DBSLASH_PROFILE_SET_PATTERN"],
		"SetProfile",
	},
}

function Dongle.InjectDBCommands(cmd, db, ...)
	assert(3, commands[cmd], string.format(L["MUST_CALLFROM_SLASH"], "InjectDBCommands"))
	assert(3, databases[db], string.format(L["BAD_ARGUMENT_DB"], 2, "InjectDBCommands"))
	local argc = select("#", ...)
	assert(3, argc > 0, L["INJECTDB_USAGE"])

	for i=1,argc do
		local cmdname = string.lower(select(i, ...))
		local entry = dbcommands[cmdname]
		assert(entry, L["INJECTDB_USAGE"])
		local func = entry[3]

		local handler
		if cmdname == "list" then
			handler = function(...)
				local profiles = db:GetProfiles()
				db.parent:Print(L["DBSLASH_PROFILE_LIST_OUT"] .. "\n" .. strjoin("\n", unpack(profiles)))
			end
		else
			handler = function(...) db[entry[3]](db, ...) end
		end

		cmd:RegisterSlashHandler(entry[1], entry[2], handler)
	end
end

--[[-------------------------------------------------------------------------
	Internal Message/Event Handlers
---------------------------------------------------------------------------]]

local function PLAYER_LOGOUT(event)
	Dongle:ClearDBDefaults()
	for k,v in pairs(registry) do
		local obj = v.obj
		if type(obj["Disable"]) == "function" then
			safecall(obj["Disable"], obj)
		end
	end
end

local PLAYER_LOGIN
do
	local lockPlayerLogin = false

	function PLAYER_LOGIN()
		if lockPlayerLogin then return end

		lockPlayerLogin = true

		local obj = table.remove(loadorder, 1)
		while obj do
			if type(obj.Enable) == "function" then
				safecall(obj.Enable, obj)
			end
			obj = table.remove(loadorder, 1)
		end

		lockPlayerLogin = false
	end
end

local function ADDON_LOADED(event, ...)
	local obj = table.remove(loadqueue, 1)
	while obj do
		table.insert(loadorder, obj)

		if type(obj.Initialize) == "function" then
			safecall(obj.Initialize, obj)
		end

		obj = table.remove(loadqueue, 1)
	end

	if IsLoggedIn() then
		PLAYER_LOGIN()
	end
end

local function DONGLE_PROFILE_CHANGED(msg, db, parent, sv_name, profileKey)
	local children = db.children
	if children then
		for i,namespace in ipairs(children) do
			local old = namespace.profile
			local defaults = namespace.defaults and namespace.defaults.profile

			if defaults then
				-- Remove the defaults from the old profile
				removeDefaults(old, defaults)
			end

			namespace.profile = nil
			namespace.keys["profile"] = profileKey
		end
	end
end

--[[-------------------------------------------------------------------------
	DongleStub required functions and registration
---------------------------------------------------------------------------]]

function Dongle:GetVersion() return major,minor end

local function Activate(self, old)
	if old then
		registry = old.registry or registry
		lookup = old.lookup or lookup
		loadqueue = old.loadqueue or loadqueue
		loadorder = old.loadorder or loadorder
		events = old.events or events
		databases = old.databases or databases
		commands = old.commands or commands
		messages = old.messages or messages
		frame = old.frame or CreateFrame("Frame")
		timers = old.timers or timers
		heap = old.heap or heap
	else
		frame = CreateFrame("Frame")
		local reg = {obj = self, name = "Dongle"}
		registry[major] = reg
		lookup[self] = reg
		lookup[major] = reg
	end

	self.registry = registry
	self.lookup = lookup
	self.loadqueue = loadqueue
	self.loadorder = loadorder
	self.events = events
	self.databases = databases
	self.commands = commands
	self.messages = messages
	self.frame = frame
	self.timers = timers
	self.heap = heap

	frame:SetScript("OnEvent", OnEvent)
	frame:SetScript("OnUpdate", OnUpdate)

	-- Lets ensure the lookup table has our entry
	-- This fixes an issue with upgrades
	lookup[self] = lookup[major]

	-- Register for events using Dongle itself
	self:RegisterEvent("ADDON_LOADED", ADDON_LOADED)
	self:RegisterEvent("PLAYER_LOGIN", PLAYER_LOGIN)
	self:RegisterEvent("PLAYER_LOGOUT", PLAYER_LOGOUT)
	self:RegisterMessage("DONGLE_PROFILE_CHANGED", DONGLE_PROFILE_CHANGED)

	-- Convert all the modules handles
	for name,obj in pairs(registry) do
		for k,v in ipairs(methods) do
			obj[k] = self[v]
		end
	end

	-- Convert all database methods
	for db in pairs(databases) do
		for idx,method in ipairs(dbMethods) do
			db[method] = self[method]
		end
	end

	-- Convert all slash command methods
	for cmd in pairs(commands) do
		for idx,method in ipairs(slashCmdMethods) do
			cmd[method] = self[method]
		end
	end
end

Dongle = DongleStub:Register(Dongle, Activate)