--[[	*** DataStore ***
Written by : Thaoky, EU-Marécages de Zangar
July 15th, 2009

This is the main DataStore module, its purpose is to be a single point of contact for common operations between client addons and other DataStore modules.
For instance, it prevents client addons from calling a different :GetCharacter() in each module, as the value returned by the main module can be passed to the other ones.

Other services offered by DataStore:
	- DataStore Events ; possibility to trigger and respond to DataStore's own events (see the respective modules for details)
	- Tracks guild members status in a slightly more accurate way than with GUILD_ROSTER_UPDATE alone.
	- Guild member info can be requested by character name (DataStore:GetGuildMemberInfo(member)) rather than by index (GetGuildRosterInfo)
	- Tracks online guild members' alts, used mostly by other DataStore modules, but can also be used by client addons.
		Note: a "main" is the currently connected player, "alts" are all his other characters in the same guild. The notions of "main" & "alts" are thus only valid for live data, nothing else.
--]]
DataStore = LibStub("AceAddon-3.0"):NewAddon("DataStore", "AceConsole-3.0", "AceEvent-3.0", "AceComm-3.0", "AceSerializer-3.0")

local addon = DataStore

local THIS_ACCOUNT = "Default"
local commPrefix = "DataStore"
local Characters				-- pointer to the part of the DB that contains character data

local RegisteredModules = {}
local RegisteredMethods = {}

local guildMembersIndexes = {} 	-- hash table containing guild member info
local onlineMembers = {}			-- simple hash table to track online members:		["member"] = true (or nil)
local onlineMembersAlts = {}		-- simple hash table to track online members' alts:	["member"] = "alt1|alt2|alt3..."

-- Message types
local MSG_ANNOUNCELOGIN				= 1	-- broacast at login
local MSG_LOGINREPLY					= 2	-- reply to MSG_ANNOUNCELOGIN

local AddonDB_Defaults = {
	global = {
		Characters = {
			['*'] = {				-- ["Account.Realm.Name"] 
				faction = nil,
				guildName = nil,		-- nil = not in a guild, as returned by GetGuildInfo("player")
			}
		}
	}
}

local function GetKey(name, realm, account)
	-- default values
	name = name or UnitName("player")
	realm = realm or GetRealmName()
	account = account or THIS_ACCOUNT
	
	return format("%s.%s.%s", account, realm, name)
end

local function GetDBVersion()
	return addon.db.global.Version or 0
end

local function SetDBVersion(version)
	addon.db.global.Version = version
end

local DBUpdaters = {
	-- Table of functions, each one updates to its index's version
	--	ex: [3] = the function that upgrades from v2 to v3
	[1] = function(self)
			-- This function moves character keys from the "global" level to the "Characters" sub-table
			-- keys are also changed from a simple boolean (previously set to true) to a table. Only faction & guildname are tracked (for later use)
			for k, v in pairs(addon.db.global) do
				if type(v) == "boolean" then
					if not addon.db.global.Characters[k].faction then		-- for characters other than the current one ..
						addon.db.global.Characters[k].faction = "" -- set the faction field to create the table.
					end
					addon.db.global[k] = nil	-- kill the key at the old location
				end
			end
		end,
}

local function UpdateDB()
	local version = GetDBVersion()
	
	for i = (version+1), #DBUpdaters do		-- start from latest version +1 to the very last
		DBUpdaters[i]()
		SetDBVersion(i)
	end
end

local function GetAlts(guild)
	-- returns a | delimited string containing the list of alts in the same guild
	guild = guild or GetGuildInfo("player")
	if not guild then	return end
		
	local out = {}
	for k, v in pairs(Characters) do
		local accountKey, realmKey, charKey = strsplit(".", k)
		
		if accountKey and accountKey == THIS_ACCOUNT then			-- same account
			if realmKey and realmKey == GetRealmName() then			-- same realm
				if charKey and charKey ~= UnitName("player") then	-- skip current char
					if v.guildName and v.guildName == guild then		-- same guild (to send only guilded alts, privacy concern, do not change this)
						table.insert(out, charKey)
					end
				end
			end
		end
	end
	return table.concat(out, "|")
end

local function SaveAlts(sender, alts)
	if addon:IsGuildMemberOnline(sender) then		-- should always be true, but we never know
		onlineMembersAlts[sender] = alts				-- "alt1|alt2|alt3..."
	end
end

local function GuildBroadcast(messageType, ...)
	local serializedData = addon:Serialize(messageType, ...)
	addon:SendCommMessage(commPrefix, serializedData, "GUILD")
end

local function GuildWhisper(player, messageType, ...)
	if addon:IsGuildMemberOnline(player) then
		local serializedData = addon:Serialize(messageType, ...)
		addon:SendCommMessage(commPrefix, serializedData, "WHISPER", player)
	end
end

-- *** Event Handlers ***
local currentGuildName

local function OnPlayerGuildUpdate()
	-- at login this event is called between OnEnable and PLAYER_ALIVE, where GetGuildInfo returns a wrong value
	-- however, the value returned here is correct
	if IsInGuild() and not currentGuildName then		-- the event may be triggered multiple times, and GetGuildInfo may return incoherent values in subsequent calls, so only save if we have no value.
		currentGuildName = GetGuildInfo("player")
		if currentGuildName then	
			-- the first time a valid value is found, broadcast to guild, it must happen here for a standard login, but won't work here after a reloadui since this event is not triggered
			GuildBroadcast(MSG_ANNOUNCELOGIN, GetAlts(currentGuildName))
			addon:TriggerDataStoreEvent("DATASTORE_ANNOUNCELOGIN")
		end
	end
	Characters[GetKey()].guildName = currentGuildName
end

local function OnPlayerAlive()
	Characters[GetKey()].faction = UnitFactionGroup("player")
	OnPlayerGuildUpdate()
end

local function OnGuildRosterUpdate()
	wipe(guildMembersIndexes)
	for i=1, GetNumGuildMembers(true) do		-- browse all players (online & offline)
		local name, _, _, _, _, _, _, _, onlineStatus = GetGuildRosterInfo(i)
		if name then
			guildMembersIndexes[name] = i
			onlineMembers[name] = onlineStatus
		end
	end
end

local msgOffline = gsub(ERR_FRIEND_OFFLINE_S, "%%s", "(.+)")		-- this turns "%s has gone offline." into "(.+) has gone offline."

local function OnChatMsgSystem(event, arg)
	if arg then
		local member = arg:match(msgOffline)
		if member then
			-- guild roster update can be triggered every 10 secs max, so if a players logs in & out right after, sending him message will result in "No player named xx"
			-- marking him as offline prevents this
			onlineMembers[member] = nil
			onlineMembersAlts[member] = nil
		end
	end
end

-- *** Guild Comm ***
local function OnAnnounceLogin(sender, alts)
	if sender ~= UnitName("player") then						-- don't send back to self
		GuildWhisper(sender, MSG_LOGINREPLY, GetAlts())		-- reply by sending my own alts ..
	end
	SaveAlts(sender, alts)											-- .. and save received data
end

local function OnLoginReply(sender, alts)
	SaveAlts(sender, alts)
end

local GuildCommCallbacks = {
	[MSG_ANNOUNCELOGIN] = OnAnnounceLogin,
	[MSG_LOGINREPLY] = OnLoginReply,
}

local function GuildCommHandler(prefix, message, distribution, sender)
	local success, msgType, msgData = addon:Deserialize(message)
	
	if success and msgType then
		local func = GuildCommCallbacks[msgType]
		
		if func then
			func(sender, msgData)
		end
	end
end

-- Explanation of this piece of code
-- Whenever DataStore:MethodXXX(arg1, arg2, etc..) is called, this attempts to find the method in the registered list
-- If this method is character related, we intercept the string (ex: ["Default.RealmZZZ.CharYYY") and get the associated character table in the module that owns these data
-- since we actually pass a table to registered methods, the "conversion" is done here.

--[[	*** Sample code ***
	local character = DataStore:GetCharacter()

	-- while the implementation of GetNumSpells in DataStore_Spells expects a table as first parameter, the string value returned by GetCharacter is converted on the fly
	-- this service prevents having to maintain a separate pointer to each character table in the respective DataStore_* modules.
	local n = DataStore:GetNumSpells(character, "Fire")	
	print(n)
--]]

local lookupMethods = { __index = function(self, key)
	return function(self, arg1, ...)
		if not RegisteredMethods[key] then
--			print(format("DataStore : method <%s> is missing.", key))			-- enable this in Debug only, there's a risk that this function gets called unexpectedly
			return
		end
	
		if RegisteredMethods[key].isCharBased then		-- if this method is character related, the first expected parameter is the character
			local owner = RegisteredMethods[key].owner
			arg1 = owner.Characters[arg1]				-- turns a "string" parameter into a table, fully intended.
			
			if not arg1 then return end
		end
		return RegisteredMethods[key].func(arg1, ...)
	end
end }

function addon:OnInitialize()
	addon.db = LibStub("AceDB-3.0"):New("DataStoreDB", AddonDB_Defaults)
	
	Characters = addon.db.global.Characters
	UpdateDB()
	
	setmetatable(addon, lookupMethods)
end

function addon:OnEnable()
	addon:RegisterEvent("PLAYER_ALIVE", OnPlayerAlive)
	addon:RegisterEvent("PLAYER_GUILD_UPDATE", OnPlayerGuildUpdate)				-- for gkick, gquit, etc..
	
	if IsInGuild() then
		addon:RegisterEvent("GUILD_ROSTER_UPDATE", OnGuildRosterUpdate)
		-- we only care about "%s has come online" or "%s has gone offline", so register only if player is in a guild
		addon:RegisterEvent("CHAT_MSG_SYSTEM", OnChatMsgSystem)
		addon:RegisterComm(commPrefix, GuildCommHandler)
		
		local guild = GetGuildInfo("player")		-- will be nil in a standard login (called too soon), but ok for a reloadui.
		if guild then
			GuildBroadcast(MSG_ANNOUNCELOGIN, GetAlts(guild))
			addon:TriggerDataStoreEvent("DATASTORE_ANNOUNCELOGIN")
		end
	end
end

function addon:OnDisable()
	addon:UnregisterEvent("PLAYER_ALIVE")
	addon:UnregisterEvent("PLAYER_GUILD_UPDATE")
	addon:UnregisterEvent("GUILD_ROSTER_UPDATE")
	addon:UnregisterEvent("CHAT_MSG_SYSTEM")
end

-- *** DB functions ***
function addon:RegisterModule(moduleName, module, publicMethods)
	assert(type(moduleName) == "string")
	assert(type(module) == "table")
	
	if not RegisteredModules[moduleName] then		-- add the module's database address (addon.db.global) to the list of known modules
		RegisteredModules[moduleName] = module
		local db = module.db.global

		-- simplifies the life of child modules, and prepares a few pointers for them
		module.ThisCharacter = db.Characters[GetKey()]
		module.Characters = db.Characters

		-- register module's public method
		for methodName, method in pairs(publicMethods) do
			if RegisteredMethods[methodName] then
				print(format("DataStore:RegisterMethod() : adding method for module <%s> failed.", moduleName))
				print(format("DataStore:RegisterMethod() : method <%s> already exists !", methodName))
				return 
			end
			
			RegisteredMethods[methodName] = {
				func = method,
				owner = module, 			-- module that owns this method & associated data
			}
		end
	end
end

function addon:SetCharacterBasedMethod(methodName)
	-- flags a given method as character based
	if RegisteredMethods[methodName] then
		-- this will take care of error checking before calling the registered method, and pass the appropriate character table as argument
		RegisteredMethods[methodName].isCharBased = true
	end
end

function addon:IsModuleEnabled(name)
	assert(type(name) == "string")
	
	if RegisteredModules[name] then
		return true
	end
end

function addon:GetCharacter(name, realm, account)
	local key = GetKey(name, realm, account)
	if Characters[key] then		-- if the key is known, return it to caller, it can be passed to other modules 
		return key
	end
end

function addon:GetCharacters(realm, account)
	-- get a list of characters on a given realm/account
	realm = realm or GetRealmName()
	account = account or THIS_ACCOUNT
	
	local out = {}
	local accountKey, realmKey, charKey
	for k, _ in pairs(Characters) do
		accountKey, realmKey, charKey = strsplit(".", k)
		
		if accountKey and realmKey then
			if accountKey == account and realmKey == realm then
				out[charKey] = k	
				-- allows this kind of iteration:
					-- for characterName, character in pairs(DS:GetCharacters(realm, account)) do
						-- do stuff with characterName only
						-- or do stuff with the "character" key to pass to other DataStore functions
					-- end
			end
		end
	end
	
	return out
end

function addon:DeleteCharacter(name, realm, account)
	local key = GetKey(name, realm, account)
	if not Characters[key] then return end
	
	-- delete the character in all modules
	for moduleName, moduleDB in pairs(RegisteredModules) do
		if moduleDB.Characters then
			moduleDB.Characters[key] = nil
		end
	end
	
	-- delete the key in DataStore
	Characters[key] = nil
end

function addon:DeleteRealm(realm, account)
	for name, _ in pairs(addon:GetCharacters(realm, account)) do
		addon:DeleteCharacter(name, realm, account)
	end
end

function addon:GetRealms(account)
	account = account or THIS_ACCOUNT
	
	local out = {}
	local accountKey, realmKey
	for k, _ in pairs(Characters) do
		accountKey, realmKey = strsplit(".", k)
		
		if accountKey and realmKey then
			if accountKey == account then
				out[realmKey] = true
				-- allows this kind of iteration:
					-- for realmName in pairs(DS:GetRealms( account)) do
					-- end
			end
		end
	end
	return out
end

function addon:GetAccounts()
	local out = {}
	local accountKey
	for k, _ in pairs(Characters) do
		accountKey = strsplit(".", k)
		
		if accountKey then
			out[accountKey] = true
				-- allows this kind of iteration:
					-- for accountName in pairs(DS:GetAccounts()) do
					-- end
		end
	end
	return out
end

function addon:GetModules()
	return RegisteredModules
		-- for moduleName, module in pairs(DS:GetModules()) do
		-- end
end

function addon:GetCharacterTable(module, name, realm, account)
	-- module can be either the module name (string) or the module table
	-- ex: DS:GetCharacterTable("DataStore_Containers", ...) or DS:GetCharacterTable(DataStore_Containers, ...)
	if type(module) == "string" then
		module = RegisteredModules[module]
	end
	
	assert(type(module) == "table")
	return module.Characters[GetKey(name, realm, account)]
end

function addon:GetModuleLastUpdate(module, name, realm, account)
	-- module can be either the module name (string) or the module table
	-- ex: DS:GetModuleLastUpdate("DataStore_Containers", ...) or DS:GetModuleLastUpdate(DataStore_Containers, ...)
	if type(module) == "string" then
		module = RegisteredModules[module]
	end
	
	assert(type(module) == "table")
	
	local key = GetKey(name, realm, account)
	
	return module.Characters[key].lastUpdate
end

function addon:ImportData(module, data, name, realm, account)
	-- module can be either the module name (string) or the module table
	-- ex: DS:ImportData("DataStore_Containers", ...) or DS:ImportData(DataStore_Containers, ...)
	if type(module) == "string" then
		module = RegisteredModules[module]
	end
	
	assert(type(module) == "table")
	module.Characters[GetKey(name, realm, account)] = data
end

function addon:ImportCharacter(key, faction, guild)
	-- after data has been imported, add a player entry to the DB, so that it becomes "visible" to the outside world.
	-- in other words, the correct sequence of operations should be something like:
	--	DataStore:ImportData(DataStore_Talents)
	--	DataStore:ImportData(DataStore_Spells)
	--	DataStore:ImportCharacter(key, faction, guild)
	
	Characters[key].faction = faction
	Characters[key].guildName = guild
end

function addon:SetOption(module, option, value)
	-- module can be either the module name (string) or the module table
	-- ex: DS:SetOption("DataStore_Containers", ...) or DS:SetOption(DataStore_Containers, ...)
	if type(module) == "string" then
		module = RegisteredModules[module]
	end
	
	assert(type(module) == "table")

	if module.db.global.Options then
		module.db.global.Options[option] = value
	end
end

function addon:GetOption(module, option)
	-- module can be either the module name (string) or the module table
	-- ex: DS:GetOption("DataStore_Containers", ...) or DS:GetOption(DataStore_Containers, ...)
	if type(module) == "string" then
		module = RegisteredModules[module]
	end
	
	assert(type(module) == "table")
	
	if module.db.global.Options then
		return module.db.global.Options[option]
	end
end

-- *** Guild stuff ***
function addon:GetGuildMemberInfo(member)
	-- returns the same info as the genuine GetGuildRosterInfo(), but it can be called by character name instead of by index.
	local index = guildMembersIndexes[member]
	if index then
		-- name, rank, rankIndex, level, class, zone, note, officernote, online, status, englishClass = GetGuildRosterInfo(index)
		return GetGuildRosterInfo(index)
	end
end

function addon:IsGuildMemberOnline(member)
	return onlineMembers[member]
end

function addon:GetNameOfMain(player)
	-- returns the name of the guild mate to whom an alt belongs
	
	-- ex, player x has alts a, b, c	
	if onlineMembers[player] then			-- if x is passed ..it's the main
		return player							-- return it
	end
	
	for member, alts in pairs(onlineMembersAlts) do		--if b is passed, browse all online players who sent their alts
		for alt in alts:gmatch("(%a+)") do					-- browse the list of alts
			if alt == player then								-- alt found ?
				return member										-- return the name of his main (currently connected)
			end
		end	
	end
end

-- *** Event manager ***

local RegisteredListeners

local function RegisterDataStoreEvent(self, event, method)
	assert(type(event) == "string")
	
	-- RegisteredListeners
	--	["EVENT_TYPE_1"]
	--		["Listener 1"] = func abc			-- function to call when EVENT_TYPE_1 is triggered
	--		["Listener 2"] = func xyz
	RegisteredListeners = RegisteredListeners or {}
	RegisteredListeners[event] = RegisteredListeners[event] or {}
	
	-- 3 types of calls are allowed (same as AceEvent-3.0)
	if type(method) == "nil" then
		--	MyAddon:RegisterDataStoreEvent("NAME_OF_EVENT")
			-- prototype : function MyAddon:NAME_OF_EVENT(arg1, arg2, arg3)
		RegisteredListeners[event][self.name] = self[event]
			
	elseif type(method) == "string" then
		--	MyAddon:RegisterDataStoreEvent("NAME_OF_EVENT", "MyHandlerMethod")	
			-- prototype : function MyAddon:MyHandlerMethod(arg1, arg2, arg3)
		RegisteredListeners[event][self.name] = self[method]
	
	elseif type(method) == "function" then
		--	MyAddon:RegisterDataStoreEvent("NAME_OF_EVENT", MyHandlerMethod)
			-- prototype : local function MyHandlerMethod(self, arg1, arg2, arg3)
		RegisteredListeners[event][self.name] = method
	end
end

local function UnregisterDataStoreEvent(self, event)
	if RegisteredListeners and RegisteredListeners[event] then
		RegisteredListeners[event][self.name] = nil
	end
end

function addon:RegisterEventListener(listener)
	-- In your_addon:OnEnable(), call this method with your addon's Ace3 object name as parameter
	-- ex:  DataStore:RegisterEventListener("MyAddon")
	-- this is only necessary if you intend to respond to events triggered by DataStore
	assert(type(listener) == "string")
	assert(type(_G[listener]) == "table")
	
	-- this allows listeners to call :
	-- MyAddon:RegisterDataStoreEvent()
	-- MyAddon:UnregisterDataStoreEvent()
	_G[listener].RegisterDataStoreEvent = RegisterDataStoreEvent
	_G[listener].UnregisterDataStoreEvent = UnregisterDataStoreEvent

	if not _G[listener].name then		-- Ace 3 addons have this property set
		_G[listener].name = listener
	end
end

function addon:TriggerDataStoreEvent(event, ...)
	if RegisteredListeners and RegisteredListeners[event] then
		for k, func in pairs(RegisteredListeners[event]) do		-- call all listeners for this event
			func(self, ...)
		end
	end
end
