--[[
Name: AceLibrary
Revision: $Rev: 7540 $
Author(s): ckknight (ckknight@gmail.com)
           cladhaire (cladhaire@gmail.com)
Inspired By: Iriel (iriel@vigilance-committee.org)
             Tekkub (tekkub@gmail.com)
Website: http://www.wowace.com/
Documentation: http://wiki.wowace.com/index.php/AceLibrary
SVN: http://svn.wowace.com/root/trunk/Ace2/AceLibrary
Description: Versioning library to handle other library instances, upgrading,
             and proper access.
             It also provides a base for libraries to work off of, providing
             proper error tools. It is handy because all the errors occur in the
             file that called it, not in the library file itself.
Dependencies: None
]]

local ACELIBRARY_MAJOR = "AceLibrary"
local ACELIBRARY_MINOR = "$Revision: 7540 $"

-- CHANGE DEBUG TO ``false`` ON RELEASE -------------------
local DEBUG = true
-- CHANGE DEBUG TO ``false`` ON RELEASE -------------------

local _G = getfenv(0)
local previous = _G[ACELIBRARY_MAJOR]
if previous and not previous:IsNewVersion(ACELIBRARY_MAJOR, ACELIBRARY_MINOR) then return end

-- @table AceLibrary
-- @brief System to handle all versioning of libraries.
local AceLibrary = {}
local AceLibrary_mt = {}
setmetatable(AceLibrary, AceLibrary_mt)

local tmp
local function error(self, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
	if not tmp then
		tmp = {}
	else
		for k in pairs(tmp) do tmp[k] = nil end
		table.setn(tmp, 0)
	end
	
	tmp.n = 0
	table.insert(tmp, a1)
	table.insert(tmp, a2)
	table.insert(tmp, a3)
	table.insert(tmp, a4)
	table.insert(tmp, a5)
	table.insert(tmp, a6)
	table.insert(tmp, a7)
	table.insert(tmp, a8)
	table.insert(tmp, a9)
	table.insert(tmp, a10)
	table.insert(tmp, a11)
	table.insert(tmp, a12)
	table.insert(tmp, a13)
	table.insert(tmp, a14)
	table.insert(tmp, a15)
	table.insert(tmp, a16)
	table.insert(tmp, a17)
	table.insert(tmp, a18)
	table.insert(tmp, a19)
	table.insert(tmp, a20)
	
	local stack = debugstack()
	if not message then
		local _,_,second = string.find(stack, "\n(.-)\n")
		message = "error raised! " .. second
	else
		for i = 1,table.getn(tmp) do
			tmp[i] = tostring(tmp[i])
		end
		for i = 1,10 do
			table.insert(tmp, "nil")
		end
		message = string.format(message, unpack(tmp))
	end
	
	if getmetatable(self) and getmetatable(self).__tostring then
		message = string.format("%s: %s", tostring(self), message)
	elseif type(self.GetLibraryVersion) == "function" and AceLibrary:HasInstance(self:GetLibraryVersion()) then
		message = string.format("%s: %s", self:GetLibraryVersion(), message)
	elseif type(self.class) == "table" and type(self.class.GetLibraryVersion) == "function" and AceLibrary:HasInstance(self.class:GetLibraryVersion()) then
		message = string.format("%s: %s", self.class:GetLibraryVersion(), message)
	end
	
	local first = string.gsub(stack, "\n.*", "")
	local file = string.gsub(first, ".*\\(.*).lua:%d+: .*", "%1")
	file = string.gsub(file, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1")
	
	local i = 0
	for s in string.gfind(stack, "\n([^\n]*)") do
		i = i + 1
		if not string.find(s, file .. "%.lua:%d+:") then
			file = string.gsub(s, "^.*\\(.*).lua:%d+: .*", "%1")
			file = string.gsub(file, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1")
			break
		end
	end
	local j = 0
	for s in string.gfind(stack, "\n([^\n]*)") do
		j = j + 1
		if j > i and not string.find(s, file .. "%.lua:%d+:") then
			_G.error(message, j + 1)
			return
		end
	end
	_G.error(message, 2)
	return
end

local function assert(self, condition, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
	if not condition then
		if not message then
			local stack = debugstack()
			local _,_,second = string.find(stack, "\n(.-)\n")
			message = "assertion failed! " .. second
		end
		error(self, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
		return
	end
	return condition
end

local function argCheck(self, arg, num, kind, kind2, kind3, kind4, kind5)
	if type(num) ~= "number" then
		error(self, "Bad argument #3 to `argCheck' (number expected, got %s)", type(num))
	elseif type(kind) ~= "string" then
		error(self, "Bad argument #4 to `argCheck' (string expected, got %s)", type(kind))
	end
	local errored = false
	arg = type(arg)
	if arg ~= kind and arg ~= kind2 and arg ~= kind3 and arg ~= kind4 and arg ~= kind5 then
		local _,_,func = string.find(debugstack(), "`argCheck'.-([`<].-['>])")
		if not func then
			_,_,func = string.find(debugstack(), "([`<].-['>])")
		end
		if kind5 then
			error(self, "Bad argument #%s to %s (%s, %s, %s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, kind4, kind5, arg)
		elseif kind4 then
			error(self, "Bad argument #%s to %s (%s, %s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, kind4, arg)
		elseif kind3 then
			error(self, "Bad argument #%s to %s (%s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, arg)
		elseif kind2 then
			error(self, "Bad argument #%s to %s (%s or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, arg)
		else
			error(self, "Bad argument #%s to %s (%s expected, got %s)", tonumber(num) or 0/0, func, kind, arg)
		end
	end
end

local function pcall(self, func, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
	a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 = _G.pcall(func, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20)
	if not a1 then
		error(self, string.gsub(a2, ".-%.lua:%d-: ", ""))
	else
		return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20
	end
end

local recurse = {}
local function addToPositions(t, major)
	if not AceLibrary.positions[t] or AceLibrary.positions[t] == major then
		t[recurse] = true
		AceLibrary.positions[t] = major
		for k,v in pairs(t) do
			if type(v) == "table" and not v[recurse] then
				addToPositions(v, major)
			end
			if type(k) == "table" and not k[recurse] then
				addToPositions(k, major)
			end
		end
		local mt = getmetatable(t)
		if mt and not mt[recurse] then
			addToPositions(mt, major)
		end
		t[recurse] = nil
	end
end

local function svnRevisionToNumber(text)
	if type(text) == "string" then
		if string.find(text, "^%$Revision: (%d+) %$$") then
			return tonumber((string.gsub(text, "^%$Revision: (%d+) %$$", "%1")))
		elseif string.find(text, "^%$Rev: (%d+) %$$") then
			return tonumber((string.gsub(text, "^%$Rev: (%d+) %$$", "%1")))
		elseif string.find(text, "^%$LastChangedRevision: (%d+) %$$") then
			return tonumber((string.gsub(text, "^%$LastChangedRevision: (%d+) %$$", "%1")))
		end
	elseif type(text) == "number" then
		return text
	end
	return nil
end

local crawlReplace
do
	local recurse = {}
	local function func(t, to, from)
		if recurse[t] then
			return
		end
		recurse[t] = true
		local mt = getmetatable(t)
		setmetatable(t, nil)
		t[from], t[to] = nil, t[from]
		for k,v in pairs(t) do
			if v == from then
				t[k] = to
			elseif type(v) == "table" then
				if not recurse[v] then
					func(v, to, from)
				end
			end
			
			if type(k) == "table" then
				if not recurse[k] then
					func(k, to, from)
				end
			end
		end
		setmetatable(t, mt)
		if mt then
			if mt == from then
				setmetatable(t, to)
			elseif not recurse[mt] then
				func(mt, to, from)
			end
		end
	end
	function crawlReplace(t, to, from)
		func(t, to, from)
		for k in pairs(recurse) do
			recurse[k] = nil
		end
	end
end

-- @function destroyTable
-- @brief    remove all the contents of a table
-- @param t  table to destroy
local function destroyTable(t)
	setmetatable(t, nil)
	for k,v in pairs(t) do t[k] = nil end
	table.setn(t, 0)
end

local function isFrame(frame)
	return type(frame) == "table" and type(frame[0]) == "userdata" and type(frame.IsFrameType) == "function" and getmetatable(frame) and type(getmetatable(frame).__index) == "function"
end

local new, del
do
	local tables = setmetatable({}, {__mode = "k"})
	
	function new()
		local t = next(tables)
		if t then
			tables[t] = nil
			return t
		else
			return {}
		end
	end
	
	function del(t, depth)
		if depth and depth > 0 then
			for k,v in pairs(t) do
				if type(v) == "table" and not isFrame(v) then
					del(v, depth - 1)
				end
			end
		end
		destroyTable(t)
		tables[t] = true
	end
end

-- @function   copyTable
-- @brief      Create a shallow copy of a table and return it.
-- @param from The table to copy from
-- @return     A shallow copy of the table
local function copyTable(from)
	local to = new()
	for k,v in pairs(from) do to[k]=v end
	table.setn(to, table.getn(from))
	setmetatable(to, getmetatable(from))
	return to
end

-- @function         deepTransfer
-- @brief            Fully transfer all data, keeping proper previous table
--                   backreferences stable.
-- @param to         The table with which data is to be injected into
-- @param from       The table whose data will be injected into the first
-- @param saveFields If available, a shallow copy of the basic data is saved
--                   in here.
-- @param list       The account of table references
-- @param list2      The current status on which tables have been traversed.
local deepTransfer
do
	-- @function   examine
	-- @brief      Take account of all the table references to be shared
	--             between the to and from tables.
	-- @param to   The table with which data is to be injected into
	-- @param from The table whose data will be injected into the first
	-- @param list An account of the table references
	local function examine(to, from, list, major)
		list[from] = to
		for k in pairs(from) do
			if to[k] and type(from[k]) == "table" and type(to[k]) == "table" and not list[from[k]] then
				if from[k] == to[k] then
					list[from[k]] = to[k]
				elseif AceLibrary.positions[from[v]] ~= major and AceLibrary.positions[from[v]] then
					list[from[k]] = from[k]
				elseif not list[from[k]] then
					examine(to[k], from[k], list, major)
				end
			end
		end
		return list
	end
	
	function deepTransfer(to, from, saveFields, major, list, list2)
		setmetatable(to, nil)
		local createdList
		if not list then
			createdList = true
			list = new()
			list2 = new()
			examine(to, from, list, major)
		end
		list2[to] = to
		for k,v in pairs(to) do
			if type(from[k]) ~= "table" or type(v) ~= "table" or isFrame(v) then
				if saveFields then
					saveFields[k] = v
				end
				to[k] = nil
			elseif v ~= _G then
				if saveFields then
					saveFields[k] = copyTable(v)
				end
			end
		end
		for k in pairs(from) do
			if to[k] and to[k] ~= from[k] and AceLibrary.positions[to[k]] == major and from[k] ~= _G then
				if not list2[to[k]] then
					deepTransfer(to[k], from[k], nil, major, list, list2)
				end
				to[k] = list[to[k]] or list2[to[k]]
			else
				to[k] = from[k]
			end
		end
		table.setn(to, table.getn(from))
		setmetatable(to, getmetatable(from))
		local mt = getmetatable(to)
		if mt then
			if list[mt] then
				setmetatable(to, list[mt])
			elseif mt.__index and list[mt.__index] then
				mt.__index = list[mt.__index]
			end
		end
		destroyTable(from)
		if createdList then
			del(list)
			del(list2)
		end
	end
end

-- @method      IsNewVersion
-- @brief       Obtain whether the supplied version would be an upgrade to the
--              current version. This allows for bypass code in library
--              declaration.
-- @param major A string representing the major version
-- @param minor An integer or an svn revision string representing the minor version
-- @return      whether the supplied version would be newer than what is
--              currently available.
function AceLibrary:IsNewVersion(major, minor)
	argCheck(self, major, 2, "string")
	if type(minor) == "string" then
		local m = svnRevisionToNumber(minor)
		if m then
			minor = m
		else
			_G.error(string.format("Bad argument #3 to  `IsNewVersion'. Must be a number or SVN revision string. %q is not appropriate", minor), 2)
		end
	end
	argCheck(self, minor, 3, "number")
	local data = self.libs[major]
	if not data then
		return true
	end
	return data.minor < minor
end

-- @method      HasInstance
-- @brief       Returns whether an instance exists. This allows for optional support of a library.
-- @param major A string representing the major version.
-- @param minor (optional) An integer or an svn revision string representing the minor version.
-- @return      Whether an instance exists.
function AceLibrary:HasInstance(major, minor)
	argCheck(self, major, 2, "string")
	if minor then
		if type(minor) == "string" then
			local m = svnRevisionToNumber(minor)
			if m then
				minor = m
			else
				_G.error(string.format("Bad argument #3 to  `HasInstance'. Must be a number or SVN revision string. %q is not appropriate", minor), 2)
			end
		end
		argCheck(self, minor, 3, "number")
		if not self.libs[major] then
			return
		end
		return self.libs[major].minor == minor
	end
	return self.libs[major] and true
end

-- @method      GetInstance
-- @brief       Returns the library with the given major/minor version.
-- @param major A string representing the major version.
-- @param minor (optional) An integer or an svn revision string representing the minor version.
-- @return      The library with the given major/minor version.
function AceLibrary:GetInstance(major, minor)
	argCheck(self, major, 2, "string")

	local data = self.libs[major]
	if not data then
		_G.error(string.format("Cannot find a library instance of %s.", major), 2)
		return
	end
	if minor then
		if type(minor) == "string" then
			local m = svnRevisionToNumber(minor)
			if m then
				minor = m
			else
				_G.error(string.format("Bad argument #3 to  `GetInstance'. Must be a number or SVN revision string. %q is not appropriate", minor), 2)
			end
		end
		argCheck(self, minor, 2, "number")
		if data.minor ~= minor then
			_G.error(string.format("Cannot find a library instance of %s, minor version %d.", major, minor), 2)
			return
		end
	end
	return data.instance
end

-- Syntax sugar.  AceLibrary("FooBar-1.0")
AceLibrary_mt.__call = AceLibrary.GetInstance

local donothing

local AceEvent

-- @method               Register
-- @brief                Registers a new version of a given library.
-- @param newInstance    the library to register
-- @param major          the major version of the library
-- @param minor          the minor version of the library
-- @param activateFunc   (optional) A function to be called when the library is
--                       fully activated. Takes the arguments
--                       (newInstance [, oldInstance, oldDeactivateFunc]). If
--                       oldInstance is given, you should probably call
--                       oldDeactivateFunc(oldInstance).
-- @param deactivateFunc (optional) A function to be called by a newer library's
--                       activateFunc.
-- @param externalFunc   (optional) A function to be called whenever a new
--                       library is registered.
function AceLibrary:Register(newInstance, major, minor, activateFunc, deactivateFunc, externalFunc)
	argCheck(self, newInstance, 2, "table")
	argCheck(self, major, 3, "string")
	if type(minor) == "string" then
		local m = svnRevisionToNumber(minor)
		if m then
			minor = m
		else
			_G.error(string.format("Bad argument #4 to  `Register'. Must be a number or SVN revision string. %q is not appropriate", minor), 2)
		end
	end
	argCheck(self, minor, 4, "number")
	if math.floor(minor) ~= minor or minor < 0 then
		error(self, "Bad argument #4 to `Register' (integer >= 0 expected, got %s)", minor)
	end
	argCheck(self, activateFunc, 5, "function", "nil")
	argCheck(self, deactivateFunc, 6, "function", "nil")
	argCheck(self, externalFunc, 7, "function", "nil")
	if not deactivateFunc then
		if not donothing then
			donothing = function() end
		end
		deactivateFunc = donothing
	end
	local data = self.libs[major]
	if not data then
		-- This is new
		local instance = copyTable(newInstance)
		crawlReplace(instance, instance, newInstance)
		destroyTable(newInstance)
		if AceLibrary == newInstance then
			self = instance
			AceLibrary = instance
		end
		self.libs[major] = {
			instance = instance,
			minor = minor,
			deactivateFunc = deactivateFunc,
			externalFunc = externalFunc,
		}
		function instance:GetLibraryVersion()
			return major, minor
		end
		if not instance.error then
			instance.error = error
		end
		if not instance.assert then
			instance.assert = assert
		end
		if not instance.argCheck then
			instance.argCheck = argCheck
		end
		if not instance.pcall then
			instance.pcall = pcall
		end
		addToPositions(instance, major)
		if activateFunc then
			activateFunc(instance, nil, nil) -- no old version, so explicit nil
		end
		
		if externalFunc then
			for k,data in pairs(self.libs) do
				if k ~= major then
					externalFunc(instance, k, data.instance)
				end
			end
		end
		
		for k,data in pairs(self.libs) do
			if k ~= major and data.externalFunc then
				data.externalFunc(data.instance, major, instance)
			end
		end
		if major == "AceEvent-2.0" then
			AceEvent = instance
		end
		if AceEvent then
			AceEvent.TriggerEvent(self, "AceLibrary_Register", major, instance)
		end
		
		return instance
	end
	local instance = data.instance
	if minor <= data.minor then
		if DEBUG then
			-- This one is already obsolete, raise an error.
			error(string.format("Obsolete library registered. %s is already registered at version %d. You are trying to register version %d. Hint: if not AceLibrary:IsNewVersion(%q, %d) then return end", major, data.minor, minor, major, minor), 2)
			return
		end
		return instance
	end
	-- This is an update
	local oldInstance = new()
	
	addToPositions(newInstance, major)
	local isAceLibrary = (AceLibrary == newInstance)
	local old_error, old_assert, old_argCheck, old_pcall
	if isAceLibrary then
		self = instance
		AceLibrary = instance
		
		old_error = instance.error
		old_assert = instance.assert
		old_argCheck = instance.argCheck
		old_pcall = instance.pcall
		
		self.error = error
		self.assert = assert
		self.argCheck = argCheck
		self.pcall = pcall
	end
	deepTransfer(instance, newInstance, oldInstance, major)
	crawlReplace(instance, instance, newInstance)
	local oldDeactivateFunc = data.deactivateFunc
	data.minor = minor
	data.deactivateFunc = deactivateFunc
	data.externalFunc = externalFunc
	function instance:GetLibraryVersion()
		return major, minor
	end
	if not instance.error then
		instance.error = error
	end
	if not instance.assert then
		instance.assert = assert
	end
	if not instance.argCheck then
		instance.argCheck = argCheck
	end
	if not instance.pcall then
		instance.pcall = pcall
	end
	if isAceLibrary then
		for _,v in pairs(self.libs) do
			local i = type(v) == "table" and v.instance
			if type(i) == "table" then
				if i.error == old_error or not i.error then
					i.error = error
				end
				if i.assert == old_assert or not i.assert then
					i.assert = assert
				end
				if i.argCheck == old_argCheck or not i.argCheck then
					i.argCheck = argCheck
				end
				if i.pcall == old_pcall or not i.pcall then
					i.pcall = pcall
				end
			end
		end
	end
	if activateFunc then
		activateFunc(instance, oldInstance, oldDeactivateFunc)
	else
		oldDeactivateFunc(oldInstance)
	end
	del(oldInstance)
	
	if externalFunc then
		for k,data in pairs(self.libs) do
			if k ~= major then
				externalFunc(instance, k, data.instance)
			end
		end
	end
	
	return instance
end

local iter
function AceLibrary:IterateLibraries()
	if not iter then
		local function iter(t, k)
			k = next(t, k)
			if not k then
				return nil
			else
				return k, t[k].instance
			end
		end
	end
	return iter, self.libs, nil
end

-- @function            Activate
-- @brief               The activateFunc for AceLibrary itself. Called when
--                      AceLibrary properly registers.
-- @param self          Reference to AceLibrary
-- @param oldLib        (optional) Reference to an old version of AceLibrary
-- @param oldDeactivate (optional) Function to deactivate the old lib
local function activate(self, oldLib, oldDeactivate)
	if not self.libs then
		if oldLib then
			self.libs = oldLib.libs
		end
		if not self.libs then
			self.libs = {}
		end
	end
	if not self.positions then
		if oldLib then
			self.positions = oldLib.positions
		end
		if not self.positions then
			self.positions = setmetatable({}, { __mode = "k" })
		end
	end
	
	-- Expose the library in the global environment
	_G[ACELIBRARY_MAJOR] = self
	
	if oldDeactivate then
		oldDeactivate(oldLib)
	end
end

if not previous then
	previous = AceLibrary
end
if not previous.libs then
	previous.libs = {}
end
AceLibrary.libs = previous.libs
if not previous.positions then
	previous.positions = setmetatable({}, { __mode = "k" })
end
AceLibrary.positions = previous.positions
AceLibrary:Register(AceLibrary, ACELIBRARY_MAJOR, ACELIBRARY_MINOR, activate)
