local MER, W, WF, F, E, I, V, P, G, L = unpack(ElvUI_MerathilisUI)
local module = MER:GetModule("MER_Cooldown")
local MERS = MER:GetModule("MER_Skins")
local WS = W:GetModule("Skins")
local S = E:GetModule("Skins")

local GetTime = GetTime
local select, pairs, unpack = select, pairs, unpack
local band = bit.band
local wipe = wipe
local tinsert, tremove = table.insert, table.remove

local CreateFrame = CreateFrame
local GetPetActionInfo = GetPetActionInfo
local GetSpellName = C_Spell.GetSpellName
local GetSpellTexture = C_Spell.GetSpellTexture
local GetSpellCooldown = C_Spell.GetSpellCooldown
local GetItemInfo = C_Item.GetItemInfo
local GetPetActionCooldown = GetPetActionCooldown
local IsInInstance = IsInInstance
local GetActionInfo = GetActionInfo
local GetActionTexture = C_Spell and C_Spell.GetActionTexture or GetActionTexture
local GetInventoryItemID = GetInventoryItemID
local GetInventoryItemTexture = GetInventoryItemTexture
local CombatLogGetCurrentEventInfo = CombatLogGetCurrentEventInfo
local hooksecurefunc = hooksecurefunc

local GetItemCooldown = C_Container.GetItemCooldown
local GetContainerItemID = C_Container.GetContainerItemID
local SpeakText = C_VoiceChat.SpeakText

local ignoredSpells, invertIgnored
module.cooldowns, module.animating, module.watching = {}, {}, {}

local DCP = CreateFrame("Frame", nil, E.UIParent)
DCP:SetAlpha(0)
DCP:SetScript("OnEvent", function(self, event, ...)
	self[event](self, ...)
end)
module.DCP = DCP

local DCPT = DCP:CreateTexture(nil, "BORDER")
DCPT:SetAllPoints(DCP)
-- MERS:CreateBDFrame(DCP)
MERS:CreateSD(DCP, 2, 2)
WS:CreateBackdropShadow(DCP)

local defaultSettings = {
	enable = false,
	fadeInTime = 0.3,
	fadeOutTime = 0.6,
	maxAlpha = 0.8,
	animScale = 1.5,
	iconSize = 50,
	holdTime = 0.3,
	petOverlay = { 1, 1, 1, 1 },
	ignoredSpells = {},
	invertIgnored = false,
	enablePet = false,
	x = UIParent:GetWidth() * UIParent:GetEffectiveScale() / 2,
	y = UIParent:GetHeight() * UIParent:GetEffectiveScale() / 2,
}

local defaultSettingsPerChar = {
	ignoredSpells = {},
	invertIgnored = false,
}

-----------------------
-- Utility Functions --
-----------------------
local function tcount(tab)
	local n = 0
	for _ in pairs(tab) do
		n = n + 1
	end

	return n
end

local function memoize(f)
	local cache = nil

	local memoized = {}

	local function get()
		if cache == nil then
			cache = f()
		end

		return cache
	end

	memoized.resetCache = function()
		cache = nil
	end

	setmetatable(memoized, { __call = get })

	return memoized
end

local function GetPetActionIndexByName(name)
	for i = 1, _G.NUM_PET_ACTION_SLOTS, 1 do
		if GetPetActionInfo(i) == name then
			return i
		end
	end

	return nil
end

--------------------------
-- Cooldown / Animation --
--------------------------
local elapsed = 0
local runtimer = 0
local function OnUpdate(_, update)
	module.db = E.db.mui.cooldownFlash
	elapsed = elapsed + update
	if elapsed > 0.05 then
		for i, v in pairs(module.watching) do
			if GetTime() >= v[1] + 0.5 then
				local getCooldownDetails
				if v[2] == "spell" then
					getCooldownDetails = memoize(function()
						local cooldownInfo = GetSpellCooldown(v[3])
						return {
							name = GetSpellName(v[3]),
							texture = GetSpellTexture(v[3]),
							start = cooldownInfo.startTime,
							duration = cooldownInfo.duration,
							enabled = cooldownInfo.isEnabled,
							modRate = cooldownInfo.modRate,
						}
					end)
				elseif v[2] == "item" then
					getCooldownDetails = memoize(function()
						local start, duration, enabled = GetItemCooldown(i)

						return {
							name = GetItemInfo(i),
							texture = v[3],
							start = start,
							duration = duration,
							enabled = enabled,
						}
					end)
				elseif v[2] == "pet" then
					getCooldownDetails = memoize(function()
						local name, texture = GetPetActionInfo(v[3])
						local start, duration, enabled = GetPetActionCooldown(v[3])
						return {
							name = name,
							texture = texture,
							isPet = true,
							start = start,
							duration = duration,
							enabled = enabled,
						}
					end)
				end

				local cooldown = getCooldownDetails()
				if (module.db.ignoredSpells[cooldown.name] ~= nil) ~= module.db.invertIgnored then
					module.watching[i] = nil
				else
					if cooldown.enabled ~= 0 then
						if cooldown.duration and cooldown.duration > 2.0 and cooldown.texture then
							module.cooldowns[i] = getCooldownDetails
						end
					end
					if not (cooldown.enabled == 0 and v[2] == "spell") then
						module.watching[i] = nil
					end
				end
			end
		end

		for i, getCooldownDetails in pairs(module.cooldowns) do
			local cooldown = getCooldownDetails()
			if not cooldown.duration or not cooldown.start then
				return
			end
			local remaining = cooldown.duration - (GetTime() - cooldown.start)
			if remaining <= 0 then
				tinsert(module.animating, { cooldown.texture, cooldown.isPet, cooldown.name })
				module.cooldowns[i] = nil
			end
		end

		elapsed = 0
		if #module.animating == 0 and tcount(module.watching) == 0 and tcount(module.cooldowns) == 0 then
			DCP:SetScript("OnUpdate", nil)
			return
		end
	end

	if #module.animating > 0 then
		runtimer = runtimer + update
		if runtimer > (module.db.fadeInTime + module.db.holdTime + module.db.fadeOutTime) then
			tremove(module.animating, 1)
			runtimer = 0
			DCPT:SetTexture(nil)
			DCPT:SetVertexColor(1, 1, 1, 1)
			DCP:SetAlpha(0)
			DCP:SetSize(module.db.iconSize, module.db.iconSize)
		else
			if not DCPT:GetTexture() then
				DCPT:SetTexture(module.animating[1][1])
				S:HandleIcon(DCPT)
				if module.animating[1][2] then
					DCPT:SetVertexColor(unpack(module.db.petOverlay))
				end
				if module.db.tts then
					local tts = GetSpellName(module.animating[1][3])
					if module.db.ttsvoice and tts then
						SpeakText(
							module.db.ttsvoice,
							tts,
							Enum.VoiceTtsDestination.LocalPlayback,
							0,
							module.db.ttsvolume
						)
					end
				end
			end
			local alpha = module.db.maxAlpha
			if runtimer < module.db.fadeInTime then
				alpha = module.db.maxAlpha * (runtimer / module.db.fadeInTime)
			elseif runtimer >= module.db.fadeInTime + module.db.holdTime then
				alpha = module.db.maxAlpha
					- (
						module.db.maxAlpha
						* ((runtimer - module.db.holdTime - module.db.fadeInTime) / module.db.fadeOutTime)
					)
			end
			DCP:SetAlpha(alpha)
			local scale = module.db.iconSize
				+ (
					module.db.iconSize
					* (
						(module.db.animScale - 1)
						* (runtimer / (module.db.fadeInTime + module.db.holdTime + module.db.fadeOutTime))
					)
				)
			DCP:SetWidth(scale)
			DCP:SetHeight(scale)
		end
	end
end

--------------------
-- Event Handlers --
--------------------
function DCP:ADDON_LOADED()
	if not MERData_DCP then
		MERData_DCP = { unpack(defaultSettings) }
	else
		for i, v in pairs(defaultSettings) do
			if not MERData_DCP[i] then
				MERData_DCP[i] = v
			end
		end
	end

	if not MERData_DCPCharacter then
		MERData_DCPCharacter = { unpack(defaultSettingsPerChar) }

		if MERData_DCP.ignoredSpells then
			MERData_DCPCharacter.ignoredSpells = MERData_DCP.ignoredSpells
		end
	else
		for i, v in pairs(defaultSettingsPerChar) do
			if not MERData_DCPCharacter[i] then
				MERData_DCPCharacter = v
			end
		end
	end

	self:UnregisterEvent("ADDON_LOADED")
end

function DCP:SPELL_UPDATE_COOLDOWN()
	for _, getCooldownDetails in pairs(module.cooldowns) do
		getCooldownDetails.resetCache()
	end
end

function DCP:UNIT_SPELLCAST_SUCCEEDED(unit, _, spellID)
	if unit == "player" then
		local texture = GetSpellTexture(spellID)
		local t1 = GetInventoryItemTexture("player", 13)
		local t2 = GetInventoryItemTexture("player", 14)
		if texture == t1 or texture == t2 then
			return
		end -- Fix wrong buff cd for trinket
		module.watching[spellID] = { GetTime(), "spell", spellID }
		if not self:IsMouseEnabled() then
			self:SetScript("OnUpdate", OnUpdate)
		end
	end
end

function DCP:COMBAT_LOG_EVENT_UNFILTERED()
	local _, eventType, _, _, _, sourceFlags, _, _, _, _, _, spellID = CombatLogGetCurrentEventInfo()
	local isPet = band(sourceFlags, _G.COMBATLOG_OBJECT_TYPE_PET) == _G.COMBATLOG_OBJECT_TYPE_PET
	local isMine = band(sourceFlags, _G.COMBATLOG_OBJECT_AFFILIATION_MINE) == _G.COMBATLOG_OBJECT_AFFILIATION_MINE

	if eventType == "SPELL_CAST_SUCCESS" then
		if isPet and isMine then
			local name = GetSpellName(spellID)
			local index = GetPetActionIndexByName(name)
			if index and not select(7, GetPetActionInfo(index)) then
				module.watching[spellID] = { GetTime(), "pet", index }
			elseif not index and spellID then
				module.watching[spellID] = { GetTime(), "spell", spellID }
			else
				return
			end
			self:SetScript("OnUpdate", OnUpdate)
		end
	end
end

function DCP:PLAYER_ENTERING_WORLD()
	local inInstance, instanceType = IsInInstance()
	if inInstance and instanceType == "arena" then
		self:SetScript("OnUpdate", nil)
		wipe(module.cooldowns)
		wipe(module.watching)
	end
end

hooksecurefunc("UseAction", function(slot)
	local actionType, itemID, subType = GetActionInfo(slot)
	if actionType == "item" then
		local texture = GetActionTexture(slot)
		module.watching[itemID] = { GetTime(), "item", texture }
	end
end)

hooksecurefunc("UseInventoryItem", function(slot)
	local itemID = GetInventoryItemID("player", slot)
	if itemID then
		local texture = GetInventoryItemTexture("player", slot)
		module.watching[itemID] = { GetTime(), "item", texture }
	end
end)

hooksecurefunc(C_Container, "UseContainerItem", function(bag, slot)
	local itemID = GetContainerItemID(bag, slot)

	if itemID then
		local texture = select(10, GetItemInfo(itemID))
		module.watching[itemID] = { GetTime(), "item", texture }
	end
end)

function module:EnableCooldownFlash()
	DCP:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
	DCP:RegisterEvent("PLAYER_ENTERING_WORLD")
	DCP:RegisterEvent("ADDON_LOADED")
	DCP:RegisterEvent("SPELL_UPDATE_COOLDOWN")
	if self.db.enablePet then
		DCP:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
	end
end

function module:DisableCooldownFlash()
	DCP:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
	DCP:UnregisterEvent("PLAYER_ENTERING_WORLD")
	DCP:UnregisterEvent("ADDON_LOADED")
	DCP:UnregisterEvent("SPELL_UPDATE_COOLDOWN")
	DCP:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end

function module:TestMode()
	module.db = E.db.mui.cooldownFlash

	tinsert(module.animating, { "Interface\\Icons\\Ability_CriticalStrike", nil, "Spell Name" })
	DCP:SetScript("OnUpdate", OnUpdate)

	if module.db.tts then
		local tts = GetSpellName(33786)
		SpeakText(module.db.ttsvoice, tts, Enum.VoiceTtsDestination.LocalPlayback, 0, module.db.ttsvolume)
	end
end

function module:Initialize()
	module.db = E.db.mui.cooldownFlash

	if self.db.enable then
		self:EnableCooldownFlash()
	elseif not self.db.enable then
		self:DisableCooldownFlash()
	end

	DCP:Size(self.db.iconSize, self.db.iconSize)
	DCP:Point("CENTER", E.UIParent, "CENTER")

	E:CreateMover(
		DCP,
		"MER_CooldownFlashMover",
		MER.Title .. L["CooldownFlashMover"],
		true,
		nil,
		nil,
		"ALL,SOLO,MERATHILISUI",
		nil,
		"mui,modules,cooldowns,cooldownFlash"
	)
end

MER:RegisterModule(module:GetName())
