assert(oRA, "oRA not found!")


------------------------------
--      Are you local?      --
------------------------------

local PLAYER = UnitName("player")


local BS = AceLibrary("Babble-Spell-2.0")
local Compost = AceLibrary("Compost-2.0")
local Roster = AceLibrary("RosterLib-2.0")

local L = AceLibrary("AceLocale-2.0"):new("oRAOCoolDown")


local CoolDowns = {
	[BS["Rebirth"]] = {
		["duration"] = 1800,
		["ra_id"] = 1,
	},
	[BS["Reincarnation"]] = {
		["duration"] = 3600,
		["ra_id"] = 2,
	},
	[BS["Soulstone Resurrection"]] = {
		["duration"] = 1800,
		["ra_id"] = 3,
	},
}
local CoolDownNames, _CoolDowns_Lookup, _CoolDowns_AmbigiousLookup = {}, {}, {}

local UnitCoolDownsTables = {}


-----------------------------------
--      Iterators for Buffs      --
-----------------------------------

local function _DoNothing() end

local function Iterate(cdname)
	local cd = CoolDowns[cdname]
	if cd then
		local duration = cd["duration"]
		if duration then
			local cdname, now, texture = cd["name"], GetTime(), cd["texture"]

			local iterator = function(_UnitCoolDownsTables, unitname)
				local unitcds, endtime
				repeat
					unitname, unitcds = next(_UnitCoolDownsTables, unitname)
					if not unitcds then return end
					endtime = unitcds[cdname]
				until endtime and (endtime > now)
				return unitname, texture, duration, endtime - now
			end

			return iterator, UnitCoolDownsTables
		end
	end

	return _DoNothing
end

local function IterateUnit(unitname)
	local unitcds = UnitCoolDownsTables[unitname]
	if unitcds then
		local now = GetTime()
		
		local iterator = function(_unitcds, cdname)
			local cd, endtime
			repeat
				cdname, endtime = next(_unitcds, cdname)
				if not cdname then return end
			until endtime > now
			local cd, timeleft = CoolDowns[cdname], endtime - now
			return cdname, cd["texture"], cd["duration"] or timeleft, timeleft
		end

		return iterator, unitcds
	end

	return _DoNothing
end


----------------------------
--      Localization      --
----------------------------

L:RegisterTranslations("enUS", function() return {
	["cooldownoptional"] = true,
	["Optional/CoolDown"] = true,
	["cd"] = true,
	["CoolDown"] = true,
	["Options for cooldowns."] = true,
	["print"] = true,
	["Print cooldown times"] = true,
	["Print the remaining cooldown times."] = true,

	["Cooldowns"] = true,
	["unitcd"] = true,
	["Cooldowns for %s"] = true,
	["Cooldowns (per unit)"] = true,
} end)


----------------------------------
--      Module Declaration      --
----------------------------------

oRAOCoolDown = oRA:NewModule(L["cooldownoptional"])

oRAOCoolDown.defaults = {
}
oRAOCoolDown.optional = true
oRAOCoolDown.name = L["Optional/CoolDown"]
oRAOCoolDown.consoleCmd = L["cd"]
oRAOCoolDown.consoleOptions = {
	type = "group",
	name = L["CoolDown"],
	desc = L["Options for cooldowns."],
	args = {
		[L["print"]] = {
			type = "execute",
			name = L["Print cooldown times"],
			desc = L["Print the remaining cooldown times."],
			func = function()
				oRAOCoolDown:PrintTimesLeft()
			end,
		},
		["test"] = {
			type = "text",
			name = "Test Sync Message",
			desc = "Test with a given sync message.",
			get = false,
			set = function(v)
				oRAOCoolDown:UnitCoolDownReceived(v, PLAYER)
			end,
			usage = "<sync message>",
			visible = function() oRAOCoolDown:IsDebugging() end,
		},
	}
}

-- make buff tables available to other mods
oRAOCoolDown.CoolDowns = CoolDowns
oRAOCoolDown.UnitCoolDownsTables = UnitCoolDownsTables


--------------------------------
--      Unit Buff Tables      --
--------------------------------

-- fallback for unit CoolDowns table lookup
local function _UnitCoolDownsTable_Index(unitcds, cdname)
	-- check if the cd can be found using an alternate (unique) key
	local cd = _CoolDowns_Lookup[cdname]
	if cd then return unitcds[cd["name"]] end

	-- check if the cd can be found using ambigious keys
	local possiblecds = _CoolDowns_AmbigiousLookup[cdname]
	if possiblecds then
		local endtime
		for _, cd in pairs(possiblecds) do
			endtime = unitcds[cd["name"]]
			if endtime then return endtime end
		end
	end
end


local function UnitCoolDownsTable_New()
	local t, mt = Compost:Acquire(), Compost:Acquire()
	mt["__index"] = _UnitCoolDownsTable_Index
	setmetatable(t, mt)
	return t
end

local function UnitCoolDownsTable_Free(t)
	Compost:Reclaim(getmetatable(t))
	Compost:Reclaim(t)
end


------------------------------
--      Initialization      --
------------------------------

function oRAOCoolDown:OnInitialize()
	local core, name = self.core, L["cooldownoptional"]

	-- register at core
	core:RegisterModule(name, self)
	core:RegisterDefaults(name, "profile", self.defaults)
	self.db = core:AcquireDBNamespace(name)
	self.debugFrame = core.debugFrame

	local existingcds, existingcd, ra_id, texture
	for cdname, cd in pairs(CoolDowns) do
		cd["name"] = cdname
		table.insert(CoolDownNames, cdname)

		texture = BS:GetSpellIcon(cdname)
		cd["texture"] = texture

		ra_id = cd["ra_id"]
		if ra_id then
			_CoolDowns_Lookup[tostring(ra_id)] = cd

			if not cd["group"] then cd["group"] = ra_id end
		end

		existingcds = _CoolDowns_AmbigiousLookup[texture]
		if existingcds then
			table.insert(existingcds, buff)
		else
			existingcd = _CoolDowns_Lookup[texture]
			if existingcd then
				_CoolDowns_AmbigiousLookup[texture] = { existingcd, cd }
				_CoolDowns_Lookup[texture] = nil
			else
				-- so far this texture is unique
				_CoolDowns_Lookup[texture] = cd
			end
		end
	end

	setmetatable(CoolDowns, {__index = _CoolDowns_Lookup})
	table.sort(CoolDownNames)

	self:SetDebugging(true)
end

function oRAOCoolDown:OnEnable()
	-- acquire or reclaim tables for units joining or leaving the raid
    self:RegisterEvent("RosterLib_RosterChanged", "RosterChanged")

	-- CTRA check
    self:RegisterCheck("CD", "UnitCoolDownReceived")

	-- create player entry
	self:UnitJoined(PLAYER)

	-- register our timber bars
	if oRAOTimer then
		oRAOTimer:Register(
			L["cd"],
			"oRAOCoolDown_CoolDownUpdated",
			Iterate,
			L["Cooldowns"],
			nil,
			CoolDownNames
		)
		oRAOTimer:Register(
			L["unitcd"],
			"oRAOCoolDown_UnitUpdated",
			IterateUnit,
			L["Cooldowns (per unit)"],
			L["Cooldowns for %s"]
		)
	end
end


------------------------------
--      Event Handlers      --
------------------------------

-- handle units joining and leaving the party or raid
function oRAOCoolDown:RosterChanged(units)
	local name, oldname
	for _, unit in pairs(units) do
		name, oldname = unit["name"], unit["oldname"]
		if not oldname then
			self:UnitJoined(name)
		elseif not name then
			self:UnitLeft(oldname)
		end
	end
end


-- handle received buff durations
function oRAOCoolDown:UnitCoolDownReceived(message, sender)
	-- we only receive messages from party/raid - the unit has to be in the roster
	local unitcds = UnitCoolDownsTables[sender]

	local _, _, _cd, timeleft = string.find(message, "CD ([%d]+) ([%d]+)")
	timeleft = tonumber(timeleft)
	if not timeleft then
		self:Debug("SYNC from "..sender..": '"..message.."' has invalid format")
		return
	end
	local cd = CoolDowns[_cd]
	if not cd then
		self:Debug("SYNC from "..sender..": cooldown '".._cd.."' not found")
		return
	end

	self:CoolDownChanged(sender, cd["name"], timeleft * 60, "SYNC")
end


---------------------------------
--      Utility Functions      --
---------------------------------

function oRAOCoolDown:UnitJoined(unitname)
	self:Debug(unitname.." joined")

	local unit = Roster:GetUnitObjectFromName(unitname)
	if not UnitCoolDownsTables[unitname] then
		UnitCoolDownsTables[unitname] = UnitCoolDownsTable_New()
	end
	if unit then
		unit["ora_cooldowns"] = unitcds
	end
end

function oRAOCoolDown:UnitLeft()
	local unitcds = UnitCoolDownsTables[oldname]
	if not unitcds then return end	-- should never happen, but i got it when a raid changed into a BG raid... /shrug
	UnitCoolDownsTables[oldname] = nil
	
	for cdname, _ in pairs(unitcds) do
		self:TriggerEvent("oRAOCoolDown_CoolDownUpdated", cdname)
	end
	self:TriggerEvent("oRAOCoolDown_UnitUpdated", oldname)

	UnitCoolDownsTable_Free(unitcds)
end


function oRAOCoolDown:CoolDownChanged(unitname, cdname, timeleft, source)
	local unitcds = UnitCoolDownsTables[unitname]
	if not unitcds then return end

	local now = GetTime()
	local endtime = now + timeleft

	if math.abs(endtime - (unitcds[cdname] or 0)) > 1 then
		self:Debug(source..": "..unitname.."'s "..cdname.." is on cooldown for "..timeleft)
		unitcds[cdname] = endtime

		self:TriggerEvent("oRAOCoolDown_CoolDownUpdated", cdname)
		self:TriggerEvent("oRAOCoolDown_UnitUpdated", sender)
	end
end


function oRAOCoolDown:PrintTimesLeft()
	local now, endtime, time

	now = GetTime()
	for unitname, unitcds in UnitCoolDownsTables do
		self:Print("CoolDowns for "..unitname..":")

		for cdname, endtime in pairs(unitcds) do
			-- is the end time not in the past?
			time = endtime - now
			if time > 0 then self:Print("   "..cdname..": "..SecondsToTimeAbbrev(time)) end
		end
	end
end
