-- ============================================================================
-- Housing Decor Guide - Theme Engine
-- Manages theme registry and skinners for live theme switching
-- ============================================================================

local addonName, privateDB = ...

-- Use the addon's global namespace (created in HDG_Core.lua)
local HDG = _G.HousingDecorGuide

HDG.Theme = {}

-- Weak table: automatically stops tracking widgets if they are garbage collected
HDG.Theme.registry = setmetatable({}, { __mode = "k" })
HDG.Theme.currentScheme = nil  -- Set during initialization

-- ============================================================================
-- CENTRALIZED BACKDROP
-- ============================================================================

HDG.Theme.BACKDROP_FLAT = {
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 1,
    insets = { left = 0, right = 0, top = 0, bottom = 0 }
}

HDG.Theme.BACKDROP_BORDERLESS = {
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = nil,
    tile = false,
}

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

function HDG.Theme:Initialize()
    -- Set initial scheme based on saved config
    local themeName = "HDGDefault"
    if HDG.Store and HDG.Store.state and HDG.Store.state.config then
        themeName = HDG.Store.state.config.theme or "HDGDefault"
    elseif HDG_DB and HDG_DB.config and HDG_DB.config.theme then
        themeName = HDG_DB.config.theme or "HDGDefault"
    end

    self.currentScheme = HDG.Colors.Schemes[themeName] or HDG.Colors.Schemes.HDGDefault

    -- Rebuild hex cache now that we have the correct scheme
    if HDG.Theme.RebuildHexCache then
        HDG.Theme.RebuildHexCache()
    end

    -- Listen for theme update events
    -- Supports: payload.themeName to change theme, or empty payload to just refresh
    if HDG.EventBus then
        HDG.EventBus:Register("HDG_THEME_UPDATE", function(payload)
            if payload.themeName then
                self.currentScheme = HDG.Colors.Schemes[payload.themeName] or HDG.Colors.Schemes.HDGDefault
            end
            self:UpdateAll()
        end)
    end
end

-- ============================================================================
-- UPDATE ALL REGISTERED WIDGETS
-- ============================================================================

function HDG.Theme:UpdateAll()
    -- Rebuild hex cache before updating widgets
    if HDG.Theme.RebuildHexCache then
        HDG.Theme.RebuildHexCache()
    end
    for widget, widgetType in pairs(self.registry) do
        if self.Skinners[widgetType] then
            self.Skinners[widgetType](widget, self.currentScheme)
        end
    end
end

-- ============================================================================
-- REGISTER WIDGET
-- ============================================================================

function HDG.Theme:Register(widget, widgetType)
    self.registry[widget] = widgetType
    -- Apply current theme immediately
    if self.Skinners[widgetType] and self.currentScheme then
        self.Skinners[widgetType](widget, self.currentScheme)
    end
end

-- ============================================================================
-- GET CURRENT SCHEME
-- ============================================================================

function HDG.Theme:GetScheme()
    return self.currentScheme or HDG.Colors.Schemes.HDGDefault
end

-- ============================================================================
-- BACKGROUND OPACITY HELPER
-- User-configurable background transparency
-- ============================================================================

local function GetBgOpacity()
    if HDG.Store and HDG.Store.state and HDG.Store.state.config then
        return HDG.Store.state.config.bgOpacity or 0.95
    end
    return 0.95
end

HDG.Theme.GetBgOpacity = GetBgOpacity

-- ============================================================================
-- TEXT SHADOW HELPER
-- Apply text shadow based on scheme.isLight flag (light vs dark themes)
-- ============================================================================

local function ApplyTextShadow(fontString, scheme)
    if not fontString or not fontString.SetShadowOffset then return end
    fontString:SetShadowOffset(1, -1)
    if scheme.isLight then
        -- Light themes: subtle shadow using text's own color at low opacity
        local r, g, b = fontString:GetTextColor()
        fontString:SetShadowColor(r, g, b, 0.4)
    else
        -- Dark themes: strong black shadow for contrast
        fontString:SetShadowColor(0, 0, 0, 1)
    end
end

HDG.Theme.ApplyTextShadow = ApplyTextShadow

-- ============================================================================
-- APPLY FONT HELPER
-- Applies font with user-configurable scale + text shadow in one call
-- ============================================================================

local function ApplyFont(fontString, scheme, fontType, sizeOverride)
    if not fontString or not fontString.SetFont then return end
    fontType = fontType or "body"

    -- Get user-selected font family file path
    local fontFile = HDG.Colors.GetFontFile()

    -- Get size from scheme or fallback (unless overridden)
    local fontDef = scheme and scheme.fonts and scheme.fonts[fontType]
    local size, flags
    if sizeOverride then
        size = sizeOverride
        flags = fontDef and fontDef.flags or ""
    elseif fontDef then
        size = fontDef.size
        flags = fontDef.flags or ""
    else
        -- Fallback sizes
        if fontType == "header" then
            size, flags = 14, ""
        elseif fontType == "small" then
            size, flags = 10, ""
        elseif fontType == "xsmall" then
            size, flags = 8, ""
        elseif fontType == "tiny" then
            size, flags = 6, ""
        else
            size, flags = 12, ""
        end
    end

    -- Get user-configurable font scale
    local fontScale = 0
    if HDG.Store and HDG.Store.state and HDG.Store.state.config then
        fontScale = HDG.Store.state.config.fontScale or 0
    end
    size = size + fontScale

    fontString:SetFont(fontFile, size, flags)
    ApplyTextShadow(fontString, scheme)
end

HDG.Theme.ApplyFont = ApplyFont

-- Legacy helper for compatibility (returns tuple instead of applying)
-- FIXED: Now includes fontScale and fontFamily from user config
local function GetFontWithScale(scheme, fontType)
    local fontScale = 0
    if HDG.Store and HDG.Store.state and HDG.Store.state.config then
        fontScale = HDG.Store.state.config.fontScale or 0
    end

    -- Get user-selected font family file path
    local fontFile = HDG.Colors.GetFontFile()

    -- Get size from scheme or fallback
    local fontDef = scheme and scheme.fonts and scheme.fonts[fontType]
    if fontDef then
        return fontFile, fontDef.size + fontScale, fontDef.flags or ""
    end

    -- Fallback sizes
    if fontType == "header" then
        return fontFile, 14 + fontScale, ""
    elseif fontType == "small" then
        return fontFile, 10 + fontScale, ""
    elseif fontType == "xsmall" then
        return fontFile, 8 + fontScale, ""
    elseif fontType == "tiny" then
        return fontFile, 6 + fontScale, ""
    else
        return fontFile, 12 + fontScale, ""
    end
end

HDG.Theme.GetFontWithScale = GetFontWithScale

-- ============================================================================
-- CANONICAL HELPERS (for module consumption - use these instead of local copies)
-- ============================================================================

-- Get current scheme (shortcut for modules)
function HDG.Theme.GetCurrentScheme()
    return HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
end

-- Register widget (shortcut for modules)
function HDG.Theme.RegisterWidget(widget, widgetType)
    HDG.Theme:Register(widget, widgetType)
end

-- Get color code string for text (replaces hardcoded |cFFFFFFFF)
-- Returns WoW escape code like "|cFF657b83" for embedding in strings
function HDG.Theme.GetTextColorCode(colorType)
    local scheme = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    local color
    if colorType == "header" then
        color = scheme.text_header
    elseif colorType == "dim" then
        color = scheme.text_dim
    elseif colorType == "accent" then
        color = scheme.accent
    else
        color = scheme.text
    end
    return string.format("|cFF%02x%02x%02x",
        math.floor(color.r * 255),
        math.floor(color.g * 255),
        math.floor(color.b * 255))
end

-- Shorthand for main text color code
function HDG.Theme.TextCode()
    return HDG.Theme.GetTextColorCode("text")
end

-- Shorthand for header text color code
function HDG.Theme.HeaderCode()
    return HDG.Theme.GetTextColorCode("header")
end

-- ============================================================================
-- COMPREHENSIVE HEX COLOR SYSTEM
-- Pre-calculated hex strings for all semantic color types
-- ============================================================================

-- Cache for hex color codes (rebuilt on theme change)
HDG.Theme.HexCache = {}

-- Core hex conversion: RGB floats (0-1) to hex string without prefix
local function RGBToHex(r, g, b)
    return string.format("%02X%02X%02X",
        math.floor((r or 1) * 255),
        math.floor((g or 1) * 255),
        math.floor((b or 1) * 255))
end

-- Convert a color table {r, g, b} to hex string
function HDG.Theme.GetHex(colorTable)
    if not colorTable then return "FFFFFF" end
    return RGBToHex(colorTable.r, colorTable.g, colorTable.b)
end

-- Get WoW escape code from color table: "|cFFRRGGBB"
function HDG.Theme.GetColorCode(colorTable)
    return "|cFF" .. HDG.Theme.GetHex(colorTable)
end

-- Rebuild hex cache from current scheme
local function RebuildHexCache()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    local cache = HDG.Theme.HexCache

    -- Core colors
    cache.text = RGBToHex(c.text.r, c.text.g, c.text.b)
    cache.text_header = RGBToHex(c.text_header.r, c.text_header.g, c.text_header.b)
    cache.text_dim = RGBToHex(c.text_dim.r, c.text_dim.g, c.text_dim.b)
    local textLabel = c.text_label or c.text  -- Fallback to text if not defined
    cache.text_label = RGBToHex(textLabel.r, textLabel.g, textLabel.b)
    cache.accent = RGBToHex(c.accent.r, c.accent.g, c.accent.b)

    -- Status colors
    cache.success = RGBToHex(c.success.r, c.success.g, c.success.b)
    cache.warning = RGBToHex(c.warning.r, c.warning.g, c.warning.b)
    cache.error = RGBToHex(c.error.r, c.error.g, c.error.b)

    -- UI element colors
    cache.bg = RGBToHex(c.bg.r, c.bg.g, c.bg.b)
    cache.panel = RGBToHex(c.panel.r, c.panel.g, c.panel.b)
    cache.border = RGBToHex(c.border.r, c.border.g, c.border.b)

    -- Recipe status colors
    cache.recipe_known = RGBToHex(c.text_recipe_known.r, c.text_recipe_known.g, c.text_recipe_known.b)
    cache.recipe_alt = RGBToHex(c.text_recipe_alt.r, c.text_recipe_alt.g, c.text_recipe_alt.b)
    cache.recipe_unknown = RGBToHex(c.text_recipe_unknown.r, c.text_recipe_unknown.g, c.text_recipe_unknown.b)
    cache.recipe_no_prof = RGBToHex(c.text_recipe_no_prof.r, c.text_recipe_no_prof.g, c.text_recipe_no_prof.b)

    -- Goblin tab colors (fallback to warning/text_dim if not defined)
    local currency = c.currency or c.warning
    local colHeader = c.column_header or c.text_dim
    cache.currency = RGBToHex(currency.r, currency.g, currency.b)
    cache.column_header = RGBToHex(colHeader.r, colHeader.g, colHeader.b)

    -- Pre-computed escape codes (with |cFF prefix)
    cache.CODE_TEXT = "|cFF" .. cache.text
    cache.CODE_HEADER = "|cFF" .. cache.text_header
    cache.CODE_DIM = "|cFF" .. cache.text_dim
    cache.CODE_LABEL = "|cFF" .. cache.text_label
    cache.CODE_ACCENT = "|cFF" .. cache.accent
    cache.CODE_SUCCESS = "|cFF" .. cache.success
    cache.CODE_WARNING = "|cFF" .. cache.warning
    cache.CODE_ERROR = "|cFF" .. cache.error
    cache.CODE_RECIPE_KNOWN = "|cFF" .. cache.recipe_known
    cache.CODE_RECIPE_ALT = "|cFF" .. cache.recipe_alt
    cache.CODE_RECIPE_UNKNOWN = "|cFF" .. cache.recipe_unknown
    cache.CODE_RECIPE_NO_PROF = "|cFF" .. cache.recipe_no_prof
    cache.CODE_CURRENCY = "|cFF" .. cache.currency
    cache.CODE_COLUMN_HEADER = "|cFF" .. cache.column_header
end

-- Expose for UpdateAll to call
HDG.Theme.RebuildHexCache = RebuildHexCache

-- Initialize cache on load
RebuildHexCache()

-- ============================================================================
-- SEMANTIC HEX GETTERS (Return "|cFFRRGGBB" escape codes)
-- Use these in string concatenation for dynamic text coloring
-- ============================================================================

function HDG.Theme.C_Text() return HDG.Theme.HexCache.CODE_TEXT end
function HDG.Theme.C_Header() return HDG.Theme.HexCache.CODE_HEADER end
function HDG.Theme.C_Dim() return HDG.Theme.HexCache.CODE_DIM end
function HDG.Theme.C_Label() return HDG.Theme.HexCache.CODE_LABEL end
function HDG.Theme.C_Accent() return HDG.Theme.HexCache.CODE_ACCENT end
function HDG.Theme.C_Success() return HDG.Theme.HexCache.CODE_SUCCESS end
function HDG.Theme.C_Warning() return HDG.Theme.HexCache.CODE_WARNING end
function HDG.Theme.C_Error() return HDG.Theme.HexCache.CODE_ERROR end
function HDG.Theme.C_RecipeKnown() return HDG.Theme.HexCache.CODE_RECIPE_KNOWN end
function HDG.Theme.C_RecipeAlt() return HDG.Theme.HexCache.CODE_RECIPE_ALT end
function HDG.Theme.C_RecipeUnknown() return HDG.Theme.HexCache.CODE_RECIPE_UNKNOWN end
function HDG.Theme.C_RecipeNoProf() return HDG.Theme.HexCache.CODE_RECIPE_NO_PROF end
function HDG.Theme.C_Currency() return HDG.Theme.HexCache.CODE_CURRENCY end
function HDG.Theme.C_ColumnHeader() return HDG.Theme.HexCache.CODE_COLUMN_HEADER end

-- ============================================================================
-- STATE COLOR GETTERS (Return r, g, b, a for toggle button backgrounds)
-- Fallback to HDGDefault values if scheme doesn't define them
-- ============================================================================

local STATE_FALLBACKS = {
    state_active   = {r=0.11, g=0.31, b=0.31, a=1.00},
    state_inactive = {r=0.18, g=0.22, b=0.24, a=1.00},
    state_hover    = {r=0.22, g=0.28, b=0.30, a=1.00},
}

function HDG.Theme.GetStateActive()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    local s = c.state_active or STATE_FALLBACKS.state_active
    return s.r, s.g, s.b, s.a or 1
end

function HDG.Theme.GetStateInactive()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    local s = c.state_inactive or STATE_FALLBACKS.state_inactive
    return s.r, s.g, s.b, s.a or 1
end

function HDG.Theme.GetStateHover()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    local s = c.state_hover or STATE_FALLBACKS.state_hover
    return s.r, s.g, s.b, s.a or 1
end

-- Column header color getter (returns RGB for SetTextColor)
function HDG.Theme.GetColumnHeaderColor()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    local col = c.column_header or c.text_dim
    return col.r, col.g, col.b, col.a or 1
end

-- Currency color getter (returns RGB for SetTextColor)
function HDG.Theme.GetCurrencyColor()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    local cur = c.currency or c.warning
    return cur.r, cur.g, cur.b, cur.a or 1
end

-- ============================================================================
-- QUALITY COLOR SYSTEM (Theme-aware item quality colors)
-- Light themes use custom colors for readability; dark themes use Blizzard defaults
-- ============================================================================

-- Default quality colors (Blizzard standard for dark themes)
local QUALITY_COLORS_DEFAULT = {
    [0] = {r=0.62, g=0.62, b=0.62}, -- Poor (gray)
    [1] = {r=1.00, g=1.00, b=1.00}, -- Common (white)
    [2] = {r=0.12, g=1.00, b=0.00}, -- Uncommon (neon green)
    [3] = {r=0.00, g=0.44, b=0.87}, -- Rare (blue)
    [4] = {r=0.64, g=0.21, b=0.93}, -- Epic (purple)
    [5] = {r=1.00, g=0.50, b=0.00}, -- Legendary (orange)
    [6] = {r=0.90, g=0.80, b=0.50}, -- Artifact (light gold)
    [7] = {r=0.00, g=0.80, b=1.00}, -- Heirloom (cyan)
}

-- Solarized quality colors (for light themes with dark text)
local QUALITY_COLORS_SOLARIZED = {
    [0] = {r=0.86, g=0.20, b=0.18}, -- Poor (Red #dc322f)
    [1] = {r=0.40, g=0.48, b=0.51}, -- Common (Dark Grey #657b83)
    [2] = {r=0.52, g=0.60, b=0.00}, -- Uncommon (Solarized Green #859900)
    [3] = {r=0.15, g=0.55, b=0.82}, -- Rare (Solarized Blue #268bd2)
    [4] = {r=0.42, g=0.44, b=0.77}, -- Epic (Violet #6c71c4)
    [5] = {r=0.83, g=0.21, b=0.51}, -- Legendary (Magenta #d33682)
    [6] = {r=0.71, g=0.54, b=0.00}, -- Artifact (Yellow #b58900)
    [7] = {r=0.80, g=0.29, b=0.09}, -- Heirloom (Solarized Orange #cb4b16)
}

-- Get theme-aware quality color (r, g, b)
function HDG.Theme.GetQualityRGB(quality)
    local scheme = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    quality = quality or 1

    -- Light themes use Solarized quality colors for readability
    if scheme.isLight then
        local qc = QUALITY_COLORS_SOLARIZED[quality] or QUALITY_COLORS_SOLARIZED[1]
        return qc.r, qc.g, qc.b
    end

    -- Dark themes: use scheme colors for Uncommon/Rare, defaults for others
    if quality == 2 and scheme.success then
        return scheme.success.r, scheme.success.g, scheme.success.b
    end
    if quality == 3 and scheme.accent then
        return scheme.accent.r, scheme.accent.g, scheme.accent.b
    end

    local qc = QUALITY_COLORS_DEFAULT[quality] or QUALITY_COLORS_DEFAULT[1]
    return qc.r, qc.g, qc.b
end

-- Get theme-aware quality hex code (returns "|cFFRRGGBB")
function HDG.Theme.GetQualityHex(quality)
    local r, g, b = HDG.Theme.GetQualityRGB(quality)
    return string.format("|cFF%02X%02X%02X", r * 255, g * 255, b * 255)
end

-- ============================================================================
-- SEMANTIC RGB GETTERS (Return r, g, b values for SetTextColor)
-- Use these for direct API calls
-- ============================================================================

function HDG.Theme.RGB_Text()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    return c.text.r, c.text.g, c.text.b
end

function HDG.Theme.RGB_Header()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    return c.text_header.r, c.text_header.g, c.text_header.b
end

function HDG.Theme.RGB_Dim()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    return c.text_dim.r, c.text_dim.g, c.text_dim.b
end

function HDG.Theme.RGB_Accent()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    return c.accent.r, c.accent.g, c.accent.b
end

function HDG.Theme.RGB_Success()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    return c.success.r, c.success.g, c.success.b
end

function HDG.Theme.RGB_Warning()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    return c.warning.r, c.warning.g, c.warning.b
end

function HDG.Theme.RGB_Error()
    local c = HDG.Theme.currentScheme or HDG.Colors.Schemes.HDGDefault
    return c.error.r, c.error.g, c.error.b
end

-- ============================================================================
-- ATLAS HELPERS (For HousingTheme atlas textures)
-- ============================================================================

-- Apply atlas background to a frame (creates texture layer, makes backdrop transparent)
-- preserveAspect: if true, uses "cover" style - fills frame while maintaining aspect ratio (crops overflow)
local function ApplyAtlasBackground(frame, atlasName, layer, preserveAspect)
    if not frame or not atlasName then return end
    layer = layer or "BACKGROUND"

    -- Create or reuse atlas texture
    if not frame._atlasTexture then
        frame._atlasTexture = frame:CreateTexture(nil, layer, nil, -8)
        frame._atlasTexture:SetAllPoints()
    end

    frame._atlasTexture:SetAtlas(atlasName, false)  -- false = don't use atlas size, stretch to fill
    frame._atlasTexture:SetTexCoord(0, 1, 0, 1)  -- Reset tex coords
    frame._atlasTexture:Show()

    -- Aspect ratio preservation (cover style) - calculated once after frame layout
    if preserveAspect then
        local atlasInfo = C_Texture.GetAtlasInfo(atlasName)
        if atlasInfo and atlasInfo.width and atlasInfo.height then
            local atlasRatio = atlasInfo.width / atlasInfo.height
            -- Defer calculation until frame has size (next frame)
            C_Timer.After(0, function()
                local fw, fh = frame:GetSize()
                if fw > 0 and fh > 0 and frame._atlasTexture then
                    local frameRatio = fw / fh
                    if frameRatio > atlasRatio then
                        -- Frame is wider: crop top/bottom
                        local crop = (1 - atlasRatio / frameRatio) / 2
                        frame._atlasTexture:SetTexCoord(0, 1, crop, 1 - crop)
                    else
                        -- Frame is taller: crop left/right
                        local crop = (1 - frameRatio / atlasRatio) / 2
                        frame._atlasTexture:SetTexCoord(crop, 1 - crop, 0, 1)
                    end
                end
            end)
        end
    end

    -- Make backdrop transparent so atlas shows through
    if frame.SetBackdropColor then
        frame:SetBackdropColor(0, 0, 0, 0)
    end
    if frame.SetBackdropBorderColor then
        frame:SetBackdropBorderColor(0, 0, 0, 0)
    end
end

-- Hide atlas textures (for non-atlas themes)
local function HideAtlasTextures(frame)
    if frame and frame._atlasTexture then
        frame._atlasTexture:Hide()
    end
    if frame and frame._borderTextures then
        for _, tex in pairs(frame._borderTextures) do
            tex:Hide()
        end
    end
end

-- Check if current scheme uses atlases
local function HasAtlas(scheme)
    return scheme and scheme.atlas ~= nil
end

HDG.Theme.ApplyAtlasBackground = ApplyAtlasBackground
HDG.Theme.HideAtlasTextures = HideAtlasTextures
HDG.Theme.HasAtlas = HasAtlas

-- ============================================================================
-- HEADER TITLE STYLING (for panel/tab header bars)
-- Creates wood sign background that fills the full header bar width
-- ============================================================================

local function StyleHeaderTitle(fontString, parent)
    if not fontString or not parent then return end

    -- Wood sign background fills full width of parent header bar
    local woodSign = parent:CreateTexture(nil, "ARTWORK", nil, -1)
    woodSign:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
    woodSign:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)
    woodSign:Hide()

    fontString.woodSignBg = woodSign
    fontString.woodSignParent = parent

    HDG.Theme.RegisterWidget(fontString, "HeaderTitle")
end

HDG.Theme.StyleHeaderTitle = StyleHeaderTitle

-- ============================================================================
-- SKINNERS (Pure functions that apply colors to widgets)
-- ============================================================================

HDG.Theme.Skinners = {

    -- Main Window skinner (top-level HDG window only - gets atlas bg)
    MainWindow = function(f, c)
        local opacity = GetBgOpacity()
        -- Atlas support for HousingTheme
        if HasAtlas(c) and c.atlas.background then
            ApplyAtlasBackground(f, c.atlas.background)
            if f.SetBackdropBorderColor then
                f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 0)  -- Hide scheme border when using atlas frame
            end
        else
            HideAtlasTextures(f)
            if f.GetBackdrop and f:GetBackdrop() then
                f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, c.bg.a * opacity)
                f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
            end
        end
        -- Toggle atlas wood frame border overlay
        if f.borderFrame then
            if c.atlas and c.atlas.windowBorder then
                f.borderFrame.tex:SetAtlas(c.atlas.windowBorder)
                f.borderFrame:Show()
            else
                f.borderFrame:Hide()
            end
        end
        if f.title then
            f.title:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, c.text_header.a)
            ApplyFont(f.title, c, "header")
        end
    end,

    -- Generic Frame skinner (no atlas, just colors)
    Frame = function(f, c)
        local opacity = GetBgOpacity()
        HideAtlasTextures(f)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, c.bg.a * opacity)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
        if f.title then
            f.title:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, c.text_header.a)
            ApplyFont(f.title, c, "header")
        end
    end,

    -- Panel skinner (atlas bg with aspect ratio preservation, no border)
    Panel = function(f, c)
        if HasAtlas(c) and c.atlas.panelBg then
            ApplyAtlasBackground(f, c.atlas.panelBg, "BACKGROUND", true)
        else
            HideAtlasTextures(f)
            if f.GetBackdrop and f:GetBackdrop() then
                f:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
                f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
            end
        end
        if f.borderFrame then
            if c.atlas and c.atlas.windowBorder then
                f.borderFrame.tex:SetAtlas(c.atlas.windowBorder)
                f.borderFrame:Show()
            else
                f.borderFrame:Hide()
            end
        end
    end,

    -- Background skinner (matches main window bg)
    Background = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, c.bg.a)
        end
    end,

    -- Panel header skinner (no atlas, colors only)
    PanelHeader = function(f, c)
        HideAtlasTextures(f)
        if f.GetBackdrop and f:GetBackdrop() then
            local ph = c.panel_header or c.panel  -- Fallback to panel if not defined
            f:SetBackdropColor(ph.r, ph.g, ph.b, ph.a)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
    end,

    -- Filter bar background (light themes use header_bar, dark use subtle bg)
    FilterBarBg = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            if c.isLight then
                local hb = c.header_bar or c.panel
                f:SetBackdropColor(hb.r, hb.g, hb.b, 0.95)
            else
                f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, 0.5)
            end
        end
    end,

    -- Header bar (title bar with search) - uses header_bar color (no atlas)
    HeaderBar = function(f, c)
        HideAtlasTextures(f)
        if f.GetBackdrop and f:GetBackdrop() then
            local hb = c.header_bar or c.panel
            f:SetBackdropColor(hb.r, hb.g, hb.b, hb.a or 1)
        end
    end,

    -- Button skinner
    Button = function(b, c)
        -- Text with font
        local fs = b:GetFontString()
        if fs then
            fs:SetTextColor(c.button_text_norm.r, c.button_text_norm.g, c.button_text_norm.b, c.button_text_norm.a)
            ApplyFont(fs, c, "body")
        end
        b._scheme = c
    end,

    -- Accent Button skinner (buttons with accent/teal styling)
    AccentButton = function(b, c)
        if b.GetBackdrop and b:GetBackdrop() then
            b:SetBackdropColor(c.accent.r, c.accent.g, c.accent.b, 0.9)
            b:SetBackdropBorderColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a)
        end
        -- Text is white on accent background
        if b.text then
            b.text:SetTextColor(1, 1, 1, 1)
            ApplyFont(b.text, c, "small")
        end
        -- Store colors for hover scripts
        b._scheme = c
    end,

    -- Text/Label skinner
    Text = function(fs, c)
        if fs.isHeader then
            fs:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, c.text_header.a)
            ApplyFont(fs, c, "header")
        elseif fs.isSmall then
            fs:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            ApplyFont(fs, c, "small")
        else
            fs:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            ApplyFont(fs, c, "body")
        end
    end,

    -- Progress Bar skinner
    ProgressBar = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
        if f.fill then
            f.fill:SetVertexColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a)
        end
        if f.text then
            f.text:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            ApplyFont(f.text, c, "body")
        end
    end,

    -- ============================================================================
    -- HDG-SPECIFIC SKINNERS
    -- ============================================================================

    -- Recipe Row skinner (no atlas, colors only)
    RecipeRow = function(f, c)
        HideAtlasTextures(f)
        if f.GetBackdrop and f:GetBackdrop() then
            -- Known recipes get accent tint, unknown get panel color
            if f.isKnown then
                f:SetBackdropColor(c.accent.r, c.accent.g, c.accent.b, 0.15)
            else
                f:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
            end
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
        if f.nameText then
            if f.isKnown then
                f.nameText:SetTextColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a)
            else
                f.nameText:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            end
            ApplyFont(f.nameText, c, "body")
        end
        if f.professionText then
            f.professionText:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, c.text_dim.a)
            ApplyFont(f.professionText, c, "small")
        end
        -- Store colors for hover scripts
        f._scheme = c
    end,

    -- Material Row skinner
    MaterialRow = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
        if f.nameText then
            f.nameText:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            ApplyFont(f.nameText, c, "body")
        end
        if f.countText then
            -- Color based on have/need state
            if f.hasEnough then
                f.countText:SetTextColor(c.success.r, c.success.g, c.success.b, c.success.a)
            else
                f.countText:SetTextColor(c.error.r, c.error.g, c.error.b, c.error.a)
            end
            ApplyFont(f.countText, c, "body")
        end
        -- Store colors for hover scripts
        f._scheme = c
    end,

    -- Lumber Row skinner
    LumberRow = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
        if f.nameText then
            f.nameText:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            ApplyFont(f.nameText, c, "body")
        end
        if f.neededText then
            f.neededText:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, c.text_dim.a)
            ApplyFont(f.neededText, c, "small")
        end
        if f.haveText then
            -- Color based on have/need state
            if f.hasEnough then
                f.haveText:SetTextColor(c.success.r, c.success.g, c.success.b, c.success.a)
            else
                f.haveText:SetTextColor(c.warning.r, c.warning.g, c.warning.b, c.warning.a)
            end
            ApplyFont(f.haveText, c, "body")
        end
        -- Store colors for hover scripts
        f._scheme = c
    end,

    -- Shopping List Item skinner
    ShoppingItem = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
        if f.nameText then
            f.nameText:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            ApplyFont(f.nameText, c, "body")
        end
        if f.qtyText then
            f.qtyText:SetTextColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a)
            ApplyFont(f.qtyText, c, "body")
        end
        -- Store colors for hover scripts
        f._scheme = c
    end,

    -- Tab Button skinner (atlas-based, text only)
    TabButton = function(f, c)
        local fs = f:GetFontString()
        if fs then
            if f.isActive then
                fs:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            else
                fs:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, c.text_dim.a)
            end
            ApplyFont(fs, c, "small")
        end
        f._scheme = c
    end,

    -- Profession Header skinner (collapsible headers)
    ProfessionHeader = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, 0.8)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
        if f.nameText then
            f.nameText:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, c.text_header.a)
            ApplyFont(f.nameText, c, "header")
        end
        if f.countText then
            f.countText:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, c.text_dim.a)
            ApplyFont(f.countText, c, "small")
        end
        if f.accentBar then
            f.accentBar:SetVertexColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a)
        end
        -- Store colors for hover scripts
        f._scheme = c
    end,

    -- Scroll Frame skinner
    ScrollFrame = function(f, c)
        local scrollBar = f.ScrollBar
        if scrollBar then
            local thumb = scrollBar:GetThumbTexture()
            if thumb then
                if c.atlas and c.atlas.scrollThumb then
                    thumb:SetAtlas(c.atlas.scrollThumb)
                    thumb:SetVertexColor(1, 1, 1, 1)  -- Reset vertex color for atlas
                else
                    thumb:SetTexture("Interface\\Buttons\\WHITE8x8")
                    thumb:SetVertexColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a)
                end
            end
        end
    end,

    -- Checkbox skinner (atlas-based boxTex + legacy boxBg/boxBorder)
    Checkbox = function(f, c)
        if f.boxTex then -- atlas-based checkbox
            f.boxTex:SetVertexColor(c.border.r, c.border.g, c.border.b, 1)
        end
        if f.boxBg then -- legacy ColorTexture checkbox (GoblinTab)
            f.boxBg:SetColorTexture(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
        end
        if f.boxBorder then
            for _, tex in pairs(f.boxBorder) do
                tex:SetVertexColor(c.border.r, c.border.g, c.border.b, 1)
            end
        end
        if f.check then
            f.check:SetVertexColor(c.success.r, c.success.g, c.success.b, 1)
        end
        if f.text then
            f.text:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            ApplyFont(f.text, c, "small")
        end
    end,

    -- Filter Button skinner (active/inactive toggle)
    FilterButton = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            if f.isActive then
                f:SetBackdropColor(c.accent.r, c.accent.g, c.accent.b, 0.3)
                f:SetBackdropBorderColor(c.accent.r, c.accent.g, c.accent.b, 0.8)
            else
                f:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
                f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
            end
        end
        local fs = f:GetFontString()
        if fs then
            if f.isActive then
                fs:SetTextColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a)
            else
                fs:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            end
            ApplyFont(fs, c, "small")
        end
        -- Store colors for hover scripts
        f._scheme = c
    end,

    -- Chip Button skinner (atlas-based pill, text color only)
    ChipButton = function(f, c)
        if f.text then
            ApplyFont(f.text, c, "small")
            if f.isActive then
                f.text:SetTextColor(1, 1, 1, 1)
            else
                f.text:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
            end
        end
        f._scheme = c
    end,

    -- Dropdown skinner (button container with popup)
    Dropdown = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, c.panel.a)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, c.border.a)
        end
        local fs = f.GetFontString and f:GetFontString()
        if fs then
            fs:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            ApplyFont(fs, c, "small")
        end
        f._scheme = c
    end,

    -- Search Box skinner (EditBox with backdrop)
    SearchBox = function(f, c)
        if f.GetBackdrop and f:GetBackdrop() then
            f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, 1)
            f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 0.8)
        end
        -- Style the text color
        if f.SetTextColor then
            f:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
        end
        -- Style instructions (placeholder text) for Blizzard SearchBoxTemplate
        if f.Instructions then
            f.Instructions:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, c.text_dim.a)
        end
        -- Store colors for focus scripts
        f._scheme = c
    end,

    -- Main Tab skinner (icon+text buttons with accent underline)
    MainTab = function(f, c)
        HideAtlasTextures(f)
        if f.GetBackdrop and f:GetBackdrop() then
            if f.isActive then
                f:SetBackdropColor(c.accent.r, c.accent.g, c.accent.b, 0.15)
                f:SetBackdropBorderColor(c.accent.r, c.accent.g, c.accent.b, 0.8)
            else
                f:SetBackdropColor(c.bg.r, c.bg.g, c.bg.b, 0.3)
                f:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 0.6)
            end
        end
        if f.text then
            if f.isActive then
                f.text:SetTextColor(c.text.r, c.text.g, c.text.b, c.text.a)
            else
                f.text:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, c.text_dim.a)
            end
            ApplyFont(f.text, c, "small")
        end
        if f.activeBar then
            f.activeBar:SetColorTexture(c.accent.r, c.accent.g, c.accent.b, 1)
        end
        f._scheme = c
    end,

    -- Separator skinner (horizontal/vertical lines)
    Separator = function(f, c)
        if f.SetColorTexture then
            f:SetColorTexture(c.border.r, c.border.g, c.border.b, 0.6)
        end
    end,

    -- Header Title skinner (panel/tab header bar titles with wood sign background)
    HeaderTitle = function(f, c)
        f:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, c.text_header.a or 1)
        ApplyFont(f, c, "body")
        -- Wood sign background fills full header width
        if f.woodSignBg then
            if c.atlas and c.atlas.titleWoodSign then
                f.woodSignBg:SetAtlas(c.atlas.titleWoodSign)
                f.woodSignBg:Show()
            else
                f.woodSignBg:Hide()
            end
        end
    end,

    -- Section Title skinner (accent-colored section headers in scroll content)
    SectionTitle = function(f, c)
        f:SetTextColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a)
        ApplyFont(f, c, "header")
    end,

    -- Panel Title skinner (legacy - use HeaderTitle for new code)
    PanelTitle = function(f, c)
        f:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, c.text_header.a)
        ApplyFont(f, c, "header")
        if f.woodSignBg then
            if c.atlas and c.atlas.titleWoodSign then
                f.woodSignBg:SetAtlas(c.atlas.titleWoodSign)
                f.woodSignBg:Show()
            else
                f.woodSignBg:Hide()
            end
        end
    end,

    -- Section Header skinner (accent-colored section titles)
    SectionHeader = function(f, c)
        f:SetTextColor(c.accent.r, c.accent.g, c.accent.b, c.accent.a or 1)
        ApplyFont(f, c, "header")
    end,
}

-- ============================================================================
-- EXPOSE TO PRIVATEDB FOR OTHER MODULES
-- ============================================================================

privateDB.HDG_Theme = HDG.Theme
