-- This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
-- To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/
-- or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.

local MereHealingFrames, privateVars = ...

MereHealingFrames.BuffManager = {}
MereHealingFrames.BuffManager.PlayerBuffs = {}

MereHealingFrames.BuffManager.BlockedBuff = {}
MereHealingFrames.BuffManager.CleansableBuff = {}
MereHealingFrames.BuffManager.PriorityBuff = {}

MereHealingFrames.BuffManager.TickingTrackers = {}

local TickingTrackers = MereHealingFrames.BuffManager.TickingTrackers
local TotalTickers = 0

local InspectTimeFrame = Inspect.Time.Frame

local function FrameTick()
    MereHealingFrames.Events.BumpEventCounter("Event.System.Update.Begin")
    local currentTime = InspectTimeFrame()
    for tracker, value in pairs(TickingTrackers) do
        if value then
            tracker:Tick(currentTime)
        end
    end
end

local function ReferenceTickEvent()
    if (TotalTickers == 0) then
        Command.Event.Attach(Event.System.Update.Begin, FrameTick, "MereHealingFrame:FrameTick");
    end
    TotalTickers = TotalTickers + 1
end

local function DeReferenceTickEvent()
    TotalTickers = TotalTickers - 1

    if (TotalTickers == 0) then
        Command.Event.Detach(Event.System.Update.Begin, FrameTick, "MereHealingFrame:FrameTick");
    end
end

local function RegisterForTick(tracker)
    if (not TickingTrackers[tracker]) then
        TickingTrackers[tracker] = true
        ReferenceTickEvent()
    end
end

local function DeregisterForTick(tracker)
    if TickingTrackers[tracker] then
        TickingTrackers[tracker] = nil
        DeReferenceTickEvent()
    end
end

local PlayerBuffs = MereHealingFrames.BuffManager.PlayerBuffs

function PlayerBuffs:new(playerInfo)
	local this = {}
	setmetatable(this, self)
	self.__index = self

	this.playerInfo = playerInfo
	this.BuffTracking = {}
	this.NameTracking = {}
	this.CleanseTracking = {}
	this.UIHandlers = {}
	this.BuffTrackers = {}
	this:RefreshBuffSet()
	return this
end

function PlayerBuffs:RefreshBuffSet()
	local newBuffSet = MereHealingFrames.BuffSets.CurrentSet
	-- first have to clear out all the current UI
	MereHealingFrames.Debug(2, "Updating with new buffs")

	if self.playerInfo.isFake then
		return
	end

	for i=1, 20 do
		local Handlers = self.UIHandlers[i] or {}
		for _, BuffIcon in pairs(Handlers) do
			BuffIcon:Clear()
		end
	end
	-- then clear down the internal tables
	for slot, tracker in pairs(self.BuffTrackers) do
		DeregisterForTick(tracker)
	end
	self.BuffTracking = {}
	self.NameTracking = {}
	self.CleanseTracking = {}
	self.BuffTrackers = {}

	-- then build new tables
	for trackerName, buffConfig in pairs(newBuffSet.Trackers) do
		if buffConfig.buffType == "priority" then
			local slots = buffConfig.buffSlots or {}
			local tracker = MereHealingFrames.BuffManager.PriorityBuff:new(self, buffConfig.buffList, buffConfig.debuffList, buffConfig.debuffsOverBuffs, buffConfig.buffCaster, buffConfig.debuffCaster, slots)
			for i, value in pairs(slots) do
				if value then
					self.BuffTrackers[i] = tracker
				end
			end
		elseif buffConfig.buffType == "cleansable" then
			local slots = buffConfig.buffSlots or {}
			local tracker = MereHealingFrames.BuffManager.CleansableBuff:new(self, slots)
			for i, value in pairs(slots) do
				if value then
					self.BuffTrackers[i] = tracker
				end
			end
		end
	end

	-- and push any existing buffs into the system
	local buffs = Inspect.Buff.List(self.playerInfo.unitId)
	self:BuffAdd(buffs)
end

function PlayerBuffs:ResetBuffs()
	for buffId, buffTracker in pairs(self.BuffTracking or {}) do
		for _, tracker in ipairs(buffTracker or {}) do
			tracker:BuffRemove(buffId)
		end
	end

	self.BuffTracking = {}

	local buffs = Inspect.Buff.List(self.playerInfo.unitId)
	self:BuffAdd(buffs)
end

function PlayerBuffs:BuffAdd(buffs)
	if not buffs then return end

	local buffDetails = Inspect.Buff.Detail(self.playerInfo.unitId, buffs)
	for buffId, buffDetail in pairs(buffDetails) do

		if self.BuffTracking[buffId] then
			-- this buff is already tracked, so nothing to do here
			break
		end
		MereHealingFrames.BuffCache.UpdateCache(buffDetail)

		local trackers = self.NameTracking[buffDetail.name] or {}
		for i, tracker in ipairs(trackers) do
			tracker:BuffAdd(buffId, buffDetail)
		end

		if buffDetail.curse or buffDetail.disease or buffDetail.poison then
			self:CleansableBuffAdd(buffId, buffDetail)
		end
	end
end

function PlayerBuffs:RegisterBuffId(buffId, tracker)
	local buffTracking = self.BuffTracking[buffId] or {}
	table.insert(buffTracking, tracker)
	self.BuffTracking[buffId] = buffTracking
end

function PlayerBuffs:BuffChange(buffs)
	local buffDetails = Inspect.Buff.Detail(self.playerInfo.unitId, buffs)
	for buffId, buffDetail in pairs(buffDetails) do
		for id, tracker in ipairs(self.BuffTracking[buffId] or {}) do
			tracker:BuffChange(buffId, buffDetail)
		end
	end
end

function PlayerBuffs:BuffRemove(buffs)
	for buffId, v in pairs(buffs) do
		for id, tracker in ipairs(self.BuffTracking[buffId] or {}) do
			tracker:BuffRemove(buffId)
		end
		self.BuffTracking[buffId] = nil
	end
end

function PlayerBuffs:CleansableBuffAdd(buffId, buffDetail)
	for buffSetId, cleanseTracker in pairs(self.CleanseTracking or {}) do
		cleanseTracker:BuffAdd(buffId, buffDetail)
	end
end

function PlayerBuffs:AddUIBuff(buffIcon, buffSetId)
	local handlers = self.UIHandlers[buffSetId] or {}
	handlers[buffIcon] = buffIcon
	self.UIHandlers[buffSetId] = handlers

	if self.BuffTrackers[buffSetId] then
		self.BuffTrackers[buffSetId]:UpdateIcons()
	end
end

function PlayerBuffs:RemoveUIBuff(buffIcon, buffSetId)
	local handlers = self.UIHandlers[buffSetId]
    if handlers == nil then return end

    handlers[buffIcon] = nil
end

function PlayerBuffs:Tick(currentTime)
	for key, buffTracker in pairs(self.BuffTrackers) do
		buffTracker:Tick(currentTime)
	end
end

local PriorityBuff = MereHealingFrames.BuffManager.PriorityBuff

function PriorityBuff:new(playerBuffs, buffList, debuffList, debuffsOverBuffs, buffCaster, debuffCaster, buffSlots)
	local this = {}
	setmetatable(this, self)
	self.__index = self

	this.playerBuffs = playerBuffs
	this.buffList = buffList or ""
	this.debuffList = debuffList or ""
	if debuffsOverBuffs == nil then
		this.debuffsOverBuffs = true
	else
		this.debuffsOverBuffs = debuffsOverBuffs
	end
	this.buffCaster = buffCaster or "self"
	this.debuffCaster = debuffCaster or "self"
	this.buffSlots = buffSlots

	this.BuffNameToDetails = {}
	this.DeBuffNameToDetails = {}
	this.priorityToDetails = {}
	this.BuffIdToDetails = {}

	this:SetupPriorityLists()
	this:HookPlayerBuffs()

	this.CurrentPriority = nil

	return this
end

function PriorityBuff:SetupPriorityLists()
	self.CurrentPriority = 1
	if self.debuffsOverBuffs then
		self:SetupDebuffPriorityLists()
		self:SetupBuffPriorityLists()
	else
		self:SetupBuffPriorityLists()
		self:SetupDebuffPriorityLists()
	end
	self.CurrentPriority = nil
end

function PriorityBuff:SetupBuffPriorityLists()
	self.BuffNameToDetails = self:SetupPriorityList(self.buffList, true)
end

function PriorityBuff:SetupDebuffPriorityLists()
	self.DeBuffNameToDetails = self:SetupPriorityList(self.debuffList, false)
end

function PriorityBuff:SetupPriorityList(list, isBuff)
	local nameToDetails = {}
	for buffName, _ in string.gsplit(list, ",") do
		local trimmedBuffName = buffName:trim()
        if trimmedBuffName ~= "" then
            local details = {
                priority = self.CurrentPriority,
                buff = isBuff,
                name = trimmedBuffName,
                buffId = nil,
                stacks = 0,
                begin = nil,
                duration = 0,
                expiryTime = nil,
            }
            table.insert(self.priorityToDetails, details)
            nameToDetails[trimmedBuffName] = details

            self.CurrentPriority = self.CurrentPriority + 1
        end
	end
	return nameToDetails
end


function PriorityBuff:HookPlayerBuffs()
	for i, details in ipairs(self.priorityToDetails) do
		local tracking = self.playerBuffs.NameTracking[details.name] or {}
		table.insert(tracking, self)
		self.playerBuffs.NameTracking[details.name] = tracking
	end
end

function PriorityBuff:UnHookPlayerBuffs()
	local tracking = self.playerBuffs.NameTracking[details.name]
	local id = nil
	for i, tracker in ipairs(tracking) do
		if tracker == self then
			id = i
			break
		end
	end
	if id ~= nil then
		table.remove(self.playerBuffs.NameTracking, id)
	end
end

function PriorityBuff:BuffAdd(buffId, buffDetail)
	local details

	local mySpell = buffDetail.caster == MereHealingFrames.PlayerId
	local unitSpell = buffDetail.caster == self.playerBuffs.playerInfo.unitId

	MereHealingFrames.Debug(2, "new buff, mySpell: %s, unitSpell: %s", tostring(mySpell), tostring(unitSpell))
    MereHealingFrames.DebugDump(8, self.DeBuffNameToDetails, self.BuffNameToDetails )
    if buffDetail.debuff then
		if (self.debuffCaster ~= "any") and
			((self.debuffCaster == "self" and not mySpell) or
			(self.debuffCaster == "unit" and not unitSpell))
		then
			return
		end
		details = self.DeBuffNameToDetails[buffDetail.name]
	else
		if (self.buffCaster ~= "any") and
			((self.buffCaster == "self" and not mySpell) or
			(self.buffCaster == "unit" and not unitSpell))
		then
			return
		end
		details = self.BuffNameToDetails[buffDetail.name]
    end

    -- the buff/debuff has the same name, and so if we're only tracking one part of the buff/debuff
    -- we need to ignore the other part, so the above will come out with a details == nil if we're not
    -- interested in the buff/debuff  I guess we could split the buff/debuff lists
    if details == nil then return end

	details.buffId = buffId
	details.stacks = buffDetail.stack
	details.begin = buffDetail.begin
	details.duration = buffDetail.duration
	if (buffDetail.begin and buffDetail.duration) then
		details.expiryTime = buffDetail.begin + buffDetail.duration
	else
		details.expiryTime = nil
	end

	self.BuffIdToDetails[buffId] = details

	if self.CurrentPriority ~= nil then
		if (self.CurrentPriority > details.priority) then
			self.CurrentPriority = details.priority
			self:UpdateIcons()
		end
	else
		RegisterForTick(self)
		self.CurrentPriority = details.priority
		self:UpdateIcons()
	end

	self.playerBuffs:RegisterBuffId(buffId, self)
	RegisterForTick(self)
end

function PriorityBuff:BuffChange(buffId, buffDetail)
	local details = self.BuffIdToDetails[buffId]

	if not details or details.buffId ~= buffId then
		return
	end

	details.stacks = buffDetail.stack
	details.begin = buffDetail.begin
	details.duration = buffDetail.duration
	if (buffDetail.begin and buffDetail.duration) then
		details.expiryTime = buffDetail.begin + buffDetail.duration
	else
		details.expiryTime = nil
	end

	if self.CurrentPriority == details.priority then
		self:UpdateIcons()
	end
end

function PriorityBuff:BuffRemove(buffId)
	local details = self.BuffIdToDetails[buffId]
	if details and details.buffId == buffId then
		details.buffId = nil
		details.stacks = 0
		details.begin = nil
		details.duration = 0
		details.expiryTime = nil

		if self.CurrentPriority == details.priority then
			self:ScanForNewPriority()
		end
	else
		self:ScanForNewPriority()
	end

	self.BuffIdToDetails[buffId] = nil
end

function PriorityBuff:ScanForNewPriority()
	self.CurrentPriority = nil

	for i, details in ipairs(self.priorityToDetails) do
		if details.buffId ~= nil then
			self.CurrentPriority = details.priority
			break
		end
	end

	if self.CurrentPriority == nil then
		DeregisterForTick(self)
	else
		RegisterForTick(self)
	end

	self:UpdateIcons()
end

function PriorityBuff:UpdateIcons()
	local currentTime = Inspect.Time.Frame()
	if self.CurrentPriority == nil then
		self:UpdateAllIcons(function (buffIcon) buffIcon:Clear() end)
	else
		local buffDetails = self.priorityToDetails[self.CurrentPriority]

		local buffState = "buff"
		if not buffDetails.buff then
			buffState = "debuff"
		end

		local timeLeft = nil
		if buffDetails.duration then
			timeLeft = buffDetails.expiryTime - currentTime
		end

		self:UpdateAllIcons(function (buffIcon)
			buffIcon:UpdateIcon(buffDetails.name, buffDetails.stacks, buffDetails.remaining, buffState)
			buffIcon:UpdateTimer(timeLeft)
			end
			)
	end
end

function PriorityBuff:UpdateStacks(stackSize)
	self:UpdateAllIcons(function (buffIcon) buffIcon:UpdateCounter(stackSize) end)
end

function PriorityBuff:Tick(currentTime)
	local timeLeft
	local buffDetails = self.priorityToDetails[self.CurrentPriority]

	if buffDetails and buffDetails.expiryTime then
		timeLeft = buffDetails.expiryTime - currentTime
	else
		return
	end

	self:UpdateAllIcons(function (buffIcon) buffIcon:UpdateTimer(timeLeft) end)
end

function PriorityBuff:UpdateAllIcons(updateFunc)
	for i, value in pairs(self.buffSlots or {}) do
		if value then
			local Handlers = self.playerBuffs.UIHandlers[i] or {}
			for _, BuffIcon in pairs(Handlers) do
				updateFunc(BuffIcon)
			end
		end
	end
end