-- VDS_Theme
-- ============================================================================
-- Scheme-driven paint. Weak-table Skinners registry. Theme:Register(widget,
-- role[, state]) registers a widget for paint; Theme:Reload() walks the
-- registry and re-applies the current scheme to every registered widget.

VDS = VDS or {}
VDS.Theme = VDS.Theme or { registry = setmetatable({}, { __mode = "k" }),
                          states   = setmetatable({}, { __mode = "k" }) }

local T = VDS.Theme

T.OWNED_COLOR_NAMESPACES = {
    text = true, surface = true, border = true, semantic = true, button = true,
}

T.BACKDROP_FLAT = {
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 1,
}

-- ============================================================================
-- Helpers
-- ============================================================================

local function setBackdropColor(f, c)
    if f.SetBackdropColor and c then f:SetBackdropColor(c.r, c.g, c.b, c.a or 1) end
end

local function setBackdropBorderColor(f, c)
    if f.SetBackdropBorderColor and c then f:SetBackdropBorderColor(c.r, c.g, c.b, c.a or 1) end
end

local function setBackdrop(f, bd)
    if f.SetBackdrop then f:SetBackdrop(bd) end  -- exception(optional): Frame skinner accepts non-BackdropTemplate frames
end

local function setTextColor(f, c)
    if f.SetTextColor and c then f:SetTextColor(c.r, c.g, c.b, c.a or 1) end
end

-- ============================================================================
-- Scheme access
-- ============================================================================

local function currentSchemeName()
    -- invariant §4: Store + state shape guaranteed post-Init; scheme never deleted.
    return VDS.Store.state.account.config.scheme
end

local function currentScheme()
    return _G.VDS_Schemes[currentSchemeName()] or {}  -- unknown scheme key -> empty
end

-- :GetMetric("spacing.md") -> number, or nil if path doesn't resolve.
-- Layout uses this for `padding = "lg"` / `gap = "sm"` token resolution.
function T:GetMetric(path)
    local scheme = currentScheme()
    if not scheme or not scheme.metrics then return nil end
    local node = scheme.metrics
    for part in string.gmatch(path, "[^.]+") do
        if type(node) ~= "table" then return nil end
        node = node[part]
    end
    return node
end

function T:GetColor(token)
    local ns, key = token:match("^([^.]+)%.(.+)$")
    if not ns then return nil end
    local scheme = currentScheme()
    local bucket = scheme[ns]
    if type(bucket) ~= "table" then return nil end
    local v = bucket[key]
    if type(v) == "table" and v.r then return v end
    -- Nested: text.shadow / button.default.bg.normal
    if type(v) == "table" then
        local rest = key:match("%.(.+)")
        if rest then
            for k in rest:gmatch("[^.]+") do v = v and v[k] end
            if type(v) == "table" and v.r then return v end
        end
    end
    -- Deep token: split dots and walk
    local parts = {}
    for p in token:gmatch("[^.]+") do parts[#parts + 1] = p end
    local node = scheme
    for _, p in ipairs(parts) do
        if type(node) ~= "table" then return nil end
        node = node[p]
    end
    if type(node) == "table" and node.r then return node end
    return nil
end

function T:ColorCode(token)
    local ns = token:match("^([^.]+)")
    if not self.OWNED_COLOR_NAMESPACES[ns] then
        local hint = ""
        -- Palette is loaded by .toc before any consumer; strict read.
        if VDS.Palette.OWNED_NAMESPACES[ns] then
            hint = " -- use VDS.Palette:ColorCode instead"
        end
        error(("Theme:ColorCode: namespace %q not owned by Theme%s"):format(ns, hint), 2)
    end
    local c = self:GetColor(token)
    if not c then error(("Theme:ColorCode: token %q does not resolve"):format(token), 2) end
    return string.format("|cFF%02X%02X%02X",
        math.floor(c.r * 255 + 0.5),
        math.floor(c.g * 255 + 0.5),
        math.floor(c.b * 255 + 0.5))
end

-- ============================================================================
-- Lift-helpers (cookbook 15): patterns used 3+ times across row widgets.
-- 1-2-site patterns (left-edge accent, top-accent line, inner shadow) stay inline.
-- ============================================================================

local WHITE = VDS.Constants.TEXTURES.WHITE

-- Idempotent full-fill overlay. Exposed as Theme method for dynamic builders
-- (e.g. chipStrip per-chip buttons) that aren't registered in WidgetTypes.
local function ensureOverlay(widget, fieldName, sublayer, sublevel)
    local field = "_" .. fieldName
    widget[field] = widget[field] or widget:CreateTexture(nil, sublayer, nil, sublevel)
    widget[field]:SetAllPoints()
    return widget[field]
end
T.EnsureOverlay = function(_, widget, fieldName, sublayer, sublevel)
    return ensureOverlay(widget, fieldName, sublayer, sublevel)
end

local function ensureHoverHighlight(widget) -- HIGHLIGHT-layer white tint, donor 0.06 alpha
    widget._hover = widget._hover or widget:CreateTexture(nil, "HIGHLIGHT")
    widget._hover:SetAllPoints()
    widget._hover:SetTexture(WHITE)
    widget._hover:SetVertexColor(1, 1, 1, 0.06)
end
T.EnsureHoverHighlight = function(_, widget) ensureHoverHighlight(widget) end

-- ============================================================================
-- Skinners. ENGINE-DEFAULT minimal set; extend per cookbook 05.
-- ============================================================================

T.Skinners = {
    -- Default Frame is a no-op: donor FloatingWidget paints the outer panel_bg
    -- texture so inner panels stay transparent. FramePanel opts into backdrop paint.
    Frame = function(_frame, _scheme)
    end,

    FramePanel = function(frame, _scheme)
        setBackdrop(frame, T.BACKDROP_FLAT)
        setBackdropColor(frame, T:GetColor("surface.panel"))
        setBackdropBorderColor(frame, T:GetColor("border.default"))
    end,

    -- Button: ports VDS_Framework.lua:15-46 / HDGR_Theme.lua:300.
    -- Atlas border tinting: white * scheme color = scheme color; active swaps to -depressed- variants.
    Button = function(button, _scheme, state)
        state = state or {}
        local active = state.active and true or false

        if active then
            if button._activeNormalAtlas and button.SetNormalAtlas then
                button:SetNormalAtlas(button._activeNormalAtlas)
            end
            if button._activeHighlightAtlas and button.SetHighlightAtlas then
                button:SetHighlightAtlas(button._activeHighlightAtlas)
            end
        else
            if button._normalAtlas and button.SetNormalAtlas then
                button:SetNormalAtlas(button._normalAtlas)
            end
            if button._highlightAtlas and button.SetHighlightAtlas then
                button:SetHighlightAtlas(button._highlightAtlas)
            end
        end

        local accent      = T:GetColor("semantic.accent") -- active border = brand accent; normal = subtle border
        -- variant "success" tints the whole button green so the primary action
        -- (Apply) stands out from the neutral Revert/Snapshot buttons.
        local borderTint  = (button._lsVariant == "success" and T:GetColor("semantic.success"))
                            or (active and accent)
                            or T:GetColor("button.default.border.normal")
        for _, getter in ipairs({ "GetNormalTexture", "GetHighlightTexture",
                                  "GetPushedTexture", "GetDisabledTexture" }) do
            local tex = button[getter] and button[getter](button)
            if tex and tex.SetVertexColor and borderTint then  -- exception(optional): state textures optional on custom buttons; tint token can be unset
                tex:SetVertexColor(borderTint.r, borderTint.g, borderTint.b, 1)
            end
        end

        -- success variant: a bold green wash over the button face so it clearly
        -- stands out (tinting the dark atlas alone is too faint). Inset so the
        -- green atlas border still reads as an edge; sits below the OVERLAY text.
        if button._lsVariant == "success" then
            local g = T:GetColor("semantic.success") or { r = 0.40, g = 0.70, b = 0.30 }
            local fill = button._successFill
            if not fill then
                fill = button:CreateTexture(nil, "ARTWORK", nil, 2)
                fill:SetTexture("Interface\\Buttons\\WHITE8x8")
                fill:SetPoint("TOPLEFT",     button, "TOPLEFT",      3, -3)
                fill:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -3,  3)
                button._successFill = fill
            end
            fill:SetVertexColor(g.r, g.g, g.b, button:IsEnabled() and 0.55 or 0.18)
        end

        local fs = button.GetFontString and button:GetFontString() or nil
        if fs then
            local warn = state.warn and true or false
            local tc = warn and T:GetColor("semantic.error")
                or (active and T:GetColor("text.inverse"))
                or  T:GetColor("button.default.text.normal")
            if tc then setTextColor(fs, tc) end
            -- SetTextColor RESETS the FontString's region alpha (verified
            -- in-game 12.0.5: GetAlpha() reads 1 after SetTextColor) -- it
            -- silently undid the SetEnabled hook's 0.4 disabled fade on every
            -- repaint, leaving disabled buttons looking enabled. Re-assert the
            -- state-derived alpha after every paint: warn 0.9 (error red
            -- legible on the disabled atlas), else mirror the SetEnabled fade.
            local enabled = button:IsEnabled()
            fs:SetAlpha(warn and 0.9 or (enabled and 1.0 or 0.4))
        end
    end,

    -- MinimalScrollBar Thumb: vertex color persists across mixin hover/down atlas swaps.
    -- Cool grey slightly brighter than surface.panel so thumb reads as interactive.
    ScrollThumb = function(thumb)
        if not thumb then return end
        local r, g, b = 0.40, 0.40, 0.45
        if thumb.Begin  and thumb.Begin.SetVertexColor  then thumb.Begin:SetVertexColor(r, g, b, 1)  end  -- exception(boundary): MinimalScrollBar internals vary by client
        if thumb.Middle and thumb.Middle.SetVertexColor then thumb.Middle:SetVertexColor(r, g, b, 1) end  -- exception(boundary): MinimalScrollBar internals vary by client
        if thumb["End"] and thumb["End"].SetVertexColor then thumb["End"]:SetVertexColor(r, g, b, 1) end
    end,

    Text     = function(text) setTextColor(text, T:GetColor("text.default")) end,
    TextDim  = function(text) setTextColor(text, T:GetColor("text.dim"))     end,
    TextStatus = function(text) setTextColor(text, T:GetColor("text.status")) end,

    -- Row text roles: routed through Skinner so scheme switches re-paint FontStrings.
    TextRowName = function(text)
        local c = T:GetColor("text.primary") or T:GetColor("text.default")
        if c then setTextColor(text, c) end
    end,
    TextRowHeader = function(text)
        local c = T:GetColor("semantic.accent.warm")
                  or T:GetColor("semantic.accent") or { r = 0.90, g = 0.80, b = 0.50, a = 1 }
        setTextColor(text, c)
    end,
    TextRowMeta = function(text)
        local c = T:GetColor("text.dim") or { r = 0.55, g = 0.55, b = 0.58, a = 1 }
        setTextColor(text, c)
    end,

    -- Warehouse stock roles.
    TextOverrideBadge  = function(fs) setTextColor(fs, VDS.Palette:GetColor("accent.teal"))  end,
    TextStockTarget    = function(fs) setTextColor(fs, VDS.Palette:GetColor("accent.teal"))  end,
    TextStockNeed      = function(fs) setTextColor(fs, T:GetColor("semantic.error"))           end,
    TextStockOverstock = function(fs) setTextColor(fs, T:GetColor("text.dim"))                end,
    TextStockOk        = function(fs) setTextColor(fs, T:GetColor("text.dim"))                end,
    TextStockCost      = function(fs) setTextColor(fs, VDS.Palette:GetColor("accent.gold"))  end,
    TextReset          = function(fs) setTextColor(fs, T:GetColor("text.dim"))                end,

    RowBg = function(tex)
        local c = T:GetColor("surface.panel") or { r = 0.13, g = 0.13, b = 0.15, a = 0.4 }
        if tex.SetVertexColor then  -- exception(optional): role paints Texture or FontString regions; method differs
            tex:SetVertexColor(c.r, c.g, c.b, c.a or 0.4)  -- exception(optional): scheme color alpha optional; 0.4 wash default
        end
    end,

    progressbar = function(bar, _scheme, state)
        local variant = (state and state.variant) or "accent"  -- exception(nullable): no stored widget state = default variant
        local fill = T:GetColor("semantic." .. variant) or T:GetColor("semantic.accent")
        if bar.SetStatusBarColor and fill then
            bar:SetStatusBarColor(fill.r, fill.g, fill.b, 1)
        end
    end,

    -- DetailStrip: ports VDS_Framework.lua:563-607 -- textured cardstock bg + inner top shadow.
    DetailStrip = function(widget, _scheme, _state)
        local bg = ensureOverlay(widget, "stripBg", "BACKGROUND", 0)
        bg:SetTexture("Interface\\AddOns\\VamoosesDyeStudio\\textures\\vds_swatch_tex")
        bg:SetVertexColor(0.18, 0.18, 0.18, 0.75)

        widget._innerShadow = widget._innerShadow or widget:CreateTexture(nil, "ARTWORK", nil, -1) -- inner top shadow
        widget._innerShadow:ClearAllPoints()
        widget._innerShadow:SetPoint("TOPLEFT", 0, 0)
        widget._innerShadow:SetPoint("TOPRIGHT", 0, 0)
        widget._innerShadow:SetHeight(10)
        widget._innerShadow:SetTexture(WHITE)
        widget._innerShadow:SetGradient("VERTICAL",
            _G.CreateColor(0, 0, 0, 0),
            _G.CreateColor(0, 0, 0, 0.6))
    end,

    -- CategoryHeader: ports VDS_Framework.lua:511-556. Not yet used; ready for category grouping.
    CategoryHeader = function(widget, _scheme, _state)
        local bg = ensureOverlay(widget, "headerBg", "BACKGROUND", 0)
        bg:SetTexture(WHITE)
        local panelC = T:GetColor("surface.panel")
        if panelC then bg:SetVertexColor(panelC.r, panelC.g, panelC.b, 0.8) end

        widget._topAccent = widget._topAccent or widget:CreateTexture(nil, "ARTWORK")
        widget._topAccent:ClearAllPoints()
        widget._topAccent:SetPoint("TOPLEFT", 0, 0)
        widget._topAccent:SetPoint("TOPRIGHT", 0, 0)
        widget._topAccent:SetHeight(1)
        widget._topAccent:SetTexture(WHITE)
        local accent = VDS.Palette:GetColor("accent.teal")
        widget._topAccent:SetVertexColor(accent.r, accent.g, accent.b, 0.4)
    end,

    -- DyeRow/HerbRow chrome lives in row factories; row binders own Show/Hide
    -- on every bind (cross-kind frame recycling).

    -- ===== Window chrome =====
    -- vds_panel_bg is white-ish; SetVertexColor multiply gives consistent dark cool-grey.
    WindowBg = function(tex)
        local c = T:GetColor("surface.panel")
        if c then tex:SetVertexColor(c.r, c.g, c.b, 1) end
    end,

    -- Pure-black alpha edge; no scheme dependency (lifted-card effect on any bg).
    WindowEdge = function(tex)
        tex:SetVertexColor(0, 0, 0, 0.4)
    end,

    -- Hover swaps grey -> semantic.error (destructive interaction signal).
    CloseIcon = function(tex, _scheme, state)
        local hover = state and state.hover  -- exception(nullable): no stored widget state = not hovered
        local c = hover
            and T:GetColor("semantic.error")
            or  T:GetColor("text.dim")
        if c then tex:SetVertexColor(c.r, c.g, c.b, 1) end
    end,

    -- ===== Row chrome =====
    -- Darker than surface.panel so the header reads as a recessed band.
    RowHeaderBg = function(tex)
        local c = T:GetColor("surface.panel") or { r = 0.12, g = 0.12, b = 0.14 }
        tex:SetVertexColor(c.r * 0.9, c.g * 0.9, c.b * 0.9, 0.7)
    end,

    RowHoverHl = function(tex)
        tex:SetVertexColor(1, 1, 1, 0.06)
    end,

    -- "VDS gold": low alpha for bg fill, full alpha for left-edge accent.
    RowSelectedBg = function(tex)
        local c = VDS.Palette:GetColor("accent.gold")
        tex:SetVertexColor(c.r, c.g, c.b, 0.10)
    end,

    RowSelAccent = function(tex)
        local c = VDS.Palette:GetColor("accent.gold")
        tex:SetVertexColor(c.r, c.g, c.b, 1)
    end,

    -- Teal variant for custom-palette selected rows (vs. gold for dye rows).
    RowCustomSelectedBg = function(tex)
        local c = VDS.Palette:GetColor("accent.teal")
        tex:SetVertexColor(c.r * 0.55, c.g * 0.7, c.b * 0.7, 0.20)
    end,

    RowCustomAccent = function(tex)
        local c = VDS.Palette:GetColor("accent.teal")
        tex:SetVertexColor(c.r, c.g, c.b, 1)
    end,
}

-- ============================================================================
-- Register + Apply
-- ============================================================================

function T:Register(widget, role, state, _env)
    if not widget or not role then return false end
    -- LESSON (VFN eef37c30): unknown role -> warn, not silent no-op.
    -- CopyDialog shipped with Theme:Register(f, "Panel") for months with no Skinners["Panel"].
    local skin = self.Skinners[role]
    if not skin then
        VDS.Log:Warn("theme",
            "Theme:Register: unknown role " .. tostring(role) ..
            " -- frame will NOT be painted. Add Skinners[" .. tostring(role) .. "].")
    end
    self.registry[widget] = role
    if state ~= nil then self.states[widget] = state end
    if skin then skin(widget, currentScheme(), self.states[widget]) end
    return skin ~= nil
end

-- cookbook 15: row factories call SetState + Apply in init().
function T:SetState(widget, state)
    if not widget then return end
    self.states[widget] = state
end

function T:Apply(widget, role, _env)
    if not widget then return false end
    local resolvedRole = role or self.registry[widget]
    local skin = self.Skinners[resolvedRole]
    if not skin then return false end
    skin(widget, currentScheme(), self.states[widget])
    return true
end

function T:Reload()
    for w, role in pairs(self.registry) do
        local skin = self.Skinners[role]
        if skin then skin(w, currentScheme(), self.states[w]) end
    end
end
