setfenv(1, VoiceOver)
SoundQueueUI = {}

local LibDataBroker = LibStub("LibDataBroker-1.1")
local LibDBIcon = LibStub("LibDBIcon-1.0")

local PORTRAIT_SIZE = 120
local PORTRAIT_ATLAS_SIZE = 512
local PORTRAIT_ATLAS_BORDER_SIZE = 416
local PORTRAIT_ATLAS_VIEWPORT_SIZE = 348
local PORTRAIT_BORDER_SCALE = PORTRAIT_SIZE / PORTRAIT_ATLAS_VIEWPORT_SIZE
local PORTRAIT_BORDER_SIZE = PORTRAIT_ATLAS_BORDER_SIZE * PORTRAIT_BORDER_SCALE
local PORTRAIT_BORDER_OUTSET = 34 * PORTRAIT_BORDER_SCALE
local PORTRAIT_LINE_WIDTH = 56 * PORTRAIT_BORDER_SCALE
local FRAME_WIDTH_WITHOUT_PORTRAIT = 300
local HIDE_GOSSIP_OPTIONS = true -- Disable to allow gossip options to show up in the button list, like quests
local WAIT_FOR_ANIMATION_FINISH_BEFORE_IDLE = true
local CAN_MODEL_LOAD_CACHE = Version:IsRetailOrAboveLegacyVersion(60000)

-- Helper function to get the current atlas texture path
local function GetAtlasTexturePath()
    local atlasIndex = Addon.db.profile.SoundQueueUI.PortraitFrameAtlas or 0
    local atlasPath = [[Interface\AddOns\AI_VoiceOver\Textures\PortraitFrameAtlas]]
    if atlasIndex > 0 then
        atlasPath = atlasPath .. atlasIndex
    end
    return atlasPath
end

do
    local font = CreateFont("VoiceOverNameFont")
    font:SetFont(GameFontNormal:GetFont(), 16, "")
    font:SetShadowColor(0, 0, 0)
    font:SetShadowOffset(1, -1)
    font:SetJustifyH("LEFT")
    font:SetJustifyV("TOP")
end
do
    local font = CreateFont("VoiceOverButtonFont")
    font:SetFont(GameFontNormal:GetFont(), 13, "")
    font:SetShadowColor(0, 0, 0)
    font:SetShadowOffset(1, -1)
    font:SetJustifyH("LEFT")
    font:SetJustifyV("MIDDLE")
end

function SoundQueueUI:Initialize()
    self:InitDisplay()
    self:InitPortraitLine()
    self:InitPortrait()
    self:InitMover()
    self:InitMinimapButton()

    self:RefreshConfig()
end

function SoundQueueUI:InitDisplay()
    self.frame = CreateFrame("Frame", "VoiceOverFrame", UIParent, "BackdropTemplate")
    function self.frame:Reset()
        self:SetWidth(PORTRAIT_SIZE + FRAME_WIDTH_WITHOUT_PORTRAIT)
        self:SetHeight(PORTRAIT_SIZE)
        self:ClearAllPoints()
        self:SetPoint("BOTTOM", 0, 200)
    end

    self.frame:Reset()
    self.frame:SetMovable(true)         -- Allow the frame to be moved
    self.frame:SetResizable(true)       -- Allow the frame to be resized
    self.frame:SetClampedToScreen(true) -- Prevent from being dragged off-screen
    self.frame:SetUserPlaced(true)
    self.frame:SetFrameStrata(Addon.db.profile.SoundQueueUI.FrameStrata)

    -- Create a background gradient behind the queue container
    self.frame.background = self.frame:CreateTexture(nil, "BACKGROUND")
    self.frame.background:SetPoint("RIGHT")
    self.frame.background:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\BackgroundGradient]])

    -- Create a button to resize the main frame
    self.frame.resizer = CreateFrame("Button", nil, self.frame)
    self.frame.resizer:SetPoint("BOTTOMRIGHT")
    self.frame.resizer:SetSize(16, 16)
    self.frame.resizer:SetNormalTexture([[Interface\AddOns\AI_VoiceOver\Textures\SizeGrabber-Up]])
    self.frame.resizer:SetPushedTexture([[Interface\AddOns\AI_VoiceOver\Textures\SizeGrabber-Down]])
    self.frame.resizer:SetHighlightTexture([[Interface\AddOns\AI_VoiceOver\Textures\SizeGrabber-Highlight]])
    self.frame.resizer:HookScript("OnEnter", function() SetCursor([[Interface\Cursor\UI-Cursor-SizeRight]]) end)
    self.frame.resizer:HookScript("OnLeave", function() SetCursor(nil) end)
    self.frame.resizer:HookScript("OnMouseDown", function()
        self.frame.resizer:GetHighlightTexture():Hide()
        self.frame:StartSizing("BOTTOMRIGHT")
    end)
    self.frame.resizer:HookScript("OnMouseUp", function()
        self.frame.resizer:GetHighlightTexture():Show()
        self.frame:StopMovingOrSizing()
    end)

    -- Creature queue buttons container
    self.frame.container = CreateFrame("Frame", nil, self.frame)
    self.frame.container:SetPoint("RIGHT")
    self.frame.container.buttons = {}
    function self.frame.container.buttons:Update()
        for _, button in ipairs(self) do
            button:Update()
        end
    end

    -- Create NPC name text
    local SKIP_GOSSIP_BUTTON_OFFSET = -6
    self.frame.container.name = self.frame.container:CreateFontString(nil, "ARTWORK", "VoiceOverNameFont")
    self.frame.container.name:SetPoint("TOPLEFT")
    self.frame.container.name:SetWordWrap(false)
    self.frame.container.name:SetTextColor(214 / 255, 214 / 255, 214 / 255)
    function self.frame.container.name:Update()
        local containerWidth = Version.IsLegacyVanilla and
            ((self:GetParent():GetRight() or 0) - (self:GetParent():GetLeft() or 0)) or
            self:GetParent():GetWidth() -- 1.12 quirk
        self:SetWidth(0)
        self:SetText(self:GetText())
        self:SetWidth(math.min(
            containerWidth - (SoundQueueUI.frame.container.stopGossip:IsShown() and 32 + SKIP_GOSSIP_BUTTON_OFFSET or 0),
            self:GetStringWidth() + 1))
    end

    -- Create Stop Gossip button
    self.frame.container.stopGossip = CreateFrame("Button", nil, self.frame.container)
    self.frame.container.stopGossip:SetSize(32, 32)
    self.frame.container.stopGossip:SetPoint("BOTTOMLEFT", self.frame.container.name, "RIGHT", SKIP_GOSSIP_BUTTON_OFFSET,
        0)
    function self.frame.container.stopGossip:SetGossipCount(gossipCount)
        local texture = gossipCount > 1 and [[Interface\AddOns\AI_VoiceOver\Textures\StopGossipMore]] or
            [[Interface\AddOns\AI_VoiceOver\Textures\StopGossip]]
        self:SetShown(gossipCount > 0)
        self:EnableMouse(SoundQueue:CanBePaused() or gossipCount > 1)
        self:SetHighlightTexture(texture, "ADD")
        self:SetNormalTexture(texture)
        self:SetPushedTexture(texture)

        self.tooltip = SoundQueue:CanBePaused() and (gossipCount > 1 and "Next Gossip" or "Stop Gossip") or
            "Skip Remaining Gossips"
        if GameTooltip:GetOwner() == self then
            GameTooltip:SetText(self.tooltip)
            GameTooltip:Show()
        end
    end

    self.frame.container.stopGossip:SetGossipCount(0)
    self.frame.container.stopGossip:GetHighlightTexture():SetAlpha(0.5)
    self.frame.container.stopGossip:GetPushedTexture():SetAlpha(0.5)
    self.frame.container.stopGossip:HookScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_NONE")
        GameTooltip:SetPoint("LEFT", self, "RIGHT")
        GameTooltip:SetText(self.tooltip)
        GameTooltip:Show()
    end)
    self.frame.container.stopGossip:HookScript("OnLeave", GameTooltip_Hide)
    self.frame.container.stopGossip:HookScript("OnClick", function()
        PlaySound(SOUNDKIT.U_CHAT_SCROLL_BUTTON)
        if SoundQueue:CanBePaused() then
            -- Skip current
            local soundData = SoundQueue:GetCurrentSound()
            if soundData and Enums.SoundEvent:IsGossipEvent(soundData.event) then
                SoundQueue:RemoveSoundFromQueue(soundData)
            end
        else
            -- Skip all remaining
            local soundData = SoundQueue:GetNextSound()
            while soundData and Enums.SoundEvent:IsGossipEvent(soundData.event) do
                SoundQueue:RemoveSoundFromQueue(soundData)
                soundData = SoundQueue:GetNextSound()
            end
        end
    end)

    self.frame:HookScript("OnSizeChanged", function()
        self.frame.container.name:Update()
        self.frame.container.buttons:Update()
    end)
end

function SoundQueueUI:InitPortraitLine()
    -- Create a vertical line that will be visible instead of the portrait if the player turned the portrait off
    self.frame.portraitLine = self.frame:CreateTexture(nil, "BORDER")
    self.frame.portraitLine:SetPoint("TOPLEFT", -PORTRAIT_LINE_WIDTH / 2 + 2, PORTRAIT_BORDER_OUTSET)
    self.frame.portraitLine:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMLEFT", PORTRAIT_LINE_WIDTH / 2 + 2,
        -PORTRAIT_BORDER_OUTSET)
    self.frame.portraitLine:SetTexture(GetAtlasTexturePath())
    self.frame.portraitLine:SetTexCoord(456 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE, 0,
        PORTRAIT_ATLAS_BORDER_SIZE / PORTRAIT_ATLAS_SIZE)

    -- Create a play/pause button on the vertical line that will be visible if the player turned the portrait off
    self.frame.miniPause = CreateFrame("Button", nil, self.frame)
    self.frame.miniPause:SetSize(26, 26)
    --self.frame.miniPause:SetPoint("CENTER", self.frame.container.name, "LEFT", -20 + 2, 0) -- Use this to make the button be placed next to NPC name instead of always centered
    self.frame.miniPause:SetPoint("CENTER", self.frame.container, "LEFT", -20 + 2, 0)
    self.frame.miniPause:SetNormalTexture(GetAtlasTexturePath())
    self.frame.miniPause:GetNormalTexture():ClearAllPoints()
    self.frame.miniPause:GetNormalTexture():SetPoint("CENTER")
    self.frame.miniPause:GetNormalTexture():SetSize(14, 14)
    self.frame.miniPause:SetPushedTexture(GetAtlasTexturePath())
    self.frame.miniPause:GetPushedTexture():ClearAllPoints()
    self.frame.miniPause:GetPushedTexture():SetPoint("CENTER")
    self.frame.miniPause:GetPushedTexture():SetSize(12, 12)
    self.frame.miniPause.background = self.frame.miniPause:CreateTexture(nil, "BACKGROUND")
    self.frame.miniPause.background:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\SettingsButton]])
    self.frame.miniPause.background:SetPoint("CENTER")
    self.frame.miniPause.background:SetSize(32, 32)
    function self.frame.miniPause:Update()
        if Addon.db.char.IsPaused then
            self:GetNormalTexture():SetTexCoord(0 / PORTRAIT_ATLAS_SIZE, 93 / PORTRAIT_ATLAS_SIZE,
                419 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
            self:GetPushedTexture():SetTexCoord(0 / PORTRAIT_ATLAS_SIZE, 93 / PORTRAIT_ATLAS_SIZE,
                419 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
            self:GetNormalTexture():SetAlpha(MouseIsOver(self) and 1 or 0.75)
        else
            self:GetNormalTexture():SetTexCoord(93 / PORTRAIT_ATLAS_SIZE, 186 / PORTRAIT_ATLAS_SIZE,
                419 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
            self:GetPushedTexture():SetTexCoord(93 / PORTRAIT_ATLAS_SIZE, 186 / PORTRAIT_ATLAS_SIZE,
                419 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
            self:GetNormalTexture():SetAlpha(MouseIsOver(self) and 1 or 0.75)
        end
    end

    self.frame.miniPause:Update()
    self.frame.miniPause:HookScript("OnEnter", function(self)
        self:GetNormalTexture():SetAlpha(1)
    end)
    self.frame.miniPause:HookScript("OnLeave", function(self)
        self:GetNormalTexture():SetAlpha(0.75)
    end)
    self.frame.miniPause:HookScript("OnClick", function()
        PlaySound(SOUNDKIT.U_CHAT_SCROLL_BUTTON)
        SoundQueue:TogglePauseQueue()
    end)
end

local function ShouldShowBookFor(soundData)
    if soundData.modelFrame then
        if type(soundData.modelFrame:GetModel()) ~= "string" then
            return true -- Show book for missing and unloaded models
        end
        return false
    elseif Version.IsLegacyBurningCrusade then
        return true -- No support for Model:SetCreature in 2.4.3
    end

    local guid = soundData.unitGUID
    local type = guid and Utils:GetGUIDType(guid)
    if not type then
        return true -- A fallback in case the GUID is missing
    elseif type == Enums.GUID.Item then
        return true -- Maybe display the item icon in the future
    elseif type == Enums.GUID.GameObject then
        return true -- Maybe display the object model in the future
    end
    return false
end

function SoundQueueUI:InitPortrait()
    -- Create a container frame for the portrait
    self.frame.portrait = CreateFrame("Frame", nil, self.frame)
    self.frame.portrait:SetPoint("TOPLEFT")
    self.frame.portrait:SetSize(PORTRAIT_SIZE, PORTRAIT_SIZE)

    function self.frame.portrait:Configure(soundData)
        if not self:IsShown() then
            return
        end

        if not soundData then
            self.model:Hide()
            self.bookModel:Hide()
        elseif ShouldShowBookFor(soundData) then
            self.model:Hide()
            self.bookModel:Show()

            -- Usar el creatureID del soundData o un valor por defecto
            local creatureID = soundData.creatureID or 15608
            self.bookModel:SetCreature(creatureID)
            self.bookModel:SetCustomCamera(0) -- Agregar la cámara 0 para el libro
            self.bookModel:SetAnimation(60)
            self.bookModel:SetScript("OnUpdate", function(self, elapsed)
                if not self.animDuration then
                    self.animDuration = Utils:GetModelAnimationDuration(self:GetModelFileID(), 60)
                end
                if not self.animSequence then
                    self.animSequence = { 60, 65 }
                    self.animIndex = 1
                end
                if GetTime() - (self.animtimer or 0) >= (self.animDuration or 2) + 0.2 then
                    self.animtimer = GetTime()
                    self:SetAnimation(self.animSequence[self.animIndex])
                    self.animIndex = self.animIndex % #self.animSequence + 1
                    self.animDuration = Utils:GetModelAnimationDuration(self:GetModelFileID(),
                        self.animSequence[self.animIndex])
                end
            end)
        else
            self.model = soundData.modelFrame or self.model

            if not self.model._initialized then
                self.model._initialized = true
                self.model:HookScript("OnHide", function(self)
                    self:ClearModel()
                    self.oldCreatureID = nil
                    self.animation = nil
                    self.animDuration = nil
                    self.animDelay = nil
                    self.animtimer = nil
                    self.modelIDPrinted = nil
                end)
                self.model:HookScript("OnUpdate", function(self, elapsed)
                    if CAN_MODEL_LOAD_CACHE and self.oldCreatureID and not self:GetModelFileID() then
                        self:SetCreature(self.oldCreatureID)
                        if not self:GetModelFileID() then
                            return
                        end
                    end
                    local uniqueModelIDs = {

                        --3950118,
                        878772,
                        878773,
                        878774,
                        878775,
                        4519034,
                        576555,
                        1000764,
                        5548261,
                        5489804,
                        5353632,
                        5767091,
                        893080,
                        4220448,
                        5680838,
                        986648,
                        1721003,
                        1717164,
                        5482015,
                        --3950118,
                        968705,
                        2618947,
                        5794846,
                        959310,
                        4207724,
                        5517447,
                        997378,
                        4217881,
                        1890759,
                        3024833,
                        4498203,
                        5764885,
                        119376,
                        3058051,
                        6047739,
                        5145420

                    }
                    local modelID = self:GetModelFileID()
                    if tContains(uniqueModelIDs, modelID) then
                        self:SetCustomCamera(1)

                        self:SetRotation(0.5)        -- Rotación en radianes (0-6.28)
                        self:SetCameraDistance(1.10) -- Distancia de la cámara al modelo
                        self:SetCameraFacing(0)      -- Ángulo de la cámara en radianes
                    elseif modelID == 1066978 then
                        self:SetCustomCamera(1)
                        --self:ClearModel()
                        --self:SetCreature(1066978)
                        --self:SetCameraPosition(0, 2, 1)  -- Posición de la cámara
                        --self:SetCameraTarget(0, 0, 0)    -- Punto de enfoque
                        --self:SetCameraDistance(2.5)      -- Distancia
                        --self:SetCameraFacing(0)          -- Ángulo
                        self:SetPosition(0, -0.2, -0.1)
                        self:SetScale(0.85)
                        self:SetRotation(0.5)               -- Escala del modelo
                        --self:SetAnimation(60)            -- Animación
                        -- Posición del modelo
                        --self:SetFacing(0)   
                        -- Rotación del modelo
                    else
                        self:SetCustomCamera(0)
                    end
                    if UnitName("player") == "Arckanys" or UnitName("player") == "Disparos" or UnitName("player") == "Maguno" then
                        if not self.modelIDPrinted then
                            DEFAULT_CHAT_FRAME:AddMessage("Interacted ModelID: " .. modelID, 0, 1, 0)
                            self.modelIDPrinted = true
                        end
                    end
                    if self.animation and not self.animDuration then
                        self.animDuration = Utils:GetModelAnimationDuration(self:GetModelFileID(), self.animation)
                        if self.animDuration and self.animDuration == 0 then
                            self.animation = 0
                            self:SetAnimation(self.animation)
                        end
                    end
                    if self.animDelay and not Addon.db.char.IsPaused then
                        self.animDelay = self.animDelay - elapsed
                        if self.animDelay < 0 then
                            self.animDelay = nil
                        else
                            return
                        end
                    end
                    local isPaused = not SoundQueue:IsPlaying()
                    if not WAIT_FOR_ANIMATION_FINISH_BEFORE_IDLE and self.animation and self.animation ~= 0 and not self.animDelay and isPaused then
                        self.animation = 0
                        self.animtimer = nil
                        self:SetAnimation(self.animation)
                    end
                    if not self.animSequence then
                        self.animSequence = { 60, 65 }
                        self.animIndex = 1
                    end
                    if self.animation and GetTime() - (self.animtimer or 0) >= (self.animDuration or 2) + 0.2 then
                        if isPaused or self.animation == 0 then
                            if WAIT_FOR_ANIMATION_FINISH_BEFORE_IDLE and self.animation ~= 0 and not self.animDelay then
                                self.animation = 0
                                self.animtimer = nil
                                self:SetAnimation(self.animation)
                            end
                        else
                            self.animtimer = GetTime()
                            self.animation = self.animSequence[self.animIndex]
                            self:SetAnimation(self.animation)
                            self.animIndex = self.animIndex % #self.animSequence + 1
                            self.animDuration = Utils:GetModelAnimationDuration(self:GetModelFileID(), self.animation)
                        end
                    end
                    self.wasPaused = isPaused
                end)
            end
            self.model:Show()
            self.bookModel:Hide()
            self.model:SetAllPoints()
            local creatureID
            if soundData.unitGUID then
                creatureID = Utils:GetIDFromGUID(soundData.unitGUID)
            else
                creatureID = soundData.id
            end
            if creatureID ~= self.model.oldCreatureID then
                if CAN_MODEL_LOAD_CACHE then
                    self.model:ClearModel()
                end
                self.model:SetCreature(creatureID)
                self.model:SetCustomCamera(0)
                self.model.animation = 60
                self.model.animDuration = nil
                self.model.animDelay = soundData.delay
                self.model.animtimer = nil
                self.model.oldCreatureID = creatureID
            elseif self.model.wasPaused and SoundQueue:IsPlaying() then
                self.model.animation = 60
                self.model.animDuration = nil
                self.model.animDelay = soundData.delay
            end
        end
    end

    self.frame.portrait.background = self.frame.portrait:CreateTexture(nil, "BACKGROUND")
    self.frame.portrait.background:SetAllPoints()
    self.frame.portrait.background:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\PortraitFrameBackground]] .. (Addon.db.profile.SoundQueueUI.PortraitBackgroundTexture or 0))
    self.frame.portrait.model = CreateFrame("DressUpModel", nil, self.frame.portrait)
    self.frame.portrait.defaultModel = self.frame.portrait.model
    self.frame.portrait.bookModel = CreateFrame("DressUpModel", nil, self.frame.portrait)
    self.frame.portrait.bookModel:SetAllPoints()
    self.frame.portrait.bookModel:SetCreature(15608)
    self.frame.portrait.bookModel:HookScript("OnShow", function(self)
        self:SetCustomCamera(0)
    end)
    self.frame.portrait.bookModel:SetAnimation(60)
    self.frame.portrait.bookModel:HookScript("OnUpdate", function(self, elapsed)
        if not self.animDuration then
            self.animDuration = Utils:GetModelAnimationDuration(self:GetModelFileID(), 60)
        end
        if not self.animSequence then
            self.animSequence = { 60, 60, 65, 60, 60 }
            self.animIndex = 1
        end
        if GetTime() - (self.animtimer or 0) >= (self.animDuration or 2) + 0.2 then
            self.animtimer = GetTime()
            self:SetAnimation(self.animSequence[self.animIndex])
            self.animIndex = self.animIndex % #self.animSequence + 1
            self.animDuration = Utils:GetModelAnimationDuration(self:GetModelFileID(), self.animSequence[self.animIndex])
        end
    end)
    self.frame.portrait.bookModel:Hide()

    -- Create PlayerModel for visual effect animation overlay
    self.frame.portrait.effectModel = CreateFrame("PlayerModel", nil, UIParent)
    self.frame.portrait.effectModel:SetKeepModelOnHide(true)
    self.frame.portrait.effectModel:SetFrameLevel(self.frame.portrait:GetFrameLevel() + 5) -- Above everything

    -- Apply detached/attached state
    local isDetached = Addon.db.profile.SoundQueueUI.PortraitEffectDetached
    if isDetached then
        -- Detached mode: movable and independent
        self.frame.portrait.effectModel:SetParent(UIParent)
        self.frame.portrait.effectModel:SetMovable(true)
        self.frame.portrait.effectModel:EnableMouse(true)
        self.frame.portrait.effectModel:RegisterForDrag("LeftButton")
        self.frame.portrait.effectModel:SetScript("OnDragStart", function(self) self:StartMoving() end)
        self.frame.portrait.effectModel:SetScript("OnDragStop", function(self)
            self:StopMovingOrSizing()
            -- Save position
            local point, _, relativePoint, x, y = self:GetPoint()
            Addon.db.profile.SoundQueueUI.PortraitEffectPosition = {
                point = point,
                relativePoint = relativePoint,
                x = x,
                y = y
            }
        end)
        -- Restore saved position or default
        local pos = Addon.db.profile.SoundQueueUI.PortraitEffectPosition
        if pos then
            self.frame.portrait.effectModel:SetPoint(pos.point, UIParent, pos.relativePoint, pos.x, pos.y)
        else
            self.frame.portrait.effectModel:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
        end
        local frameSize = Addon.db.profile.SoundQueueUI.PortraitEffectFrameSize or 120
        self.frame.portrait.effectModel:SetSize(frameSize, frameSize)
    else
        -- Attached mode: follows portrait
        self.frame.portrait.effectModel:ClearAllPoints()
        self.frame.portrait.effectModel:SetAllPoints(self.frame.portrait)
        self.frame.portrait.effectModel:SetParent(self.frame.portrait)
    end

    -- Show/hide effect based on enabled setting
    local effectEnabled = Addon.db.profile.SoundQueueUI.PortraitEffectEnabled
    if effectEnabled == nil then effectEnabled = false end -- Default to disabled
    if effectEnabled then
        self.frame.portrait.effectModel:Show()
    else
        self.frame.portrait.effectModel:Hide()
    end

    -- Configure the visual effect model using saved FileDataID
    local effectFileID = Addon.db.profile.SoundQueueUI.PortraitEffectFileID or 6217457
    local success = pcall(self.frame.portrait.effectModel.SetModel, self.frame.portrait.effectModel, effectFileID)
    if success then
        self.frame.portrait.effectModel:SetPortraitZoom(0)
        self.frame.portrait.effectModel:ClearTransform()

        -- Apply saved position (X, Y, Z)
        local x = Addon.db.profile.SoundQueueUI.PortraitEffectX or 0
        local y = Addon.db.profile.SoundQueueUI.PortraitEffectY or 0
        local z = Addon.db.profile.SoundQueueUI.PortraitEffectZ or 0
        self.frame.portrait.effectModel:SetPosition(z, x, y) -- Note: SetPosition takes (z, x, y)

        -- Apply saved rotation
        local rotation = Addon.db.profile.SoundQueueUI.PortraitEffectRotation or 0
        local radians = math.rad(rotation)
        self.frame.portrait.effectModel:SetFacing(radians)

        self.frame.portrait.effectModel:SetAnimation(0)
    end

    -- Hook portrait visibility to control effectModel when detached
    if Addon.db.profile.SoundQueueUI.PortraitEffectDetached then
        -- When detached, effectModel should follow portrait visibility
        self.frame.portrait:HookScript("OnShow", function()
            if self.frame.portrait.effectModel then
                self.frame.portrait.effectModel:Show()
            end
        end)
        self.frame.portrait:HookScript("OnHide", function()
            if self.frame.portrait.effectModel then
                self.frame.portrait.effectModel:Hide()
            end
        end)

        -- Set initial visibility based on portrait state
        if self.frame.portrait:IsShown() then
            self.frame.portrait.effectModel:Show()
        else
            self.frame.portrait.effectModel:Hide()
        end
    end

    self.frame.portrait.pause = CreateFrame("Button", nil, self.frame.portrait)
    self.frame.portrait.pause:SetFrameLevel(self.frame.portrait.model:GetFrameLevel() + 1)
    self.frame.portrait.pause:SetAllPoints()
    self.frame.portrait.pause.background = self.frame.portrait.pause:CreateTexture(nil, "BACKGROUND")
    self.frame.portrait.pause.background:SetAllPoints()
    self.frame.portrait.pause.background:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\PortraitFrameBackground]] .. (Addon.db.profile.SoundQueueUI.PortraitBackgroundTexture or 0))
    self.frame.portrait.pause.background:SetAlpha(0.75)
    self.frame.portrait.pause:SetNormalTexture(GetAtlasTexturePath())
    self.frame.portrait.pause:GetNormalTexture():ClearAllPoints()
    self.frame.portrait.pause:GetNormalTexture():SetPoint("CENTER")
    self.frame.portrait.pause:GetNormalTexture():SetSize(32, 32)
    self.frame.portrait.pause:SetPushedTexture(GetAtlasTexturePath())
    self.frame.portrait.pause:GetPushedTexture():ClearAllPoints()
    self.frame.portrait.pause:GetPushedTexture():SetPoint("CENTER")
    self.frame.portrait.pause:GetPushedTexture():SetSize(28, 28)
    function self.frame.portrait.pause:Update()
        if Addon.db.char.IsPaused then
            if SoundQueue:IsPlaying() then
                self.background:Hide()
            else
                self.background:Show()
            end
            self:GetNormalTexture():SetTexCoord(0 / PORTRAIT_ATLAS_SIZE, 93 / PORTRAIT_ATLAS_SIZE,
                419 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
            self:GetPushedTexture():SetTexCoord(0 / PORTRAIT_ATLAS_SIZE, 93 / PORTRAIT_ATLAS_SIZE,
                419 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
            self:GetNormalTexture():SetAlpha(MouseIsOver(self) and 1 or 0.75)
        else
            self.background:Hide()
            self:GetNormalTexture():SetTexCoord(93 / PORTRAIT_ATLAS_SIZE, 186 / PORTRAIT_ATLAS_SIZE,
                419 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
            self:GetPushedTexture():SetTexCoord(93 / PORTRAIT_ATLAS_SIZE, 186 / PORTRAIT_ATLAS_SIZE,
                419 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
            self:GetNormalTexture():SetAlpha(MouseIsOver(self) and 1 or 0)
        end
    end

    self.frame.portrait.pause:Update()
    self.frame.portrait.pause:HookScript("OnEnter", function(self)
        self:GetNormalTexture():SetAlpha(1)
    end)
    self.frame.portrait.pause:HookScript("OnLeave", function(self)
        self:GetNormalTexture():SetAlpha(Addon.db.char.IsPaused and 0.75 or 0)
    end)
    self.frame.portrait.pause:HookScript("OnClick", function()
        PlaySound(SOUNDKIT.U_CHAT_SCROLL_BUTTON)
        SoundQueue:TogglePauseQueue()
    end)
    self.frame.portrait.border = CreateFrame("Frame", nil, self.frame.portrait)
    self.frame.portrait.border:SetFrameLevel(self.frame.portrait.pause:GetFrameLevel() + 1)
    self.frame.portrait.border:SetAllPoints()
    self.frame.portrait.border.texture = self.frame.portrait.border:CreateTexture(nil, "BORDER")
    self.frame.portrait.border.texture:SetSize(PORTRAIT_BORDER_SIZE, PORTRAIT_BORDER_SIZE)
    self.frame.portrait.border.texture:SetPoint("TOPLEFT", -PORTRAIT_BORDER_OUTSET, PORTRAIT_BORDER_OUTSET)
    self.frame.portrait.border.texture:SetPoint("BOTTOMRIGHT", PORTRAIT_BORDER_OUTSET, -PORTRAIT_BORDER_OUTSET)
    self.frame.portrait.border.texture:SetTexture(GetAtlasTexturePath())
    self.frame.portrait.border.texture:SetTexCoord(0, PORTRAIT_ATLAS_BORDER_SIZE / PORTRAIT_ATLAS_SIZE, 0,
        PORTRAIT_ATLAS_BORDER_SIZE / PORTRAIT_ATLAS_SIZE)
end

function SoundQueueUI:InitMover()
    -- Create a button that lets the player drag the frame around
    self.frame.mover = CreateFrame("Button", nil, self.frame.portrait.border)
    self.frame.mover:SetSize(26, 26)
    self.frame.mover:SetPoint("CENTER", self.frame.portrait.border, "BOTTOMLEFT", 5, 6)
    self.frame.mover:SetNormalTexture(GetAtlasTexturePath())
    self.frame.mover:GetNormalTexture():SetTexCoord(462 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE,
        462 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
    self.frame.mover:GetNormalTexture():ClearAllPoints()
    self.frame.mover:GetNormalTexture():SetPoint("CENTER")
    self.frame.mover:GetNormalTexture():SetSize(16, 16)
    self.frame.mover:SetPushedTexture(GetAtlasTexturePath())
    self.frame.mover:GetPushedTexture():SetTexCoord(462 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE,
        462 / PORTRAIT_ATLAS_SIZE, 512 / PORTRAIT_ATLAS_SIZE)
    self.frame.mover:GetPushedTexture():ClearAllPoints()
    self.frame.mover:GetPushedTexture():SetPoint("CENTER")
    self.frame.mover:GetPushedTexture():SetSize(14, 14)
    self.frame.mover.background = self.frame.mover:CreateTexture(nil, "BACKGROUND")
    self.frame.mover.background:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\SettingsButton]])
    self.frame.mover.background:SetPoint("CENTER")
    self.frame.mover.background:SetSize(32, 32)
    self.frame.mover:HookScript("OnEnter", function(self)
        if Addon.db.profile.SoundQueueUI.LockFrame then return end
        SetCursor([[Interface\Cursor\UI-Cursor-Move]])
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText("Move the Frame", HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
        GameTooltip:AddLine("Frame position can be locked in settings.", NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g,
            NORMAL_FONT_COLOR.b, 1)
        GameTooltip:Show()
    end)
    self.frame.mover:HookScript("OnLeave", function(self)
        SetCursor(nil)
        GameTooltip_Hide()
    end)
    self.frame.mover:HookScript("OnMouseDown", function()
        if Addon.db.profile.SoundQueueUI.LockFrame then return end
        self.frame:StartMoving()
    end)
    self.frame.mover:HookScript("OnMouseUp", function()
        if Addon.db.profile.SoundQueueUI.LockFrame then return end
        self.frame:StopMovingOrSizing()
    end)
end

function SoundQueueUI:InitMinimapButton()
    local buttons =
    {
        { "LeftButton",   "Left Click" },
        { "MiddleButton", "Middle Click" },
        { "RightButton",  "Right Click" },
    }
    local object = LibDataBroker:NewDataObject("VoiceOver", {
        type = "launcher",
        text = "VoiceOver",
        icon = [[Interface\AddOns\AI_VoiceOver\Textures\MinimapButton]],

        OnClick = function(self, button)
            local command = Addon.db.profile.MinimapButton.Commands[button]
            if command and command ~= "" then
                local handler = Options.table.args.SlashCommands.args[command]
                if handler and handler.func then
                    PlaySound(SOUNDKIT.U_CHAT_SCROLL_BUTTON)
                    handler.func()
                end
            end
        end,

        OnTooltipShow = function(tooltip)
            tooltip:SetText("VoiceOver")
            for _, info in ipairs(buttons) do
                local button, text = unpack(info)
                local command = Addon.db.profile.MinimapButton.Commands[button]
                if command and command ~= "" then
                    local handler = Options.table.args.SlashCommands.args[command]
                    if handler and handler.name then
                        tooltip:AddLine(format("%s%s:|r %s", GRAY_FONT_COLOR_CODE, text, handler.name))
                    end
                end
            end
            tooltip:Show()
        end,
    })
    LibDBIcon:Register("VoiceOver", object, Addon.db.profile.MinimapButton.LibDBIcon)
end

function SoundQueueUI:RefreshConfig()
    -- Update background textures
    local textureIndex = Addon.db.profile.SoundQueueUI.PortraitBackgroundTexture or 0
    local texturePath = [[Interface\AddOns\AI_VoiceOver\Textures\PortraitFrameBackground]] .. textureIndex
    if self.frame.portrait.background then
        self.frame.portrait.background:SetTexture(texturePath)
    end
    if self.frame.portrait.pause.background then
        self.frame.portrait.pause.background:SetTexture(texturePath)
    end

    -- Update atlas textures
    local atlasPath = GetAtlasTexturePath()
    if self.frame.portraitLine then
        self.frame.portraitLine:SetTexture(atlasPath)
    end
    if self.frame.miniPause then
        self.frame.miniPause:SetNormalTexture(atlasPath)
        self.frame.miniPause:SetPushedTexture(atlasPath)
    end
    if self.frame.portrait.pause then
        self.frame.portrait.pause:SetNormalTexture(atlasPath)
        self.frame.portrait.pause:SetPushedTexture(atlasPath)
    end
    if self.frame.portrait.border and self.frame.portrait.border.texture then
        self.frame.portrait.border.texture:SetTexture(atlasPath)
    end
    if self.frame.mover then
        self.frame.mover:SetNormalTexture(atlasPath)
        self.frame.mover:SetPushedTexture(atlasPath)
    end

    if Addon.db.profile.SoundQueueUI.HidePortrait then
        if self.frame.portrait:IsShown() then
            self.frame:SetWidth(self.frame:GetWidth() - PORTRAIT_SIZE)
        end
        self.frame:SetResizeBounds(100, PORTRAIT_SIZE, 10000, PORTRAIT_SIZE)

        self.frame.portraitLine:Show()
        self.frame.portrait:Hide()
        self.frame.miniPause:Show()

        self.frame.container:SetPoint("LEFT", 20, 0)
        self.frame.background:SetPoint("TOPLEFT")
        self.frame.background:SetPoint("BOTTOMLEFT")

        self.frame.mover:SetParent(self.frame)
        self.frame.mover:SetPoint("CENTER", self.frame, "BOTTOMLEFT", 2, 6)
    else
        if not self.frame.portrait:IsShown() then
            self.frame:SetWidth(self.frame:GetWidth() + PORTRAIT_SIZE)
        end
        self.frame:SetResizeBounds(PORTRAIT_SIZE + 100, PORTRAIT_SIZE, 10000, PORTRAIT_SIZE)

        self.frame.portraitLine:Hide()
        self.frame.portrait:Show()
        self.frame.miniPause:Hide()

        self.frame.container:SetPoint("LEFT", self.frame.portrait, "RIGHT", 15, 0)
        self.frame.background:SetPoint("TOPLEFT", self.frame.portrait, "TOPRIGHT")
        self.frame.background:SetPoint("BOTTOMLEFT", self.frame.portrait, "BOTTOMRIGHT")

        self.frame.mover:SetParent(self.frame.portrait.border)
        self.frame.mover:SetPoint("CENTER", self.frame.portrait.border, "BOTTOMLEFT", 5, 6)
    end

    self.frame.mover:SetShown(not Addon.db.profile.SoundQueueUI.LockFrame)
    self.frame.resizer:SetShown(not Addon.db.profile.SoundQueueUI.LockFrame)
    self.frame:SetScale(Addon.db.profile.SoundQueueUI.FrameScale)

    self:UpdateSoundQueueDisplay()
    LibDBIcon:Refresh("VoiceOver", Addon.db.profile.MinimapButton.LibDBIcon)
end

function SoundQueueUI:CreateButton(i)
    local button = CreateFrame("Button", nil, self.frame.container)
    self.frame.container.buttons[i] = button

    button:SetID(i)
    button:SetHeight(20)

    button.textWidget = button:CreateFontString(nil, "OVERLAY", "VoiceOverButtonFont")
    button.textWidget:SetWordWrap(true)

    button.iconWidget = button:CreateTexture(nil, "ARTWORK")
    button.iconWidget:SetSize(16, 16)
    button.iconWidget:SetPoint("CENTER", button, "LEFT", 16 / 2, 0)

    function button:Configure(soundData)
        self.soundData = soundData
        self:Update()
    end

    function button:Update(pushed, hovered)
        if pushed == nil then pushed = self.pushed else self.pushed = pushed end
        if hovered == nil then hovered = self.hovered else self.hovered = hovered end
        local soundData = self.soundData
        if not soundData then
            self:Hide()
            return
        end
        self:Show()
        local soundDataBeingPlayed = SoundQueue:GetCurrentSound()
        local isBeingPlayed = soundData == soundDataBeingPlayed
        local buttonIndex = self:GetID()

        if isBeingPlayed then
            self:SetAlpha(1)
            self.textWidget:SetShadowColor(0, 0, 0, 1)
            self:SetPoint("TOPLEFT", SoundQueueUI.frame.container.name, "BOTTOMLEFT", 0, -2)
            self:EnableMouse(SoundQueue:CanBePaused())
        else
            local isCollapsedGossipBeingPlayed = soundDataBeingPlayed and
                Enums.SoundEvent:IsGossipEvent(soundDataBeingPlayed.event) and
                (HIDE_GOSSIP_OPTIONS or not soundDataBeingPlayed.title)
            local queuePosition = buttonIndex - (isCollapsedGossipBeingPlayed and 0 or 1)
            local alpha = math.max(0.1, math.min(1, 1 - (queuePosition - 1) / 3))
            self:SetAlpha(alpha)
            self.textWidget:SetShadowColor(0, 0, 0, 0.5 + 0.5 * alpha) -- Technically isn't necessary, but it looks better this way
            self:SetPoint("TOPLEFT",
                SoundQueueUI.frame.container.buttons[buttonIndex - 1] or SoundQueueUI.frame.container.name, "BOTTOMLEFT",
                0, queuePosition == 1 and -8 or -2)
            self:EnableMouse(true)
        end

        self.textWidget:ClearAllPoints()
        self.textWidget:SetPoint("LEFT", 16 + 5, 0)
        self.textWidget:SetText(soundData.title)
        local containerWidth = Version.IsLegacyVanilla and
            ((self:GetParent():GetRight() or 0) - (self:GetParent():GetLeft() or 0)) or
            self:GetParent():GetWidth() -- 1.12 quirk
        self:SetWidth(math.min(containerWidth, 16 + 5 + self.textWidget:GetWidth() + 1))
        self.textWidget:SetPoint("RIGHT")

        if hovered then
            local r, g, b = 225 / 255, 20 / 255, 8 / 255
            if pushed then
                r, g, b = r * 0.75, g * 0.75, b * 0.75
            end
            self:SetAlpha(1)
            self.textWidget:SetTextColor(r, g, b)
            self.textWidget:SetShadowColor(0, 0, 0, 1)
            self.iconWidget:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\SoundQueueBulletDelete]])
            self.iconWidget:SetSize(14, 14)
        else
            if isBeingPlayed then
                local event = soundData.event
                if Enums.SoundEvent:IsQuestEvent(event) then
                    self.textWidget:SetTextColor(245 / 255, 204 / 255, 24 / 255)
                    if event == Enums.SoundEvent.QuestAccept then
                        self.iconWidget:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\SoundQueueBulletAccept]])
                    elseif event == Enums.SoundEvent.QuestProgress then
                        self.iconWidget:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\SoundQueueBulletProgress]])
                    elseif event == Enums.SoundEvent.QuestComplete then
                        self.iconWidget:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\SoundQueueBulletComplete]])
                    end
                elseif Enums.SoundEvent:IsGossipEvent(event) then
                    self.textWidget:SetTextColor(1, 1, 1)
                    self.iconWidget:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\SoundQueueBulletGossip]])
                end
                self.iconWidget:SetSize(14, 14)
            else
                self.textWidget:SetTextColor(123 / 255, 147 / 255, 167 / 255)
                self.iconWidget:SetTexture([[Interface\AddOns\AI_VoiceOver\Textures\SoundQueueBulletQueue]])
                self.iconWidget:SetSize(22, 22)
            end
        end
    end

    button:HookScript("OnClick", function(self) SoundQueue:RemoveSoundFromQueue(self.soundData) end)
    button:HookScript("OnMouseDown", function(self) self:Update(true) end)
    button:HookScript("OnMouseUp", function(self) self:Update(false) end)
    button:HookScript("OnEnter", function(self) self:Update(nil, true) end)
    button:HookScript("OnLeave", function(self) self:Update(nil, false) end)
    button:Update()

    return button
end

function SoundQueueUI:UpdateSoundQueueDisplay()
    self.frame:SetShown(not Addon.db.profile.SoundQueueUI.HideFrame and not SoundQueue:IsEmpty())

    self:UpdatePauseDisplay()

    self.frame.portrait:Configure(SoundQueue:GetCurrentSound())

    self.frame.container:SetHeight(self.frame:GetHeight())
    self.frame.container:Hide() -- 1.12 quirk
    self.frame.container:Show()
    local gossipCount
    local lastButtonIndex = 0
    local lastContent
    local nameSet
    for i, soundData in ipairs(SoundQueue.sounds) do
        if not nameSet then
            nameSet = true
            self.frame.container.name:SetText(soundData.name)
            lastContent = self.frame.container.name
        end
        if not Enums.SoundEvent:IsGossipEvent(soundData.event) or (not HIDE_GOSSIP_OPTIONS and soundData.title) then
            if not gossipCount then
                gossipCount = i - 1
            end
            lastButtonIndex = lastButtonIndex + 1
            local button = self.frame.container.buttons[lastButtonIndex] or self:CreateButton(lastButtonIndex)
            button:Configure(soundData)
            lastContent = button
        end
        if lastButtonIndex == 4 then
            break
        end
    end
    for i = lastButtonIndex + 1, getn(self.frame.container.buttons) do
        self.frame.container.buttons[i]:Configure(nil)
    end

    self.frame.container.stopGossip:SetGossipCount(gossipCount or SoundQueue:GetQueueSize())
    self.frame.container.name:Update()

    -- Align the container vertically to the middle
    if lastContent then
        local contentTop = self.frame.container.name:GetTop() or 0
        local contentBottom = lastContent:GetBottom() or 0
        self.frame.container:SetHeight(contentTop - contentBottom)
    else
        self.frame.container:Hide()
    end
    -- Refresh again after updating layout (same frame in modern WoW, next frame in old WoW) to update name truncation and button hover state depending on the new button positions after realignment
    Addon:ScheduleTimer(function()
        self.frame.container.buttons:Update()
        self.frame.container.name:Update()
    end, 0)
end

function SoundQueueUI:UpdatePauseDisplay()
    self.frame.miniPause:Update()
    self.frame.portrait.pause:Update()
end
