-- VDS_Components
-- ============================================================================
-- Widget kinds: label, button, frame, panel. Each kind declares build +
-- dispatch.push for the BindingEngine. Extend per your addon's needs.

VDS = VDS or {}
VDS.UI = VDS.UI or {}

-- Width reserved for vertical scrollbar beside content.
VDS.UI.SCROLLBOX_SCROLLBAR_RESERVE = 6

local UI = VDS.UI

-- per-token font helper; falls back to GameFontNormal* family.
function UI.applyFontRole(fs, role)
    local font
    if role == "heading"    then font = _G.GameFontNormalHuge
    elseif role == "subheading" then font = _G.GameFontNormalLarge
    elseif role == "body"   then font = _G.GameFontNormal
    elseif role == "small"  then font = _G.GameFontNormalSmall
    elseif role == "caption" then font = _G.GameFontNormalTiny
    end
    if font and fs.SetFontObject then fs:SetFontObject(font) end
end

function UI:Label(parent, text, font, justifyH, opts)
    if not parent then return nil end
    local fs = parent:CreateFontString(nil, "OVERLAY")
    UI.applyFontRole(fs, font or "body")
    if justifyH and fs.SetJustifyH then fs:SetJustifyH(justifyH) end
    opts = opts or {}
    if opts.justifyV then fs:SetJustifyV(opts.justifyV) end
    if opts.wrap ~= nil then fs:SetWordWrap(opts.wrap and true or false) end
    if text and text ~= "" then fs:SetText(text) end
    local w = fs:GetStringWidth()
    if w > 0 then fs._intrinsicWidth = math.ceil(w) + 4 end
    return fs
end

local function dispatchText(widget, values)
    if values.text == nil then return end
    widget:SetText(tostring(values.text))
    local w = widget:GetStringWidth()
    if w > 0 then widget._intrinsicWidth = math.ceil(w) + 4 end
end

VDS.WidgetTypes:Register("label", {
    build = function(parent, spec)
        return UI:Label(parent, spec.text or "", spec.font, spec.justifyH, {
            wrap = spec.wrap,
            justifyV = spec.justifyV,
        })
    end,
    dispatch     = { fields = { "text" }, push = dispatchText },
    specFields   = { "text", "font", "justifyH", "justifyV", "wrap" },
    -- Labels always render text -- validator enforces `font` role on the spec.
    requiresFont = function(_spec) return true end,
})

-- Atlas button (HDGR pattern, HDGR_Components.lua:527): common-button-tertiary-*
-- atlases; 4 states (normal/hover/pressed/disabled). Button Skinner paints
-- border per scheme; SetActive swaps to -depressed- variants.
function UI:Button(parent, text, opts)
    if not parent or not CreateFrame then return nil end
    opts = opts or {}

    local btn = CreateFrame("Button", nil, parent)
    btn:EnableMouse(true)
    btn:RegisterForClicks("AnyUp")
    -- Disabled buttons swallow OnEnter by default; tooltips matter MOST on a
    -- gated button (e.g. Apply's why-is-this-locked explainer).
    btn:SetMotionScriptsWhileDisabled(true)

    btn:SetNormalAtlas("common-button-tertiary-normal")
    btn:SetHighlightAtlas("common-button-tertiary-hover")
    btn:SetPushedAtlas("common-button-tertiary-pressed")
    btn:SetDisabledAtlas("common-button-tertiary-disabled")
    btn._normalAtlas          = "common-button-tertiary-normal"
    btn._activeNormalAtlas    = "common-button-tertiary-depressed-normal"
    btn._highlightAtlas       = "common-button-tertiary-hover"
    btn._activeHighlightAtlas = "common-button-tertiary-depressed-hover"

    -- State textures default to atlas-native size; re-anchor to fill the button.
    for _, getter in ipairs({ "GetNormalTexture", "GetHighlightTexture",
                              "GetPushedTexture", "GetDisabledTexture" }) do
        local t = btn[getter](btn)
        if t then
            t:ClearAllPoints()
            t:SetAllPoints(btn)
        end
    end

    -- Atlas buttons have no inherited Text region; seed GameFontHighlightSmall
    -- so SetText doesn't error before applyFontRole swaps in the role-font.
    local fs = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    UI.applyFontRole(fs, opts.font or "body")
    if opts.justifyH == "LEFT" then
        fs:SetPoint("LEFT",  btn, "LEFT",  10, 1)
        fs:SetPoint("RIGHT", btn, "RIGHT", -8, 1)
        fs:SetJustifyH("LEFT")
    elseif opts.justifyH == "RIGHT" then
        fs:SetPoint("LEFT",  btn, "LEFT",   8, 1)
        fs:SetPoint("RIGHT", btn, "RIGHT", -10, 1)
        fs:SetJustifyH("RIGHT")
    else
        fs:SetPoint("CENTER", btn, "CENTER", 0, 1)  -- +1 to compensate atlas's down-shaded interior
    end
    btn:SetFontString(fs)
    btn:SetText(text or "")

    function btn:RefreshIntrinsicWidth()
        local w = fs:GetStringWidth()
        if w > 0 then self._intrinsicWidth = math.ceil(w) + 24 end
    end
    btn:RefreshIntrinsicWidth()

    function btn:SetActive(active)
        self._lsActive = active and true or false
        VDS.Theme:SetState(self, { active = self._lsActive })
        VDS.Theme:Apply(self, "Button")
    end

    -- Atlas swap on SetEnabled is subtle; custom FontString isn't auto-managed
    -- by Blizzard's NormalFont/DisabledFont pair. Hook to also fade text alpha
    -- so disabled state is visually obvious.
    local origSetEnabled = btn.SetEnabled
    function btn:SetEnabled(enabled)
        origSetEnabled(self, enabled)
        if fs then fs:SetAlpha(enabled and 1.0 or 0.4) end
        -- MotionScriptsWhileDisabled keeps tooltips alive on locked buttons,
        -- but also lets the hover atlas light up -- a disabled button under
        -- the cursor read as enabled. Suppress the highlight while disabled.
        local hl = self:GetHighlightTexture()
        if hl then hl:SetAlpha(enabled and 1.0 or 0) end
    end

    return btn
end

local function dispatchButton(widget, values)
    if values.text ~= nil then
        widget:SetText(tostring(values.text))
        widget:RefreshIntrinsicWidth()
    end
    if values.enabled ~= nil then
        widget:SetEnabled(values.enabled and true or false)
    end
    if values.active ~= nil then
        widget:SetActive(values.active and true or false)
    end
end

VDS.WidgetTypes:Register("button", {
    build = function(parent, spec)
        local btn = UI:Button(parent, spec.text or "", { font = spec.font, justifyH = spec.justifyH })
        btn._lsVariant = spec.variant or "default"
        if spec.enabled == false then btn:SetEnabled(false) end
        -- Tooltip dispatch: spec.tooltip is { recipe = "Name" } | false.
        -- Inline strings/tables/functions in specs are NOT allowed -- every
        -- tooltip lives in VDS.TooltipRecipes for centralization, locale
        -- seam, and style consistency. Validator enforces shape at boot.
        if type(spec.tooltip) == "table" and spec.tooltip.recipe then
            -- VDS.TooltipRecipes is engine-loaded pre-LayoutConfig (TOC line 72).
            -- Validator guarantees the recipe exists at boot, so strict access.
            local recipe = VDS.TooltipRecipes[spec.tooltip.recipe]
            if recipe ~= nil then VDS.TooltipEngine:Attach(btn, recipe) end
        end
        return btn
    end,
    skin         = "Button",
    -- dispatch.fields stays single-element so scalar bindings (selector returns
    -- a string for `text`) work via normaliseBinding's 1-field path. Multi-field
    -- binding for enabled+text uses the explicit table form:
    --   binding = { text = "sel.a", enabled = "sel.b" }
    -- and dispatchButton applies whichever fields appear in `values`.
    dispatch     = { fields = { "text" }, push = dispatchButton },
    -- `tooltip` is a UNIVERSAL_WIDGET_FIELDS entry (per Layout.lua) -- not duplicated here.
    specFields   = { "text", "font", "variant", "action", "justifyH", "enabled" },
    -- Buttons render a text label -- validator enforces `font` role on the spec.
    requiresFont = function(_spec) return true end,
})

VDS.WidgetTypes:Register("frame", {
    build = function(parent, _spec)
        return CreateFrame("Frame", nil, parent, "BackdropTemplate")
    end,
    skin       = "Frame",
    specFields = {},
})

VDS.WidgetTypes:Register("spacer", {
    build = function(parent, _spec)
        return CreateFrame("Frame", nil, parent)
    end,
    specFields = {},
})

-- Thin horizontal hairline. Spec.height controls the frame height; the line
-- itself is 1px regardless and sits in the frame's vertical center.
VDS.WidgetTypes:Register("divider", {
    build = function(parent, _spec)
        local f = CreateFrame("Frame", nil, parent)
        local line = f:CreateTexture(nil, "ARTWORK")
        line:SetTexture("Interface\\Buttons\\WHITE8x8")
        line:SetVertexColor(0.45, 0.45, 0.52, 0.55)
        line:SetHeight(1)
        line:SetPoint("LEFT",  f, "LEFT",  0, 0)
        line:SetPoint("RIGHT", f, "RIGHT", 0, 0)
        return f
    end,
    specFields = {},
})
