local _, addonTable = ...
local WipedQoL = addonTable.WipedQoL
local TRANSLATE = addonTable.TRANSLATE or function(msg) return msg end

local VALID_MODIFIERS = {
    SHIFT = true,
    CTRL = true,
    ALT = true,
}

local VALID_AUTO_LOOT_ROLL_MODES = {
    pass = true,
    greed = true,
    need = true,
    transmog = true,
}

local AUTO_LOOT_ROLL_TYPES = {
    pass = LE_LOOT_ROLL_TYPE_PASS or 0,
    need = LE_LOOT_ROLL_TYPE_NEED or 1,
    greed = LE_LOOT_ROLL_TYPE_GREED or 2,
    transmog = LE_LOOT_ROLL_TYPE_TRANSMOG or 4,
}

local AUTO_LOOT_ROLL_PRIORITY = {
    need = { "need", "transmog", "greed", "pass" },
    transmog = { "transmog", "greed", "pass" },
    greed = { "greed", "pass" },
    pass = { "pass" },
}
local MOUNT_ANIMATION_PREVIEW_MOUNT_ID = 1818
local function NormalizeQuestPauseKey(value)
    value = tostring(value or "SHIFT"):upper()
    if VALID_MODIFIERS[value] then
        return value
    end

    return "SHIFT"
end

local function NormalizeAutoLootRollMode(value)
    value = tostring(value or "pass"):lower()
    if VALID_AUTO_LOOT_ROLL_MODES[value] then
        return value
    end

    return "pass"
end

local function IsPauseKeyDown(key)
    if key == "SHIFT" then
        return IsShiftKeyDown()
    end

    if key == "CTRL" then
        return IsControlKeyDown()
    end

    if key == "ALT" then
        return IsAltKeyDown()
    end

    return false
end

local function IsSecretValue(value)
    return issecretvalue and issecretvalue(value) == true
end

local function IsLegacyLootContent()
    if C_Loot and C_Loot.IsLegacyLootModeEnabled then
        return C_Loot.IsLegacyLootModeEnabled() == true
    end

    if not IsInInstance or not GetInstanceInfo then
        return false
    end

    local inInstance, instanceType = IsInInstance()
    if not inInstance or (instanceType ~= "party" and instanceType ~= "raid") then
        return false
    end

    local _, _, difficultyID = GetInstanceInfo()
    return IsLegacyDifficulty and IsLegacyDifficulty(difficultyID) == true
end

local function GetAutoLootRollType(mode)
    return AUTO_LOOT_ROLL_TYPES[NormalizeAutoLootRollMode(mode)]
end

local function GetAvailableAutoLootRollModes(rollID)
    if not GetLootRollItemInfo then
        return {
            need = true,
            greed = true,
            transmog = true,
            pass = true,
        }
    end

    local _, _, _, _, _, canNeed, canGreed, _, _, _, _, _, canTransmog = GetLootRollItemInfo(rollID)
    return {
        need = canNeed == true,
        greed = canGreed == true,
        transmog = canTransmog == true,
        pass = true,
    }
end

local function CanUseAutoLootRollMode(availableModes, mode)
    if mode == "pass" then
        return true
    end

    return availableModes and availableModes[mode] == true
end

local function ResolveAutoLootRollMode(rollID, selectedMode)
    local availableModes = GetAvailableAutoLootRollModes(rollID)
    local priority = AUTO_LOOT_ROLL_PRIORITY[NormalizeAutoLootRollMode(selectedMode)] or AUTO_LOOT_ROLL_PRIORITY.pass

    for _, mode in ipairs(priority) do
        if CanUseAutoLootRollMode(availableModes, mode) then
            return mode
        end
    end

    return "pass"
end

local function ClickNpcDialogOption(option)
    if not option or not C_GossipInfo then
        return false
    end

    if option.orderIndex and C_GossipInfo.SelectOptionByIndex then
        C_GossipInfo.SelectOptionByIndex(option.orderIndex)
        return true
    end

    if option.gossipOptionID and C_GossipInfo.SelectOption then
        C_GossipInfo.SelectOption(option.gossipOptionID)
        return true
    end

    return false
end

local function GetNpcDialogOptions()
    return C_GossipInfo and C_GossipInfo.GetOptions and C_GossipInfo.GetOptions() or {}
end

local function GetNpcAvailableQuests()
    return C_GossipInfo and C_GossipInfo.GetAvailableQuests and C_GossipInfo.GetAvailableQuests() or {}
end

local function GetNpcActiveQuests()
    if not C_GossipInfo or not C_GossipInfo.GetActiveQuests then
        return {}
    end

    return C_GossipInfo.GetActiveQuests() or {}
end

local function ShouldUseStoryOption(option)
    return option and option.flags == 1 and (option.orderIndex or option.gossipOptionID)
end

function WipedQoL:GetQuestAutomationSettings()
    local modules = self.db and self.db.profile and self.db.profile.modules
    if not modules then
        return {}
    end

    if type(modules.questAutomation) ~= "table" then
        modules.questAutomation = {
            enabled = false,
            modifier = "SHIFT",
        }
    end

    local settings = modules.questAutomation
    settings.enabled = settings.enabled == true
    settings.modifier = NormalizeQuestPauseKey(settings.modifier)
    return settings
end

function WipedQoL:GetAutoSignUpSettings()
    local modules = self.db and self.db.profile and self.db.profile.modules
    if not modules then
        return {}
    end

    if type(modules.autoSignUp) ~= "table" then
        modules.autoSignUp = {
            enabled = false,
            modifier = "SHIFT",
        }
    end

    local settings = modules.autoSignUp
    settings.enabled = settings.enabled == true
    settings.modifier = NormalizeQuestPauseKey(settings.modifier)
    return settings
end

function WipedQoL:SetAutoSignUpEnabled(enabled)
    local settings = self:GetAutoSignUpSettings()
    settings.enabled = enabled == true
    if self.ApplyAutoSignUpEvents then
        self:ApplyAutoSignUpEvents()
    end
    if self.RefreshAutoSignUpQuestingUI then
        self:RefreshAutoSignUpQuestingUI()
    end
    if self.RefreshAutoSignUpGameplayUI then
        self:RefreshAutoSignUpGameplayUI()
    end
end

function WipedQoL:SetAutoSignUpModifier(value)
    local settings = self:GetAutoSignUpSettings()
    settings.modifier = NormalizeQuestPauseKey(value)
end

function WipedQoL:IsAutoSignUpActive()
    local settings = self:GetAutoSignUpSettings()
    if settings.enabled ~= true then
        return false
    end

    return not IsPauseKeyDown(NormalizeQuestPauseKey(settings.modifier))
end

function WipedQoL:GetAutoLootRollSettings()
    local modules = self.db and self.db.profile and self.db.profile.modules
    if not modules then
        return {}
    end

    if type(modules.autoLootRoll) ~= "table" then
        modules.autoLootRoll = {
            enabled = false,
            mode = "pass",
            legacyOnly = true,
        }
    end

    local settings = modules.autoLootRoll
    settings.enabled = settings.enabled == true
    settings.mode = NormalizeAutoLootRollMode(settings.mode)
    settings.legacyOnly = settings.legacyOnly ~= false
    return settings
end

function WipedQoL:SetAutoLootRollEnabled(enabled)
    local settings = self:GetAutoLootRollSettings()
    settings.enabled = enabled == true
    if self.RefreshDatapanelAutoLootRoll then
        self:RefreshDatapanelAutoLootRoll()
    end
    if not (self.IsInCombat and self:IsInCombat())
        and self.datapanelAutoLootRollMenu
        and self.datapanelAutoLootRollMenu:IsShown()
        and self.datapanelFrame
        and self.datapanelFrame.autoLootRollFrame
    then
        self:OpenDatapanelAutoLootRollMenu(self.datapanelFrame.autoLootRollFrame)
    end
    if self.RefreshAutoLootRollRollingUI then
        self:RefreshAutoLootRollRollingUI()
    end
end

function WipedQoL:SetAutoLootRollMode(mode)
    local settings = self:GetAutoLootRollSettings()
    settings.mode = NormalizeAutoLootRollMode(mode)
    if self.RefreshDatapanelAutoLootRoll then
        self:RefreshDatapanelAutoLootRoll()
    end
    if not (self.IsInCombat and self:IsInCombat())
        and self.datapanelAutoLootRollMenu
        and self.datapanelAutoLootRollMenu:IsShown()
        and self.datapanelFrame
        and self.datapanelFrame.autoLootRollFrame
    then
        self:OpenDatapanelAutoLootRollMenu(self.datapanelFrame.autoLootRollFrame)
    end
    if self.RefreshAutoLootRollRollingUI then
        self:RefreshAutoLootRollRollingUI()
    end
end

function WipedQoL:SetQuestAutomationModifier(value)
    local settings = self:GetQuestAutomationSettings()
    settings.modifier = NormalizeQuestPauseKey(value)
end

local function IsGroupFinderLoaded()
    if C_AddOns and C_AddOns.IsAddOnLoaded then
        return C_AddOns.IsAddOnLoaded("Blizzard_GroupFinder") == true
    end

    return IsAddOnLoaded and IsAddOnLoaded("Blizzard_GroupFinder") == true
end

local function FindAutoSignUpEntry(frame)
    for _ = 1, 8 do
        if not frame then
            return nil
        end
        if frame.resultID then
            return frame
        end
        frame = frame.GetParent and frame:GetParent()
    end
end

local function GetPreferredAutoSignUpRoles()
    if UnitGroupRolesAssigned then
        local role = UnitGroupRolesAssigned("player")
        if not IsSecretValue(role) and role and role ~= "NONE" then
            return role == "TANK", role == "HEALER", role == "DAMAGER"
        end
    end

    if GetSpecializationRole then
        local specialization
        if C_SpecializationInfo and C_SpecializationInfo.GetSpecialization then
            specialization = C_SpecializationInfo.GetSpecialization()
        elseif GetSpecialization then
            specialization = GetSpecialization()
        end

        if not IsSecretValue(specialization) and specialization then
            local role = GetSpecializationRole(specialization)
            if not IsSecretValue(role) and role and role ~= "NONE" then
                return role == "TANK", role == "HEALER", role == "DAMAGER"
            end
        end
    end

    if GetLFGRoles then
        local _, tank, healer, damage = GetLFGRoles()
        if not IsSecretValue(tank) and not IsSecretValue(healer) and not IsSecretValue(damage) then
            tank = tank == true
            healer = healer == true
            damage = damage == true
            if tank or healer or damage then
                return tank, healer, damage
            end
        end
    end

    return false, false, false
end

local function SetAutoSignUpRole(roleButton, selected)
    local checkButton = roleButton and (roleButton.CheckButton or roleButton.checkButton)
    if checkButton and checkButton:IsEnabled() then
        checkButton:SetChecked(selected)
    end
end

function WipedQoL:TryAutoSignUpEntry(entry, button)
    if not self:IsAutoSignUpActive() or button == "RightButton" then
        return
    end

    if not entry or not entry.resultID then
        return
    end

    local panel = LFGListFrame and LFGListFrame.SearchPanel
    if not panel or not panel.SignUpButton then
        return
    end

    if LFGListSearchPanelUtil_CanSelectResult and not LFGListSearchPanelUtil_CanSelectResult(entry.resultID) then
        return
    end

    if panel.selectedResult ~= entry.resultID and LFGListSearchPanel_SelectResult then
        LFGListSearchPanel_SelectResult(panel, entry.resultID)
    end

    if not panel.SignUpButton:IsEnabled() or not LFGListSearchPanel_SignUp then
        return
    end

    local clickTime = GetTime()
    if self.autoSignUpLastEntry == entry and clickTime - (self.autoSignUpLastClickTime or 0) < 0.1 then
        return
    end
    self.autoSignUpLastEntry = entry
    self.autoSignUpLastClickTime = clickTime

    local pendingToken = {}
    self.autoSignUpPendingDialog = pendingToken
    if C_Timer and C_Timer.After then
        C_Timer.After(4, function()
            if WipedQoL.autoSignUpPendingDialog == pendingToken then
                WipedQoL.autoSignUpPendingDialog = nil
            end
        end)
    end
    LFGListSearchPanel_SignUp(panel)
end

local autoSignUpHookedFrames = setmetatable({}, { __mode = "k" })

local function HookAutoSignUpEntryTree(parent)
    if not parent then
        return
    end

    if not autoSignUpHookedFrames[parent] then
        autoSignUpHookedFrames[parent] = true
        parent:HookScript("OnMouseUp", function(clickedFrame, button)
            WipedQoL:TryAutoSignUpEntry(FindAutoSignUpEntry(clickedFrame), button)
        end)
    end

    if not parent.GetChildren then
        return
    end

    local children = { parent:GetChildren() }
    for _, child in ipairs(children) do
        HookAutoSignUpEntryTree(child)
    end
end

function WipedQoL:InstallAutoSignUpHooks()
    if self.autoSignUpHooksInstalled then
        return true
    end

    if type(LFGListSearchEntry_OnClick) ~= "function" then
        return false
    end

    self.autoSignUpHooksInstalled = true
    hooksecurefunc("LFGListSearchEntry_OnClick", function(entry, button)
        if not autoSignUpHookedFrames[entry] then
            WipedQoL:TryAutoSignUpEntry(entry, button)
        end
    end)

    if type(LFGListSearchEntry_Update) == "function" then
        hooksecurefunc("LFGListSearchEntry_Update", function(entry)
            HookAutoSignUpEntryTree(entry)
        end)
    end

    if LFGListApplicationDialog then
        LFGListApplicationDialog:HookScript("OnShow", function(dialog)
            if not WipedQoL.autoSignUpPendingDialog then
                return
            end

            if not WipedQoL:IsAutoSignUpActive() then
                WipedQoL.autoSignUpPendingDialog = nil
                return
            end

            WipedQoL.autoSignUpPendingDialog = nil
            if dialog and dialog.SignUpButton and dialog.SignUpButton:IsEnabled() then
                dialog.SignUpButton:Click()
            end
        end)
    end

    return true
end

function WipedQoL:ApplyAutoSignUpEvents()
    local settings = self:GetAutoSignUpSettings()
    if settings.enabled == true then
        self:InstallAutoSignUpHooks()
    else
        self.autoSignUpPendingDialog = nil
    end

    if not self.autoSignUpRoleFrame then
        return
    end

    if settings.enabled == true then
        self.autoSignUpRoleFrame:RegisterEvent("LFG_ROLE_CHECK_SHOW")
    else
        self.autoSignUpRoleFrame:UnregisterEvent("LFG_ROLE_CHECK_SHOW")
    end
end

function WipedQoL:SetupAutoSignUp()
    if self.autoSignUpSetupDone then
        return
    end

    self.autoSignUpSetupDone = true
    self.autoSignUpRoleFrame = CreateFrame("Frame")
    self.autoSignUpRoleFrame:SetScript("OnEvent", function()
        if not WipedQoL:IsAutoSignUpActive() then
            WipedQoL.autoSignUpPendingDialog = nil
            return
        end

        local tank, healer, damage = GetPreferredAutoSignUpRoles()
        SetAutoSignUpRole(LFDRoleCheckPopupRoleButtonTank, tank)
        SetAutoSignUpRole(LFDRoleCheckPopupRoleButtonHealer, healer)
        SetAutoSignUpRole(LFDRoleCheckPopupRoleButtonDPS, damage)
        if LFDRoleCheckPopupAcceptButton then
            LFDRoleCheckPopupAcceptButton:Enable()
            LFDRoleCheckPopupAcceptButton:Click()
        end
        WipedQoL.autoSignUpPendingDialog = nil
    end)

    if IsGroupFinderLoaded() then
        self:InstallAutoSignUpHooks()
    else
        self:RegisterEvent("ADDON_LOADED", "HandleAutoSignUpAddonLoaded")
    end

    self:ApplyAutoSignUpEvents()
end

function WipedQoL:HandleAutoSignUpAddonLoaded(_, addonName)
    if addonName ~= "Blizzard_GroupFinder" then
        return
    end

    self:InstallAutoSignUpHooks()
    self:ApplyAutoSignUpEvents()
    self:UnregisterEvent("ADDON_LOADED")
end

function WipedQoL:IsQuestAutomationActive()
    if self:IsInCombat() then
        return false
    end

    local settings = self:GetQuestAutomationSettings()
    if not settings.enabled then
        return false
    end

    return not IsPauseKeyDown(NormalizeQuestPauseKey(settings.modifier))
end

function WipedQoL:GetMountAnimationSettings()
    local modules = self.db and self.db.profile and self.db.profile.modules
    if not modules then
        return {}
    end

    if type(modules.mountAnimation) ~= "table" then
        modules.mountAnimation = {
            enabled = false,
        }
    end

    local settings = modules.mountAnimation
    settings.enabled = settings.enabled == true
    return settings
end

function WipedQoL:SetMountAnimationEnabled(enabled)
    local settings = self:GetMountAnimationSettings()
    settings.enabled = enabled == true
    if self.ApplyMountLootTracking then
        self:ApplyMountLootTracking()
    end
    if settings.enabled ~= true then
        self:StopMountAcquisitionAnimation()
    end

    if self.RefreshMountAnimationUI then
        self:RefreshMountAnimationUI()
    end
end

function WipedQoL:SetupQuestAutomation()
    if self.questAutomationEventsRegistered then
        return
    end

    self.questAutomationEventsRegistered = true
    self.questAutomationPendingAccept = self.questAutomationPendingAccept or {}
    self.questAutomationSeenDialogOptions = self.questAutomationSeenDialogOptions or {}

    self:RegisterEvent("GOSSIP_CLOSED", "HandleQuestAutomationGossipClosed")
    self:RegisterEvent("GOSSIP_SHOW", "HandleQuestAutomationGossipShow")
    self:RegisterEvent("QUEST_COMPLETE", "HandleQuestAutomationComplete")
    self:RegisterEvent("QUEST_DATA_LOAD_RESULT", "HandleQuestAutomationDataLoadResult")
    self:RegisterEvent("QUEST_DETAIL", "HandleQuestAutomationDetail")
    self:RegisterEvent("QUEST_GREETING", "HandleQuestAutomationGreeting")
    self:RegisterEvent("QUEST_PROGRESS", "HandleQuestAutomationProgress")
    self:RegisterEvent("START_LOOT_ROLL", "HandleAutoLootRollStarted")
    self:RegisterEvent("CONFIRM_LOOT_ROLL", "HandleAutoLootRollConfirm")
    self:ApplyMountLootTracking()
end

function WipedQoL:HandleQuestAutomationGossipClosed()
    self.questAutomationSeenDialogOptions = {}
end

function WipedQoL:HandleQuestAutomationGossipShow()
    if not self:IsQuestAutomationActive() or not C_GossipInfo then
        return
    end

    for _, quest in pairs(GetNpcActiveQuests()) do
        if quest.isComplete and quest.questID and C_GossipInfo.SelectActiveQuest then
            C_GossipInfo.SelectActiveQuest(quest.questID)
        end
    end

    local availableQuests = GetNpcAvailableQuests()
    if #availableQuests > 0 then
        for _, quest in pairs(availableQuests) do
            if quest.questID and C_GossipInfo.SelectAvailableQuest then
                C_GossipInfo.SelectAvailableQuest(quest.questID)
            end
        end
        return
    end

    local options = GetNpcDialogOptions()
    if #options == 1 then
        local option = options[1]
        local optionKey = option and (option.gossipOptionID or option.orderIndex)
        if optionKey and not self.questAutomationSeenDialogOptions[optionKey] then
            self.questAutomationSeenDialogOptions[optionKey] = true
            ClickNpcDialogOption(option)
        end
        return
    end

    if #options > 1 then
        for _, option in pairs(options) do
            if ShouldUseStoryOption(option) then
                ClickNpcDialogOption(option)
                return
            end
        end
    end
end

function WipedQoL:HandleQuestAutomationComplete()
    if not self:IsQuestAutomationActive() then
        return
    end

    local numQuestRewards = GetNumQuestChoices()
    if numQuestRewards == 1 then
        GetQuestReward(1)
    elseif numQuestRewards == 0 then
        GetQuestReward()
    end
end

function WipedQoL:HandleQuestAutomationDataLoadResult(_, questID)
    if not questID or not self.questAutomationPendingAccept or not self.questAutomationPendingAccept[questID] then
        return
    end

    self.questAutomationPendingAccept[questID] = nil
    if not self:IsQuestAutomationActive() then
        return
    end

    AcceptQuest()
    if QuestFrame and QuestFrame:IsShown() then
        QuestFrame:Hide()
    end
end

function WipedQoL:HandleQuestAutomationDetail()
    if not self:IsQuestAutomationActive() then
        return
    end

    local questID = GetQuestID and GetQuestID()
    if questID and C_QuestLog and C_QuestLog.RequestLoadQuestByID then
        self.questAutomationPendingAccept[questID] = true
        C_QuestLog.RequestLoadQuestByID(questID)
        return
    end

    AcceptQuest()
end

function WipedQoL:HandleQuestAutomationGreeting()
    if not self:IsQuestAutomationActive() then
        return
    end

    for index = 1, GetNumAvailableQuests() do
        SelectAvailableQuest(index)
    end

    for index = 1, GetNumActiveQuests() do
        if select(2, GetActiveTitle(index)) then
            SelectActiveQuest(index)
        end
    end
end

function WipedQoL:HandleQuestAutomationProgress()
    if self:IsQuestAutomationActive() and IsQuestCompletable() then
        CompleteQuest()
    end
end

local function GetCarriedItemCounts()
    local counts = {}
    if not C_Container or not C_Container.GetContainerNumSlots or not C_Container.GetContainerItemInfo then
        return counts
    end

    for bag = 0, (NUM_BAG_SLOTS or 4) do
        local slotCount = C_Container.GetContainerNumSlots(bag) or 0
        for slot = 1, slotCount do
            local itemInfo = C_Container.GetContainerItemInfo(bag, slot)
            local itemID = itemInfo and itemInfo.itemID
            local stackCount = itemInfo and itemInfo.stackCount
            if not IsSecretValue(itemID) and not IsSecretValue(stackCount) and itemID then
                itemID = tonumber(itemID)
                stackCount = tonumber(stackCount) or 1
                if itemID then
                    counts[itemID] = (counts[itemID] or 0) + stackCount
                end
            end
        end
    end

    return counts
end

function WipedQoL:ApplyMountLootTracking()
    local soundEnabled = self:GetMountSoundSettings().enabled == true
    local animationEnabled = self:GetMountAnimationSettings().enabled == true
    local shouldTrack = soundEnabled or animationEnabled

    if not self.mountLootTrackingFrame then
        self.mountLootTrackingFrame = CreateFrame("Frame")
        self.mountLootTrackingFrame:SetScript("OnEvent", function()
            WipedQoL:HandleMountLootBagUpdate()
        end)
    end

    if shouldTrack and not self.mountLootTrackingActive then
        self.mountLootBagCounts = GetCarriedItemCounts()
        self.mountLootTrackingFrame:RegisterEvent("BAG_UPDATE_DELAYED")
        self.mountLootTrackingActive = true
    elseif not shouldTrack and self.mountLootTrackingActive then
        self.mountLootTrackingFrame:UnregisterEvent("BAG_UPDATE_DELAYED")
        self.mountLootTrackingActive = nil
        self.mountLootBagCounts = nil
    end
end

function WipedQoL:HandleMountLootBagUpdate()
    local previousCounts = self.mountLootBagCounts
    local currentCounts = GetCarriedItemCounts()
    self.mountLootBagCounts = currentCounts
    if not previousCounts then
        return
    end

    for itemID, count in pairs(currentCounts) do
        if count > (previousCounts[itemID] or 0) then
            self:HandlePotentialMountLoot(itemID)
        end
    end
end

function WipedQoL:HandlePotentialMountLoot(itemID)
    if not C_MountJournal or not C_MountJournal.GetMountFromItem then
        return
    end

    local ok, mountID = pcall(C_MountJournal.GetMountFromItem, itemID)
    if not ok or IsSecretValue(mountID) or not mountID then
        return
    end

    if C_MountJournal.PlayerHasMount then
        local knownOk, isKnown = pcall(C_MountJournal.PlayerHasMount, mountID)
        if knownOk and not IsSecretValue(isKnown) and isKnown == true then
            return
        end
    end

    local soundSettings = self:GetMountSoundSettings()
    if soundSettings.enabled == true then
        self:PlaySoundPlayback(self:GetSoundPath(soundSettings.sound))
    end

    if self:GetMountAnimationSettings().enabled == true then
        local itemName
        if C_Item and C_Item.GetItemNameByID then
            local nameOk, resolvedName = pcall(C_Item.GetItemNameByID, itemID)
            if nameOk and not IsSecretValue(resolvedName) then
                itemName = resolvedName
            end
        end

        local function PlayAnimation(resolvedName)
            C_Timer.After(0.1, function()
                self:PlayMountAcquisitionAnimation(mountID, resolvedName)
            end)
        end

        if itemName then
            PlayAnimation(itemName)
        elseif Item and Item.CreateFromItemID then
            local mountItem = Item:CreateFromItemID(itemID)
            mountItem:ContinueOnItemLoad(function()
                local loadedName = mountItem:GetItemName()
                PlayAnimation(not IsSecretValue(loadedName) and loadedName or nil)
            end)
        else
            PlayAnimation()
        end
    end
end

local function CreateMountAnimationIcon(addon)
    local overlay = addon.mountAnimationOverlay
    local frame = CreateFrame("Frame", nil, overlay)
    frame:SetSize(96, 96)
    frame:Hide()

    frame.glow = frame:CreateTexture(nil, "BACKGROUND")
    frame.glow:SetPoint("CENTER", frame, "CENTER", 0, 0)
    frame.glow:SetSize(150, 150)
    frame.glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    frame.glow:SetBlendMode("ADD")
    frame.glow:SetVertexColor(1, 0.82, 0.18, 0.8)

    frame.icon = frame:CreateTexture(nil, "ARTWORK")
    frame.icon:SetAllPoints(frame)
    frame.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    frame.border = frame:CreateTexture(nil, "OVERLAY")
    frame.border:SetPoint("TOPLEFT", frame, "TOPLEFT", -3, 3)
    frame.border:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 3, -3)
    frame.border:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    frame.border:SetBlendMode("ADD")
    frame.border:SetVertexColor(1, 0.72, 0.08, 1)

    local animation = frame:CreateAnimationGroup()
    frame.animation = animation

    frame.fadeIn = animation:CreateAnimation("Alpha")
    frame.fadeIn:SetOrder(1)
    frame.fadeIn:SetDuration(0.08)
    frame.fadeIn:SetFromAlpha(0)
    frame.fadeIn:SetToAlpha(1)

    frame.pop = animation:CreateAnimation("Scale")
    frame.pop:SetOrder(1)
    frame.pop:SetDuration(0.14)
    frame.pop:SetScaleFrom(0.3, 0.3)
    frame.pop:SetScaleTo(1.15, 1.15)

    frame.travel = animation:CreateAnimation("Translation")
    frame.travel:SetOrder(2)
    frame.travel:SetSmoothing("OUT")

    frame.spin = animation:CreateAnimation("Rotation")
    frame.spin:SetOrder(2)

    frame.zoom = animation:CreateAnimation("Scale")
    frame.zoom:SetOrder(3)
    frame.zoom:SetDuration(0.28)
    frame.zoom:SetScaleFrom(1, 1)
    frame.zoom:SetScaleTo(2.35, 2.35)

    frame.fadeOut = animation:CreateAnimation("Alpha")
    frame.fadeOut:SetOrder(3)
    frame.fadeOut:SetDuration(0.28)
    frame.fadeOut:SetFromAlpha(1)
    frame.fadeOut:SetToAlpha(0)

    local function FinishIconAnimation()
        if frame.active then
            frame.active = nil
            overlay.activeIcons = math.max(0, (overlay.activeIcons or 1) - 1)
        end
        frame:Hide()
        if (overlay.activeIcons or 0) == 0 and not (overlay.titleFrame and overlay.titleFrame:IsShown()) then
            overlay:Hide()
        end
    end
    animation:SetScript("OnFinished", FinishIconAnimation)

    local finaleAnimation = frame:CreateAnimationGroup()
    frame.finaleAnimation = finaleAnimation

    local finaleFadeIn = finaleAnimation:CreateAnimation("Alpha")
    finaleFadeIn:SetOrder(1)
    finaleFadeIn:SetDuration(0.08)
    finaleFadeIn:SetFromAlpha(0)
    finaleFadeIn:SetToAlpha(1)

    local finalePop = finaleAnimation:CreateAnimation("Scale")
    finalePop:SetOrder(1)
    finalePop:SetDuration(0.1)
    finalePop:SetScaleFrom(0.7, 0.7)
    finalePop:SetScaleTo(1, 1)
    finalePop:SetOrigin("CENTER", 0, 0)

    local shakeOffsets = { 10, -20, 17, -10, 3 }
    for order, offsetX in ipairs(shakeOffsets) do
        local shake = finaleAnimation:CreateAnimation("Translation")
        shake:SetOrder(order + 1)
        shake:SetDuration(0.055)
        shake:SetOffset(offsetX, math.random(-3, 3))
    end

    local finaleZoom = finaleAnimation:CreateAnimation("Scale")
    finaleZoom:SetOrder(#shakeOffsets + 2)
    finaleZoom:SetDuration(0.58)
    finaleZoom:SetScaleFrom(1, 1)
    finaleZoom:SetScaleTo(3.1, 3.1)
    finaleZoom:SetOrigin("CENTER", 0, 0)

    local finaleFadeOut = finaleAnimation:CreateAnimation("Alpha")
    finaleFadeOut:SetOrder(#shakeOffsets + 2)
    finaleFadeOut:SetDuration(0.58)
    finaleFadeOut:SetFromAlpha(1)
    finaleFadeOut:SetToAlpha(0)
    finaleAnimation:SetScript("OnFinished", FinishIconAnimation)

    return frame
end

function WipedQoL:GetMountAnimationOverlay()
    if self.mountAnimationOverlay then
        return self.mountAnimationOverlay
    end

    local overlay = CreateFrame("Frame", nil, UIParent)
    overlay:SetAllPoints(UIParent)
    overlay:SetFrameStrata("FULLSCREEN_DIALOG")
    overlay:SetFrameLevel(500)
    overlay:EnableMouse(false)
    overlay:Hide()
    overlay.icons = {}
    overlay.activeIcons = 0

    local titleIconFrame = CreateFrame("Frame", nil, overlay)
    titleIconFrame:SetPoint("TOP", UIParent, "TOP", 0, -94)
    titleIconFrame:SetSize(78, 78)
    titleIconFrame:Hide()
    overlay.titleIconFrame = titleIconFrame

    titleIconFrame.glow = titleIconFrame:CreateTexture(nil, "BACKGROUND")
    titleIconFrame.glow:SetPoint("CENTER", titleIconFrame, "CENTER", 0, 0)
    titleIconFrame.glow:SetSize(116, 116)
    titleIconFrame.glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    titleIconFrame.glow:SetBlendMode("ADD")
    titleIconFrame.glow:SetVertexColor(1, 0.82, 0.18, 0.75)

    titleIconFrame.icon = titleIconFrame:CreateTexture(nil, "ARTWORK")
    titleIconFrame.icon:SetAllPoints(titleIconFrame)
    titleIconFrame.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    local borderTop = titleIconFrame:CreateTexture(nil, "OVERLAY")
    borderTop:SetColorTexture(1, 0.72, 0.08, 1)
    borderTop:SetPoint("BOTTOMLEFT", titleIconFrame, "TOPLEFT", -4, 0)
    borderTop:SetPoint("BOTTOMRIGHT", titleIconFrame, "TOPRIGHT", 4, 0)
    borderTop:SetHeight(4)

    local borderBottom = titleIconFrame:CreateTexture(nil, "OVERLAY")
    borderBottom:SetColorTexture(1, 0.72, 0.08, 1)
    borderBottom:SetPoint("TOPLEFT", titleIconFrame, "BOTTOMLEFT", -4, 0)
    borderBottom:SetPoint("TOPRIGHT", titleIconFrame, "BOTTOMRIGHT", 4, 0)
    borderBottom:SetHeight(4)

    local borderLeft = titleIconFrame:CreateTexture(nil, "OVERLAY")
    borderLeft:SetColorTexture(1, 0.72, 0.08, 1)
    borderLeft:SetPoint("TOPRIGHT", titleIconFrame, "TOPLEFT", 0, 4)
    borderLeft:SetPoint("BOTTOMRIGHT", titleIconFrame, "BOTTOMLEFT", 0, -4)
    borderLeft:SetWidth(4)

    local borderRight = titleIconFrame:CreateTexture(nil, "OVERLAY")
    borderRight:SetColorTexture(1, 0.72, 0.08, 1)
    borderRight:SetPoint("TOPLEFT", titleIconFrame, "TOPRIGHT", 0, 4)
    borderRight:SetPoint("BOTTOMLEFT", titleIconFrame, "BOTTOMRIGHT", 0, -4)
    borderRight:SetWidth(4)

    titleIconFrame:SetScript("OnUpdate", function(self, elapsed)
        self.pulseElapsed = (self.pulseElapsed or 0) + elapsed
        local pulse = (math.sin((self.pulseElapsed / 1.1) * math.pi * 2) + 1) * 0.5
        self.icon:SetAlpha(0.82 + (0.18 * pulse))
        self.glow:SetAlpha(0.42 + (0.38 * pulse))
    end)

    titleIconFrame.fade = titleIconFrame:CreateAnimationGroup()
    local titleIconFadeAlpha = titleIconFrame.fade:CreateAnimation("Alpha")
    titleIconFadeAlpha:SetDuration(0.65)
    titleIconFadeAlpha:SetFromAlpha(1)
    titleIconFadeAlpha:SetToAlpha(0)
    titleIconFrame.fade:SetScript("OnFinished", function()
        titleIconFrame:Hide()
        if (overlay.activeIcons or 0) == 0 and not (overlay.titleFrame and overlay.titleFrame:IsShown()) then
            overlay:Hide()
        end
    end)

    local titleFrame = CreateFrame("Frame", nil, overlay)
    titleFrame:SetPoint("TOP", UIParent, "TOP", 0, -144)
    titleFrame:SetSize(900, 128)
    titleFrame:Hide()
    overlay.titleFrame = titleFrame

    titleFrame.text = titleFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    titleFrame.text:SetPoint("CENTER", titleFrame, "CENTER", 0, 0)
    titleFrame.text:SetJustifyH("CENTER")
    titleFrame.text:SetJustifyV("MIDDLE")
    titleFrame.text:SetWordWrap(true)
    if titleFrame.text.SetNonSpaceWrap then
        titleFrame.text:SetNonSpaceWrap(true)
    end
    titleFrame.text:SetTextColor(1, 0.82, 0.14, 1)
    titleFrame.text:SetShadowColor(0, 0, 0, 1)
    titleFrame.text:SetShadowOffset(3, -3)
    if addonTable.ApplyFont then
        addonTable.ApplyFont(titleFrame.text, 34, "OUTLINE")
    end
    titleFrame.fontPath, _, titleFrame.fontFlags = titleFrame.text:GetFont()
    titleFrame.baseFontSize = 34
    titleFrame:SetScript("OnUpdate", function(self, elapsed)
        if not self.pulseActive or not self.fontPath then
            return
        end

        self.pulseElapsed = (self.pulseElapsed or 0) + elapsed
        self.fontUpdateElapsed = (self.fontUpdateElapsed or 0) + elapsed
        if self.fontUpdateElapsed < (1 / 30) then
            return
        end
        self.fontUpdateElapsed = 0

        local fontSize
        if self.pulseElapsed < 0.28 then
            local progress = self.pulseElapsed / 0.28
            local eased = 1 - ((1 - progress) * (1 - progress))
            fontSize = 18 + ((self.baseFontSize - 18) * eased)
        else
            local pulseTime = self.pulseElapsed - 0.28
            fontSize = self.baseFontSize + (3 * math.sin((pulseTime / 0.76) * math.pi * 2))
        end
        self.text:SetFont(self.fontPath, fontSize, self.fontFlags)
    end)

    titleFrame.intro = titleFrame:CreateAnimationGroup()
    local titleIntroAlpha = titleFrame.intro:CreateAnimation("Alpha")
    titleIntroAlpha:SetOrder(1)
    titleIntroAlpha:SetDuration(0.25)
    titleIntroAlpha:SetFromAlpha(0)
    titleIntroAlpha:SetToAlpha(1)

    titleFrame.fade = titleFrame:CreateAnimationGroup()
    local titleFadeAlpha = titleFrame.fade:CreateAnimation("Alpha")
    titleFadeAlpha:SetDuration(0.65)
    titleFadeAlpha:SetFromAlpha(1)
    titleFadeAlpha:SetToAlpha(0)
    titleFrame.fade:SetScript("OnFinished", function()
        titleFrame.pulseActive = nil
        titleFrame:Hide()
        if (overlay.activeIcons or 0) == 0 and not titleIconFrame:IsShown() then
            overlay:Hide()
        end
    end)

    self.mountAnimationOverlay = overlay
    return overlay
end

function WipedQoL:ShowMountAnimationTitle(itemName, iconTexture, animationToken)
    local overlay = self:GetMountAnimationOverlay()
    local titleFrame = overlay.titleFrame
    local titleIconFrame = overlay.titleIconFrame
    local maxWidth = math.max(400, math.min(1000, (UIParent:GetWidth() or 1080) - 80))
    titleFrame:SetWidth(maxWidth)
    titleFrame.text:SetWidth(maxWidth)
    titleFrame.text:SetText(tostring(itemName or "Mount"))
    if titleFrame.fontPath then
        titleFrame.text:SetFont(titleFrame.fontPath, 18, titleFrame.fontFlags)
    end

    titleFrame.intro:Stop()
    titleFrame.fade:Stop()
    titleFrame.pulseActive = true
    titleFrame.pulseElapsed = 0
    titleFrame.fontUpdateElapsed = 0
    titleFrame:SetAlpha(1)
    titleFrame:Show()
    titleIconFrame.fade:Stop()
    titleIconFrame.icon:SetTexture(iconTexture)
    titleIconFrame.pulseElapsed = 0
    titleIconFrame:SetAlpha(1)
    titleIconFrame:Show()
    overlay:Show()
    titleFrame.intro:Play()

    C_Timer.After(11.25, function()
        if self.mountAnimationToken == animationToken and titleFrame:IsShown() then
            titleFrame.fade:Play()
            titleIconFrame.fade:Play()
        end
    end)
end

function WipedQoL:AcquireMountAnimationIcon()
    local overlay = self:GetMountAnimationOverlay()
    for _, frame in ipairs(overlay.icons) do
        if not frame.active then
            return frame
        end
    end

    if #overlay.icons >= 18 then
        return nil
    end

    local frame = CreateMountAnimationIcon(self)
    overlay.icons[#overlay.icons + 1] = frame
    return frame
end

function WipedQoL:LaunchMountAnimationIcon(iconTexture, startX, startY, endX, endY, size, isFinale)
    local frame = self:AcquireMountAnimationIcon()
    if not frame then
        return
    end

    local overlay = self:GetMountAnimationOverlay()
    frame.active = true
    overlay.activeIcons = (overlay.activeIcons or 0) + 1
    overlay:Show()

    frame.animation:Stop()
    frame.finaleAnimation:Stop()
    frame:ClearAllPoints()
    frame:SetPoint("CENTER", UIParent, "CENTER", startX, startY)
    frame:SetSize(size, size)
    frame.icon:SetTexture(iconTexture)
    frame:SetAlpha(1)

    frame:Show()
    if isFinale then
        frame.finaleAnimation:Play()
    else
        local travelDuration = 0.42 + (math.random(0, 42) / 100)
        frame.travel:SetDuration(travelDuration)
        frame.travel:SetOffset(endX - startX, endY - startY)
        frame.spin:SetDuration(travelDuration)
        frame.spin:SetDegrees(math.random(0, 1) == 0 and -math.random(120, 420) or math.random(120, 420))
        frame.animation:Play()
    end
end

function WipedQoL:StopMountAcquisitionAnimation()
    local previewSoundToken = self.mountAnimationPreviewSoundToken
    self.mountAnimationToken = nil
    self.mountAnimationPreviewToken = nil
    self.mountAnimationPreviewSoundToken = nil
    local overlay = self.mountAnimationOverlay
    if not overlay then
        if previewSoundToken and self.mountSoundPreviewToken == previewSoundToken then
            self:StopMountSoundPreview()
        end
        if self.RefreshMountAnimationUI then
            self:RefreshMountAnimationUI()
        end
        return
    end

    for _, frame in ipairs(overlay.icons) do
        if frame.animation:IsPlaying() then
            frame.animation:Stop()
        end
        if frame.finaleAnimation:IsPlaying() then
            frame.finaleAnimation:Stop()
        end
        frame.active = nil
        frame:Hide()
    end
    if overlay.titleFrame then
        overlay.titleFrame.intro:Stop()
        overlay.titleFrame.fade:Stop()
        overlay.titleFrame.pulseActive = nil
        overlay.titleFrame:Hide()
    end
    if overlay.titleIconFrame then
        overlay.titleIconFrame.fade:Stop()
        overlay.titleIconFrame:Hide()
    end
    overlay.activeIcons = 0
    overlay:Hide()
    if previewSoundToken and self.mountSoundPreviewToken == previewSoundToken then
        self:StopMountSoundPreview()
    end
    if self.RefreshMountAnimationUI then
        self:RefreshMountAnimationUI()
    end
end

function WipedQoL:IsMountAnimationPreviewPlaying()
    return self.mountAnimationPreviewToken ~= nil
        and self.mountAnimationPreviewToken == self.mountAnimationToken
end

function WipedQoL:StopMountAnimationPreview()
    self:StopMountAcquisitionAnimation()
end

function WipedQoL:PlayMountAnimationPreview()
    if self:GetMountAnimationSettings().enabled ~= true then
        return
    end

    self:PlayMountAcquisitionAnimation(
        MOUNT_ANIMATION_PREVIEW_MOUNT_ID,
        TRANSLATE("Anu'relos, Flame's Guidance")
    )
    if not self.mountAnimationToken then
        return
    end

    self.mountAnimationPreviewToken = self.mountAnimationToken
    self:PlayMountSoundPreview()
    self.mountAnimationPreviewSoundToken = self.mountSoundPreviewToken
    if self.RefreshMountAnimationUI then
        self:RefreshMountAnimationUI()
    end
end

function WipedQoL:PlayMountAcquisitionAnimation(mountID, itemName)
    local settings = self:GetMountAnimationSettings()
    if settings.enabled ~= true or IsSecretValue(mountID) or not mountID then
        return
    end

    if not C_MountJournal or not C_MountJournal.GetMountInfoByID then
        return
    end

    local ok, mountName, _, iconTexture = pcall(C_MountJournal.GetMountInfoByID, mountID)
    if not ok or IsSecretValue(iconTexture) or not iconTexture then
        return
    end
    if IsSecretValue(mountName) then
        mountName = nil
    end

    self:StopMountAcquisitionAnimation()
    local animationToken = {}
    self.mountAnimationToken = animationToken
    self:ShowMountAnimationTitle(mountName or itemName, iconTexture, animationToken)

    local screenWidth = math.max(800, math.floor(UIParent:GetWidth() or 800))
    local screenHeight = math.max(600, math.floor(UIParent:GetHeight() or 600))
    local maxX = math.floor(screenWidth * 0.46)
    local maxY = math.floor(screenHeight * 0.42)

    for index = 1, 68 do
        C_Timer.After((index - 1) * 0.15, function()
            if self.mountAnimationToken ~= animationToken or self:GetMountAnimationSettings().enabled ~= true then
                return
            end

            local startX
            local endX
            if index % 2 == 0 then
                startX = -maxX
                endX = math.random(math.floor(maxX * 0.2), maxX)
            else
                startX = maxX
                endX = -math.random(math.floor(maxX * 0.2), maxX)
            end

            local startY = math.random(-maxY, maxY)
            local endY = math.random(-maxY, maxY)
            local size = math.random(64, 126)
            self:LaunchMountAnimationIcon(iconTexture, startX, startY, endX, endY, size)
        end)
    end

    for index = 1, 4 do
        C_Timer.After(11.25 + ((index - 1) * 0.14), function()
            if self.mountAnimationToken ~= animationToken or self:GetMountAnimationSettings().enabled ~= true then
                return
            end

            local spread = 110
            self:LaunchMountAnimationIcon(
                iconTexture,
                math.random(-spread, spread),
                math.random(-spread, spread),
                math.random(-30, 30),
                math.random(-30, 30),
                138 + (index * 12),
                true
            )
        end)
    end

    C_Timer.After(13.2, function()
        if self.mountAnimationToken == animationToken then
            self.mountAnimationToken = nil
            if self.mountAnimationPreviewToken == animationToken then
                local previewSoundToken = self.mountAnimationPreviewSoundToken
                self.mountAnimationPreviewToken = nil
                self.mountAnimationPreviewSoundToken = nil
                if previewSoundToken and self.mountSoundPreviewToken == previewSoundToken then
                    self:StopMountSoundPreview()
                end
                if self.RefreshMountAnimationUI then
                    self:RefreshMountAnimationUI()
                end
            end
        end
    end)
end

function WipedQoL:IsAutoLootRollActive()
    local settings = self:GetAutoLootRollSettings()
    if settings.enabled ~= true then
        return false
    end

    return settings.legacyOnly ~= true or IsLegacyLootContent()
end

function WipedQoL:HandleAutoLootRollStarted(_, rollID)
    rollID = tonumber(rollID)
    if not rollID or not RollOnLoot or not self:IsAutoLootRollActive() then
        return
    end

    local settings = self:GetAutoLootRollSettings()
    local mode = ResolveAutoLootRollMode(rollID, settings.mode)
    local rollType = GetAutoLootRollType(mode)
    if rollType == nil then
        return
    end

    self.autoLootRollPendingConfirms = self.autoLootRollPendingConfirms or {}
    self.autoLootRollPendingConfirms[rollID] = rollType
    local success = pcall(RollOnLoot, rollID, rollType)
    if not success then
        self.autoLootRollPendingConfirms[rollID] = nil
    end
end

function WipedQoL:HandleAutoLootRollConfirm(_, rollID, rollType)
    rollID = tonumber(rollID)
    rollType = tonumber(rollType)
    local pending = self.autoLootRollPendingConfirms
    if not rollID or not pending or pending[rollID] ~= rollType then
        return
    end

    pending[rollID] = nil
    if ConfirmLootRoll then
        pcall(ConfirmLootRoll, rollID, rollType)
    end
end
