local Cast = {}
local L = ShadowUF.L
local FADE_TIME = 0.30

ShadowUF:RegisterModule(Cast, "castBar", L["Cast bar"], true)

-- I'm not really thrilled with this method of detecting fake unit casts, mostly because it's inefficient and ugly
local function monitorFakeCast(self)
	local spell, displayName, icon, startTime, endTime, isTradeSkill, castID, notInterruptible, spellID = UnitCastingInfo(self.parent.unit)
	local isChannelled
	if( not spell ) then
		spell, displayName, icon, startTime, endTime, isTradeSkill, notInterruptible, spellID = UnitChannelInfo(self.parent.unit)
		isChannelled = true
	end

	-- Cast started
	if( not self.endTime and endTime ) then
		self.endTime = endTime
		self.notInterruptible = notInterruptible
		self.spellName = spell
		self.spellID = spellID
		Cast:UpdateCast(self.parent, self.parent.unit, isChannelled, spell, displayName, icon, startTime, endTime, isTradeSkill, notInterruptible, spellID, castID)
	-- Cast stopped
	elseif( self.endTime and not endTime ) then
		if( GetTime() <= (self.endTime / 1000) ) then
			Cast:EventInterruptCast(self.parent, nil, self.parent.unit, nil, self.spellID)
		end

		self.notInterruptible = nil
		self.spellName = nil
		self.endTime = nil
		return
	end

	-- Cast delayed
	if( self.endTime and endTime ~= self.endTime ) then
		self.endTime = endTime
		Cast:UpdateDelay(self.parent, spell, displayName, icon, startTime, endTime)
	end

	-- Cast interruptible status changed
	if( self.spellName and self.notInterruptible ~= notInterruptible ) then
		self.notInterruptible = notInterruptible
		if( notInterruptible ) then
			Cast:EventUninterruptible(self.parent)
		else
			Cast:EventInterruptible(self.parent)
		end
	end
end

local function createFakeCastMonitor(frame)
	if( not frame.castBar.monitor ) then
		frame.castBar.monitor = C_Timer.NewTicker(0.10, monitorFakeCast)
		frame.castBar.monitor.parent = frame
	end
end

local function cancelFakeCastMonitor(frame)
	if( frame.castBar and frame.castBar.monitor ) then
		frame.castBar.monitor:Cancel()
		frame.castBar.monitor = nil
	end
end

function Cast:OnEnable(frame)
	if( not frame.castBar ) then
		frame.castBar = CreateFrame("Frame", nil, frame)
		frame.castBar.bar = ShadowUF.Units:CreateBar(frame)
		frame.castBar.background = frame.castBar.bar.background
		frame.castBar.bar.parent = frame
		frame.castBar.bar.background = frame.castBar.background

		frame.castBar.icon = frame.castBar.bar:CreateTexture(nil, "ARTWORK")
		frame.castBar.bar.name = frame.castBar.bar:CreateFontString(nil, "ARTWORK")
		frame.castBar.bar.time = frame.castBar.bar:CreateFontString(nil, "ARTWORK")
	end

	if( ShadowUF.fakeUnits[frame.unitType] ) then
		createFakeCastMonitor(frame)
		frame:RegisterUpdateFunc(self, "UpdateFakeCast")
		return
	end

	frame:RegisterUnitEvent("UNIT_SPELLCAST_START", self, "EventUpdateCast")
	frame:RegisterUnitEvent("UNIT_SPELLCAST_STOP", self, "EventStopCast")
	frame:RegisterUnitEvent("UNIT_SPELLCAST_FAILED", self, "EventStopCast")
	frame:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", self, "EventInterruptCast")
	frame:RegisterUnitEvent("UNIT_SPELLCAST_DELAYED", self, "EventDelayCast")
	frame:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", self, "EventCastSucceeded")

	frame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", self, "EventUpdateChannel")
	frame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", self, "EventStopCast")
	--frame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_INTERRUPTED", self, "EventInterruptCast")
	frame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", self, "EventDelayChannel")

	frame:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTIBLE", self, "EventInterruptible")
	frame:RegisterUnitEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE", self, "EventUninterruptible")

	frame:RegisterUpdateFunc(self, "UpdateCurrentCast")
end

function Cast:OnLayoutApplied(frame, config)
	if( not frame.visibility.castBar ) then return end

	-- Set textures
	frame.castBar.bar:SetStatusBarTexture(ShadowUF.Layout.mediaPath.statusbar)
	frame.castBar.bar:SetStatusBarColor(0, 0, 0, 0)
	frame.castBar.bar:GetStatusBarTexture():SetHorizTile(false)
	frame.castBar.background:SetVertexColor(0, 0, 0, 0)
	frame.castBar.background:SetHorizTile(false)

	-- Setup fill
	frame.castBar.bar:SetOrientation(config.castBar.vertical and "VERTICAL" or "HORIZONTAL")
	frame.castBar.bar:SetReverseFill(config.castBar.reverse and true or false)

	-- Setup the main bar + icon
	frame.castBar.bar:ClearAllPoints()
	frame.castBar.bar:SetHeight(frame.castBar:GetHeight())
	frame.castBar.bar:SetValue(0)
	frame.castBar.bar:SetMinMaxValues(0, 1)

	-- Use the entire bars width and show the icon
	if( config.castBar.icon == "HIDE" ) then
		frame.castBar.bar:SetWidth(frame.castBar:GetWidth())
		frame.castBar.bar:SetAllPoints(frame.castBar)
		frame.castBar.icon:Hide()
	-- Shift the bar to the side and show an icon
	else
		frame.castBar.bar:SetWidth(frame.castBar:GetWidth() - frame.castBar:GetHeight())
		frame.castBar.icon:ClearAllPoints()
		frame.castBar.icon:SetWidth(frame.castBar:GetHeight())
		frame.castBar.icon:SetHeight(frame.castBar:GetHeight())
		frame.castBar.icon:Show()

		if( config.castBar.icon == "LEFT" ) then
			frame.castBar.bar:SetPoint("TOPLEFT", frame.castBar, "TOPLEFT", frame.castBar:GetHeight() + 1, 0)
			frame.castBar.icon:SetPoint("TOPRIGHT", frame.castBar.bar, "TOPLEFT", -1, 0)
		else
			frame.castBar.bar:SetPoint("TOPLEFT", frame.castBar, "TOPLEFT", 1, 0)
			frame.castBar.icon:SetPoint("TOPLEFT", frame.castBar.bar, "TOPRIGHT", 0, 0)
		end
	end

	-- Set the font at the very least, so it doesn't error when we set text on it even if it isn't being shown
	ShadowUF.Layout:ToggleVisibility(frame.castBar.bar.name, config.castBar.name.enabled)
	if( config.castBar.name.enabled ) then
		frame.castBar.bar.name:SetParent(frame.highFrame)
		frame.castBar.bar.name:SetWidth(frame.castBar.bar:GetWidth() * 0.75)
		frame.castBar.bar.name:SetHeight(ShadowUF.db.profile.font.size + 1)
		frame.castBar.bar.name:SetJustifyH(ShadowUF.Layout:GetJustify(config.castBar.name))

		ShadowUF.Layout:AnchorFrame(frame.castBar.bar, frame.castBar.bar.name, config.castBar.name)
		ShadowUF.Layout:SetupFontString(frame.castBar.bar.name, config.castBar.name.size)
	end

	ShadowUF.Layout:ToggleVisibility(frame.castBar.bar.time, config.castBar.time.enabled)
	if( config.castBar.time.enabled ) then
		frame.castBar.bar.time:SetParent(frame.highFrame)
		frame.castBar.bar.time:SetWidth(frame.castBar.bar:GetWidth() * 0.25)
		frame.castBar.bar.time:SetHeight(ShadowUF.db.profile.font.size + 1)
		frame.castBar.bar.time:SetJustifyH(ShadowUF.Layout:GetJustify(config.castBar.time))

		ShadowUF.Layout:AnchorFrame(frame.castBar.bar, frame.castBar.bar.time, config.castBar.time)
		ShadowUF.Layout:SetupFontString(frame.castBar.bar.time, config.castBar.time.size)
	end

	-- So we don't have to check the entire thing in an OnUpdate
	frame.castBar.bar.time.enabled = config.castBar.time.enabled

	if( config.castBar.autoHide and not UnitCastingInfo(frame.unit) and not UnitChannelInfo(frame.unit) ) then
		ShadowUF.Layout:SetBarVisibility(frame, "castBar", false)
	end
end

function Cast:OnDisable(frame, unit)
	frame:UnregisterAll(self)

	if( frame.castBar ) then
		cancelFakeCastMonitor(frame)

		frame.castBar.bar.name:Hide()
		frame.castBar.bar.time:Hide()
		frame.castBar.bar:Hide()
	end
end

-- Easy coloring
local function setBarColor(self, r, g, b)
	self.parent:SetBlockColor(self, "castBar", r, g, b)
end

-- Cast OnUpdates
local function fadeOnUpdate(self, elapsed)
	self.fadeElapsed = self.fadeElapsed - elapsed

	if( self.fadeElapsed <= 0 ) then
		self.fadeElapsed = nil
		self.name:Hide()
		self.time:Hide()
		self:Hide()

		local frame = self:GetParent()
		if( ShadowUF.db.profile.units[frame.unitType].castBar.autoHide ) then
			ShadowUF.Layout:SetBarVisibility(frame, "castBar", false)
		end
	else
		local alpha = self.fadeElapsed / self.fadeStart
		self:SetAlpha(alpha)
		self.time:SetAlpha(alpha)
		self.name:SetAlpha(alpha)
	end
end

local function castOnUpdate(self, elapsed)
	local time = GetTime()
	self.elapsed = self.elapsed + (time - self.lastUpdate)
	self.lastUpdate = time
	self:SetValue(self.elapsed)

	if( self.elapsed <= 0 ) then
		self.elapsed = 0
	end

	if( self.time.enabled ) then
		local timeLeft = self.endSeconds - self.elapsed
		if( timeLeft <= 0 ) then
			self.time:SetText("0.0")
		elseif( self.pushback == 0 ) then
			self.time:SetFormattedText("%.1f", timeLeft)
		else
			self.time:SetFormattedText("|cffff0000%.1f|r %.1f", self.pushback, timeLeft)
		end
	end

	-- Cast finished, do a quick fade
	if( self.elapsed >= self.endSeconds ) then
		setBarColor(self, ShadowUF.db.profile.castColors.finished.r, ShadowUF.db.profile.castColors.finished.g, ShadowUF.db.profile.castColors.finished.b)

		self.spellName = nil
		self.fadeElapsed = FADE_TIME
		self.fadeStart = FADE_TIME
		self:SetScript("OnUpdate", fadeOnUpdate)
	end
end

local function channelOnUpdate(self, elapsed)
	local time = GetTime()
	self.elapsed = self.elapsed - (time - self.lastUpdate)
	self.lastUpdate = time
	self:SetValue(self.elapsed)

	if( self.elapsed <= 0 ) then
		self.elapsed = 0
	end

	if( self.time.enabled ) then
		if( self.elapsed <= 0 ) then
			self.time:SetText("0.0")
		elseif( self.pushback == 0 ) then
			self.time:SetFormattedText("%.1f", self.elapsed)
		else
			self.time:SetFormattedText("|cffff0000%.1f|r %.1f", self.pushback, self.elapsed)
		end
	end

	-- Channel finished, do a quick fade
	if( self.elapsed <= 0 ) then
		setBarColor(self, ShadowUF.db.profile.castColors.finished.r, ShadowUF.db.profile.castColors.finished.g, ShadowUF.db.profile.castColors.finished.b)

		self.spellName = nil
		self.fadeElapsed = FADE_TIME
		self.fadeStart = FADE_TIME
		self:SetScript("OnUpdate", fadeOnUpdate)
	end
end

function Cast:UpdateCurrentCast(frame)
	if( UnitCastingInfo(frame.unit) ) then
		local name, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible, spellID = UnitCastingInfo(frame.unit)
		self:UpdateCast(frame, frame.unit, false, name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID, castID)
	elseif( UnitChannelInfo(frame.unit) ) then
		local name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID = UnitChannelInfo(frame.unit)
		self:UpdateCast(frame, frame.unit, true, name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID)
	else
		if( ShadowUF.db.profile.units[frame.unitType].castBar.autoHide ) then
			ShadowUF.Layout:SetBarVisibility(frame, "castBar", false)
		end

		setBarColor(frame.castBar.bar, 0, 0, 0)

		frame.castBar.bar.spellName = nil
		frame.castBar.bar.name:Hide()
		frame.castBar.bar.time:Hide()
		frame.castBar.bar:Hide()
	end
end

-- Cast updated/changed
function Cast:EventUpdateCast(frame)
	local name, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible, spellID = UnitCastingInfo(frame.unit)
	self:UpdateCast(frame, frame.unit, false, name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID, castID)
end

function Cast:EventDelayCast(frame)
	local name, text, texture, startTime, endTime, isTradeSkill, castID, notInterruptible, spellID = UnitCastingInfo(frame.unit)
	self:UpdateDelay(frame, name, text, texture, startTime, endTime)
end

-- Channel updated/changed
function Cast:EventUpdateChannel(frame)
	local name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID = UnitChannelInfo(frame.unit)
	self:UpdateCast(frame, frame.unit, true, name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID)
end

function Cast:EventDelayChannel(frame)
	local name, text, texture, startTime, endTime, isTradeSkill, notInterruptible, spellID = UnitChannelInfo(frame.unit)
	self:UpdateDelay(frame, name, text, texture, startTime, endTime)
end

-- Cast finished
function Cast:EventStopCast(frame, event, unit, castID, spellID)
	local cast = frame.castBar.bar
	if( event == "UNIT_SPELLCAST_CHANNEL_STOP" and not castID ) then castID = spellID end
	if( cast.castID ~= castID or ( event == "UNIT_SPELLCAST_FAILED" and cast.isChannelled ) ) then return end
	if( cast.time.enabled ) then
		cast.time:SetText("0.0")
	end

	--setBarColor(cast, ShadowUF.db.profile.castColors.interrupted.r, ShadowUF.db.profile.castColors.interrupted.g, ShadowUF.db.profile.castColors.interrupted.b)
	if( ShadowUF.db.profile.units[frame.unitType].castBar.autoHide ) then
		ShadowUF.Layout:SetBarVisibility(frame, "castBar", true)
	end

	cast.spellName = nil
	cast.spellID = nil
	cast.castID = nil
	cast.fadeElapsed = FADE_TIME
	cast.fadeStart = FADE_TIME
	cast:SetScript("OnUpdate", fadeOnUpdate)
	cast:SetMinMaxValues(0, 1)
	cast:SetValue(1)
	cast:Show()
end

-- Cast interrupted
function Cast:EventInterruptCast(frame, event, unit, castID, spellID)
	local cast = frame.castBar.bar
	if( castID and cast.castID ~= castID ) then return end

	setBarColor(cast, ShadowUF.db.profile.castColors.interrupted.r, ShadowUF.db.profile.castColors.interrupted.g, ShadowUF.db.profile.castColors.interrupted.b)
	if( ShadowUF.db.profile.units[frame.unitType].castBar.autoHide ) then
		ShadowUF.Layout:SetBarVisibility(frame, "castBar", true)
	end

	if( ShadowUF.db.profile.units[frame.unitType].castBar.name.enabled ) then
		cast.name:SetText(L["Interrupted"])
	end

	cast.spellID = nil
	cast.fadeElapsed = FADE_TIME + 0.20
	cast.fadeStart = cast.fadeElapsed
	cast:SetScript("OnUpdate", fadeOnUpdate)
	cast:SetMinMaxValues(0, 1)
	cast:SetValue(1)
	cast:Show()
end

-- Cast succeeded
function Cast:EventCastSucceeded(frame, event, unit, castID, spellID)
	local cast = frame.castBar.bar
	if( not cast.isChannelled and cast.castID == castID ) then
		setBarColor(cast, ShadowUF.db.profile.castColors.finished.r, ShadowUF.db.profile.castColors.finished.g, ShadowUF.db.profile.castColors.finished.b)
	end
end

-- Interruptible status changed
function Cast:EventInterruptible(frame)
	local cast = frame.castBar.bar
	if( cast.isChannelled ) then
		setBarColor(cast, ShadowUF.db.profile.castColors.channel.r, ShadowUF.db.profile.castColors.channel.g, ShadowUF.db.profile.castColors.channel.b)
	else
		setBarColor(cast, ShadowUF.db.profile.castColors.cast.r, ShadowUF.db.profile.castColors.cast.g, ShadowUF.db.profile.castColors.cast.b)
	end
end

function Cast:EventUninterruptible(frame)
	setBarColor(frame.castBar.bar, ShadowUF.db.profile.castColors.uninterruptible.r, ShadowUF.db.profile.castColors.uninterruptible.g, ShadowUF.db.profile.castColors.uninterruptible.b)
end

function Cast:UpdateDelay(frame, spell, displayName, icon, startTime, endTime)
	if( not spell or not frame.castBar.bar.startTime ) then return end
	local cast = frame.castBar.bar
	startTime = startTime / 1000
	endTime = endTime / 1000

	-- For a channel, delay is a negative value so using plus is fine here
	local delay = startTime - cast.startTime
	if( not cast.isChannelled ) then
		cast.endSeconds = cast.endSeconds + delay
		cast:SetMinMaxValues(0, cast.endSeconds)
	else
		cast.elapsed = cast.elapsed + delay
	end

	cast.pushback = cast.pushback + delay
	cast.lastUpdate = GetTime()
	cast.startTime = startTime
	cast.endTime = endTime
end

-- Update the actual bar
function Cast:UpdateCast(frame, unit, channelled, spell, displayName, icon, startTime, endTime, isTradeSkill, notInterruptible, spellID, castID)
	if( not spell ) then return end
	local cast = frame.castBar.bar
	if( ShadowUF.db.profile.units[frame.unitType].castBar.autoHide ) then
		ShadowUF.Layout:SetBarVisibility(frame, "castBar", true)
	end

	-- Set casted spell
	if( ShadowUF.db.profile.units[frame.unitType].castBar.name.enabled ) then
		cast.name:SetText(spell)
		cast.name:SetAlpha(ShadowUF.db.profile.bars.alpha)
		cast.name:Show()
	end

	-- Show cast time
	if( cast.time.enabled ) then
		cast.time:SetAlpha(1)
		cast.time:Show()
	end

	-- Set spell icon
	if( ShadowUF.db.profile.units[frame.unitType].castBar.icon ~= "HIDE" ) then
		frame.castBar.icon:SetTexture(icon)
		frame.castBar.icon:Show()
	end

	-- Setup cast info
	cast.isChannelled = channelled
	cast.startTime = startTime / 1000
	cast.endTime = endTime / 1000
	cast.endSeconds = cast.endTime - cast.startTime
	cast.elapsed = cast.isChannelled and cast.endSeconds or 0
	cast.spellName = spell
	cast.spellID = spellID
	cast.castID = channelled and spellID or castID
	cast.pushback = 0
	cast.lastUpdate = cast.startTime
	cast:SetMinMaxValues(0, cast.endSeconds)
	cast:SetValue(cast.elapsed)
	cast:SetAlpha(ShadowUF.db.profile.bars.alpha)
	cast:Show()

	if( cast.isChannelled ) then
		cast:SetScript("OnUpdate", channelOnUpdate)
	else
		cast:SetScript("OnUpdate", castOnUpdate)
	end

	if( notInterruptible ) then
		setBarColor(cast, ShadowUF.db.profile.castColors.uninterruptible.r, ShadowUF.db.profile.castColors.uninterruptible.g, ShadowUF.db.profile.castColors.uninterruptible.b)
	elseif( cast.isChannelled ) then
		setBarColor(cast, ShadowUF.db.profile.castColors.channel.r, ShadowUF.db.profile.castColors.channel.g, ShadowUF.db.profile.castColors.channel.b)
	else
		setBarColor(cast, ShadowUF.db.profile.castColors.cast.r, ShadowUF.db.profile.castColors.cast.g, ShadowUF.db.profile.castColors.cast.b)
	end
end

-- Trigger checks on fake cast
function Cast:UpdateFakeCast(f)
	local monitor = f.castBar.monitor
	monitor.endTime = nil
	monitor.notInterruptible = nil
	monitor.spellName = nil
	monitor.spellID = nil
	monitorFakeCast(monitor)
end
