--------------------------------------------------------------------------------
-- TO DO
-- add ability to change how many dps/healers you want to send down
-- add slash command for forced talent scanning
--------------------------------------------------------------------------------

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

local mod = BigWigs:NewBoss("Garajal Cross Over", 896)
if not mod then return end
mod:RegisterEnableMob(60143, 60385)
-- XXX make this local once done debugging
LGIST = LibStub:GetLibrary("LibGroupInSpecT-1.0")

--------------------------------------------------------------------------------
-- Locals
--

local DPStoCrossOver = 3 -- changed in :OnEngage
local HealerToCrossOver = 1
local frailSoul, voodooDoll, crossedOver, conduitOn, spiritualInnervation = (GetSpellInfo(117723)), (GetSpellInfo(122151)), (GetSpellInfo(116161)), (GetSpellInfo(122181)), (GetSpellInfo(117549))
local potentialTargets = {}
local lastCrossedOver = {}
local mainMod = BigWigs.modules.Bosses.modules["Gara'jal the Spiritbinder"]
local replacementList = mod:NewTargetList()
local crossOverList = mod:NewTargetList()
local diff, numRaid
local fullRaidSpecScan
local lastGroup = {}
local lastTotemTime
local pName = UnitName("player")


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

local ML = mainMod:GetLocale("enUS", true)

local L = mod:NewLocale("enUS", true)
if L then
	L.announce = "Raid Announce"
	L.announce_desc = "Broadcast messages to Raid Warning, requires promoted or leader."

	L.cross_over = "Cross Over"
	L.cross_over_desc = "Warning for who should cross over with the most recent totem."
	L.cross_over_icon = 116161

	L.not_enough_DPS = "DPS assigned to cross over: %d (which might be low, tried to assign %d)"
	L.not_enough_HEALER = "Healer assigned to cross over: %d (which might be low, tried to assign %d)"

	L.missing_spec = "Specialization scan data missing for %s, falling back to blizzard roles for them"

	L.replacement = "%s go replace %s at the current totem!"
	L.no_replacement = "No replacement found for %s!"
end
L = mod:GetLocale()

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

function mod:GetOptions()
	return {"announce", {"cross_over", "FLASHSHAKE"}}
end

function mod:OnBossEnable()
	self:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT", "CheckBossStatus")

	self:Death("Disable", 60143)
end

function mod:OnEngage()
	wipe(potentialTargets)
	wipe(lastCrossedOver)
	wipe(lastGroup)
	fullRaidSpecScan = true
	lastTotemTime = nil
	local numRaid = GetNumGroupMembers()
	local diff = GetRaidDifficulty()
	if diff == 2 or diff == 4 then
		DPStoCrossOver = 3
	else
		DPStoCrossOver = 2
	end
	for i=1, numRaid do
		local name, _, subgroup = GetRaidRosterInfo(i)
		local info = LGIST:GetCachedInfo(UnitGUID(name))
		if diff == 2 or diff == 4 then
			if subgroup < 6 then
				potentialTargets[name] = true
				if not info.spec_role then
					if type(fullRaidSpecScan) == "table" then
						fullRaidSpecScan[name] = true
					else
						fullRaidSpecScan = {}
						fullRaidSpecScan[name] = true
					end
				end
			end
		else
			if subgroup < 3 then
				potentialTargets[name] = true
				if not info.spec_role then
					if type(fullRaidSpecScan) == "table" then
						fullRaidSpecScan[name] = true
					else
						fullRaidSpecScan = {}
						fullRaidSpecScan[name] = true
					end
				end
			end
		end
	end
	if type(fullRaidSpecScan) == "table" then
		local specMissingList = table.concat(fullRaidSpecScan, ", ")
		print(L["missing_spec"]:format(specMissingList))
	end
end

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

local function isDebuffed(player)
	if UnitExists(player) then
		if UnitDebuff(player, frailSoul) or UnitDebuff(player, voodooDoll) or UnitDebuff(player, crossedOver) or UnitDebuff(player, conduitOn) then
			return true
		else
			return nil
		end
	end
end

local function getSortedTableFromPotentialTargets()
	local sortedTable = {}
	for k, v in pairs(potentialTargets) do
		if v then
			-- check for those recently crossed over back to the living world
			if type(v) == "number" then -- this should almost assure we are on heroic
				if mod:Heroic() then -- but this makes sure when we go from heroic to normal nothing unexpected happens
					if (GetTime() - potentialTargets[k]) > 0 then -- the 3 sec have passed since crossed over faded
						-- recheck if the person has debuffs
						if not isDebuffed(k) then
							table.insert(sortedTable, k)
						end
					end
				end
			else -- boolean
				table.insert(sortedTable, k)
			end
		end
	end
	table.sort(sortedTable, function(a,b) return a<b end)
	return sortedTable
end

local function getReplacementDamager()
	local replacement = false
	local sortedTable = getSortedTableFromPotentialTargets()
	-- do the usual rounds for picking a damager
	for k, player in ipairs(sortedTable) do
		if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
		local info = LGIST:GetCachedInfo(UnitGUID(player))
		-- first try and assign ranged dps without the dps buff
			if not UnitBuff(player, spiritualInnervation) then
				if info.spec_role and info.spec_role == "DAMAGER" then
					if info.class == "HUNTER" or info.class == "MAGE" or info.class == "PRIEST" or info.class == "WARLOCK" or
					(info.class == "DRUID" and info.spec_index == 1) or (info.class == "SHAMAN" and info.spec_index == 1) then
						replacement = player
					end
				end
			end
		end
	end
	if not replacement then
		-- assign ranged with the dps buff
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) and UnitBuff(player, spiritualInnervation) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if info.spec_role and info.spec_role == "DAMAGER" then
					if info.class == "HUNTER" or info.class == "MAGE" or info.class == "PRIEST" or info.class == "WARLOCK" or
					(info.class == "DRUID" and info.spec_index == 1) or (info.class == "SHAMAN" and info.spec_index == 1) then
						replacement = player
					end
				end
			end
		end
	end
	if not replacement then
		-- try melee
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if info.spec_role and info.spec_role == "DAMAGER" then
					if info.class == "DEATHKNIGHT" or info.class == "MONK" or info.class == "WARRIOR" or info.class == "ROGUE" or
					info.class == "PALADIN" or (info.class == "DRUID" and info.spec_index == 2) or (info.class == "SHAMAN" and info.spec_index == 2) then
						replacement = player
					end
				end
			end
		end
	end
	if not replacement then
		-- one last pass to fill up the list based on blizz role assignments, in case the lib talent scanning supplied no data
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if not info.spec_role and UnitGroupRolesAssigned(player) == "DAMAGER" and not dpsAssigned[player] then
					replacement = player
				end
			end
		end
	end
	return replacement
end

local function getReplacementHealer()
	local replacement = false
	local sortedTable = getSortedTableFromPotentialTargets()
	-- do the usual rounds for picking a healer
	for k, player in ipairs(sortedTable) do
		-- assign non disc priest low mana healers
		if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
			local info = LGIST:GetCachedInfo(UnitGUID(player))
			if info.spec_role and info.spec_role == "HEALER" then
				if not (info.class == "PRIEST" and info.spec_index == 1) then -- not disc priest
					local maxMana = UnitPowerMax(player)
					local currMana = UnitPower(player)
					if maxMana and currMana then
						if (currMana/maxMana*100) < 25 then -- low mana
							replacement = player
						end
					end
				end
			end
		end
	end
	if not replacement then
		-- get the higher mana healers
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if info.spec_role and info.spec_role == "HEALER" then
					if not (info.class == "PRIEST" and info.spec_index == 1) then -- non disc
						replacement = player
					end
				end
			end
		end
	end
	if not replacement then
		-- get disc priests now since nothing else left
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if info.spec_role and info.spec_role == "HEALER" then
					if info.class == "PRIEST" and info.spec_index == 1 then -- disc
						replacement = player
					end
				end
			end
		end
	end
	if not replacement then
		-- one last pass to fill up the list based on blizz role assignments, in case the lib talent scanning supplied no data
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if not info.spec_role and UnitGroupRolesAssigned(player) == "HEALER" then
					replacement = player
				end
			end
		end
	end
	return replacement
end

do
	local f = CreateFrame("Frame")
	local total = 0
	f:SetScript("OnUpdate", function(self, elapsed)
		total = total + elapsed
		if total > 0.05 then
			local numRaid = GetNumGroupMembers()
			local diff = GetRaidDifficulty()
			-- keep track of debuffs
			for i=1, numRaid do
				local name, _, subgroup = GetRaidRosterInfo(i)
				-- keep track of when was crossed over last seen on the target
				if UnitExists(name) then
					if UnitDebuff(name, crossedOver) then
						lastCrossedOver[name] = GetTime() -- because debuff can be removed earlier then it's duration
					end
				end
				-- check if there are any active debuffs on eligible targets
				if diff == 2 or diff == 4 then
					if subgroup < 6 then
						if isDebuffed(name) then
							potentialTargets[name] = false
						else
							if name then
								potentialTargets[name] = true
							end
						end
					end
				else
					if subgroup < 3 then
						if isDebuffed(name) then
							potentialTargets[name] = false
						else
							if name then
								potentialTargets[name] = true
							end
						end
					end
				end
			end

			-- now check if we are in heroic, if so, handle frail soul being applied late
			if mod:Heroic() then
				for k, v in pairs(lastCrossedOver) do
					if (GetTime() - v) < 1 then -- less than 1 sec ago still had corssed over
						potentialTargets[k] = v + 3 -- should have frail soul by now
					end
				end
			end
		end
	end)
end

-- We use a hook intead of the standard API to make sure our messages are printed after the module's messages
local origOnSync = mainMod.OnSync
function mainMod:OnSync(...)
	origOnSync(mainMod, ...)
	local sync, rest = ...
	if sync == "DollsApplied" and rest then
		for dolledPlayer in string.gmatch(rest, "%S+") do
			potentialTargets[dolledPlayer] = false
			local replacement = false
			if not lastTotemTime then lastTotemTime = 1 end
			-- someone standing near a totem just got voodoo doll and was in the last group
			-- or it has been less than 5 sec since last group assignment and someone from that group got voodoo doll
			if (UnitDebuff(dolledPlayer, conduitOn) and lastGroup[dolledPlayer]) or (((GetTime() - lastTotemTime) < 5) and lastGroup[dolledPlayer] )then
				local info = LGIST:GetCachedInfo(UnitGUID(dolledPlayer))
				if info.spec_role and info.spec_role == "HEALER" then
					replacement = getReplacementHealer()
				end
				if UnitGroupRolesAssigned(dolledPlayer) == "HEALER" and not replacement then
					replacement = getReplacementHealer()
				end
				if info.spec_role and info.spec_role == "DAMAGER" then
					replacement = getReplacementDamager()
				end
				if UnitGroupRolesAssigned(dolledPlayer) == "DAMAGER" and not replacement then
					replacement = getReplacementDamager()
				end
				if replacement then
					replacementList[1] = replacement
					replacementList[2] = dolledPlayer
					if UnitIsUnit("player", replacement) or UnitIsUnit("player", dolledPlayer) then
						mod:FlashShake("cross_over")
						mod:PlaySound("cross_over", "Info")
					end
					mod:LocalMessage("cross_over", L["replacement"]:format(replacementList[1], replacementList[2]), "Important", 116161)
					if bit.band(self.db.profile.announce, BigWigs.C.MESSAGE) == BigWigs.C.MESSAGE and (UnitIsGroupLeader("player") or UnitIsGroupAssistant("player")) then
						SendChatMessage(L["replacement"]:format(replacement, dolledPlayer), "RAID_WARNING")
					end
					wipe(replacementList)
				else
					print(L["no_replacement"]:format(dolledPlayer))
				end
			end
		end
	elseif sync == "DollsRemoved" and rest then
		for dolledPlayer in string.gmatch(rest, "%S+") do
			potentialTargets[dolledPlayer] = true
		end
	elseif sync == "Totem" then
		-- sort the table
		lastTotemTime = GetTime()
		wipe(lastGroup)
		local lowManaHealersAssigned = {}
		local dpsAssigned = {} -- just to be safe
		local sortedTable = getSortedTableFromPotentialTargets()
		local healerCounter, DPScounter = 0, 0
		for k, player in ipairs(sortedTable) do
			-- assign non disc priest low mana healers
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if healerCounter < HealerToCrossOver then
					if info.spec_role and info.spec_role == "HEALER" then
						if not (info.class == "PRIEST" and info.spec_index == 1) then -- not disc priest
							local maxMana = UnitPowerMax(player)
							local currMana = UnitPower(player)
							if maxMana and currMana then
								if (currMana/maxMana*100) < 25 then -- low mana
									lowManaHealersAssigned[player] = true
									lastGroup[player] = true
									healerCounter = healerCounter + 1
								end
							end
						end
					end
				end
				-- first try and assign ranged dps without the dps buff
				if DPScounter < DPStoCrossOver then
					if not UnitBuff(player, spiritualInnervation) then
						if info.spec_role and info.spec_role == "DAMAGER" then
							if info.class == "HUNTER" or info.class == "MAGE" or info.class == "PRIEST" or info.class == "WARLOCK" or
						(info.class == "DRUID" and info.spec_index == 1) or (info.class == "SHAMAN" and info.spec_index == 1) then
								dpsAssigned[player] = true
								lastGroup[player] = true
								DPScounter = DPScounter + 1
							end
						end
					end
				end
			end
		end
		-- get the higher mana healers
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if healerCounter < HealerToCrossOver then
					if info.spec_role and info.spec_role == "HEALER" then
						if not (info.class == "PRIEST" and info.spec_index == 1) then -- non disc
							if not lowManaHealersAssigned[player] then -- not an already assigned healer
								lastGroup[player] = true
								healerCounter = healerCounter + 1
							end
						end
					end
				end
			end
		end
		-- get disc priests now since nothing else left
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if healerCounter < HealerToCrossOver then
					if info.spec_role and info.spec_role == "HEALER" then
						if info.class == "PRIEST" and info.spec_index == 1 then -- disc
							lastGroup[player] = true
							healerCounter = healerCounter + 1
						end
					end
				end
			end
		end
		-- assign ranged with the dps buff
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) and UnitBuff(player, spiritualInnervation) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if DPScounter < DPStoCrossOver then
					if info.spec_role and info.spec_role == "DAMAGER" then
						if info.class == "HUNTER" or info.class == "MAGE" or info.class == "PRIEST" or info.class == "WARLOCK" or
						(info.class == "DRUID" and info.spec_index == 1) or (info.class == "SHAMAN" and info.spec_index == 1) then
							if not dpsAssigned[player] then
								lastGroup[player] = true
								DPScounter = DPScounter + 1
							end
						end
					end
				end
			end
		end
		-- fill in remaining dps slots with melee
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if DPScounter < DPStoCrossOver then
					if info.spec_role and info.spec_role == "DAMAGER" then
						if info.class == "DEATHKNIGHT" or info.class == "MONK" or info.class == "WARRIOR" or info.class == "ROGUE" or
						info.class == "PALADIN" or (info.class == "DRUID" and info.spec_index == 2) or (info.class == "SHAMAN" and info.spec_index == 2) then
							lastGroup[player] = true
							DPScounter = DPScounter + 1
						end
					end
				end
			end
		end
		-- one last pass to fill up the list based on blizz role assignments, in case the lib talent scanning supplied no data
		for k, player in ipairs(sortedTable) do
			if not UnitIsDeadOrGhost(player) and UnitIsConnected(player) then
				local info = LGIST:GetCachedInfo(UnitGUID(player))
				if healerCounter < HealerToCrossOver then
					if not info.spec_role and UnitGroupRolesAssigned(player) == "HEALER" and not lowManaHealersAssigned[player] then
						lastGroup[player] = true
						healerCounter = healerCounter + 1
					end
				end
				if DPScounter < DPStoCrossOver then
					if not info.spec_role and UnitGroupRolesAssigned(player) == "DAMAGER" and not dpsAssigned[player] then
						lastGroup[player] = true
						DPScounter = DPScounter + 1
					end
				end
			end
		end

		-- copy the last group to the announcing table
		local s = ""
		for k, _ in pairs(lastGroup) do
			s = ("%s, %s"):format(k, s)
			crossOverList[#crossOverList+1] = k
			if UnitIsUnit("player", k) then
				mod:FlashShake("cross_over")
				mod:PlaySound("cross_over", "Info")
			end
		end

		if healerCounter < HealerToCrossOver then
			print(L["not_enough_DPS"]:format(healerCounter, HealerToCrossOver))
		end
		if DPScounter < DPStoCrossOver then
			print(L["not_enough_HEALER"]:format(DPScounter, DPStoCrossOver))
		end
		mod:LocalMessage("cross_over", L["cross_over"]..(": %s"):format(table.concat(crossOverList, ", ")), "Important", 116161)
		wipe(crossOverList)
		if bit.band(mod.db.profile.announce, BigWigs.C.MESSAGE) == BigWigs.C.MESSAGE and (UnitIsGroupLeader("player") or UnitIsGroupAssistant("player")) then
			SendChatMessage(L["cross_over"]..(": %s"):format(s), "RAID_WARNING")
		end
	end
end

