local addonName, L = ...

L.textOverlay = {
    frame = nil,
    fontString = nil,
    shown = false,
    previewActive = false,
    previewProfileKey = nil,
    editModeEnabled = false,
    editModeProfileKey = nil,
    sequenceTicker = nil,
    sequenceState = "idle",
    activeConfig = nil,
    activeProfileKey = nil,
    activeContext = nil,
    startedAt = nil,
    lastPose = nil,
    cachedFontFile = nil,
    cachedFontHeight = nil,
    cachedDisplayText = nil,
}

local textOverlay = L.textOverlay
local TEXT_OVERLAY_SCREEN_MARGIN = 24

local function ShouldRefreshTextOverlayUI(self, profileKey)
    return self.IsTabbedSettingsActive and self:IsTabbedSettingsActive()
end

local function ClampTextOverlayPosition(x, y)
    local frame = textOverlay.frame
    local parent = UIParent
    if not frame or not parent then
        return x or 0, y or 0
    end

    local parentWidth = parent:GetWidth() or 0
    local parentHeight = parent:GetHeight() or 0
    local frameWidth = frame:GetWidth() or 0
    local frameHeight = frame:GetHeight() or 0

    local halfAvailableX = math.max(0, (parentWidth * 0.5) - (frameWidth * 0.5) - TEXT_OVERLAY_SCREEN_MARGIN)
    local halfAvailableY = math.max(0, (parentHeight * 0.5) - (frameHeight * 0.5) - TEXT_OVERLAY_SCREEN_MARGIN)

    return L:Clamp(x or 0, -halfAvailableX, halfAvailableX),
        L:Clamp(y or 0, -halfAvailableY, halfAvailableY)
end


local function GetTextFontHeight(scale)
    local resolvedScale = tonumber(scale) or L.DEFAULT_TEXT_SCALE
    return math.max(10, math.floor((48 * resolvedScale) + 0.5))
end

local function GetTextOverlaySequenceTotalDuration(self, profileKey)
    -- PI request text is not tied to a buff timer, so its visible duration is
    -- derived from enter + hold + exit instead of a shared alert duration.
    if profileKey == "piRequest" then
        local enterDuration = math.max(0.01, tonumber(self:GetProfileDBValue(profileKey, "textEnterDuration")) or self.DEFAULT_TEXT_ENTER_DURATION)
        local holdDuration = math.max(0, tonumber(self:GetProfileDBValue(profileKey, "textHoldDuration")) or self.DEFAULT_TEXT_HOLD_DURATION)
        local exitDuration = math.max(0.01, tonumber(self:GetProfileDBValue(profileKey, "textExitDuration")) or self.DEFAULT_TEXT_EXIT_DURATION)
        return enterDuration + holdDuration + exitDuration
    end

    return self:GetProfileDuration(profileKey)
end

function L:GetTextOverlaySequenceTotalDuration(profileKey)
    profileKey = profileKey or textOverlay.activeProfileKey or textOverlay.editModeProfileKey or "bloodlust"
    return GetTextOverlaySequenceTotalDuration(self, profileKey)
end

function L:EnsureTextOverlayFrame()
    if textOverlay.frame then
        return textOverlay.frame, textOverlay.fontString
    end

    local frame = CreateFrame("Frame", "LustomizerTextOverlayFrame", UIParent)
    frame:SetSize(512, 128)
    frame:SetMovable(true)
    frame:EnableMouse(false)
    frame:RegisterForDrag("LeftButton")
    frame:SetClampedToScreen(false)
    frame:SetFrameStrata("DIALOG")
    frame:Hide()

    local fontString = frame:CreateFontString(nil, "ARTWORK")
    fontString:SetPoint("CENTER", frame, "CENTER", 0, 0)
    fontString:SetJustifyH("CENTER")
    fontString:SetJustifyV("MIDDLE")
    fontString:SetSpacing(2)
    fontString:SetShadowOffset(2, -2)
    fontString:SetShadowColor(0, 0, 0, 0.9)

    frame:SetScript("OnDragStart", function(self)
        if textOverlay.editModeEnabled then
            self:StartMoving()
        end
    end)

    frame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()

        local profileKey = textOverlay.editModeProfileKey or "bloodlust"
        local cx, cy = self:GetCenter()
        local ux, uy = UIParent:GetCenter()
        if cx and cy and ux and uy then
            L:SetProfileDBValue(profileKey, "textX", math.floor((cx - ux) + 0.5))
            L:SetProfileDBValue(profileKey, "textY", math.floor((cy - uy) + 0.5))
        end

        self:ClearAllPoints()
        self:SetPoint(
            "CENTER",
            UIParent,
            "CENTER",
            L:GetProfileDBValue(profileKey, "textX"),
            L:GetProfileDBValue(profileKey, "textY")
        )

        -- Persist the snapped text anchor immediately so the settings UI and live
        -- overlay stay aligned even if the panel closes right after dragging.
        if L.NotifySettingsChanged then
            L:NotifySettingsChanged("text_position", profileKey)
        end
    end)

    textOverlay.frame = frame
    textOverlay.fontString = fontString
    return frame, fontString
end

function L:SetTextOverlayFontScale(multiplier)
    self:EnsureTextOverlayFrame()
    textOverlay.fontString:SetScale(multiplier or 1)
end

function L:ApplyTextOverlayPose(pose)
    self:EnsureTextOverlayFrame()
    local x = pose.x or 0
    local y = pose.y or 0
    local alpha = pose.alpha or 1
    local fontScale = pose.fontScale or 1
    local lastPose = textOverlay.lastPose

    if textOverlay.editModeEnabled ~= true then
        x, y = ClampTextOverlayPosition(x, y)
    end

    -- Update only what changed to keep ticker-driven playback cheap and to avoid
    -- unnecessary point churn while the text overlay is animating.
    if (not lastPose) or lastPose.x ~= x or lastPose.y ~= y then
        textOverlay.frame:ClearAllPoints()
        textOverlay.frame:SetPoint("CENTER", UIParent, "CENTER", x, y)
    end
    if (not lastPose) or lastPose.alpha ~= alpha then
        textOverlay.frame:SetAlpha(alpha)
    end
    if (not lastPose) or lastPose.fontScale ~= fontScale then
        self:SetTextOverlayFontScale(fontScale)
    end

    textOverlay.lastPose = {
        x = x,
        y = y,
        alpha = alpha,
        fontScale = fontScale,
    }
end

function L:GetTextOverlayBaseText(profileKey, context)
    profileKey = profileKey or textOverlay.activeProfileKey or textOverlay.editModeProfileKey or "bloodlust"

    local text = self:Trim(self:GetProfileDBValue(profileKey, "textOverlayText"))
    if text == "" then
        local profile = self:GetAlertProfileDefinition(profileKey)
        text = profile.defaultTextOverlayText or self.DEFAULT_TEXT_OVERLAY_TEXT
    end

    return self:ExpandAlertText(text, context or textOverlay.activeContext)
end

function L:GetTextOverlayRemainingSeconds(profileKey)
    profileKey = profileKey or textOverlay.activeProfileKey or "bloodlust"
    if not textOverlay.startedAt then
        return GetTextOverlaySequenceTotalDuration(self, profileKey)
    end
    return math.max(0, GetTextOverlaySequenceTotalDuration(self, profileKey) - (GetTime() - textOverlay.startedAt))
end

function L:BuildTextOverlayDisplayText(remainingSeconds, profileKey, context)
    profileKey = profileKey or textOverlay.activeProfileKey or textOverlay.editModeProfileKey or "bloodlust"

    local baseText = self:GetTextOverlayBaseText(profileKey, context)
    if self:GetProfileDBValue(profileKey, "textCountdownEnabled") ~= true then
        return baseText
    end

    local remaining = remainingSeconds
    if remaining == nil then
        remaining = GetTextOverlaySequenceTotalDuration(self, profileKey)
    end

    return string.format("%s\n%.1fs", baseText, math.max(0, remaining))
end

function L:UpdateTextOverlayMetrics()
    self:EnsureTextOverlayFrame()
    local width = math.max(160, math.floor((textOverlay.fontString:GetStringWidth() or 0) + 32))
    local height = math.max(64, math.floor((textOverlay.fontString:GetStringHeight() or 0) + 24))
    textOverlay.frame:SetSize(width, height)
end

function L:UpdateTextOverlayDisplay(remainingSeconds, profileKey, context)
    profileKey = profileKey or textOverlay.activeProfileKey or textOverlay.editModeProfileKey or "bloodlust"

    self:EnsureTextOverlayFrame()

    local font = self:EnsureValidSelectedFont(profileKey)
    local fontFile = font and font.file or [[Fonts\FRIZQT__.TTF]]
    local fontHeight = GetTextFontHeight(self:GetProfileDBValue(profileKey, "textScale"))
    local displayText = self:BuildTextOverlayDisplayText(remainingSeconds, profileKey, context)

    -- Cache the rendered font choice/text so we only recompute glyph layout when
    -- the selected font, size, or visible text actually changes.
    if textOverlay.cachedFontFile ~= fontFile or textOverlay.cachedFontHeight ~= fontHeight then
        textOverlay.fontString:SetFont(fontFile, fontHeight, "OUTLINE")
        textOverlay.cachedFontFile = fontFile
        textOverlay.cachedFontHeight = fontHeight
        textOverlay.cachedDisplayText = nil
    end

    if textOverlay.cachedDisplayText ~= displayText then
        textOverlay.fontString:SetText(displayText)
        textOverlay.cachedDisplayText = displayText
        self:UpdateTextOverlayMetrics()
    end
end

function L:ApplyTextOverlayVisuals(remainingSeconds, profileKey, context)
    profileKey = profileKey or textOverlay.activeProfileKey or textOverlay.editModeProfileKey or "bloodlust"

    self:UpdateTextOverlayDisplay(remainingSeconds, profileKey, context)
    textOverlay.fontString:SetTextColor(
        self:GetProfileDBValue(profileKey, "textColorR"),
        self:GetProfileDBValue(profileKey, "textColorG"),
        self:GetProfileDBValue(profileKey, "textColorB"),
        self:GetProfileDBValue(profileKey, "textColorA")
    )
    textOverlay.fontString:SetScale(1)

    local x, y = self:GetProfileDBValue(profileKey, "textX"), self:GetProfileDBValue(profileKey, "textY")
    if textOverlay.editModeEnabled ~= true then
        x, y = ClampTextOverlayPosition(x, y)
    end

    self:ApplyTextOverlayPose({
        x = x,
        y = y,
        alpha = 1,
        fontScale = 1,
    })
    textOverlay.frame:SetClampedToScreen(textOverlay.editModeEnabled)
end

function L:StopTextOverlaySequence(hideCompletely, suppressUIRefresh)
    local activeProfileKey = textOverlay.activeProfileKey

    if textOverlay.sequenceTicker then
        textOverlay.sequenceTicker:Cancel()
        textOverlay.sequenceTicker = nil
    end

    textOverlay.sequenceState = "idle"
    textOverlay.activeConfig = nil
    textOverlay.activeProfileKey = nil
    textOverlay.activeContext = nil
    textOverlay.previewActive = false
    textOverlay.previewProfileKey = nil
    textOverlay.lastPose = nil
    textOverlay.cachedFontFile = nil
    textOverlay.cachedFontHeight = nil
    textOverlay.cachedDisplayText = nil

    if hideCompletely and textOverlay.frame and not textOverlay.editModeEnabled then
        textOverlay.frame:Hide()
        textOverlay.shown = false
    end

    if (not suppressUIRefresh) and self.NotifySettingsChanged and ShouldRefreshTextOverlayUI(self, activeProfileKey or "bloodlust") then
        self:NotifySettingsChanged("text_sequence_stopped", activeProfileKey)
    end

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

function L:BuildTextOverlayConfig(profileKey)
    profileKey = profileKey or "bloodlust"

    local scale = self:GetProfileDBValue(profileKey, "textScale")
    local finalX = self:GetProfileDBValue(profileKey, "textX")
    local finalY = self:GetProfileDBValue(profileKey, "textY")
    local direction = self:GetProfileDBValue(profileKey, "textDirection")
    local enterOffsetX, enterOffsetY = self:GetDirectionalOffset(direction, 240, 72, scale)
    local exitOffsetX, exitOffsetY = self:GetDirectionalOffset(direction, 240, 72, scale)

    -- Resolve one playback config up front so enter/loop/exit logic can stay clean
    -- and deterministic while the ticker only evaluates poses and colors.
    local config = {
        profileKey = profileKey,
        finalX = finalX,
        finalY = finalY,
        enterX = finalX + enterOffsetX,
        enterY = finalY + enterOffsetY,
        exitX = finalX + exitOffsetX,
        exitY = finalY + exitOffsetY,
        enterDuration = math.max(0.01, tonumber(self:GetProfileDBValue(profileKey, "textEnterDuration")) or self.DEFAULT_TEXT_ENTER_DURATION),
        exitDuration = math.max(0.01, tonumber(self:GetProfileDBValue(profileKey, "textExitDuration")) or self.DEFAULT_TEXT_EXIT_DURATION),
        enterEasing = self:GetProfileDBValue(profileKey, "textEnterEasing"),
        exitEasing = self:GetProfileDBValue(profileKey, "textExitEasing"),
        activeDuration = 0,
        startMoveEnabled = self:GetProfileDBValue(profileKey, "textStartMoveEnabled") == true,
        startFadeEnabled = self:GetProfileDBValue(profileKey, "textStartFadeEnabled") == true,
        loopPulseEnabled = self:GetProfileDBValue(profileKey, "textLoopPulseEnabled") == true,
        loopColorCycleEnabled = self:GetProfileDBValue(profileKey, "textLoopColorCycleEnabled") == true,
        pulseScale = self:Clamp(tonumber(self:GetProfileDBValue(profileKey, "textPulseScale")) or self.DEFAULT_TEXT_PULSE_SCALE, 0, 0.5),
        pulseStepDuration = math.max(0.01, tonumber(self:GetProfileDBValue(profileKey, "textPulseStepDuration")) or self.DEFAULT_TEXT_PULSE_STEP_DURATION),
        colorCycleDuration = math.max(0.01, tonumber(self:GetProfileDBValue(profileKey, "textColorCycleDuration")) or self.DEFAULT_TEXT_COLOR_CYCLE_DURATION),
        endMoveEnabled = self:GetProfileDBValue(profileKey, "textEndMoveEnabled") == true,
        endFadeEnabled = self:GetProfileDBValue(profileKey, "textEndFadeEnabled") == true,
        color1 = {
            self:GetProfileDBValue(profileKey, "textColorR"),
            self:GetProfileDBValue(profileKey, "textColorG"),
            self:GetProfileDBValue(profileKey, "textColorB"),
            self:GetProfileDBValue(profileKey, "textColorA"),
        },
        holdUntilHidden = false,
        color2 = {
            self:GetProfileDBValue(profileKey, "textAltColorR"),
            self:GetProfileDBValue(profileKey, "textAltColorG"),
            self:GetProfileDBValue(profileKey, "textAltColorB"),
            self:GetProfileDBValue(profileKey, "textAltColorA"),
        },
    }

    if profileKey == "piRequest" then
        config.activeDuration = math.max(0, tonumber(self:GetProfileDBValue(profileKey, "textHoldDuration")) or self.DEFAULT_TEXT_HOLD_DURATION)
    else
        config.activeDuration = math.max(0, self:GetProfileDuration(profileKey) - config.enterDuration - config.exitDuration)
    end
    return config
end

function L:GetTextOverlayColorAtTime(config, elapsed)
    if not config.loopColorCycleEnabled then
        return unpack(config.color1)
    end

    -- Bounce between the primary/secondary colors instead of wrapping hard back to
    -- the first color; this keeps the loop visually smooth at both ends.
    local step = math.max(0.01, config.colorCycleDuration)
    local cycle = step * 2
    local phase = elapsed % cycle
    local blend

    if phase < step then
        blend = self:EaseInOutSine(phase / step)
    else
        blend = self:EaseInOutSine((cycle - phase) / step)
    end

    return self:Lerp(config.color1[1], config.color2[1], blend),
        self:Lerp(config.color1[2], config.color2[2], blend),
        self:Lerp(config.color1[3], config.color2[3], blend),
        self:Lerp(config.color1[4], config.color2[4], blend)
end

function L:ApplyTextOverlayAnimatedVisuals(config, remainingSeconds, elapsed, context)
    self:UpdateTextOverlayDisplay(remainingSeconds, config.profileKey, context)

    local r, g, b, a = self:GetTextOverlayColorAtTime(config, elapsed or 0)
    textOverlay.fontString:SetTextColor(r, g, b, a)
end

function L:EvaluateTextOverlayEnterPose(config, elapsed)
    local progress = self:ApplyEasing(config.enterEasing, elapsed / config.enterDuration)
    local pose = {
        x = config.finalX,
        y = config.finalY,
        alpha = 1,
        fontScale = 1,
    }

    if config.startMoveEnabled then
        pose.x = self:Lerp(config.enterX, config.finalX, progress)
        pose.y = self:Lerp(config.enterY, config.finalY, progress)
    end

    if config.startFadeEnabled then
        pose.alpha = self:Lerp(0, 1, progress)
    end

    return pose
end

function L:EvaluateTextOverlayLoopPose(config, elapsed)
    local fontScale = 1
    if config.loopPulseEnabled then
        fontScale = self:GetWobbleTextureScaleAtTime(config.pulseScale, config.pulseStepDuration, elapsed)
    end

    return {
        x = config.finalX,
        y = config.finalY,
        alpha = 1,
        fontScale = fontScale,
    }
end

function L:EvaluateTextOverlayExitPose(config, elapsed)
    local progress = self:ApplyEasing(config.exitEasing, elapsed / config.exitDuration)
    local pose = {
        x = config.finalX,
        y = config.finalY,
        alpha = 1,
        fontScale = 1,
    }

    if config.endMoveEnabled then
        pose.x = self:Lerp(config.finalX, config.exitX, progress)
        pose.y = self:Lerp(config.finalY, config.exitY, progress)
    end

    if config.endFadeEnabled then
        pose.alpha = self:Lerp(1, 0, progress)
    end

    return pose
end

function L:StartTextOverlayPlayback(kind, isPreview, profileKey, context)
    profileKey = profileKey or textOverlay.activeProfileKey or "bloodlust"

    if kind ~= "exit" and self:GetProfileDBValue(profileKey, "useTextOverlay") ~= true then
        return
    end

    local previewActive = (kind == "exit" and textOverlay.previewActive == true) or isPreview == true

    self:EnsureTextOverlayFrame()
    self:StopTextOverlaySequence(false, true)
    textOverlay.previewActive = previewActive
    textOverlay.previewProfileKey = previewActive and profileKey or nil
    textOverlay.activeProfileKey = profileKey
    textOverlay.activeContext = context

    local config = self:BuildTextOverlayConfig(profileKey)
    textOverlay.activeConfig = config
    if kind ~= "exit" then
        textOverlay.startedAt = GetTime()
    elseif not textOverlay.startedAt then
        textOverlay.startedAt = GetTime()
    end

    textOverlay.shown = true
    textOverlay.frame:SetClampedToScreen(false)
    textOverlay.frame:Show()

    -- Exit playback reuses the already-running context/display state so hiding text
    -- does not reset countdown/token-expanded content before the outro finishes.
    if kind == "exit" then
        textOverlay.sequenceState = "exit"
        self:ApplyTextOverlayAnimatedVisuals(config, self:GetTextOverlayRemainingSeconds(profileKey), 0, context)
        self:ApplyTextOverlayPose(self:EvaluateTextOverlayExitPose(config, 0))
    else
        textOverlay.sequenceState = "enter"
        self:ApplyTextOverlayAnimatedVisuals(config, GetTextOverlaySequenceTotalDuration(self, profileKey), 0, context)
        self:ApplyTextOverlayPose(self:EvaluateTextOverlayEnterPose(config, 0))
    end

    local startTime = GetTime()
    textOverlay.sequenceTicker = C_Timer.NewTicker(0.016, function()
        if not textOverlay.frame then
            self:StopTextOverlaySequence(true)
            return
        end

        local elapsed = GetTime() - startTime
        local remaining = self:GetTextOverlayRemainingSeconds(profileKey)

        if kind == "exit" then
            if elapsed < config.exitDuration then
                textOverlay.sequenceState = "exit"
                self:ApplyTextOverlayAnimatedVisuals(config, remaining, elapsed, context)
                self:ApplyTextOverlayPose(self:EvaluateTextOverlayExitPose(config, elapsed))
                return
            end

            self:StopTextOverlaySequence(true)
            return
        end

        if elapsed < config.enterDuration then
            textOverlay.sequenceState = "enter"
            self:ApplyTextOverlayAnimatedVisuals(config, remaining, elapsed, context)
            self:ApplyTextOverlayPose(self:EvaluateTextOverlayEnterPose(config, elapsed))
            return
        end

        local loopElapsed = elapsed - config.enterDuration
        -- Reserved for future text overlays that should stay looping until some
        -- external event hides them, without changing the phase machinery again.
        if config.holdUntilHidden then
            textOverlay.sequenceState = "loop"
            self:ApplyTextOverlayAnimatedVisuals(config, remaining, loopElapsed, context)
            self:ApplyTextOverlayPose(self:EvaluateTextOverlayLoopPose(config, loopElapsed))
            return
        end

        if loopElapsed < config.activeDuration then
            textOverlay.sequenceState = "loop"
            self:ApplyTextOverlayAnimatedVisuals(config, remaining, loopElapsed, context)
            self:ApplyTextOverlayPose(self:EvaluateTextOverlayLoopPose(config, loopElapsed))
            return
        end

        local exitElapsed = loopElapsed - config.activeDuration
        if exitElapsed < config.exitDuration then
            textOverlay.sequenceState = "exit"
            self:ApplyTextOverlayAnimatedVisuals(config, remaining, exitElapsed, context)
            self:ApplyTextOverlayPose(self:EvaluateTextOverlayExitPose(config, exitElapsed))
            return
        end

        self:StopTextOverlaySequence(true)
    end)

    if self.NotifySettingsChanged and ShouldRefreshTextOverlayUI(self, profileKey) then
        self:NotifySettingsChanged("text_preview_shown", profileKey)
    end
end

function L:ShowTextOverlayPreview(profileKey, context)
    profileKey = profileKey or "bloodlust"
    self:SetTextEditMode(false, profileKey)
    self:StartTextOverlayPlayback("full", true, profileKey, context)
    if self.EnsurePreviewVisibilityMonitor then
        self:EnsurePreviewVisibilityMonitor()
    end
end

function L:HideTextOverlay(profileKey)
    if not textOverlay.shown or not textOverlay.frame then
        return
    end
    if profileKey and textOverlay.activeProfileKey and textOverlay.activeProfileKey ~= profileKey then
        return
    end
    self:StartTextOverlayPlayback("exit", false, profileKey or textOverlay.activeProfileKey or "bloodlust", textOverlay.activeContext)
end

function L:StopTextOverlayPreview()
    if textOverlay.previewActive ~= true then
        return
    end
    if textOverlay.sequenceState == "exit" then
        return
    end

    if textOverlay.shown and textOverlay.frame then
        self:HideTextOverlay(textOverlay.previewProfileKey)
        return
    end

    self:StopTextOverlaySequence(true)
end

function L:IsTextOverlayPreviewActive(profileKey)
    if profileKey then
        return textOverlay.previewActive == true and textOverlay.previewProfileKey == profileKey
    end
    return textOverlay.previewActive == true
end

function L:SetTextEditMode(enabled, profileKey)
    profileKey = profileKey or textOverlay.editModeProfileKey or "bloodlust"
    if enabled == true and self.IsSlashAlertTestActive and self:IsSlashAlertTestActive() then
        if self.NotifySettingsChanged then
            self:NotifySettingsChanged("text_edit_blocked_by_test", profileKey)
        end
        return
    end
    textOverlay.editModeEnabled = enabled == true
    textOverlay.editModeProfileKey = textOverlay.editModeEnabled and profileKey or nil

    if textOverlay.editModeEnabled and self.IsEditModeEnabled and self:IsEditModeEnabled() then
        self:SetEditMode(false, nil)
    end

    self:EnsureTextOverlayFrame()
    self:ApplyTextOverlayVisuals(GetTextOverlaySequenceTotalDuration(self, profileKey), profileKey)

    textOverlay.frame:EnableMouse(textOverlay.editModeEnabled)
    textOverlay.frame:SetMovable(textOverlay.editModeEnabled)
    textOverlay.frame:SetClampedToScreen(textOverlay.editModeEnabled)

    if textOverlay.editModeEnabled then
        -- Edit mode always presents a neutral, fully visible text pose so moving the
        -- anchor is predictable and not competing with animated playback.
        self:StopTextOverlaySequence(false)
        textOverlay.shown = true
        textOverlay.frame:Show()
        if self.EnsurePreviewVisibilityMonitor then
            self:EnsurePreviewVisibilityMonitor()
        end
        self:ApplyTextOverlayPose({
            x = self:GetProfileDBValue(profileKey, "textX"),
            y = self:GetProfileDBValue(profileKey, "textY"),
            alpha = 1,
            fontScale = 1,
        })
    else
        if profileKey == "bloodlust" and self.trackedDebuffPresent then
            self:StartTextOverlayPlayback("full", false, profileKey, textOverlay.activeContext)
        elseif textOverlay.activeProfileKey == profileKey and textOverlay.shown then
            self:StartTextOverlayPlayback("full", false, profileKey, textOverlay.activeContext)
        elseif textOverlay.frame then
            textOverlay.frame:Hide()
            textOverlay.shown = false
        end
    end

    if self.NotifySettingsChanged and ShouldRefreshTextOverlayUI(self, profileKey) then
        self:NotifySettingsChanged("text_edit_mode", profileKey)
    end
end

function L:IsTextEditModeEnabled(profileKey)
    if profileKey then
        return textOverlay.editModeEnabled == true and textOverlay.editModeProfileKey == profileKey
    end
    return textOverlay.editModeEnabled == true
end

function L:GetTextOverlaySequenceState()
    return textOverlay.sequenceState
end

function L:IsTextOverlayShown(profileKey)
    if profileKey then
        return textOverlay.shown == true and textOverlay.activeProfileKey == profileKey
    end
    return textOverlay.shown == true
end
