-- ============================================================================
-- Vamoose's Companion - Display
-- Listens for VC_QUOTE_READY and shows quotes via talking head or chat
-- Slipstream Dark aesthetic: flat dark glass, no atlas textures
-- ============================================================================

VC = VC or {}
VC.Display = {}

local WHITE = "Interface\\Buttons\\WHITE8x8"

-- Apply ornate, minimal, or default toast style
function VC.Display:ApplyToastStyle(f)
    local style = VC.Store:GetState().config.toastFrame or "atlas"
    local isOrnate = (style == "ornate")
    local isMinimal = (style == "minimal")
    -- NineSlice: shown only for classic
    if f._nineSliceRegions then
        for _, r in ipairs(f._nineSliceRegions) do r:SetShown(not isOrnate and not isMinimal) end
    end
    -- Ornate bg: shown only for ornate
    if f._ornateBg then f._ornateBg:SetShown(isOrnate) end
    -- Drop shadow: classic only
    if f._shadow then f._shadow:SetShown(not isOrnate and not isMinimal) end
    -- Text color
    if f.bodyText then
        if isOrnate then
            f.bodyText:SetTextColor(0.15, 0.10, 0.05, 1)
            f.bodyText:SetShadowColor(0.8, 0.7, 0.5, 0.3)
        else
            local c = VC.Theme:GetScheme()
            f.bodyText:SetTextColor(c.text.r, c.text.g, c.text.b, 1)
            f.bodyText:SetShadowColor(0, 0, 0, 0.8)
        end
    end
    -- Text insets
    if f.bodyText then
        f.bodyText:ClearAllPoints()
        if isOrnate then
            f.bodyText:SetPoint("TOPLEFT", 134, -10)
            f.bodyText:SetPoint("BOTTOMRIGHT", -56, 10)
        else
            f.bodyText:SetPoint("TOPLEFT", 134, -10)
            f.bodyText:SetPoint("BOTTOMRIGHT", -16, 10)
        end
    end
    -- Frame size and position. _baseH/_textInsetX feed ShowToast's auto-grow:
    -- each style has its own base height and body-text insets, so the growth
    -- math must read these rather than the DISPLAY constants.
    if isOrnate then
        f:SetSize(600, 255)
        f._baseH, f._textInsetX = 255, 134 + 56
    else
        f:SetSize(VC.Constants.DISPLAY.TALKING_HEAD_W, VC.Constants.DISPLAY.TALKING_HEAD_H)
        f._baseH, f._textInsetX = VC.Constants.DISPLAY.TALKING_HEAD_H, 134 + 16
    end
    local toastScale = VC.Store and VC.Store:GetState().config.toastScale or 1.0
    f:SetScale(toastScale)
    f:ClearAllPoints()
    local tp = VC.Store and VC.Store:GetState().ui.toastPoints[style]
    if tp then
        f:SetPoint(tp.point, UIParent, tp.relPoint, tp.x, tp.y)
    elseif isOrnate then
        f:SetPoint("TOP", UIParent, "TOP", 0, -100)
    else
        f:SetPoint("TOP", UIParent, "TOP", 0, -140)
    end
end


-- ============================================================================
-- TOAST MOVE MODE (drag-to-position from Display settings)
-- ============================================================================

function VC.Display:EnterMoveMode()
    local f = self.talkingHead
    if not f then self:CreateTalkingHead(); f = self.talkingHead end

    -- Cancel any in-flight animations/timers
    if self._hideTimer then self._hideTimer:Cancel(); self._hideTimer = nil end
    if self._typewriterTicker then self._typewriterTicker:Cancel(); self._typewriterTicker = nil end
    if f._exitAG and f._exitAG:IsPlaying() then f._exitAG:Stop() end
    if f._entryAG and f._entryAG:IsPlaying() then f._entryAG:Stop() end

    self._moveMode = true
    f:SetMovable(true)
    f:SetClampedToScreen(true)
    f:RegisterForDrag("LeftButton")
    f:EnableMouse(true)
    f:SetAlpha(1)

    f:SetScript("OnDragStart", function(self) self:StartMoving() end)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relPoint, x, y = self:GetPoint(1)
        local curStyle = VC.Store:GetState().config.toastFrame or "atlas"
        VC.Store:Dispatch("SET_TOAST_POSITION", {
            style = curStyle,
            point = { point = point, relTo = "UIParent", relPoint = relPoint, x = x, y = y }
        })
    end)

    -- Apply style and populate with static preview content
    self:ApplyToastStyle(f)
    local state = VC.Store:GetState()
    local companion = VC_CompanionProfiles and VC_CompanionProfiles[state.config.activeCompanion]
    f.nameText:SetText(companion and companion.name or "Companion")
    f.bodyText:SetText("This is where my quotes will appear.")

    -- Accent drag border (created once, reused)
    if not f._moveBorder then
        f._moveBorder = CreateFrame("Frame", nil, f, "BackdropTemplate")
        f._moveBorder:SetAllPoints()
        f._moveBorder:SetBackdrop({ edgeFile = WHITE, edgeSize = 2 })
        f._moveBorder:SetFrameLevel(f:GetFrameLevel() + 5)
    end
    local ac = VC.Theme and VC.Theme:GetScheme().accent or { r = 0.5, g = 0.8, b = 1 }
    f._moveBorder:SetBackdropBorderColor(ac.r, ac.g, ac.b, 0.8)
    f._moveBorder:Show()

    f:Show()
end

function VC.Display:ExitMoveMode()
    if not self._moveMode then return end
    local f = self.talkingHead
    if not f then return end

    self._moveMode = false
    f:StopMovingOrSizing()
    f:SetMovable(false)
    f:SetScript("OnDragStart", nil)
    f:SetScript("OnDragStop", nil)
    if f._moveBorder then f._moveBorder:Hide() end
    f:Hide()
end


-- ============================================================================
-- INITIALIZATION
-- ============================================================================

function VC.Display:Initialize()
    self:CreateTalkingHead()

    VC.EventBus:Register("VC_QUOTE_READY", function(data)
        self:OnQuoteReady(data)
    end)
end

-- ============================================================================
-- TALKING HEAD FRAME (Slipstream Dark: flat dark glass, no atlas)
-- ============================================================================

function VC.Display:CreateTalkingHead()
    -- Pre-create combat deferral frame (must not be created during lockdown)
    self._regenFrame = CreateFrame("Frame")
    self._regenFrame:SetScript("OnEvent", function()
        self._regenFrame:UnregisterEvent("PLAYER_REGEN_ENABLED")
        local queued = self._combatQueue
        self._combatQueue = nil
        if queued then self:ShowTalkingHead(queued) end
    end)

    local TW = VC.Constants.DISPLAY.TALKING_HEAD_W
    local TH = VC.Constants.DISPLAY.TALKING_HEAD_H
    local c  = VC.Theme:GetScheme()

    local f = CreateFrame("Frame", "VC_TalkingHead", UIParent)
    f:SetSize(TW, TH)
    f:SetPoint("TOP", UIParent, "TOP", 0, -140)
    f:SetFrameStrata("MEDIUM")
    f:SetFrameLevel(100)
    f:EnableMouse(true)
    f:SetScript("OnMouseDown", function()
        if VC.Display._moveMode then return end
        if VC.Display._hideTimer then VC.Display._hideTimer:Cancel(); VC.Display._hideTimer = nil end
        if VC.Display._typewriterTicker then
            VC.Display._typewriterTicker:Cancel()
            VC.Display._typewriterTicker = nil
            -- Show full text immediately on click
            if VC.Display._pendingFullText then
                f.bodyText:SetText(VC.Display._pendingFullText)
            end
        end
        if f._entryAG:IsPlaying() then f._entryAG:Stop() end
        if f._sheenAG:IsPlaying() then f._sheenAG:Stop() end
        f._exitAG:Play()
    end)
    f:Hide()

    -- NineSlice dark popup chrome (primary background, semi-transparent)
    NineSliceUtil.ApplyLayoutByName(f, "TooltipDefaultDarkLayout")
    -- Store NineSlice regions so we can hide/show them per style
    f._nineSliceRegions = {}
    for _, region in ipairs({f:GetRegions()}) do
        if region.SetAlpha then
            region:SetAlpha(0.75)
            f._nineSliceRegions[#f._nineSliceRegions + 1] = region
        end
    end

    -- Ornate toast background (hidden by default, shown when ornate style active)
    local ornateBg = f:CreateTexture(nil, "BACKGROUND")
    ornateBg:SetAllPoints()
    ornateBg:SetTexture("Interface\\AddOns\\VamoosesCompanion\\textures\\vc_toast_ornate")
    ornateBg:SetTexCoord(0.207, 0.793, 0.002, 0.998)
    ornateBg:Hide()
    f._ornateBg = ornateBg

    VC.Display:ApplyToastStyle(f)

    -- Legendary NineSlice border (hidden at rest, shown for legendary tier only)
    local legendaryBorder = CreateFrame("Frame", nil, f)
    legendaryBorder:SetAllPoints(f)
    NineSliceUtil.ApplyLayoutByName(legendaryBorder, "TooltipMawLayout")
    legendaryBorder:Hide()
    f._legendaryBorder = legendaryBorder

    -- Quality flash overlay (epic/legendary: thin bar, alpha animation)
    local qualityFlash = f:CreateTexture(nil, "OVERLAY")
    qualityFlash:SetHeight(2)
    qualityFlash:SetPoint("TOPLEFT", 0, 0)
    qualityFlash:SetPoint("TOPRIGHT", 0, 0)
    qualityFlash:SetTexture(WHITE)
    qualityFlash:SetAlpha(0)
    qualityFlash:Hide()
    f._qualityFlash = qualityFlash

    -- AG for quality flash (two-phase: fade in then fade out)
    local flashAG = f:CreateAnimationGroup()
    local flashIn = flashAG:CreateAnimation("Alpha")
    flashIn:SetOrder(1)
    flashIn:SetFromAlpha(0)
    flashIn:SetToAlpha(0.6)
    flashIn:SetDuration(0.08)
    local flashOut = flashAG:CreateAnimation("Alpha")
    flashOut:SetOrder(2)
    flashOut:SetFromAlpha(0.6)
    flashOut:SetToAlpha(0)
    flashOut:SetDuration(0.4)
    flashAG:SetScript("OnFinished", function() qualityFlash:Hide() end)
    f._qualityFlashAG = flashAG

    -- Drop shadow (bottom strip)
    local shadowTex = "Interface\\AddOns\\VamoosesCompanion\\textures\\vc_shadow"
    local shadow = f:CreateTexture(nil, "BACKGROUND", nil, -1)
    shadow:SetPoint("TOPLEFT", f, "BOTTOMLEFT", -2, 2)
    shadow:SetPoint("TOPRIGHT", f, "BOTTOMRIGHT", 2, 2)
    shadow:SetHeight(8)
    shadow:SetTexture(shadowTex)
    shadow:SetAlpha(0.4)
    f._shadow = shadow

    -- 3D ModelScene (left column, extended viewport for tall models)
    local scene = VC.UI:CreateModelScene(f, 0.3)
    scene:SetPoint("TOPLEFT", -110, 172)
    scene:SetPoint("BOTTOMLEFT", -110, -12)
    scene:SetWidth(340)
    scene:SetFrameLevel(f:GetFrameLevel() + 1)
    f.scene = scene

    -- Entry sheen textures (sweep on frame entry, all quality tiers)
    local sheen = f:CreateTexture(nil, "ARTWORK")
    sheen:SetAtlas("TalkingHeads-Glow-Sheen")
    sheen:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
    sheen:SetWidth(TW)
    sheen:SetHeight(60)
    sheen:SetBlendMode("ADD")
    sheen:SetAlpha(0)

    local textSheen = f:CreateTexture(nil, "ARTWORK")
    textSheen:SetAtlas("TalkingHeads-Glow-TextSheen")
    textSheen:SetPoint("TOPLEFT", f, "TOPLEFT", 114, 0)
    textSheen:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -60)
    textSheen:SetBlendMode("ADD")
    textSheen:SetAlpha(0)

    -- Sheen sweep: manual position + alpha via dummy AG (no Translation on parent frame)
    local sheenAG = f:CreateAnimationGroup()
    local sheenDummy = sheenAG:CreateAnimation("Alpha")
    sheenDummy:SetFromAlpha(1); sheenDummy:SetToAlpha(1); sheenDummy:SetDuration(0.5)
    local sheenElapsed = 0
    sheenAG:SetScript("OnPlay", function()
        sheenElapsed = 0
        sheen:SetPoint("TOPLEFT", f, "TOPLEFT", -100, 0)
        sheen:SetAlpha(0)
        textSheen:SetAlpha(0)
    end)
    sheenAG:SetScript("OnUpdate", function(_, elapsed)
        sheenElapsed = sheenElapsed + elapsed
        local t = math.min(sheenElapsed / 0.5, 1)
        sheen:SetPoint("TOPLEFT", f, "TOPLEFT", -100 + t * 700, 0)
        local a = (t < 0.3) and (t / 0.3 * 0.45) or ((1 - t) / 0.7 * 0.45)
        sheen:SetAlpha(math.max(a, 0))
        textSheen:SetAlpha(math.max(a * 0.67, 0))
    end)
    sheenAG:SetScript("OnFinished", function()
        sheen:SetAlpha(0); textSheen:SetAlpha(0)
        sheen:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
    end)
    f._sheenAG = sheenAG

    -- Speaker name (MORPHEUS 14pt, centered above model area)
    local nameText = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    nameText:SetPoint("TOPLEFT", -10, -10)
    nameText:SetWidth(140)
    nameText:SetJustifyH("CENTER")
    -- Font inherited from GameFontNormalLarge (Friz Quadrata, gold)
    nameText:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
    nameText:SetShadowOffset(1, -1)
    nameText:SetShadowColor(0, 0, 0, 1)
    f.nameText = nameText

    -- Quote body text (ARIALN 15pt, vertically centered in text area)
    local bodyText = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    bodyText:SetPoint("TOPLEFT", 134, -10)
    bodyText:SetPoint("BOTTOMRIGHT", -16, 10)
    bodyText:SetJustifyV("MIDDLE")
    -- Font inherited from GameFontHighlightLarge (Friz Quadrata, white)
    bodyText:SetTextColor(c.text.r, c.text.g, c.text.b, 1)
    bodyText:SetJustifyH("LEFT")
    bodyText:SetWordWrap(true)
    bodyText:SetMaxLines(12) -- matches max auto-grow (base 5 lines + 120px); see ShowToast sizing
    bodyText:SetShadowOffset(1, -1)
    bodyText:SetShadowColor(0, 0, 0, 0.8)
    f.bodyText = bodyText

    -- Hidden measurement twin for pre-typewriter height calc (anchored
    -- fontstrings report clipped rect height, not full wrapped height)
    local measure = f:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
    measure:SetWordWrap(true)
    measure:SetJustifyH("LEFT")
    measure:Hide()
    f._measureText = measure

    -- Theme update handler (re-applies toast style which handles text color per mode)
    VC.EventBus:Register("VC_THEME_UPDATE", function()
        local s = VC.Theme:GetScheme()
        nameText:SetTextColor(s.text_header.r, s.text_header.g, s.text_header.b, 1)
        VC.Display:ApplyToastStyle(f)
    end)

    -- Entry animation group (slide down + fade in)
    local entryAG = f:CreateAnimationGroup()
    entryAG:SetToFinalAlpha(true)  -- persist alpha=1 after animation ends
    local entryAlpha = entryAG:CreateAnimation("Alpha")
    entryAlpha:SetFromAlpha(0)
    entryAlpha:SetToAlpha(1)
    entryAlpha:SetDuration(0.3)
    entryAlpha:SetSmoothing("OUT")
    -- No translation slide — fade only (slide causes visual bounce)
    f._entryAG = entryAG

    -- Exit animation group (fade out)
    local exitAG = f:CreateAnimationGroup()
    exitAG:SetToFinalAlpha(true)
    local exitAlpha = exitAG:CreateAnimation("Alpha")
    exitAlpha:SetFromAlpha(1)
    exitAlpha:SetToAlpha(0)
    exitAlpha:SetDuration(0.4)
    exitAlpha:SetSmoothing("IN")
    exitAG:SetScript("OnFinished", function()
        if f._qualityFlash then f._qualityFlash:Hide() end
        f:Hide()
    end)
    f._exitAG = exitAG

    self.talkingHead = f
end

-- ============================================================================
-- QUOTE DISPLAY ROUTER
-- ============================================================================

function VC.Display:OnQuoteReady(data)
    local state = VC.Store:GetState()

    -- TTS: speak via VamoosesTTS if enabled globally + per-companion (no hard dependency)
    local companion = data.companion
    local compKey = companion and companion.key
    local ttsOn = state.config.ttsEnabled and (not compKey or state.config.companionTts[compKey] ~= false)
    if ttsOn and VamoosesTTS and VamoosesTTS.Speak then
        VamoosesTTS.Speak(data.quote.text, { source = "companion", name = companion and companion.name, voiceName = companion and companion.ttsVoice })
        if state.config.debug then
            DEFAULT_CHAT_FRAME:AddMessage("|cFFAED6F1[VC TTS]|r Speaking: " .. (companion and companion.name or "?"))
        end
    elseif state.config.debug then
        local reason = not state.config.ttsEnabled and "off"
            or (compKey and state.config.companionTts[compKey] == false) and "muted"
            or not VamoosesTTS and "no VTTS" or "unavailable"
        DEFAULT_CHAT_FRAME:AddMessage("|cFFAED6F1[VC TTS]|r " .. reason)
    end

    local modes = state.config.displayModes or {}
    if modes.chat then self:ShowInChat(data) end
    if modes.bubble then self:ShowTalkingHead(data) end
end

-- ============================================================================
-- TALKING HEAD MODE
-- ============================================================================

function VC.Display:IsTalkingHeadBusy()
    local blizzTH = TalkingHeadFrame
    if blizzTH and blizzTH:IsShown() then return true end
    local hdgTH = _G["HDG_VamooseTalkingHead"]
    if hdgTH and hdgTH:IsShown() then return true end
    return false
end

function VC.Display:ShowTalkingHead(data)
    if self._moveMode then return end -- suppress real quotes during move mode
    -- Defer during combat to avoid taint
    if InCombatLockdown() then
        self._combatQueue = data
        self._regenFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
        return
    end

    -- Queue if another talking head is active (poll every 0.5s, give up after 10s)
    if self:IsTalkingHeadBusy() then
        if self._queueTimer then self._queueTimer:Cancel() end
        local attempts = 0
        self._queueTimer = C_Timer.NewTicker(0.5, function(ticker)
            attempts = attempts + 1
            if InCombatLockdown() then
                ticker:Cancel()
                self._queueTimer = nil
                self._combatQueue = data
                self._regenFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
                return
            end
            if not self:IsTalkingHeadBusy() then
                ticker:Cancel()
                self._queueTimer = nil
                self:PresentTalkingHead(data)
            elseif attempts >= 20 then
                ticker:Cancel()
                self._queueTimer = nil
            end
        end)
        return
    end
    self:PresentTalkingHead(data)
end

function VC.Display:PresentTalkingHead(data)
    local f = self.talkingHead
    local companion = data.companion
    local quote = data.quote

    -- Cancel any pending timers and in-flight animations FIRST
    if self._hideTimer then self._hideTimer:Cancel(); self._hideTimer = nil end
    if self._typewriterTicker then self._typewriterTicker:Cancel(); self._typewriterTicker = nil end
    if f._exitAG:IsPlaying() then f._exitAG:Stop() end
    if f._entryAG:IsPlaying() then f._entryAG:Stop() end

    -- Hide frame before model setup so actor initializes on a hidden parent
    f:Hide()

    -- Apply current toast style (text color, frame size, bg)
    self:ApplyToastStyle(f)

    local displayID = (VC.Portrait and VC.Portrait.GetDisplayIDForCompanion)
        and VC.Portrait:GetDisplayIDForCompanion(companion)
        or (companion and companion.displayID or 19598)

    -- Auto-grow for long quotes: the base 160px toast fits ~5 lines
    -- (~225 chars), but epic/legendary prose (bond arc) runs to ~400 chars
    -- and was clipping at the MaxLines ellipsis. Measure the wrapped height
    -- at the body text's width and extend the frame DOWNWARD, shifting the
    -- scene's bottom anchor by the same amount so the model column keeps a
    -- constant height (the scene spans -172/+12 past the frame edges and
    -- AutoFitActor would otherwise rescale the companion per quote length).
    -- _baseH/_textInsetX come from ApplyToastStyle (per-style geometry).
    -- Ornate's 255px base already fits ~13 lines, so growth practically only
    -- triggers on the default style; when ornate does grow, its single-texture
    -- art stretches slightly (it is not a NineSlice) -- acceptable at +120 max.
    local baseH = f._baseH
    f._measureText:SetWidth(f:GetWidth() - f._textInsetX)
    f._measureText:SetText(quote and quote.text or "")
    local neededH = f._measureText:GetStringHeight() + 20 -- +top/bottom body padding
    local extra = math.max(0, math.min(math.ceil(neededH - baseH), 120))
    f:SetHeight(baseH + extra)
    f.scene:SetPoint("BOTTOMLEFT", -110, -12 + extra)

    -- Setup 3D model via shared ModelScene helper
    local cam = companion and companion.talkingHeadCamera or {}
    local animIntent = VC.AnimResolver and VC.AnimResolver:IntentForQuote(data.event, quote) or "TALKING"
    local animID = VC.AnimResolver and VC.AnimResolver:Resolve(animIntent, companion) or 60

    VC.UI:SetCreatureOnScene(f.scene, displayID, {
        scale = cam.scale or 1,
        posY = cam.modelY or 0,
        posZ = cam.modelZ or 0,
        yaw = cam.facing or 0,
        animID = animID,
        companion = companion,
    })

    f.nameText:SetText("")
    -- body text populated by typewriter after entry animation

    local quality = quote and quote.quality or "common"
    local qc = VC.Constants.QUALITY_COLORS[quality] or VC.Constants.QUALITY_COLORS.common

    -- Legendary border
    if quality == "legendary" then
        f._legendaryBorder:Show()
        for _, region in ipairs({f._legendaryBorder:GetRegions()}) do
            if region.SetVertexColor then
                region:SetVertexColor(1.0, 0.85, 0.3, 1.0)
            end
        end
    else
        f._legendaryBorder:Hide()
    end

    -- Quality flash for epic/legendary quotes
    if quality == "epic" or quality == "legendary" then
        f._qualityFlash:SetVertexColor(qc.r, qc.g, qc.b, 1)
        f._qualityFlash:Show()
        if f._qualityFlashAG:IsPlaying() then f._qualityFlashAG:Stop() end
        f._qualityFlashAG:Play()
    end

    -- Store full quote text for typewriter and click-to-dismiss
    local fullText = quote.text or ""
    self._pendingFullText = fullText
    f.bodyText:SetText("")

    -- Entry animation: fade in
    f:SetAlpha(0)
    f:Show()
    f._entryAG:SetScript("OnFinished", function()
        if InCombatLockdown() then f:Hide(); return end
        -- Start typewriter after entry completes
        local interval = 18 / 1000  -- 18ms per character
        local charIndex = 0
        local total = #fullText
        local baseDur = VC.Store:GetState().config.toastDuration or VC.Constants.DISPLAY.DURATION
        local dur = (quality == "epic" or quality == "legendary") and (baseDur + 4) or baseDur
        self._typewriterTicker = C_Timer.NewTicker(interval, function(ticker)
            charIndex = charIndex + 1
            f.bodyText:SetText(fullText:sub(1, charIndex))
            if charIndex >= total then
                ticker:Cancel()
                self._typewriterTicker = nil
                self._pendingFullText = nil
                -- Auto-hide after read duration
                self._hideTimer = C_Timer.NewTimer(dur, function()
                    self._hideTimer = nil
                    if InCombatLockdown() then f:Hide(); return end
                    if f._entryAG:IsPlaying() then f._entryAG:Stop() end
                    f._exitAG:Play()
                end)
            end
        end)
    end)
    f._entryAG:Play()
    if f._sheenAG:IsPlaying() then f._sheenAG:Stop() end
    f._sheenAG:Play()
end

-- ============================================================================
-- CHAT MODE
-- ============================================================================

function VC.Display:ShowInChat(data)
    local companion = data.companion
    local quote = data.quote
    local event = data.event

    local nameColor = companion and companion.chatColor or "|cFFFFFFFF"
    local name = companion and companion.name or "Companion"

    -- Event type label
    local eventLabel = ""
    if event and event.eventType then
        local label = VC.Constants.EVENT_TYPE_LABELS[event.eventType]
        if label then eventLabel = " |cFF8899AA(" .. label .. ")|r" end
    end

    -- Quality prefix symbol for rare+ (avoids rainbow chat log)
    local qualityPrefix = ""
    local quality = quote and quote.quality or "common"
    if quality == "rare" then
        qualityPrefix = "|cFF4dc9f6[R]|r "
    elseif quality == "epic" then
        qualityPrefix = "|cFF7b68ee[E]|r "
    elseif quality == "legendary" then
        qualityPrefix = "|cFFf39c12[L]|r "
    end

    local text = nameColor .. name .. ":|r" .. eventLabel .. " " .. qualityPrefix .. quote.text
    DEFAULT_CHAT_FRAME:AddMessage(text)
end
