--[[
    This code is responsible for creating the events and API associated with duration spells.
--]]

DoTimer = AsheylaLib:NewModule("DoTimer", "KBLib");

local TIME_REMAINING = "DOTIMER_TIME_REMAINING";

local GetTime = GetTime;
local match = string.match;
local abs = math.abs;
local floor = math.floor;
local lower = string.lower;
local find = string.find;
local gmatch = string.gmatch;
local GetSpellName, GetSpellTexture, UnitExists, UnitIsUnit, UnitCanAttack, 
    UnitIsGhost, UnitAffectingCombat, GetRaidTargetIndex, UnitDebuff, UnitBuff, 
    UnitChannelInfo, UnitIsPlayer, UnitPlayerControlled, UnitGUID = 
    GetSpellName, GetSpellTexture, UnitExists, UnitIsUnit, UnitCanAttack, 
    UnitIsGhost, UnitAffectingCombat, GetRaidTargetIndex, UnitDebuff, UnitBuff, 
    UnitChannelInfo, UnitIsPlayer, UnitPlayerControlled, UnitGUID;

local uniqueSpells = { --spells that can only be applied once globally, but lack of cooldown allows spamming
	["Hunter's Mark"] = 1,
	["Fear"] = 1,
	["Seduction"] = 1,
	["Prayer of Mending"] = 1,
	["Hibernate"] = 1,
	["Sap"] = 1,
	["Polymorph"] = 1,
	["Banish"] = 1,
	["Enslave Demon"] = 1,
    ["Shackle Undead"] = 1,
    ["Entangling Roots"] = 1,
    ["Cyclone"] = 1,
    ["Turn Undead"] = 1,
}

local aoeSpells, localeData;
local SpellLib = AsheylaLib:GetLibrary("SpellLib");

local spellbooktables = {BOOKTYPE_SPELL,BOOKTYPE_PET};
local getEnglish;
local cachedEnglish = {};
setmetatable(cachedEnglish, {
    __index = function(t, k)
        local v = getEnglish(k);
        t[k] = v;
        return v;
    end,
});
function getEnglish(spellname)
	local english, texture;
	for index,value in ipairs(spellbooktables) do
		local i = 1
		while GetSpellName(i, value) do
			local spell = GetSpellName(i, value);
			if spell == spellname then
				texture = GetSpellTexture(i, value);
				break;
			end
			i = i + 1;
		end
	end
	if texture and localeData[texture] then return localeData[texture].name; end
	return "unknown";
end

function DoTimer:ReturnEnglish(spellname) --returns the english name of the spell
    return cachedEnglish[spellname];
end

-- holds all the targets that currently have timers
local targets = {};

local timerModule = DoTimerTimer;
local targetModule = DoTimerTarget;

-- sets up the initialization of the mod
local internalFrame = CreateFrame("Frame");
internalFrame:RegisterEvent("PLAYER_ENTERING_WORLD");
internalFrame:SetScript("OnEvent", function(self, event, ...)
	DoTimer[event](DoTimer, ...);
end);

local updateFunc = function(self, elapsed)
	local time = GetTime();
	for _, target in ipairs(targets) do
		for timer in target:GetTimers() do
			if (time >= timer.time + timer.duration) then
                if target.name == "Party Buffs" then
                    DoTimer:UpdatePartyBuff(timer);
                else
                    target:RemoveTimer(timer);
                end
            else
                timer.announced = timer.announced or floor(timer.duration);
                local remaining = timer.duration - time + timer.time;
                if remaining <= timer.announced - 1 then
                    timer.announced = timer.announced - 1;
                    AsheylaLib:CallEvent(TIME_REMAINING, timer, timer.announced);
                end
			end
		end
	end
    if #targets == 0 then
        self:SetScript("OnUpdate", nil)
    end
end

function DoTimer:RemoveTarget(removed)
    for i, target in ipairs(targets) do
        if target == removed then
            table.remove(targets, i);
            break;
        end
    end
end

function DoTimer:GetTargets()
    return ipairs(targets);
end

function DoTimer:Commands()
    local loaded = LoadAddOn("DoTimer_Options");
    if loaded then
        GUILib:ShowPanel("DoTimer");
    end
end
DoTimer:MakeSlashCmd("/dotimer", "/dot");

function DoTimer:PLAYER_ENTERING_WORLD()
	internalFrame:UnregisterEvent("PLAYER_ENTERING_WORLD");
    local _, class = UnitClass("player");
    aoeSpells, localeData = self:DefineSpells(class);
    self:AddSettingsUpdateScript(function(self)
        if self:GetUserSet("partyBuffs") == nil then
            self:Set("partyBuffs", {
                [" spirit"] = 1,
                ["fortitude"] = 1,
                ["shadow protection"] = 1,
                ["of the wild"] = 1,
                ["arcane"] = 1,
                ["blessing of"] = 1,
                ["shout"] = 1,
                ["soulstone"] = 1,
                ["thorns"] = 1,
            });
        end
        if self:Get("enabled") then
        	internalFrame:RegisterEvent("UNIT_AURA");
            internalFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
            internalFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED");
            internalFrame:RegisterEvent("PLAYER_ALIVE");
            internalFrame:RegisterEvent("PLAYER_REGEN_ENABLED");
            local target = self:GetTarget("Party Buffs");
            if target then
                if self:Get("usePartyBuffs") then
                    for timer in target:GetTimers() do
                        if not self:Get("partyBuffs", timer.partyBuffType) then
                            target:RemoveTimer(timer);
                        end
                    end
                else
                    target:RemoveAllTimers();
                end
            end
            for _, target in ipairs(targets) do
                for timer in target:GetTimers() do
                    if not timer.isMine and not self:Get("otherTimers", string.lower(timer.name)) then
                        target:RemoveTimer(timer);
                    end
                end
            end
            self:UNIT_AURA("player");
            self:UNIT_AURA("target");
            self:UNIT_AURA("focus");
            if GetNumRaidMembers() > 0 then
                for i = 1, 40 do
                    self:UNIT_AURA("raid" .. i);
                end
            elseif GetNumPartyMembers() > 0 then
                for i = 1, 4 do
                    self:UNIT_AURA("party" .. i);
                end
            end
        else
        	internalFrame:UnregisterEvent("UNIT_AURA");
            internalFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
            internalFrame:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED");
            internalFrame:UnregisterEvent("PLAYER_ALIVE");
            internalFrame:UnregisterEvent("PLAYER_REGEN_ENABLED");
            for i = #targets, 1, -1 do
                targets[i]:RemoveAllTimers();
            end
        end
    end);
    self:UpdateSettings();
end

function DoTimer:UNIT_AURA(unit)
	if (UnitExists(unit)) then
		if (UnitIsUnit(unit, "pet")) then self:ScanAuras("pet", "HARMFUL"); end
		if UnitCanAttack("player", unit) then 
			self:ScanAuras(unit, "HARMFUL");
		else 
            if 
            (
                (not self:Get("filterUngroupedBuffs") or (UnitExists(UnitName(unit)) or UnitIsUnit(unit, "player"))) 
            and 
                (not self:Get("filterNonPlayerBuffs") or UnitIsPlayer(unit)) 
            and
                (not self:Get("filterSelf") or not UnitIsUnit("player", unit))
            )
            then
                self:ScanAuras(unit, "HELPFUL");
            end
		end
	end
end

function DoTimer:COMBAT_LOG_EVENT_UNFILTERED(timeStamp, event, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
    if (event == "UNIT_DIED" and not string.find(destName, "Training Dummy")) then
    	for i, target in ipairs(targets) do
    		if target.uniqueID == destGUID then
                target:RemoveAllTimers();
    		end
    	end
    end
end

function DoTimer:PLAYER_ALIVE()
    if not UnitIsGhost("player") then return end
    for i = #targets, 1, -1 do
        targets[i]:RemoveAllTimers();
    end
end

local f = CreateFrame("Frame");
local combatFunc = function(self, elapsed)
    self.elapsed = self.elapsed + elapsed;
    if (self.elapsed >= 3) then
        if not UnitAffectingCombat("player") then
        	for i, target in ipairs(targets) do
        		if target.type == "mob" and not string.find(target.name, "Training Dummy") then
                    target:RemoveAllTimers();
        		end
        	end
        end
        self:SetScript("OnUpdate", nil);
    end
end

function DoTimer:PLAYER_REGEN_ENABLED()
    f.elapsed = 0;
    f:SetScript("OnUpdate", combatFunc);
end

function DoTimer:UNIT_SPELLCAST_SUCCEEDED(...)
    local unit, spellName, spellRank = ...;
    if unit ~= "player" then return end;
    if self:IsAOESpell(spellName) then
        local target = self:GetTarget("No Target");
        if not target then
            target = targetModule:NewAOETarget(self);
            table.insert(targets, target);
            internalFrame:SetScript("OnUpdate", updateFunc);
        end
        local texture = SpellLib:FindSpellInfo(spellName, spellRank, "spell");
        local group = self:ReturnGroup(texture);
        if group > 0 then
            for t in target:GetTimers() do
                if self:ReturnGroup(t.texture) == group then
                    target:RemoveTimer(t);
                end
            end
        end
        local duration = self:ReturnDuration(spellName, spellRank, texture);
        local timer = timerModule:NewTimer(self, spellName, spellRank, texture, 1, duration, GetTime() + duration, "HARMFUL", true);
        target:AddTimer(timer);
    end
    if self:ReturnEnglish(spellName) == "Totemic Call" then
        local target = self:GetTarget("No Target");
        for timer in target:GetTimers() do
            local group = self:ReturnGroup(timer.texture);
            if (group >= 2 and group <= 5) then 
                target:RemoveTimer(timer);
            end
        end
    end
end

function DoTimer:ReturnGroup(texture)
	return (localeData[texture] and localeData[texture].group or 0)
end

function DoTimer:ScanAuras(unit, filter)
	local target = self:GetTarget(unit);
	if (target) then
		for timer in target:GetTimers() do
			timer.validated = false;
		end
		local newIcon = GetRaidTargetIndex(unit) or 0;
		target:UpdateIcon(newIcon);
		target.type = self:GetTargetType(unit);
	end
    local partyBuffs = self:GetTarget("Party Buffs");
    local guid = UnitGUID(unit);
    if (partyBuffs) then
        for timer in partyBuffs:GetTimers() do
            if timer.affecting[guid] then
                timer.affecting[guid].validated = false;
            end
        end
    end
	local i = 1;
    local name, rank, texture, count, debuffType, duration, endTime, isMine = UnitAura(unit, i, filter);
	while name do
		if duration > 0 then
            if isMine then
                if partyBuffs then
                    for timer in partyBuffs:GetTimers() do
                        if find(lower(name), timer.partyBuffType) then
                            if timer.affecting[guid] then
                                timer.affecting[guid].validated = true;
                            end
                        end
                    end
                end
                
    			if (not self:IsIgnoring(name, rank, texture, count, duration, endTime, filter, guid, UnitName(unit))) then
    				local adding = true;
    				if target then
    					for _i, timer in ipairs(target.timers) do
    						if (timer.module == self and timer.name == name and timer.type == filter and timer.isMine == isMine and not timer.validated) then
    							timer.validated = true;
    							timer:UpdateTimerData(name, texture, duration, endTime, count, _i, i);
    							adding = false;
    							break;
    						end
    					end
    				end
    				if (adding) then
                        if uniqueSpells[self:ReturnEnglish(name)] then
                            for i, _t in ipairs(targets) do
                                for t in _t:GetTimers() do
                                    if t.name == name then
                                        _t:RemoveTimer(t);
                                    end
                                end
                            end
                        end
                        
    					if target then
                            local group = self:ReturnGroup(texture);
                            if group > 0 then
                                for t in target:GetTimers() do
                                    if self:ReturnGroup(t.texture) == group then
                                        target:RemoveTimer(t);
                                    end
                                end
                            end
                        else
    						target = targetModule:NewTarget(self, unit);
    						table.insert(targets, target);
                            internalFrame:SetScript("OnUpdate", updateFunc);
    					end
    					timer = timerModule:NewTimer(self, name, rank, texture, count, duration, endTime, filter, isMine);
    					target:AddTimer(timer);
    				end
    			end
            elseif self:Get("otherTimers", string.lower(name)) then
                local adding = true;
                if target then
                    for _i, timer in ipairs(target.timers) do
                        if (timer.module == self and timer.name == name and timer.type == filter and timer.isMine == isMine and not timer.validated) then
                            timer.validated = true;
                            timer:UpdateTimerData(name, texture, duration, endTime, count, _i, i);
                            adding = false;
                            break;
                        end
                    end
                end
                
                if adding then
                    if not target then
                        target = targetModule:NewTarget(self, unit);
                        table.insert(targets, target);
                        internalFrame:SetScript("OnUpdate", updateFunc);
                    end
                    timer = timerModule:NewTimer(self, name, rank, texture, count, duration, endTime, filter, isMine);
                    target:AddTimer(timer);
                end
            end
		end
		
		i = i + 1;
        name, rank, texture, count, debuffType, duration, endTime, isMine = UnitAura(unit, i, filter);
	end
	
	if (target) then
		for timer in target:GetTimers() do
			if (timer.module == self and timer.type == filter and not timer.validated) then
				target:RemoveTimer(timer);
			end
		end
	end
    if (partyBuffs) then
        for timer in partyBuffs:GetTimers() do
            if timer.affecting[guid] and not timer.affecting[guid].validated then
                if UnitExists(timer.affecting[guid].person) then
                    timer.notAffecting[guid] = timer.affecting[guid];
                else
                    self:ReleaseTable(timer.affecting[guid]);
                end
                timer.affecting[guid] = nil;
                self:UpdatePartyBuff(timer);
            end
        end
    end
end

function DoTimer:AddExternalTimer(module, name, rank, texture, count, duration, endTime, filter, tarName, level, type, icon, guid)
    local tar = DoTimer:GetTargetByGUID(guid);
    if not tar then
        tar = targetModule:NewTargetByData(self, tarName, level, type, icon, guid);
        table.insert(targets, target);
        internalFrame:SetScript("OnUpdate", updateFunc);
    end
    timer = timerModule:NewTimer(module, name, rank, texture, count, duration, endTime, filter, false);
    tar:AddTimer(timer);
    return timer;
end

function DoTimer:IsIgnoring(name, rank, texture, count, duration, endTime, filter, guid, tarName)
    local maxBuffDur = self:Get("maxBuffDuration");
	return (
		(name == UnitChannelInfo("player"))
	or
		(endTime - GetTime() < .5)
	or
		(filter == "HARMFUL" and self:IsAOESpell(name))
    or
        (filter == "HELPFUL" and maxBuffDur > 0 and duration > maxBuffDur * 60)
    or
        (filter == "HELPFUL" and self:Get("usePartyBuffs") and self:IsPartyBuff(name, rank, texture, count, duration, endTime, filter, guid, tarName))
	);
end

function DoTimer:IsAOESpell(name)
	return aoeSpells[self:ReturnEnglish(name)] and true or false;
end

function DoTimer:IsPartyBuff(name, rank, texture, count, duration, endTime, filter, guid, tarName)
    --add to current party buff or make a new one
    --if new, timer.partyBuffType should be the partyBuffs string match
    --needs 2 tables: .affecting and .notAffecting
    --.affecting[guid] = {person = "Xanido", name = "Power Word: Fortitude", duration = 600, time = 12345, texture = "..."};
    --call UpdatePartyBuff(timer) at end so stack, time info can be recalculated
    if filter == "HELPFUL" then
        local matches = self:Get("partyBuffs");
        for query in pairs(matches) do
            if find(lower(name), query) then
                local target = self:GetTarget("Party Buffs");
                local buffTimer;
                if not target then
                    target = targetModule:NewPartyBuffsTarget(self);
                    table.insert(targets, target);
                    internalFrame:SetScript("OnUpdate", updateFunc);
                end
                for timer in target:GetTimers() do
                    if timer.partyBuffType == query then
                        buffTimer = timer;
                        break;
                    end
                end
                if not buffTimer then
                    buffTimer = timerModule:NewPartyBuffsTimer(self, query, name, rank, texture, 1, duration, endTime, "HELPFUL");
                    target:AddTimer(buffTimer);
                end
                buffTimer.affecting[guid] = buffTimer.affecting[guid] or buffTimer.notAffecting[guid];
                buffTimer.notAffecting[guid] = nil;
                if buffTimer.affecting[guid] then
                    self:UpdatePartyBuff(buffTimer, guid, name, rank, texture, duration, endTime);
                else
                    buffTimer.affecting[guid] = self:AcquireTable();
                    local t = buffTimer.affecting[guid];
                    t.person = tarName;
                    t.name = name;
                    t.rank = rank;
                    t.duration = duration;
                    t.time = endTime - duration;
                    t.texture = texture;
                    self:UpdatePartyBuff(buffTimer);
                end
                buffTimer.affecting[guid].validated = true;
                return true;
            end
        end
    end
    
    return false;
end

function DoTimer:UpdatePartyBuff(timer, guid, name, rank, texture, duration, endTime)
    local target = self:GetTarget("Party Buffs");
    if guid then
        local t = timer.affecting[guid];
        if t then
            t.name = name;
            t.rank = rank;
            t.duration = duration;
            t.time = endTime - duration;
            t.texture = texture;
        end
    end
    local mainBuff, remainingTime;
    local stack = 0;
    for guid, buffInfo in pairs(timer.affecting) do
        local remaining = buffInfo.duration - GetTime() + buffInfo.time;
        if remaining > 0 then
            if not mainBuff then
                mainBuff = buffInfo;
                remainingTime = remaining;
            else
                if remaining < remainingTime then
                    mainBuff = buffInfo;
                    remainingTime = remaining;
                end
            end
            stack = stack + 1;
        else
            if UnitExists(buffInfo.person) then
                timer.notAffecting[guid] = buffInfo;
            else
                self:ReleaseTable(buffInfo);
            end
            timer.affecting[guid] = nil;
        end
    end
    
    if stack == 0 then
        target:RemoveTimer(timer);
    else
        timer:UpdateTimerData(mainBuff.name, mainBuff.texture, mainBuff.duration, mainBuff.time + mainBuff.duration, stack);
    end
end

local partyFrame = CreateFrame("Frame");
partyFrame:RegisterEvent("RAID_ROSTER_UPDATE");
partyFrame:RegisterEvent("PARTY_MEMBERS_CHANGED");
partyFrame:SetScript("OnEvent", function(self, event)
    local target = DoTimer:GetTarget("Party Buffs");
    if target then
        for timer in target:GetTimers() do
            local changed = false;
            for guid, data in pairs(timer.notAffecting) do
                if not UnitExists(data.person) then
                    timer.notAffecting[guid] = nil;
                    DoTimer:ReleaseTable(data);
                    changed = true;
                end
            end
            if DoTimer:Get("filterUngroupedBuffs") then
                for guid, data in pairs(timer.affecting) do
                    if not UnitExists(data.person) then
                        timer.affecting[guid] = nil;
                        DoTimer:ReleaseTable(data);
                        changed = true;
                    end
                end
            end
            if changed then 
                DoTimer:UpdatePartyBuff(timer);
            end
        end
    end
    
    if GetNumRaidMembers() > 0 then
        for i = 1, GetNumRaidMembers() do
            DoTimer:UNIT_AURA("raid" .. i);
        end
    else
        if GetNumPartyMembers() > 0 then
            for i = 1, GetNumPartyMembers() do
                DoTimer:UNIT_AURA("party" .. i);
            end
        end
        DoTimer:UNIT_AURA("player");
    end
end);

function DoTimer:GetTargetType(unit)
	if (UnitIsPlayer(unit) or UnitPlayerControlled(unit)) then return "player"; end
	return "mob";
end

function DoTimer:GetTarget(unit)
    if unit == "No Target" or unit == "Party Buffs" then
        for _, target in ipairs(targets) do
            if target.name == unit then return target end
        end
    else
        local guid = UnitGUID(unit);
        for _, target in ipairs(targets) do
            if target.uniqueID == guid then return target end
        end
    end
end

function DoTimer:GetTargetByGUID(guid)
    for _, target in ipairs(targets) do
        if target.uniqueID == guid then return target end
    end
end

local getDuration;
local cachedDurations = {};
setmetatable(cachedDurations, {
    __index = function(t, k)
        local newMeta = {};
        setmetatable(newMeta, {
            __index = function(t2, k2)
                local dur = getDuration(k, k2);
                t2[k2] = dur;
                return dur;
            end
        })
        t[k] = newMeta;
        return newMeta;
    end
})
function DoTimer:ReturnDuration(spellName, spellRank, texture) --returns the duration of a spell
	if texture == "Interface\\Icons\\Spell_Fire_SearingTotem" and (tonumber(match(spellRank, "(%d+)")) or 0) >= 7 then return 60 end --yay for stupid searing totem going from "55 secs" to "1 min" >< throws off the entire system!
	return cachedDurations[spellName][spellRank];
end

function getDuration(spellName, spellRank)
    local texture = SpellLib:FindSpellInfo(spellName, spellRank, "spell");
    for index, value in ipairs(spellbooktables) do 
		local i = 1;
		while GetSpellName(i,value) do
			local spellname, spellrank = GetSpellName(i,value);
			if spellname == spellName and ((spellrank == spellRank) or (spellRank == "") or (value == BOOKTYPE_PET)) then
				local text = SpellLib:GetSpellInfo(i, value, "left", "max");
				local basenumber = localeData[texture].duration;
				local multiplier = localeData[texture].multiplier;
				local truenumber;
				for value2 in gmatch(text, "(%d[%d%p]*)") do
                    value2 = tonumber(value2);
					if type(value2) == "number" and ((not truenumber) or (abs(value2 - basenumber) < abs(truenumber - basenumber))) then truenumber = value2; end
				end
                truenumber = truenumber or basenumber;
				return truenumber * multiplier;
			end
			i = i + 1;
		end
	end
	return 0;
end

local f2 = CreateFrame("Frame")
f2:RegisterEvent("PLAYER_FOCUS_CHANGED")
f2:RegisterEvent("PLAYER_TARGET_CHANGED")
f2:RegisterEvent("UNIT_PET")
f2:RegisterEvent("UPDATE_MOUSEOVER_UNIT");
f2.update = function(self)
    DoTimer:UNIT_AURA("mouseover");
end
f2:SetScript("OnEvent",function(self,event, arg1) 
    local unit;
    if (event == "PLAYER_FOCUS_CHANGED") then
        unit = "focus";
    elseif (event == "PLAYER_TARGET_CHANGED") then
        unit = "target";
    elseif (event == "UNIT_PET" and arg1 == "player") then
        unit = "pet";
    elseif (event == "UPDATE_MOUSEOVER_UNIT") then
        unit = "mouseover";
    end
    if UnitExists("mouseover") then
        self:SetScript("OnUpdate", self.update);
    else
        self:SetScript("OnUpdate", nil);
    end
    
    if unit then DoTimer:UNIT_AURA(unit); end
end)

local defaultSettings = {
    enabled = true,
    usePartyBuffs = true,
    partyBuffs = {},
    filterUngroupedBuffs = true,
    filterNonPlayerBuffs = true,
    filterSelf = false,
    maxBuffDuration = 0,
    otherTimers = {},
}
DoTimer:AddDefaultSettings(defaultSettings);
