-- VDS.TooltipEngine
-- ============================================================================
-- Drives Blizzard's GameTooltip from declarative `tooltip` fields on WidgetTypes.
-- Spec: UI_WIDGET_TAXONOMY.md s17.3. Not a custom surface; thin shim over GameTooltip.
--
-- tooltip field: table { title, body, anchor, textFn, itemID, hyperlink, extraLines }
--   OR function(self) -> that table.
-- Shape validated by WidgetTypes registry (Core/WidgetTypes.lua); engine just consumes it.

VDS = VDS or {}
VDS.TooltipEngine = VDS.TooltipEngine or {}

local TE = VDS.TooltipEngine

-- ===== Internal helpers ===================================================

local function resolveDef(widget, def) -- table -> table; function -> call(widget); string -> {body=...}; else -> nil
    if type(def) == "function" then
        local ok, result = pcall(def, widget)
        if not ok then
            -- Surface recipe-fn crashes (CLAUDE.md: silent pcall swallow is forbidden).
            -- Result is the error message when ok is false.
            VDS.Log:Warn("tooltip", "recipe fn errored: " .. tostring(result))
            return nil
        end
        if type(result) ~= "table" then return nil end
        return result
    end
    if type(def) == "table" then return def end
    -- String recipes (the common case for terse one-liners) render as a single
    -- wrapped body line. Previously returned nil here, which silently killed
    -- every string-recipe tooltip on every widget kind in the codebase.
    if type(def) == "string" then return { body = def } end
    return nil
end

-- Render a resolved tooltip table to GameTooltip.
local function renderTooltip(widget, t)
    local tooltip = _G.GameTooltip
    if not tooltip then return end

    tooltip:SetOwner(widget, t.anchor or "ANCHOR_RIGHT")

    -- Item/hyperlink: Blizzard fills body; custom title/body/extras appended below.
    if t.itemID then
        if tooltip.SetItemByID then tooltip:SetItemByID(t.itemID) end  -- exception(boundary): GameTooltip method surface varies by patch
    elseif t.hyperlink then
        if tooltip.SetHyperlink then tooltip:SetHyperlink(t.hyperlink) end  -- exception(boundary): GameTooltip method surface varies by patch
    end

    if t.title then
        tooltip:AddLine(t.title)
    end
    if t.body then
        tooltip:AddLine(t.body, 1, 1, 1, true)   -- wrap=true
    end

    if t.extraLines then
        for _, line in ipairs(t.extraLines) do
            if type(line) == "string" then
                tooltip:AddLine(line, 1, 1, 1, true)
            elseif type(line) == "table" then
                tooltip:AddLine(line.text or "",
                    line.r or 1, line.g or 1, line.b or 1,  -- exception(optional): line color fields optional; default white
                    line.wrap ~= false)  -- exception(optional): wrap defaults to on; author opts out with wrap=false
            end
        end
    end

    -- textFn called at hover time (dynamic content without re-mounting).
    if t.textFn then
        local ok, dyn = pcall(t.textFn, widget)
        if not ok then
            VDS.Log:Warn("tooltip", "textFn errored: " .. tostring(dyn))
        end
        if ok and dyn then
            if type(dyn) == "string" then
                tooltip:AddLine(dyn, 1, 1, 1, true)
            elseif type(dyn) == "table" then
                for _, line in ipairs(dyn) do
                    if type(line) == "string" then
                        tooltip:AddLine(line, 1, 1, 1, true)
                    end
                end
            end
        end
    end

    tooltip:Show()
end

-- ===== Public API =========================================================

-- Attach OnEnter/OnLeave handlers. HookScript (not SetScript) to preserve existing hover wiring.
function TE:Attach(widget, def)
    if not widget then return end
    if def == nil then return end
    if not widget.HookScript then return end

    widget:HookScript("OnEnter", function(self)
        -- DIAGNOSTIC
        TE._enterCount = (TE._enterCount or 0) + 1
        local resolved = resolveDef(self, def)
        if resolved then renderTooltip(self, resolved) end
    end)
    widget:HookScript("OnLeave", function()
        if _G.GameTooltip then _G.GameTooltip:Hide() end
    end)
end

-- Explicit show (e.g. focus without hover). Caller hides via GameTooltip:Hide().
function TE:Show(widget, def)
    local resolved = resolveDef(widget, def)
    if resolved then renderTooltip(widget, resolved) end
end

function TE:Hide() -- convenience wrapper over GameTooltip:Hide()
    if _G.GameTooltip then _G.GameTooltip:Hide() end
end

-- Resolve `spec.tooltip = { recipe = "Name" }` to the registered recipe and
-- attach. Lets widget kinds wire tooltip with one call instead of duplicating
-- the lookup boilerplate in every build() function. No-op if spec.tooltip is
-- false / missing / unrecognised shape.
function TE:AttachFromSpec(widget, spec)
    if type(spec) ~= "table" or type(spec.tooltip) ~= "table" then return end
    local recipeName = spec.tooltip.recipe
    if not recipeName then return end
    local recipe = VDS.TooltipRecipes[recipeName]  -- TooltipRecipes is TOC-loaded before any build() runs
    if recipe == nil then return end
    self:Attach(widget, recipe)
end
