local _, ns = ...
local oUF = ns.oUF
local AuraFiltered = oUF.AuraFiltered

local LibDispel = LibStub('LibDispel-1.0')
local DebuffColors = LibDispel:GetDebuffTypeColor()

local VISIBLE = 1
local HIDDEN = 0

local next = next
local wipe = wipe
local pcall = pcall
local unpack = unpack
local tinsert = tinsert

local GetTime = GetTime
local CreateFrame = CreateFrame
local UnitIsEnemy = UnitIsEnemy
local UnitReaction = UnitReaction
local GameTooltip = GameTooltip

local WrapString = C_StringUtil and C_StringUtil.WrapString
local GetAuraApplicationDisplayCount = C_UnitAuras.GetAuraApplicationDisplayCount
local GetAuraDuration = C_UnitAuras.GetAuraDuration

local function OnEnter(self)
	if GameTooltip:IsForbidden() or not self:IsVisible() then return end

	-- Avoid parenting GameTooltip to frames with anchoring restrictions,
	-- otherwise it'll inherit said restrictions which will cause issues with
	-- its further positioning, clamping, etc
	GameTooltip:SetOwner(self, (self.__owner.__restricted and 'ANCHOR_CURSOR') or self.__owner.tooltipAnchor, self.__owner.tooltipAnchorX, self.__owner.tooltipAnchorY)

	-- we need compatibility here because this wasnt implemented on Era or Mists
	oUF:SetTooltipByAuraInstanceID(GameTooltip, self.unit, self.auraInstanceID, self.filter)
end

local function OnLeave()
	if GameTooltip:IsForbidden() then return end

	GameTooltip:Hide()
end

local function UpdateValue(bar, start)
	if oUF.isRetail then
		local remain = bar.auraDuration and bar.auraDuration:GetRemainingDuration()
		if remain then
			bar:SetMinMaxValues(0, bar.aura.duration)
			bar:SetValue(remain, bar.smoothing)
		end
	else
		bar:SetMinMaxValues(0, bar.duration)

		local remain = (bar.expiration - GetTime()) / (bar.modRate or 1)
		if start and bar.SetValue_ then
			bar:SetValue_(remain)
		else
			bar:SetValue(remain)
		end
	end
end

local function OnUpdate(bar, elapsed)
	bar.elapsed = (bar.elapsed or 0) + elapsed

	if bar.elapsed > 0.01 then
		UpdateValue(bar)

		bar.elapsed = 0
	end
end

local function CreateAuraBar(element, index)
	local bar = CreateFrame('StatusBar', element:GetName() .. 'StatusBar' .. index, element)
	bar:SetStatusBarTexture([[Interface\TargetingFrame\UI-StatusBar]])
	bar:SetMinMaxValues(0, 1)
	bar:SetScript('OnEnter', OnEnter)
	bar:SetScript('OnLeave', OnLeave)
	bar:EnableMouse(false)

	local spark = bar:CreateTexture(nil, "OVERLAY", nil);
	spark:SetTexture([[Interface\CastingBar\UI-CastingBar-Spark]])
	spark:SetWidth(12)
	spark:SetBlendMode('ADD')
	spark:SetPoint('CENTER', bar:GetStatusBarTexture(), 'RIGHT')

	local icon = bar:CreateTexture(nil, 'ARTWORK')
	icon:SetPoint('RIGHT', bar, 'LEFT', -element.barSpacing, 0)
	icon:SetSize(element.height, element.height)

	local Cooldown = CreateFrame('Cooldown', '$parentCooldown', bar, 'CooldownFrameTemplate')

	local nameText = bar:CreateFontString(nil, 'OVERLAY', 'NumberFontNormal')
	nameText:SetPoint('LEFT', bar, 'LEFT', 2, 0)

	bar.icon = icon
	bar.spark = spark
	bar.Cooldown = Cooldown
	bar.nameText = nameText
	bar.__owner = element

	if(element.PostCreateBar) then element:PostCreateBar(bar) end

	return bar
end

local function CustomFilter(frame, element, unit, bar, aura)
	if (element.onlyShowPlayer and bar.isPlayer) or (not element.onlyShowPlayer and aura.auraInstanceID) then
		return true
	end
end

local function UpdateBar(element, bar)
	if oUF:IsSecretValue(bar.count) then
		if bar.aura then
			local minCount, maxCount = 2, 999
			local count = GetAuraApplicationDisplayCount(bar.unit, bar.aura.auraInstanceID, minCount, maxCount)
			bar.nameText:SetFormattedText('%s%s', count and WrapString(count, '[', '] ') or '', bar.spell)
		else
			bar.nameText:SetText(bar.spell)
		end
	elseif bar.count > 1 then
		bar.nameText:SetFormattedText('[%d] %s', bar.count, bar.spell)
	else
		bar.nameText:SetText(bar.spell)
	end

	if not bar.noTime and element.sparkEnabled then
		bar.spark:Show()
	else
		bar.spark:Hide()
	end

	local r, g, b = .2, .6, 1
	local debuffType = bar.debuffType
	if element.buffColor then
		r, g, b = unpack(element.buffColor)
	end

	if bar.filter == 'HARMFUL' then
		if oUF:IsSecretValue(debuffType) or not debuffType or debuffType == '' then
			debuffType = 'None'
		end

		local color = DebuffColors[debuffType]
		r, g, b = color.r, color.g, color.b
	end

	bar.icon:SetTexture(bar.texture)
	bar.icon:SetSize(element.height, element.height)
	bar:SetStatusBarColor(r, g, b)
	bar:SetSize(element.width, element.height)
	bar:EnableMouse(not element.disableMouse)
	bar:SetID(bar.index)
	bar:Show()

	if element.PostUpdateBar then
		element:PostUpdateBar(bar.unit, bar, bar.index, bar.position, bar.duration, bar.expiration, debuffType, bar.isStealable)
	end
end

local function AuraUpdate(frame, element, unit, aura, index, offset, filter, isDebuff, visible)
	local name, texture, count, debuffType, duration, expiration, source, isStealable, nameplateShowPersonal, spellID, canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, modRate, effect1, effect2, effect3 = oUF:UnpackAuraData(aura)
	if not name then return end

	local position = visible + offset + 1
	local bar = element[position]
	if not bar then
		bar = (element.CreateBar or CreateAuraBar) (element, position)
		tinsert(element, bar)
		element.createdBars = element.createdBars + 1
	end

	element.active[position] = bar

	bar.aura = aura
	bar.unit = unit
	bar.count = count
	bar.index = index
	bar.filter = filter
	bar.texture = texture
	bar.isDebuff = isDebuff
	bar.debuffType = debuffType
	bar.isStealable = isStealable
	bar.isPlayer = oUF:NotSecretValue(source) and (source == 'player' or source == 'vehicle') or (aura and aura.auraIsPlayer) or nil
	bar.position = position
	bar.duration = duration
	bar.expiration = expiration
	bar.modRate = modRate
	bar.spellID = spellID
	bar.spell = name
	bar.auraInstanceID = aura and aura.auraInstanceID or nil
	bar.auraDuration = aura and GetAuraDuration and GetAuraDuration(unit, aura.auraInstanceID) or nil
	bar.noTime = oUF:NotSecretValue(duration) and (duration == 0 and expiration == 0)

	if bar.Cooldown then -- same as what is in the auras file
		if bar.Cooldown.SetCooldownFromDurationObject then
			if bar.auraDuration then
				bar.Cooldown:SetCooldownFromDurationObject(bar.auraDuration)
			else
				bar.Cooldown:Clear()
			end
		elseif(duration and duration > 0) then
			bar.Cooldown:SetCooldown(expiration - duration, duration, modRate)
		else
			bar.Cooldown:Clear()
		end
	end

	local show = (element.CustomFilter or CustomFilter) (frame, element, unit, bar, aura, name, texture,
		count, debuffType, duration, expiration, source, isStealable, nameplateShowPersonal, spellID,
		canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, modRate, effect1, effect2, effect3)

	UpdateBar(element, bar)

	if bar.noTime then
		bar:SetScript('OnUpdate', nil)
	else
		UpdateValue(bar, true)

		bar:SetScript('OnUpdate', OnUpdate)
	end

	return show and VISIBLE or HIDDEN
end

local function SetPosition(element, from, to)
	local height = element.height
	local spacing = element.spacing
	local anchor = element.initialAnchor
	local barSpacing = element.barSpacing
	local growth = element.growth == 'DOWN' and -1 or 1

	for i = from, to do
		local bar = element.active[i]
		if not bar then break end

		bar:ClearAllPoints()
		bar:SetPoint(anchor, element, anchor, barSpacing, (i == 1 and 0) or (growth * ((i - 1) * (height + spacing))))

		if bar.noTime then
			bar:SetValue(1, bar.smoothing)
		end
	end
end

local function FilterBars(frame, element, unit, filter, limit, isDebuff, offset, dontHide)
	if(not offset) then offset = 0 end
	local visible = 0
	local hidden = 0

	local index = 1
	local unitAuraFiltered = AuraFiltered[filter][unit]
	local auraInstanceID, aura = next(unitAuraFiltered)
	while aura and (visible < limit) do
		local result = AuraUpdate(frame, element, unit, aura, index, offset, filter, isDebuff, visible)
		if result == VISIBLE then
			visible = visible + 1
		elseif result == HIDDEN then
			hidden = hidden + 1
		end

		index = index + 1
		auraInstanceID, aura = next(unitAuraFiltered, auraInstanceID)
	end

	if(not dontHide) then
		for i = visible + offset + 1, #element do
			element[i]:Hide()
		end
	end

	return visible, hidden
end

local function UpdateAuras(self, event, unit, updateInfo)
	local element = self.AuraBars
	if not element then return end

	if oUF:ShouldSkipAuraUpdate(self, event, unit, updateInfo) then return end

	if(element.PreUpdate) then element:PreUpdate(unit) end

	wipe(element.active)

	local isEnemy = UnitIsEnemy(unit, 'player')
	local reaction = UnitReaction(unit, 'player')
	local filter = (not isEnemy and (not reaction or reaction > 4) and (element.friendlyAuraType or 'HELPFUL')) or element.enemyAuraType or 'HARMFUL'
	local visibleAuras = FilterBars(self, element, unit, filter, element.maxBars, filter == 'HARMFUL', 0)

	element.visibleAuras = visibleAuras

	local fromRange, toRange
	if(element.PreSetPosition) then
		fromRange, toRange = element:PreSetPosition(element.maxBars)
	end

	if(fromRange or element.createdBars > element.anchoredBars) then
		(element.SetPosition or SetPosition) (element, fromRange or element.anchoredBars + 1, toRange or element.createdBars)
		element.anchoredBars = element.createdBars
	end

	if(element.PostUpdate) then element:PostUpdate(unit) end
end

local function Update(self, event, unit)
	if(self.unit ~= unit) then return end

	UpdateAuras(self, event, unit)

	-- Assume no event means someone wants to re-anchor things. This is usually
	-- done by UpdateAllElements and :ForceUpdate.
	if(event == 'ForceUpdate' or not event) then
		local element = self.AuraBars
		if(element) then
			(element.SetPosition or SetPosition) (element, 1, element.createdBars)
		end
	end
end

local function ForceUpdate(element)
	return Update(element.__owner, 'ForceUpdate', element.__owner.unit)
end

local function Enable(self)
	local element = self.AuraBars

	if(element) then
		self:RegisterEvent('UNIT_AURA', UpdateAuras)

		element.__owner = self
		element.ForceUpdate = ForceUpdate
		element.active = {}

		element.anchoredBars = 0
		element.createdBars = element.createdBars or 0
		element.width = element.width or 240
		element.height = element.height or 12
		element.sparkEnabled = element.sparkEnabled or true
		element.spacing = element.spacing or 2
		element.initialAnchor = element.initialAnchor or 'BOTTOMLEFT'
		element.growth = element.growth or 'UP'
		element.maxBars = element.maxBars or 32
		element.barSpacing = element.barSpacing or 2
		element.tooltipAnchor = element.tooltipAnchor or 'ANCHOR_BOTTOMRIGHT'
		element.tooltipAnchorX = element.tooltipAnchorX or 0
		element.tooltipAnchorY = element.tooltipAnchorY or 0

		-- Avoid parenting GameTooltip to frames with anchoring restrictions,
		-- otherwise it'll inherit said restrictions which will cause issues
		-- with its further positioning, clamping, etc
		element.__restricted = not pcall(self.GetCenter, self)

		element:Show()

		return true
	end
end

local function Disable(self)
	local element = self.AuraBars

	if(element) then
		self:UnregisterEvent('UNIT_AURA', UpdateAuras)

		element:Hide()
	end
end

oUF:AddElement('AuraBars', Update, Enable, Disable)
