--[[
# Element: Castbar

Handles the visibility and updating of spell castbars.

## Widget

Castbar - A `StatusBar` to represent spell cast/channel progress.

## Sub-Widgets

.Icon     - A `Texture` to represent spell icon.
.SafeZone - A `Texture` to represent latency.
.Shield   - A `Texture` to represent if it's possible to interrupt or spell steal.
.Spark    - A `Texture` to represent the castbar's edge.
.Text     - A `FontString` to represent spell name.
.Time     - A `FontString` to represent spell duration.

## Notes

A default texture will be applied to the StatusBar and Texture widgets if they don't have a texture or a color set.

## Options

.timeToHold      - Indicates for how many seconds the castbar should be visible after a _FAILED or _INTERRUPTED
				   event. Defaults to 0 (number)
.hideTradeSkills - Makes the element ignore casts related to crafting professions (boolean)

## Attributes

.castID           - A globally unique identifier of the currently cast spell (string?)
.casting          - Indicates whether the current spell is an ordinary cast (boolean)
.channeling       - Indicates whether the current spell is a channeled cast (boolean)
.notInterruptible - Indicates whether the current spell is interruptible (boolean)
.spellName        - The name of the currently cast/channeled spell (number)

## Examples

	-- Position and size
	local Castbar = CreateFrame('StatusBar', nil, self)
	Castbar:SetSize(20, 20)
	Castbar:SetPoint('TOP')
	Castbar:SetPoint('LEFT')
	Castbar:SetPoint('RIGHT')

	-- Add a background
	local Background = Castbar:CreateTexture(nil, 'BACKGROUND')
	Background:SetAllPoints(Castbar)
	Background:SetTexture(1, 1, 1, .5)

	-- Add a spark
	local Spark = Castbar:CreateTexture(nil, 'OVERLAY')
	Spark:SetSize(20, 20)
	Spark:SetBlendMode('ADD')
	Spark:SetPoint('CENTER', Castbar:GetStatusBarTexture(), 'RIGHT', 0, 0)

	-- Add a timer
	local Time = Castbar:CreateFontString(nil, 'OVERLAY', 'GameFontNormalSmall')
	Time:SetPoint('RIGHT', Castbar)

	-- Add spell text
	local Text = Castbar:CreateFontString(nil, 'OVERLAY', 'GameFontNormalSmall')
	Text:SetPoint('LEFT', Castbar)

	-- Add spell icon
	local Icon = Castbar:CreateTexture(nil, 'OVERLAY')
	Icon:SetSize(20, 20)
	Icon:SetPoint('TOPLEFT', Castbar, 'TOPLEFT')

	-- Add Shield
	local Shield = Castbar:CreateTexture(nil, 'OVERLAY')
	Shield:SetSize(20, 20)
	Shield:SetPoint('CENTER', Castbar)

	-- Add safezone
	local SafeZone = Castbar:CreateTexture(nil, 'OVERLAY')

	-- Register it with oUF
	Castbar.bg = Background
	Castbar.Spark = Spark
	Castbar.Time = Time
	Castbar.Text = Text
	Castbar.Icon = Icon
	Castbar.Shield = Shield
	Castbar.SafeZone = SafeZone
	self.Castbar = Castbar
--]]
local _, ns = ...
local oUF = ns.oUF
local Private = oUF.Private

local select = select
local GetNetStats = GetNetStats
local GetTime = GetTime
local UnitCastingInfo = UnitCastingInfo
local UnitChannelInfo = UnitChannelInfo
local tradeskillCurrent, tradeskillTotal, mergeTradeskill = 0, 0, false

local FALLBACK_ICON = [[Interface\ICONS\INV_Misc_QuestionMark]]

local function resetAttributes(self)
	self.castID = nil
	self.casting = nil
	self.channeling = nil
	self.notInterruptible = nil
	self.spellName = nil
end

local function CastSent(self, event, unit, _, _, target)
	local element = self.Castbar
	element.target = (target and target ~= "") and target or nil
end

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

	local element = self.Castbar

	local name, _, _, texture, startTime, endTime, isTradeSkill, castID, notInterruptible = UnitCastingInfo(unit)
	event = "UNIT_SPELLCAST_START"
	if (not name) then
		name, _, _, texture, startTime, endTime, isTradeSkill, notInterruptible = UnitChannelInfo(unit)
		event = "UNIT_SPELLCAST_CHANNEL_START"
	end

	if (not name or (isTradeSkill and element.hideTradeSkills)) then
		resetAttributes(element)
		element:Hide()
		return
	end

	endTime = endTime / 1000
	startTime = startTime / 1000

	element.max = endTime - startTime
	element.startTime = startTime
	element.delay = 0
	element.casting = (event == "UNIT_SPELLCAST_START")
	element.channeling = (event == "UNIT_SPELLCAST_CHANNEL_START")
	element.notInterruptible = notInterruptible
	element.holdTime = 0
	element.castID = castID
	element.spellName = name

	if (element.casting) then
		element.duration = GetTime() - startTime
	else
		element.duration = endTime - GetTime()
	end

	if (mergeTradeskill and isTradeSkill and self.unit == "player") then
		element.duration = element.duration + (element.max * tradeskillCurrent)
		element.max = element.max * tradeskillTotal
		element.holdTime = 1
		element.tradeSkillCastId = castID

		if (unit == "player") then
			tradeskillCurrent = tradeskillCurrent + 1
		end
	end

	element:SetMinMaxValues(0, element.max)
	element:SetValue(element.duration)

	if (element.Icon) then
		element.Icon:SetTexture(texture or FALLBACK_ICON)
	end
	if (element.Spark) then
		element.Spark:Show()
	end
	if (element.Text) then
		Private.setFont(element.Text)
		element.Text:SetText(name)
	end
	if (element.Time) then
		Private.setFont(element.Text)
		element.Time:SetText()
	end
	if (element.Shield and notInterruptible) then
		element.Shield:Shown()
	elseif (element.Shield) then
		element.Shield:Hide()
	end

	local safeZone = element.SafeZone
	if (safeZone) then
		local isHoriz = element:GetOrientation() == "HORIZONTAL"

		safeZone:ClearAllPoints()
		safeZone:SetPoint(isHoriz and "TOP" or "LEFT")
		safeZone:SetPoint(isHoriz and "BOTTOM" or "RIGHT")

		local reversed = element.GetReverseFill and element:GetReverseFill()
		if (element.casting) then
			safeZone:SetPoint(reversed and (isHoriz and "LEFT" or "BOTTOM") or (isHoriz and "RIGHT" or "TOP"))
		else
			safeZone:SetPoint(reversed and (isHoriz and "RIGHT" or "TOP") or (isHoriz and "LEFT" or "BOTTOM"))
		end

		local ratio = (select(3, GetNetStats()) / 1000) / element.max
		if (ratio > 1) then
			ratio = 1
		end

		safeZone[isHoriz and "SetWidth" or "SetHeight"](safeZone, element[isHoriz and "GetWidth" or "GetHeight"](element) * ratio)
	end

	--[[ Callback: Castbar:PostChannelStart(unit, name)
	Called after the element has been updated upon a spell channel start.

	* self - the Castbar widget
	* unit - unit for which the update has been triggered (string)
	* name - name of the channeled spell (string)
	--]]
	if (element.channeling and element.PostChannelStart) then
		element:PostChannelStart(unit, name)
	end

	--[[ Callback: Castbar:PostCastStart(unit)
	Called after the element has been updated upon a spell cast or channel start.

	* self - the Castbar widget
	* unit - the unit for which the update has been triggered (string)
	--]]
	if (element.casting and element.PostCastStart) then
		element:PostCastStart(unit, name)
	end

	element:Show()
end

local function CastUpdate(self, event, unit, _, _, castID)
	if (self.unit ~= unit) then return end

	local element = self.Castbar
	if (not element:IsShown() or element.castID ~= castID) then return end

	local name, startTime, endTime, delayed, _
	if (event == "UNIT_SPELLCAST_DELAYED") then
		name, _, _, _, startTime, endTime = UnitCastingInfo(unit)
		delayed = true
	else
		name, _, _, _, startTime, endTime = UnitChannelInfo(unit)
	end

	if (not name) then return end

	endTime = endTime / 1000
	startTime = startTime / 1000

	local delta
	if (element.casting) then
		delta = startTime - element.startTime
		element.duration = GetTime() - startTime
	else
		delta = element.startTime - startTime
		element.duration = endTime - GetTime()
	end

	if (delta < 0) then
		delta = 0
	end

	element.max = endTime - startTime
	element.startTime = startTime
	element.delay = element.delay + delta

	element:SetMinMaxValues(0, element.max)
	element:SetValue(element.duration)

	--[[ Callback: Castbar:PostCastDelayed(unit, name)
	Called after the element has been updated when a spell cast has been delayed.

	* self - the Castbar widget
	* unit - unit that the update has been triggered (string)
	* name - name of the delayed spell (string)
	--]]
	if (event == "UNIT_SPELLCAST_DELAYED" and element.PostCastDelayed) then
		return element:PostCastDelayed(unit, name)
	end

	--[[ Callback: Castbar:PostChannelUpdate(unit, name)
	Called after the element has been updated after a channeled spell has been delayed or interrupted.

	* self - the Castbar widget
	* unit - unit for which the update has been triggered (string)
	* name - name of the channeled spell (string)
	--]]
	if (event == "UNIT_SPELLCAST_CHANNEL_UPDATE" and element.PostChannelUpdate) then
		return element:PostChannelUpdate(unit, name)
	end

	--[[ Callback: Castbar:PostCastUpdate(unit)
	Called after the element has been updated when a spell cast or channel has been updated.

	* self - the Castbar widget
	* unit - the unit that the update has been triggered (string)
	--]]
	if (element.PostCastUpdate) then
		return element:PostCastUpdate(unit, name)
	end
end

local function CastStop(self, event, unit, _, _, castID)
	if (self.unit ~= unit) then return end

	local element = self.Castbar
	if (not element:IsShown() or element.castID ~= castID) then return end

	if (mergeTradeskill and self.unit == "player") then
		if (tradeskillCurrent == tradeskillTotal) then
			mergeTradeskill = false
		end
	end

	resetAttributes(element)

	--[[ Callback: Castbar:PostChannelStop(unit)
	Called after the element has been updated after a channeled spell has been completed.

	* self - the Castbar widget
	* unit - unit for which the update has been triggered (string)
	--]]
	if (event == "UNIT_SPELLCAST_CHANNEL_STOP" and element.PostChannelStop) then
		return element:PostChannelStop(unit)
	end

	--[[ Callback: Castbar:PostCastStop(unit)
	Called after the element has been updated when a spell cast or channel has stopped.

	* self    - the Castbar widget
	* unit    - the unit for which the update has been triggered (string)
	--]]
	if (element.PostCastStop) then
		return element:PostCastStop(unit)
	end
end

local function CastFail(self, event, unit, _, _, castID)
	if (self.unit ~= unit) then return end

	local element = self.Castbar
	if (not element:IsShown() or element.castID ~= castID) then return end

	if (element.Text) then
		element.Text:SetText(event == "UNIT_SPELLCAST_FAILED" and FAILED or INTERRUPTED)
	end

	if (element.Spark) then
		element.Spark:Hide()
	end

	element.holdTime = element.timeToHold or 0

	if (mergeTradeskill and self.unit == "player") then
		mergeTradeskill = false
		element.tradeSkillCastId = nil
	end

	resetAttributes(element)
	element:SetValue(element.max)

	--[[ Callback: Castbar:PostCastFail(unit)
	Called after the element has been updated upon a failed or interrupted spell cast.

	* self    - the Castbar widget
	* unit    - the unit for which the update has been triggered (string)
	--]]
	if (element.PostCastFail) then
		return element:PostCastFail(unit)
	end
end

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

	local element = self.Castbar
	if (not element:IsShown()) then return end

	element.notInterruptible = (event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE")

	if (element.Shield) then
		if element.notInterruptible then
			element.Shield:Shown()
		else
			element.Shield:Hide()
		end
	end

	--[[ Callback: Castbar:PostCastNotInterruptible(unit)
	Called after the element has been updated when a spell cast has become non-interruptible.

	* self - the Castbar widget
	* unit - unit for which the update has been triggered (string)
	--]]
	if (element.notInterruptible and element.PostCastNotInterruptible) then
		return element:PostCastNotInterruptible(unit)
	end

	--[[ Callback: Castbar:PostCastInterruptible(unit)
	Called after the element has been updated when a spell cast has become interruptible or uninterruptible.

	* self - the Castbar widget
	* unit - the unit for which the update has been triggered (string)
	--]]
	if (element.PostCastInterruptible) then
		return element:PostCastInterruptible(unit)
	end
end

local function onUpdate(self, elapsed)
	if (self.casting or self.channeling) then
		local isCasting = self.casting
		if (isCasting) then
			self.duration = self.duration + elapsed
			if (self.duration >= self.max) then
				resetAttributes(self)
				self:Hide()

				if (self.PostCastStop) then
					self:PostCastStop(self.__owner.unit)
				end

				return
			end
		else
			self.duration = self.duration - elapsed
			if (self.duration <= 0) then
				resetAttributes(self)
				self:Hide()

				if (self.PostCastStop) then
					self:PostCastStop(self.__owner.unit)
				end

				return
			end
		end

		if (self.Time) then
			if (self.delay ~= 0) then
				if (self.CustomDelayText) then
					self:CustomDelayText(self.duration)
				else
					self.Time:SetFormattedText(
						"%.1f|cffff0000%s%.2f|r",
						self.duration,
						isCasting and "+" or "-",
						self.delay
					)
				end
			else
				if (self.CustomTimeText) then
					self:CustomTimeText(self.duration)
				else
					self.Time:SetFormattedText("%.1f", self.duration)
				end
			end
		end

		self:SetValue(self.duration)
	elseif (self.holdTime > 0) then
		self.holdTime = self.holdTime - elapsed
	else
		resetAttributes(self)
		self:Hide()
	end
end

local function Update(...)
	CastStart(...)
end

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

local Enable, Disable
do
	local function CastingBarFrame_SetUnit(self, unit, showTradeSkills, showShield)
		if (self.unit ~= unit) then
			self.unit = unit
			self.showTradeSkills = showTradeSkills
			self.showShield = showShield

			self.casting = nil
			self.channeling = nil
			self.holdTime = 0
			self.fadeOut = nil

			if (unit) then
				self:RegisterEvent("UNIT_SPELLCAST_START")
				self:RegisterEvent("UNIT_SPELLCAST_STOP")
				self:RegisterEvent("UNIT_SPELLCAST_FAILED")
				self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
				self:RegisterEvent("UNIT_SPELLCAST_DELAYED")
				self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
				self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
				self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
				self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTIBLE")
				self:RegisterEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE")
				self:RegisterEvent("PLAYER_ENTERING_WORLD")

				CastingBarFrame_OnEvent(self, "PLAYER_ENTERING_WORLD")
			else
				self:UnregisterEvent("UNIT_SPELLCAST_INTERRUPTED")
				self:UnregisterEvent("UNIT_SPELLCAST_DELAYED")
				self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START")
				self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
				self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
				self:UnregisterEvent("UNIT_SPELLCAST_INTERRUPTIBLE")
				self:UnregisterEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE")
				self:UnregisterEvent("PLAYER_ENTERING_WORLD")
				self:UnregisterEvent("UNIT_SPELLCAST_START")
				self:UnregisterEvent("UNIT_SPELLCAST_STOP")
				self:UnregisterEvent("UNIT_SPELLCAST_FAILED")

				self:Hide()
			end
		end
	end

	function Enable(self, unit)
		local element = self.Castbar
		if (element and unit and not unit:match("%wtarget$")) then
			element.__owner = self
			element.ForceUpdate = ForceUpdate

			self:RegisterEvent("UNIT_SPELLCAST_START", CastStart)
			self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START", CastStart)
			self:RegisterEvent("UNIT_SPELLCAST_STOP", CastStop)
			self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP", CastStop)
			self:RegisterEvent("UNIT_SPELLCAST_DELAYED", CastUpdate)
			self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", CastUpdate)
			self:RegisterEvent("UNIT_SPELLCAST_FAILED", CastFail)
			self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED", CastFail)
			self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTIBLE", CastInterruptible)
			self:RegisterEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", CastInterruptible)

			self:RegisterEvent("UNIT_SPELLCAST_SENT", CastSent, true)

			element.holdTime = 0

			element:SetScript("OnUpdate", element.OnUpdate or onUpdate)

			if (self.unit == "player" and not (self.hasChildren or self.isChild)) then
				CastingBarFrame_SetUnit(CastingBarFrame, nil)
				CastingBarFrame_SetUnit(PetCastingBarFrame, nil)
			end

			if (element:IsObjectType("StatusBar") and not element:GetStatusBarTexture()) then
				element:SetStatusBarTexture([[Interface\TargetingFrame\UI-StatusBar]])
			end

			local spark = element.Spark
			if (spark and spark:IsObjectType("Texture") and not spark:GetTexture()) then
				spark:SetTexture([[Interface\CastingBar\UI-CastingBar-Spark]])
			end

			local shield = element.Shield
			if (shield and shield:IsObjectType("Texture") and not shield:GetTexture()) then
				shield:SetTexture([[Interface\CastingBar\UI-CastingBar-Small-Shield]])
			end

			local safeZone = element.SafeZone
			if (safeZone and safeZone:IsObjectType("Texture") and not safeZone:GetTexture()) then
				safeZone:SetVertexColor(1, 0, 0)
			end

			element:Hide()

			return true
		end
	end

	function Disable(self)
		local element = self.Castbar
		if (element) then
			element:Hide()

			self:UnregisterEvent("UNIT_SPELLCAST_START", CastStart)
			self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START", CastStart)
			self:UnregisterEvent("UNIT_SPELLCAST_DELAYED", CastUpdate)
			self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", CastUpdate)
			self:UnregisterEvent("UNIT_SPELLCAST_STOP", CastStop)
			self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP", CastStop)
			self:UnregisterEvent("UNIT_SPELLCAST_FAILED", CastFail)
			self:UnregisterEvent("UNIT_SPELLCAST_INTERRUPTED", CastFail)
			self:UnregisterEvent("UNIT_SPELLCAST_INTERRUPTIBLE", CastInterruptible)
			self:UnregisterEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", CastInterruptible)
			self:UnregisterEvent("UNIT_SPELLCAST_SENT", CastSent)

			element:SetScript("OnUpdate", nil)

			if (self.unit == "player" and not (self.hasChildren or self.isChild)) then
				CastingBarFrame_OnLoad(CastingBarFrame, "player", true, false)
				CastingBarFrame_SetUnit(CastingBarFrame, "player", true, false)
				PetCastingBarFrame_OnLoad(PetCastingBarFrame)
				CastingBarFrame_SetUnit(PetCastingBarFrame, "pet", false, false)
			end
		end
	end
end

hooksecurefunc("DoTradeSkill", function(_, num)
	tradeskillCurrent = 0
	tradeskillTotal = num or 1
	mergeTradeskill = true
end)

oUF:AddElement("Castbar", Update, Enable, Disable)