-- MSUF_Options_Toolkit.lua  (Phase 1: Complete Rewrite)
-- Widget SDK for all MSUF Options files.
-- Loads BEFORE MSUF_Options_Core.lua in the TOC.
-- Provides:
--   ns.TR(v)               Localization (single definition, all files use this)
--   ns.EnsureDB()          DB guarantee (single definition)
--   ns.UI.Check(spec)      Self-syncing checkbox
--   ns.UI.Slider(spec)     Self-syncing slider with editbox + ±
--   ns.UI.Dropdown(spec)   Spec-driven dropdown (own ListFrame, scrollable)
--   ns.UI.Label(spec)      FontString helper
--   ns.UI.Section(spec)    Section header with divider line
--   ns.UI.Panel(spec)      Dark panel box with title
--   ns.UI.Button(spec)     UIPanelButton helper
--   ns.UI.ButtonRow(defs)  Horizontal row of buttons
--   ns.UI.BuildOptionRows(parent, specs, opts) Batch builder for existing widget specs
--   ns.UI.StatusBarTextureItems(followText)  SharedMedia texture item list
--   ns.UI.QueueScrollUpdate(host, keys)      Deferred scroll-height calc
--   ns.UI.AttachTooltip(w, title, body)      GameTooltip helper
local addonName, addonNS = ...
local ns = (_G.MSUF_NS) or addonNS or {}
_G.MSUF_NS = ns

-- 1. Shared Boilerplate (all files use ns.TR / ns.EnsureDB)
ns.L = ns.L or (_G.MSUF_L) or {}
if not getmetatable(ns.L) then
    setmetatable(ns.L, { __index = function(_, k) return k end })
end

local L = ns.L
local isEn = (ns.LOCALE) == "enUS"

function ns.TR(v)
    if type(v) ~= "string" then return v end
    if isEn then return v end
    return L[v] or v
end

function ns.EnsureDB()
    if not MSUF_DB then MSUF_DB = {} end
    if type(MSUF_DB.general) ~= "table" then MSUF_DB.general = {} end
    if type(MSUF_DB.bars) ~= "table" then MSUF_DB.bars = {} end
end

-- Local aliases
local TR = ns.TR
local floor = math.floor
local format = string.format
local TEX_W8 = "Interface\\Buttons\\WHITE8x8"
local ADDON = (type(addonName) == "string" and addonName ~= "" and addonName) or "MidnightSimpleUnitFrames"

-- 2. Tooltip Helper
local function AttachTooltip(widget, titleText, bodyText)
    if not widget or (not titleText and not bodyText) then return end
    widget:HookScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        if titleText then GameTooltip:SetText(TR(titleText), 1, 1, 1) end
        if bodyText then GameTooltip:AddLine(TR(bodyText), 0.9, 0.9, 0.9, true) end
        GameTooltip:Show()
    end)
    widget:HookScript("OnLeave", function() GameTooltip:Hide() end)
end

-- 3. Style Functions
local SLIDER_TRACK_H    = 3
local SLIDER_THUMB_SIZE = 16
local SLIDER_BG_R, SLIDER_BG_G, SLIDER_BG_B = 0.10, 0.10, 0.14
local SLIDER_FILL_R, SLIDER_FILL_G, SLIDER_FILL_B = 0.27, 0.53, 0.80
local SLIDER_THUMB_R, SLIDER_THUMB_G, SLIDER_THUMB_B = 0.35, 0.62, 0.92
local SLIDER_THUMB_TEX = "Interface/AddOns/" .. ADDON .. "/Media/msuf_slider_thumb.tga"

local function HideSliderTemplateParts(slider, thumb)
    if not slider then return end

    -- Hide all template background/border textures EXCEPT thumb.
    -- Some client builds expose region type through GetObjectType rather than
    -- IsObjectType, so handle both paths.
    local _regions = { slider:GetRegions() }
    for i = 1, #_regions do
        local region = _regions[i]
        local isTexture = false
        if region and region.IsObjectType then
            isTexture = region:IsObjectType("Texture") and true or false
        end
        if (not isTexture) and region and region.GetObjectType then
            isTexture = (region:GetObjectType() == "Texture")
        end
        if isTexture and region ~= thumb and region ~= slider._msufTrack and region ~= slider._msufFill then
            region:SetAlpha(0)
            region:Hide()
        end
    end

    -- Hide Low/High labels
    local sName = slider.GetName and slider:GetName()
    if sName then
        local low  = _G[sName .. "Low"]
        local high = _G[sName .. "High"]
        if low  then low:SetAlpha(0);  low:Hide()  end
        if high then high:SetAlpha(0); high:Hide() end
    end
    if slider.Low  then slider.Low:SetAlpha(0);  slider.Low:Hide()  end
    if slider.High then slider.High:SetAlpha(0); slider.High:Hide() end
end

local function StyleSlider(slider)
    if not slider then return end

    -- Get thumb FIRST, skip it in the hide loop.
    local thumb = slider:GetThumbTexture()
    HideSliderTemplateParts(slider, thumb)

    if slider._msufStyled then return end
    slider._msufStyled = true
    slider:SetHeight(SLIDER_THUMB_SIZE)

    -- Custom track
    local track = slider:CreateTexture(nil, "BACKGROUND", nil, -1)
    track:SetHeight(SLIDER_TRACK_H)
    track:SetPoint("LEFT", slider, "LEFT", 0, 0)
    track:SetPoint("RIGHT", slider, "RIGHT", 0, 0)
    track:SetColorTexture(SLIDER_BG_R, SLIDER_BG_G, SLIDER_BG_B, 1)
    slider._msufTrack = track

    -- Fill bar
    local fill = slider:CreateTexture(nil, "BACKGROUND", nil, 0)
    fill:SetHeight(SLIDER_TRACK_H)
    fill:SetPoint("LEFT", track, "LEFT", 0, 0)
    fill:SetColorTexture(SLIDER_FILL_R, SLIDER_FILL_G, SLIDER_FILL_B, 1)
    slider._msufFill = fill

    -- Thumb: replace entirely via SetThumbTexture (bulletproof — WoW cannot reset)
    if slider.SetThumbTexture then
        slider:SetThumbTexture(SLIDER_THUMB_TEX)
        local thumb2 = slider:GetThumbTexture()
        if thumb2 then
            thumb2:SetVertexColor(SLIDER_THUMB_R, SLIDER_THUMB_G, SLIDER_THUMB_B, 1)
            thumb2:SetSize(SLIDER_THUMB_SIZE, SLIDER_THUMB_SIZE)
        end
    elseif thumb then
        thumb:SetTexture(SLIDER_THUMB_TEX)
        thumb:SetVertexColor(SLIDER_THUMB_R, SLIDER_THUMB_G, SLIDER_THUMB_B, 1)
        thumb:SetSize(SLIDER_THUMB_SIZE, SLIDER_THUMB_SIZE)
    end

    -- Fill width sync
    local function UpdateFill(self)
        local lo, hi = self:GetMinMaxValues()
        local range = hi - lo
        if range <= 0 then
            fill:SetWidth(1)
            return
        end
        local pct = (self:GetValue() - lo) / range
        local w = self:GetWidth() * pct
        if w < 1 then w = 1 end
        fill:SetWidth(w)
    end
    slider:HookScript("OnValueChanged", function(self) UpdateFill(self) end)
    slider:HookScript("OnShow",         function(self)
        HideSliderTemplateParts(self, self:GetThumbTexture())
        UpdateFill(self)
    end)
    slider:HookScript("OnSizeChanged",  function(self) UpdateFill(self) end)

    -- Hover
    slider:HookScript("OnEnter", function(self)
        if self._msufTrack then self._msufTrack:SetColorTexture(0.16, 0.16, 0.20, 1) end
    end)
    slider:HookScript("OnLeave", function(self)
        if self._msufTrack then self._msufTrack:SetColorTexture(SLIDER_BG_R, SLIDER_BG_G, SLIDER_BG_B, 1) end
    end)

    if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
            if slider:IsShown() then UpdateFill(slider) end
        end)
    else
        UpdateFill(slider)
    end
end

local function StyleSmallButton(button, isPlus)
    if not button or button._msufStyled then return end
    button._msufStyled = true
    button:SetSize(18, 18)
    local fs = button:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fs:SetText(isPlus and "+" or "\226\128\147")
    fs:SetAllPoints()
    if button.SetFontString then button:SetFontString(fs) end
    button.text = fs
end

local function StyleToggleText(cb)
    if not cb or cb._msufToggleStyled then return end
    cb._msufToggleStyled = true
    local fs = cb.text or cb.Text
    if not fs and cb.GetName and cb:GetName() then fs = _G[cb:GetName() .. "Text"] end
    if not (fs and fs.SetTextColor) then return end
    cb._msufToggleFS = fs
    local function Update()
        if cb.IsEnabled and not cb:IsEnabled() then
            fs:SetTextColor(0.35, 0.35, 0.35)
        elseif cb.GetChecked and cb:GetChecked() then
            fs:SetTextColor(1, 1, 1)
        else
            fs:SetTextColor(0.55, 0.55, 0.55)
        end
    end
    cb._msufToggleUpdate = Update
    cb:HookScript("OnShow", Update)
    cb:HookScript("OnClick", Update)
    pcall(hooksecurefunc, cb, "SetChecked", function() Update() end)
    pcall(hooksecurefunc, cb, "SetEnabled", function() Update() end)
    Update()
end

local CHECK_TEX_BOLD = "Interface/AddOns/" .. ADDON .. "/Media/msuf_check_tick_bold.tga"
local CHECK_TEX_THIN = "Interface/AddOns/" .. ADDON .. "/Media/msuf_check_tick_thin.tga"

local function StyleCheckmark(cb)
    if not cb or cb._msufCheckStyled then return end
    cb._msufCheckStyled = true
    local check = cb.GetCheckedTexture and cb:GetCheckedTexture()
    if not check and cb.GetName and cb:GetName() then check = _G[cb:GetName() .. "Check"] end
    if not (check and check.SetTexture) then return end
    local h = (cb.GetHeight and cb:GetHeight()) or 24
    check:SetTexture(h >= 24 and CHECK_TEX_BOLD or CHECK_TEX_THIN)
    check:SetTexCoord(0, 1, 0, 1)
    if check.SetBlendMode then check:SetBlendMode("BLEND") end
    if check.ClearAllPoints then
        check:ClearAllPoints()
        check:SetPoint("CENTER", cb, "CENTER", 0, 0)
        local sz = floor(h * 0.8 + 0.5)
        if sz < 12 then sz = 12 end
        check:SetSize(sz, sz)
    end
end

-- 4. Widget SDK (ns.UI)
ns.UI = ns.UI or {}
local UI = ns.UI

-- ---- Collapsible header visuals ----
UI.COLLAPSE_ARROW_CLOSED = UI.COLLAPSE_ARROW_CLOSED or { 1.00, 0.82, 0.00 }
UI.COLLAPSE_ARROW_OPEN   = UI.COLLAPSE_ARROW_OPEN   or { 1.00, 0.55, 0.12 }

function UI.ApplyCollapsibleHeaderState(chevron, hint, open)
    if chevron then
        chevron:SetRotation(open and (math.pi * 0.5) or 0)
        local col = open and UI.COLLAPSE_ARROW_OPEN or UI.COLLAPSE_ARROW_CLOSED
        chevron:SetVertexColor(col[1], col[2], col[3])
    end
    if hint and hint.SetText then
        local tr = (type(ns) == "table" and type(ns.TR) == "function") and ns.TR or function(s) return s end
        hint:SetText(open and "" or tr("click to expand"))
    end
end

_G.MSUF_ApplyCollapseVisual = UI.ApplyCollapsibleHeaderState

-- ---- 4a. Label ----
function UI.Label(spec)
    local parent = spec.parent
    local fs = parent:CreateFontString(nil, "OVERLAY", spec.font or "GameFontNormal")
    fs:SetText(TR(spec.text or ""))
    if spec.color then
        fs:SetTextColor(spec.color[1], spec.color[2], spec.color[3], spec.color[4] or 1)
    else
        fs:SetTextColor(1, 0.82, 0)
    end
    if spec.anchor then
        fs:SetPoint("TOPLEFT", spec.anchor, spec.anchorPoint or "BOTTOMLEFT", spec.x or 0, spec.y or -8)
    end
    if spec.width then fs:SetWidth(spec.width) end
    if spec.justify then fs:SetJustifyH(spec.justify) end
    return fs
end

-- ---- 4b. Section ----
function UI.Section(spec)
    local parent = spec.parent
    local header = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    header:SetText(TR(spec.title or ""))
    header:SetTextColor(1, 0.82, 0)
    if spec.anchor then
        header:SetPoint("TOPLEFT", spec.anchor, spec.anchorPoint or "BOTTOMLEFT", spec.x or 0, spec.y or -16)
    else
        header:SetPoint("TOPLEFT", parent, "TOPLEFT", spec.x or 16, spec.y or -16)
    end
    local line = parent:CreateTexture(nil, "ARTWORK")
    line:SetColorTexture(1, 1, 1, 0.08)
    line:SetHeight(1)
    line:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -4)
    line:SetWidth(spec.width or 300)
    header._msufLine = line
    return header
end

-- ---- 4c. Panel (dark box with title) ----
function UI.Panel(spec)
    local parent = spec.parent
    local p = CreateFrame("Frame", spec.name, parent, "BackdropTemplate")
    p:SetSize(spec.width or 330, spec.height or 330)
    if spec.anchor then
        p:SetPoint("TOPLEFT", spec.anchor, spec.anchorPoint or "TOPLEFT", spec.x or 0, spec.y or 0)
    else
        p:SetPoint("TOPLEFT", parent, "TOPLEFT", spec.x or 0, spec.y or 0)
    end
    p:SetBackdrop({
        bgFile = TEX_W8, edgeFile = TEX_W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    p:SetBackdropColor(0, 0, 0, spec.bgAlpha or 0)
    p:SetBackdropBorderColor(0, 0, 0, 0)
    local header = p:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    header:SetText(TR(spec.title or ""))
    header:SetTextColor(1, 0.82, 0)
    header:SetPoint("TOPLEFT", p, "TOPLEFT", 14, -14)
    local line = p:CreateTexture(nil, "ARTWORK")
    line:SetColorTexture(1, 1, 1, 0.08)
    line:SetHeight(1)
    line:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -10)
    line:SetPoint("TOPRIGHT", p, "TOPRIGHT", -14, -38)
    p._msufHeader = header
    p._msufLine = line
    return p
end

-- ---- 4d. Button ----
function UI.Button(spec)
    local b = CreateFrame("Button", spec.name, spec.parent, "UIPanelButtonTemplate")
    b:SetSize(spec.width or 140, spec.height or 24)
    b:SetText(TR(spec.text or ""))
    if spec.anchor then
        b:SetPoint("TOPLEFT", spec.anchor, spec.anchorPoint or "BOTTOMLEFT", spec.x or 0, spec.y or -8)
    end
    if spec.onClick then b:SetScript("OnClick", spec.onClick) end
    if spec.tooltip then AttachTooltip(b, spec.tooltip) end
    return b
end

-- ---- 4e. Check (self-syncing) ----
function UI.Check(spec)
    local cb = CreateFrame("CheckButton", spec.name, spec.parent, spec.template or "UICheckButtonTemplate")
    cb:RegisterForClicks("AnyUp")
    if cb.SetHitRectInsets then cb:SetHitRectInsets(0, -((spec.maxTextWidth or 220) + 8), 0, 0) end
    if spec.anchor then
        cb:SetPoint("TOPLEFT", spec.anchor, spec.anchorPoint or "BOTTOMLEFT", spec.x or 0, spec.y or -4)
    end
    -- Label
    local fs = cb.text or cb.Text
    if not fs and spec.name then fs = _G[spec.name .. "Text"] end
    if fs and fs.SetText then
        fs:SetText(TR(spec.label or ""))
        if fs.EnableMouse then fs:EnableMouse(false) end
    end
    if spec.maxTextWidth and _G.MSUF_ClampCheckboxText then
        _G.MSUF_ClampCheckboxText(cb, spec.maxTextWidth)
    end
    -- Style
    StyleToggleText(cb)
    StyleCheckmark(cb)

    local function SyncFromGetter(self)
        if spec.get then
            self:SetChecked(spec.get() and true or false)
        end
        if self._msufToggleUpdate then self._msufToggleUpdate() end
    end

    local function ApplyToDB(self, explicitValue)
        if not spec.set then return end
        local v
        if explicitValue ~= nil then
            v = explicitValue and true or false
        else
            v = self:GetChecked() and true or false
        end
        spec.set(v)
        if self._msufToggleUpdate then self._msufToggleUpdate() end
    end

    cb:SetScript("OnShow", function(self)
        SyncFromGetter(self)
    end)

    function cb:Refresh()
        SyncFromGetter(self)
    end

    cb:SetScript("OnClick", function(self)
        local checked = self:GetChecked() and true or false
        if C_Timer and C_Timer.After then
            C_Timer.After(0, function()
                if self and self.GetChecked then
                    ApplyToDB(self, checked)
                    if spec.get then SyncFromGetter(self) end
                end
            end)
        else
            ApplyToDB(self, checked)
            if spec.get then SyncFromGetter(self) end
        end
    end)

    if spec.tooltip then AttachTooltip(cb, spec.tooltip) end
    -- Search registration
    if spec.name and spec.label and _G.MSUF_Search_RegisterSlider then
        -- Search indexes by name + label text, works for any widget
    end
    return cb
end

-- ---- 4f. Slider (self-syncing; compact sliders can opt into inline numeric input) ----
function UI.Slider(spec)
    local name = spec.name
    local parent = spec.parent
    local minV, maxV, step = spec.min or 0, spec.max or 100, spec.step or 1
    local compact = spec.compact
    local compactInput = compact and spec.compactInput
    local compactInputWidth = spec.compactInputWidth or 46
    local compactInputGap = spec.compactInputGap or 8
    local compactButtonWidth = spec.compactInputButtonWidth or 18
    local compactButtonGap = spec.compactInputButtonGap or 2
    local sliderWidth = spec.width or 270
    local trackWidth = sliderWidth
    if compactInput then
        trackWidth = sliderWidth - compactInputWidth - compactInputGap - (compactButtonWidth * 2) - (compactButtonGap * 2)
        if trackWidth < 80 then trackWidth = 80 end
    end
    local sl = CreateFrame("Slider", name, parent, "OptionsSliderTemplate")
    sl:SetMinMaxValues(minV, maxV)
    sl:SetValueStep(step)
    sl:SetObeyStepOnDrag(true)
    sl:SetWidth(trackWidth)
    if spec.anchor then
        sl:SetPoint("TOPLEFT", spec.anchor, spec.anchorPoint or "BOTTOMLEFT", spec.x or 0, spec.y or -18)
    end
    sl._msufStep = step
    -- Template text regions
    local low  = name and _G[name .. "Low"]
    local high = name and _G[name .. "High"]
    local text = name and _G[name .. "Text"]
    if low  then low:SetText(spec.lowText or tostring(minV)) end
    if high then high:SetText(spec.highText or tostring(maxV)) end
    -- EditBox + ± (only for non-compact sliders)
    local eb, minus, plus
    if (not compact) or compactInput then
        eb = CreateFrame("EditBox", name and (name .. "Input"), parent, "InputBoxTemplate")
        eb:SetSize(compactInput and compactInputWidth or 60, 18)
        eb:SetAutoFocus(false); eb:SetJustifyH("CENTER")
        if compactInput then
            minus = CreateFrame("Button", name and (name .. "Minus"), parent)
            minus:SetPoint("LEFT", sl, "RIGHT", compactInputGap, 0)
            StyleSmallButton(minus, false)
            minus:SetSize(compactButtonWidth, 18)
            sl.minusButton = minus

            eb:SetPoint("LEFT", minus, "RIGHT", compactButtonGap, 0)

            plus = CreateFrame("Button", name and (name .. "Plus"), parent)
            plus:SetPoint("LEFT", eb, "RIGHT", compactButtonGap, 0)
            StyleSmallButton(plus, true)
            plus:SetSize(compactButtonWidth, 18)
            sl.plusButton = plus
        else
            eb:SetPoint("TOP", sl, "BOTTOM", 0, -6)
        end
        eb:SetFontObject(GameFontHighlightSmall); eb:SetTextColor(1, 1, 1, 1)
        sl.editBox = eb
        if not compactInput then
            minus = CreateFrame("Button", name and (name .. "Minus"), parent)
            minus:SetPoint("RIGHT", eb, "LEFT", -2, 0)
            StyleSmallButton(minus, false)
            sl.minusButton = minus
            plus = CreateFrame("Button", name and (name .. "Plus"), parent)
            plus:SetPoint("LEFT", eb, "RIGHT", 2, 0)
            StyleSmallButton(plus, true)
            sl.plusButton = plus
        end
    end
    -- Format helper
    local function FormatValue(v)
        if step >= 1 then
            return tostring(floor(v + 0.5))
        else
            return format("%.2f", v)
        end
    end
    -- Sync editbox from slider value
    local function SyncEditBox(v)
        if eb and not eb:HasFocus() then
            eb:SetText(FormatValue(v))
        end
    end
    local function SetCompanionsShown(show)
        if eb then eb:SetShown(show) end
        if minus then minus:SetShown(show) end
        if plus then plus:SetShown(show) end
    end
    -- Label update
    local function UpdateLabel(v)
        if text and text.SetText then
            if spec.formatText then
                text:SetText(spec.formatText(v))
            else
                text:SetText(TR(spec.label or ""))
            end
        end
    end
    local function SyncFromGetter(self)
        local v = spec.get and spec.get() or self:GetValue()
        if type(v) ~= "number" then v = spec.default or minV end
        if v < minV then v = minV elseif v > maxV then v = maxV end
        self._msufSkip = true
        self:SetValue(v)
        self._msufSkip = false
        SyncEditBox(v)
        UpdateLabel(v)
        SetCompanionsShown(self:IsShown())
    end
    if (not compact) or compactInput then
        -- Apply editbox value
        local function ApplyEditBox()
            local v = tonumber(eb:GetText())
            if not v then SyncEditBox(sl:GetValue()); return end
            if v < minV then v = minV elseif v > maxV then v = maxV end
            sl:SetValue(v)
        end
        eb:SetScript("OnEnterPressed", function(self) ApplyEditBox(); self:ClearFocus() end)
        eb:SetScript("OnEditFocusLost", ApplyEditBox)
        eb:SetScript("OnEscapePressed", function(self)
            SyncEditBox(sl:GetValue()); self:ClearFocus()
        end)
        if minus and plus then
            minus:SetScript("OnClick", function()
                local v = sl:GetValue() - step
                if v < minV then v = minV end
                sl:SetValue(v)
            end)
            plus:SetScript("OnClick", function()
                local v = sl:GetValue() + step
                if v > maxV then v = maxV end
                sl:SetValue(v)
            end)
        end
    end
    -- OnValueChanged
    sl._msufSkip = false
    sl:SetScript("OnValueChanged", function(self, value)
        if self._msufSkip then return end
        if step >= 1 then value = floor(value + 0.5) end
        SyncEditBox(value)
        UpdateLabel(value)
        if spec.set then spec.set(value) end
    end)
    -- Companion-widget visibility: the editbox / ± buttons are parented to
    -- `parent` (sibling of `sl`) for layout reasons, so sl:Hide()/Show() does
    -- NOT cascade to them. Hook the slider's OnShow/OnHide so callers can
    -- treat the slider as the single visibility owner without leftover
    -- "ghost" input boxes leaking through. Diff-gated to avoid redundant work.
    if eb or minus or plus then
        sl:HookScript("OnShow", function()
            SetCompanionsShown(true)
        end)
        sl:HookScript("OnHide", function()
            SetCompanionsShown(false)
        end)
        -- Mirror initial state in case the slider is created already hidden
        -- (parent hidden at construction time → no OnHide will fire).
        if not sl:IsShown() then
            SetCompanionsShown(false)
        end
    end
    -- Self-sync on Show
    sl:SetScript("OnShow", function(self)
        SyncFromGetter(self)
    end)
    function sl:Refresh()
        SyncFromGetter(self)
    end
    -- Programmatic set (no callback)
    function sl:SetValueClean(v)
        self._msufSkip = true
        self:SetValue(v)
        self._msufSkip = false
        SyncEditBox(v)
        UpdateLabel(v)
    end
    function sl:SetEnabled(enabled)
        if enabled then
            if self.Enable then self:Enable() end
            if eb then
                if eb.EnableMouse then eb:EnableMouse(true) end
                if eb.Enable then eb:Enable() end
            end
            if minus and minus.Enable then minus:Enable() end
            if plus and plus.Enable then plus:Enable() end
            if text and text.SetTextColor then text:SetTextColor(1, 1, 1) end
            if eb and eb.SetTextColor then eb:SetTextColor(1, 1, 1, 1) end
            self:SetAlpha(1)
        else
            if self.Disable then self:Disable() end
            if eb then
                if eb.EnableMouse then eb:EnableMouse(false) end
                if eb.Disable then eb:Disable() end
            end
            if minus and minus.Disable then minus:Disable() end
            if plus and plus.Disable then plus:Disable() end
            if text and text.SetTextColor then text:SetTextColor(0.55, 0.55, 0.55) end
            if eb and eb.SetTextColor then eb:SetTextColor(0.55, 0.55, 0.55, 1) end
            self:SetAlpha(0.55)
        end
    end
    StyleSlider(sl)
    -- Search registration
    if name and spec.label and type(_G.MSUF_Search_RegisterSlider) == "function" then
        _G.MSUF_Search_RegisterSlider(name, spec.label)
    end
    sl:Refresh()
    return sl
end

-- 5. Dropdown System (own ListFrame, no DropDownList1)

-- Theme
local DD_BG     = { 0.08, 0.08, 0.10, 1.00 }
local DD_BORDER  = { 0.30, 0.30, 0.35, 0.80 }
local DD_HOVER   = { 0.20, 0.20, 0.25, 1.00 }
local DD_CHECK   = { 1.00, 0.82, 0.00 }
local DD_TEXT    = { 0.90, 0.90, 0.90 }
local DD_ITEM_H  = 22

-- Singleton list frame
local _listFrame, _listOwner, _listBackdrop
local _listOpenToken = 0
local _listBackdropIgnoreUntil = 0
local _itemPool = {}
local _itemCount = 0

local function DD_Close()
    _listOpenToken = _listOpenToken + 1
    if _listFrame then _listFrame:Hide() end
    if _listBackdrop then _listBackdrop:Hide() end
    _listOwner = nil
end

local function DD_EnsureList()
    if _listFrame then return end
    -- Fullscreen click-outside catcher
    _listBackdrop = CreateFrame("Button", nil, UIParent)
    _listBackdrop:SetFrameStrata("FULLSCREEN_DIALOG")
    _listBackdrop:SetFrameLevel(900)
    _listBackdrop:SetAllPoints(UIParent)
    _listBackdrop:EnableMouse(true)
    _listBackdrop:SetScript("OnClick", function()
        if GetTime and GetTime() < (_listBackdropIgnoreUntil or 0) then return end
        DD_Close()
    end)
    _listBackdrop:Hide()
    -- List container
    local lf = CreateFrame("Frame", "MSUF_SpecDDList", UIParent, "BackdropTemplate")
    lf:SetFrameStrata("TOOLTIP")
    lf:SetFrameLevel(1000)
    if lf.SetToplevel then lf:SetToplevel(true) end
    lf:SetClampedToScreen(true)
    lf:SetBackdrop({
        bgFile = TEX_W8, edgeFile = TEX_W8, edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    lf:SetBackdropColor(DD_BG[1], DD_BG[2], DD_BG[3], DD_BG[4])
    lf:SetBackdropBorderColor(DD_BORDER[1], DD_BORDER[2], DD_BORDER[3], DD_BORDER[4])
    lf:Hide()
    -- ESC close
    lf:EnableKeyboard(true)
    lf:SetPropagateKeyboardInput(true)
    lf:SetScript("OnKeyDown", function(self, key)
        if key == "ESCAPE" then
            self:SetPropagateKeyboardInput(false)
            DD_Close()
        else
            self:SetPropagateKeyboardInput(true)
        end
    end)
    -- ScrollFrame
    local sf = CreateFrame("ScrollFrame", nil, lf, "UIPanelScrollFrameTemplate")
    sf:SetFrameStrata("TOOLTIP")
    sf:SetFrameLevel(1001)
    sf:SetPoint("TOPLEFT", lf, "TOPLEFT", 2, -2)
    sf:SetPoint("BOTTOMRIGHT", lf, "BOTTOMRIGHT", -18, 2)
    local child = CreateFrame("Frame", nil, sf)
    child:SetFrameStrata("TOOLTIP")
    child:SetFrameLevel(1002)
    child:SetSize(1, 1)
    sf:SetScrollChild(child)
    lf._sf = sf
    lf._child = child
    lf._sb = sf.ScrollBar
    -- Mousewheel
    lf:EnableMouseWheel(true)
    lf:SetScript("OnMouseWheel", function(self, delta)
        local cur = sf:GetVerticalScroll()
        local step = (self._itemH or DD_ITEM_H) * 3
        local new = cur - (delta * step)
        if new < 0 then new = 0 end
        local mx = (self._totalH or 0) - (self._visH or 0)
        if mx < 0 then mx = 0 end
        if new > mx then new = mx end
        sf:SetVerticalScroll(new)
    end)
    _listFrame = lf
end

-- Shared item click handler (reads refs from button fields)
local function DD_ItemClick(self)
    local item  = self._ddItem
    local owner = self._ddOwner
    if not (item and owner) then return end
    local key = item.key
    if key == nil then key = item.value end
    local label = TR(item.label or item.text or tostring(key or ""))
    local spec = owner._ddSpec
    if spec and spec.set then spec.set(key, item) end
    if owner.SetValue then
        owner:SetValue(key)
    else
        -- Auto-intercepted dropdown: update text via UIDropDownMenu API
        owner._ddKey = key
        if UIDropDownMenu_SetSelectedValue then pcall(UIDropDownMenu_SetSelectedValue, owner, key) end
        if UIDropDownMenu_SetText then pcall(UIDropDownMenu_SetText, owner, label) end
    end
    DD_Close()
end

local function DD_GetItem(index)
    if _itemPool[index] then return _itemPool[index] end
    DD_EnsureList()
    local btn = CreateFrame("Button", nil, _listFrame._child)
    btn:SetHeight(DD_ITEM_H)
    btn:SetFrameStrata("TOOLTIP")
    btn:SetFrameLevel(1003)
    btn:EnableMouse(true)
    -- Highlight (ADD blend so text stays bright, not darkened)
    local hl = btn:CreateTexture(nil, "HIGHLIGHT")
    hl:SetAllPoints()
    hl:SetColorTexture(DD_HOVER[1], DD_HOVER[2], DD_HOVER[3], 0.35)
    hl:SetBlendMode("ADD")
    -- Selected indicator: subtle gold left bar (hidden by default)
    local sel = btn:CreateTexture(nil, "OVERLAY")
    sel:SetColorTexture(DD_CHECK[1], DD_CHECK[2], DD_CHECK[3], 0.9)
    sel:SetSize(2, DD_ITEM_H - 4)
    sel:SetPoint("LEFT", btn, "LEFT", 2, 0)
    sel:Hide()
    btn._sel = sel
    -- Icon (optional, for texture swatches)
    local icon = btn:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("LEFT", btn, "LEFT", 10, 0)
    icon:SetSize(80, 12)
    icon:Hide()
    btn._icon = icon
    -- Label
    local label = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    label:SetPoint("LEFT", btn, "LEFT", 10, 0)
    label:SetPoint("RIGHT", btn, "RIGHT", -6, 0)
    label:SetJustifyH("LEFT")
    label:SetTextColor(DD_TEXT[1], DD_TEXT[2], DD_TEXT[3])
    btn._label = label
    -- Click
    btn:RegisterForClicks("AnyDown")
    btn:SetScript("OnClick", DD_ItemClick)
    -- Mousewheel passthrough
    btn:EnableMouseWheel(true)
    btn:SetScript("OnMouseWheel", function(_, delta)
        if _listFrame then
            local handler = _listFrame:GetScript("OnMouseWheel")
            if handler then handler(_listFrame, delta) end
        end
    end)
    _itemPool[index] = btn
    _itemCount = index
    return btn
end

local function DD_Populate(owner)
    DD_EnsureList()
    local lf = _listFrame
    local spec = owner._ddSpec
    if not spec then return end
    local items = spec.items
    if type(items) == "function" then items = items() end
    if not items then return end
    local curKey = spec.get and spec.get() or nil
    local itemH  = spec.itemHeight or DD_ITEM_H
    local maxVis = spec.maxVisible or 12
    -- List width matches the peel button (visual trigger), not UIDropDownMenu chrome
    local peelW = owner._msufPeelButton and owner._msufPeelButton.GetWidth and owner._msufPeelButton:GetWidth()
    local w = (peelW and peelW > 40 and peelW) or spec.width or 200
    local iconW  = spec.iconWidth or 80
    local iconH  = spec.iconHeight or 12
    local count  = #items
    -- Size
    local visCount = count > maxVis and maxVis or count
    local visH = visCount * itemH + 4
    local totalH = count * itemH
    local needScroll = count > maxVis
    lf:SetSize(w + (needScroll and 22 or 4), visH)
    lf._itemH = itemH; lf._totalH = totalH; lf._visH = visH
    -- Scroll child
    local child = lf._child
    child:SetWidth(w)
    child:SetHeight(totalH)
    -- Scrollbar visibility + scrollframe extent
    local sf = lf._sf
    sf:ClearAllPoints()
    sf:SetPoint("TOPLEFT", lf, "TOPLEFT", 2, -2)
    sf:SetPoint("BOTTOMRIGHT", lf, "BOTTOMRIGHT", needScroll and -18 or -2, 2)
    if lf._sb then lf._sb:SetShown(needScroll) end
    -- Populate
    for i = 1, count do
        local item = items[i]
        local itemKey = item.key
        if itemKey == nil then itemKey = item.value end
        local itemLabel = TR(item.label or item.text or tostring(itemKey or ""))
        local btn = DD_GetItem(i)
        btn:SetParent(child)
        btn:SetFrameStrata(_listFrame:GetFrameStrata())
        btn:SetFrameLevel(_listFrame:GetFrameLevel() + 3)
        btn:SetHeight(itemH)
        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", child, "TOPLEFT", 0, -(i - 1) * itemH)
        btn:SetPoint("RIGHT", child, "RIGHT", 0, 0)
        btn._sel:SetShown(itemKey == curKey or item.overrideActive == true)
        -- Icon
        if item.icon then
            btn._icon:ClearAllPoints()
            btn._icon:SetPoint("LEFT", btn, "LEFT", 10, 0)
            btn._icon:SetTexture(item.icon)
            btn._icon:SetTexCoord(0, 0.85, 0, 1)
            btn._icon:SetSize(iconW, iconH)
            btn._icon:Show()
            btn._label:ClearAllPoints()
            btn._label:SetPoint("LEFT", btn._icon, "RIGHT", 4, 0)
            btn._label:SetPoint("RIGHT", btn, "RIGHT", -6, 0)
        else
            btn._icon:Hide()
            btn._label:ClearAllPoints()
            btn._label:SetPoint("LEFT", btn, "LEFT", 10, 0)
            btn._label:SetPoint("RIGHT", btn, "RIGHT", -6, 0)
        end
        if btn._label.SetFontObject then btn._label:SetFontObject(GameFontHighlight) end
        btn._label:SetTextColor(DD_TEXT[1], DD_TEXT[2], DD_TEXT[3])
        btn._label:SetText(itemLabel)
        -- Per-item font preview (e.g. font dropdown shows each font in its own typeface)
        local appliedFont = item.fontObject ~= nil
        if item.fontObject and btn._label.SetFontObject then
            btn._label:SetFontObject(item.fontObject)
        elseif btn._fontOverridden and btn._label.SetFontObject then
            btn._label:SetFontObject(GameFontHighlight)
        end
        btn._fontOverridden = appliedFont
        btn._ddItem = item
        btn._ddOwner = owner
        btn:Show()
    end
    -- Hide excess
    for i = count + 1, _itemCount do
        local b = _itemPool[i]
        if b then b:Hide() end
    end
    if sf.SetVerticalScroll then sf:SetVerticalScroll(0) end
end

local function DD_Toggle(owner)
    DD_EnsureList()
    if _listOwner == owner and _listFrame:IsShown() then DD_Close(); return end
    if _listFrame:IsShown() then DD_Close() end
    _listOwner = owner
    _listOpenToken = _listOpenToken + 1
    local openToken = _listOpenToken
    DD_Populate(owner)
    _listFrame:ClearAllPoints()
    -- Anchor to the peel button (visible superellipse) if available, else the frame
    local anchorTo = owner._msufPeelButton or owner
    _listFrame:SetPoint("TOPLEFT", anchorTo, "BOTTOMLEFT", 0, -2)
    _listBackdrop:SetFrameStrata("FULLSCREEN_DIALOG")
    _listBackdrop:SetFrameLevel(900)
    _listFrame:SetFrameStrata("TOOLTIP")
    _listFrame:SetFrameLevel(1000)
    if _listFrame.SetToplevel then _listFrame:SetToplevel(true) end
    if _listFrame.Raise then _listFrame:Raise() end
    _listFrame:Show()
    _listBackdropIgnoreUntil = (GetTime and (GetTime() + 0.12)) or 0
    local function ShowBackdrop()
        if _listOpenToken == openToken and _listOwner == owner and _listFrame and _listFrame:IsShown() and _listBackdrop then
            _listBackdrop:Show()
        end
    end
    if C_Timer and C_Timer.After then C_Timer.After(0.04, ShowBackdrop) else ShowBackdrop() end
end

-- ---- 4g. Dropdown (spec-driven, own ListFrame, superellipse trigger) ----
function UI.Dropdown(spec)
    local parent = spec.parent
    local w = spec.width or 200
    -- Create trigger via MSUF_CreateStyledDropdown (superellipse Peel styling)
    -- Falls back to plain UIDropDownMenuTemplate if style system not loaded yet
    local CreateSD = _G.MSUF_CreateStyledDropdown
    local dd
    if CreateSD then
        dd = CreateSD(spec.name, parent)
    else
        dd = CreateFrame("Frame", spec.name, parent, "UIDropDownMenuTemplate")
    end
    UIDropDownMenu_SetWidth(dd, w)
    if spec.anchor then
        dd:ClearAllPoints()
        -- UIDropDownMenuTemplate has ~16px built-in left padding; compensate automatically
        dd:SetPoint("TOPLEFT", spec.anchor, spec.anchorPoint or "BOTTOMLEFT", spec.x or 0, spec.y or -4)
    end
    -- Neuter Blizzard init (prevent ToggleDropDownMenu from firing)
    UIDropDownMenu_Initialize(dd, function() end)
    -- Hijack click: peel button (superellipse) or native button → open our list
    local function OnSpecClick()
        DD_Toggle(dd)
    end
    local peelBtn = dd._msufPeelButton
    if peelBtn then peelBtn:SetScript("OnClick", OnSpecClick) end
    local dname = dd.GetName and dd:GetName()
    local nativeBtn = dd.Button or (dname and _G[dname .. "Button"])
    if nativeBtn then nativeBtn:SetScript("OnClick", OnSpecClick) end
    -- Spec
    dd._ddSpec = spec
    dd._ddKey = nil
    -- API: SetValue — updates both UIDropDownMenu text AND peel text
    function dd:SetValue(key)
        self._ddKey = key
        local label = tostring(key or "")
        local items = self._ddSpec.items
        if type(items) == "function" then items = items() end
        if type(items) == "table" then
            for i = 1, #items do
                local itemKey = items[i].key
                if itemKey == nil then itemKey = items[i].value end
                if itemKey == key then
                    label = TR(items[i].label or items[i].text or label)
                    break
                end
            end
        end
        -- UIDropDownMenu_SetText syncs through to peel text via Style hooks
        if UIDropDownMenu_SetSelectedValue then UIDropDownMenu_SetSelectedValue(self, key) end
        if UIDropDownMenu_SetText then UIDropDownMenu_SetText(self, label) end
    end
    function dd:GetValue() return self._ddKey end
    function dd:Refresh()
        local key = self._ddSpec.get and self._ddSpec.get() or nil
        self:SetValue(key)
        if _listOwner == self and _listFrame and _listFrame:IsShown() then
            DD_Populate(self)
        end
    end
    function dd:SetEnabled(enabled)
        if enabled then
            self:SetAlpha(1)
            if UIDropDownMenu_EnableDropDown then UIDropDownMenu_EnableDropDown(self) end
        else
            DD_Close()
            self:SetAlpha(0.45)
            if UIDropDownMenu_DisableDropDown then UIDropDownMenu_DisableDropDown(self) end
        end
    end
    -- Self-sync on Show
    dd:HookScript("OnShow", function(self) self:Refresh() end)
    dd:HookScript("OnHide", function(self)
        if _listOwner == self then DD_Close() end
    end)
    -- Initial sync
    dd:Refresh()
    if spec.tooltip then AttachTooltip(dd, spec.tooltip) end
    return dd
end

-- 6. SharedMedia Texture Items Builder
function UI.StatusBarTextureItems(followText)
    local LSM = (ns and ns.LSM) or _G.MSUF_LSM
    local list
    if LSM and type(LSM.List) == "function" then
        list = LSM:List("statusbar")
    end
    if type(list) ~= "table" or #list == 0 then
        list = { "Blizzard", "Flat", "RaidHP", "RaidPower", "Skills", "Outline" }
    end
    table.sort(list, function(a, b) return tostring(a):lower() < tostring(b):lower() end)
    local result = {}
    if followText then
        result[1] = { key = "", label = TR(followText) }
    end
    for i = 1, #list do
        local name = list[i]
        local icon
        if type(_G.MSUF_ResolveStatusbarTextureKey) == "function" then
            icon = _G.MSUF_ResolveStatusbarTextureKey(name)
        elseif LSM and type(LSM.Fetch) == "function" then
            local ok, tex = pcall(LSM.Fetch, LSM, "statusbar", name, true)
            if ok then icon = tex end
        end
        result[#result + 1] = { key = name, label = name, icon = icon }
    end
    return result
end

-- 7. Scroll Height Helper
local _scrollPending = {}
local math_ceil = math.ceil

function UI.QueueScrollUpdate(host, scrollKey, childKey, contentKey)
    if not host then return end
    local id = scrollKey or "_scroll"
    if _scrollPending[host] and _scrollPending[host][id] then return end
    _scrollPending[host] = _scrollPending[host] or {}
    _scrollPending[host][id] = true
    local function run()
        _scrollPending[host][id] = nil
        local scroll = host[scrollKey or "_scroll"]
        local child  = host[childKey or "_child"]
        if not (scroll and child) then return end
        local top = child.GetTop and child:GetTop()
        if not top then return end
        local lowest = top
        local content = host[contentKey or "_content"]
        if content and content.GetChildren then
            local regions = { content:GetChildren() }
            for i = 1, #regions do
                local r = regions[i]
                if r and r.IsShown and r:IsShown() and r.GetBottom then
                    local b = r:GetBottom()
                    if b and b < lowest then lowest = b end
                end
            end
        end
        local h = math_ceil((top - lowest) + 32)
        if h < 500 then h = 500 end
        child:SetHeight(h)
        local w = scroll:GetWidth()
        if w and w > 1 then child:SetWidth(w) end
        if scroll.UpdateScrollChildRect then scroll:UpdateScrollChildRect() end
        if _G.UIPanelScrollFrame_Update then _G.UIPanelScrollFrame_Update(scroll) end
    end
    if C_Timer and C_Timer.After then C_Timer.After(0, run) else run() end
end

-- 8. Button Row Builder
function UI.ButtonRow(parent, anchor, gap, defs)
    local row = CreateFrame("Frame", nil, parent)
    row:SetSize(1, 1)
    local buttons = {}
    local last
    gap = tonumber(gap) or 8
    for i = 1, #defs do
        local d = defs[i]
        local id = d.id or ("b" .. i)
        local btn = UI.Button({ parent = parent, name = d.name, text = d.text, onClick = d.onClick, width = d.w, height = d.h })
        buttons[id] = btn
        if not last then
            btn:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -(tonumber(d.y) or 10))
        else
            btn:SetPoint("LEFT", last, "RIGHT", gap, 0)
        end
        last = btn
    end
    if last and defs[1] then
        local first = buttons[defs[1].id or "b1"]
        if first then
            row:SetPoint("TOPLEFT", first, "TOPLEFT", 0, 0)
            row:SetPoint("BOTTOMRIGHT", last, "BOTTOMRIGHT", 0, 0)
        end
    end
    return row, buttons
end

-- 9. Backward-Compat Stubs (for Auras/Colors/EditMode)

-- These stay functional for non-migrated consumers:
local function MSUF_InitSimpleDropdown(dropdown, options, getCurrentKey, setCurrentKey, onSelect, width)
    if not dropdown then return end
    UIDropDownMenu_Initialize(dropdown, function(self, level)
        local info = UIDropDownMenu_CreateInfo()
        local cur = getCurrentKey and getCurrentKey() or nil
        for _, opt in ipairs(options or {}) do
            info.text = TR(opt.menuText or opt.label)
            info.value = opt.key
            info.checked = (opt.key == cur)
            info.func = function(btn)
                if setCurrentKey then setCurrentKey(btn.value) end
                UIDropDownMenu_SetSelectedValue(dropdown, btn.value)
                UIDropDownMenu_SetText(dropdown, TR(opt.label))
                if type(onSelect) == "function" then onSelect(btn.value, opt)
                elseif type(onSelect) == "string" and type(_G.MSUF_Options_Apply) == "function" then _G.MSUF_Options_Apply(onSelect, btn.value, opt) end
            end
            UIDropDownMenu_AddButton(info, level)
        end
    end)
    if width then UIDropDownMenu_SetWidth(dropdown, width) end
    local cur = getCurrentKey and getCurrentKey() or nil
    local label = TR((options and options[1] and options[1].label) or "")
    for _, opt in ipairs(options or {}) do
        if opt.key == cur then label = TR(opt.label); break end
    end
    UIDropDownMenu_SetSelectedValue(dropdown, cur)
    UIDropDownMenu_SetText(dropdown, label)
end

local function MSUF_SyncSimpleDropdown(dropdown, options, getCurrentKey)
    if not dropdown or not options or not getCurrentKey then return end
    local cur = getCurrentKey()
    if UIDropDownMenu_SetSelectedValue then UIDropDownMenu_SetSelectedValue(dropdown, cur) end
    for _, opt in ipairs(options) do
        if opt.key == cur then
            if UIDropDownMenu_SetText then UIDropDownMenu_SetText(dropdown, TR(opt.label)) end
            break
        end
    end
end

-- No-op stubs (consumers still call these but they do nothing with new dropdown system)
local function MSUF_MakeDropdownScrollable() end
local function MSUF_ExpandDropdownClickArea() end

-- 10. Exports
UI.AttachTooltip         = AttachTooltip
UI.StyleSlider           = StyleSlider
UI.StyleSmallButton      = StyleSmallButton
UI.StyleToggleText       = StyleToggleText
UI.StyleCheckmark        = StyleCheckmark
UI.CloseDropdown         = DD_Close

-- ns.* exports (split modules use ns.*)
ns.MSUF_AttachTooltip           = AttachTooltip
ns.MSUF_UI_Text                 = function(p, t, tmpl) local fs = p:CreateFontString(nil, "ARTWORK", tmpl or "GameFontNormal"); fs:SetText(TR(t or "")); return fs end
ns.MSUF_UI_Btn                  = function(p, n, l, fn, w, h) return UI.Button({ parent = p, name = n, text = l, onClick = fn, width = w, height = h }) end
ns.MSUF_BuildButtonRowList      = UI.ButtonRow
ns.MSUF_MakeDropdownScrollable  = MSUF_MakeDropdownScrollable
ns.MSUF_ExpandDropdownClickArea = MSUF_ExpandDropdownClickArea
ns.MSUF_InitSimpleDropdown      = MSUF_InitSimpleDropdown
ns.MSUF_SyncSimpleDropdown      = MSUF_SyncSimpleDropdown
ns.MSUF_GetLSM                  = function() return (ns and ns.LSM) or _G.MSUF_LSM end

-- _G exports (backward compat)
_G.MSUF_InitSimpleDropdown      = _G.MSUF_InitSimpleDropdown   or MSUF_InitSimpleDropdown
_G.MSUF_SyncSimpleDropdown      = _G.MSUF_SyncSimpleDropdown   or MSUF_SyncSimpleDropdown
_G.MSUF_MakeDropdownScrollable  = _G.MSUF_MakeDropdownScrollable or MSUF_MakeDropdownScrollable
_G.MSUF_ExpandDropdownClickArea = _G.MSUF_ExpandDropdownClickArea or MSUF_ExpandDropdownClickArea
_G.MSUF_CloseSpecDropdown       = DD_Close

-- Style exports (Core and other files use these)
ns.MSUF_StyleSlider             = StyleSlider
ns.MSUF_StyleSmallButton        = StyleSmallButton
ns.MSUF_StyleToggleText         = StyleToggleText
ns.MSUF_StyleCheckmark          = StyleCheckmark
_G.MSUF_StyleSlider             = StyleSlider
_G.MSUF_StyleSmallButton        = StyleSmallButton
_G.MSUF_StyleToggleText         = _G.MSUF_StyleToggleText or StyleToggleText
_G.MSUF_StyleCheckmark          = _G.MSUF_StyleCheckmark or StyleCheckmark

-- Auto-Intercept: Styled Dropdowns → Toolkit List
-- Any frame created by MSUF_CreateStyledDropdown (has ._msufPeelButton)
-- that uses UIDropDownMenu_Initialize will automatically open the
-- Toolkit's styled list instead of Blizzard's DropDownList1.
-- Zero changes needed in Player, Colors, Auras, Gameplay.
do
    -- Capture buffer for UIDropDownMenu_AddButton interception
    local _captureActive = false
    local _captureItems = {}
    local _captureOwner = nil

    -- Hook UIDropDownMenu_AddButton to capture items when we're intercepting
    local _origAddButton = UIDropDownMenu_AddButton
    if _origAddButton then
        UIDropDownMenu_AddButton = function(info, level, ...)
            if _captureActive and info then
                -- Skip title/separator items
                if not info.isTitle and not info.disabled then
                    _captureItems[#_captureItems + 1] = {
                        key    = info.value,
                        label  = info.text or tostring(info.value or ""),
                        icon   = info.icon,
                        func   = info.func,
                        arg1   = info.arg1,
                        arg2   = info.arg2,
                        checked = info.checked,
                    }
                end
                return  -- don't add to Blizzard's list
            end
            return _origAddButton(info, level, ...)
        end
    end

    -- Hook ToggleDropDownMenu to intercept styled dropdowns
    local _origToggle = ToggleDropDownMenu
    if _origToggle then
        ToggleDropDownMenu = function(level, value, dropDown, anchorName, xOff, yOff, menuList, button, autoHide)
            -- Only intercept level-1 opens on styled MSUF dropdowns
            local dd = dropDown
            if dd and dd._msufPeelButton and (level == 1 or level == nil) and dd.initialize then
                -- Capture items from the init function
                _captureActive = true
                _captureItems = {}
                _captureOwner = dd
                pcall(dd.initialize, dd, 1)
                _captureActive = false

                if #_captureItems > 0 then
                    -- Build spec for Toolkit list
                    local curKey = nil
                    for _, it in ipairs(_captureItems) do
                        local ck = it.checked
                        if type(ck) == "function" then
                            if ck() then curKey = it.key; break end
                        elseif ck then
                            curKey = it.key; break
                        end
                    end

                    -- Store a temporary spec
                    dd._ddSpec = {
                        items = _captureItems,
                        get = function() return curKey end,
                        set = function(v, item)
                            -- Find and call the original func
                            if item and item.func then
                                -- Create a fake button with .value
                                local fakeBtn = { value = v }
                                pcall(item.func, fakeBtn, item.arg1, item.arg2)
                            end
                        end,
                        width = dd._msufPeelButton and dd._msufPeelButton:GetWidth() or 200,
                    }
                    DD_Toggle(dd)
                    return
                end
            end
            -- Fallback to Blizzard's default
            return _origToggle(level, value, dropDown, anchorName, xOff, yOff, menuList, button, autoHide)
        end
    end
end

-- Also export UI.BindExistingDropdown for explicit use (e.g. refactored Bars)

-- ---- 4k. Generic spec row builder ----------------------------------------
-- Lightweight batch builder for existing widget factories. It only creates
-- widgets when an options page calls it, so it does not add any login/combat
-- work by itself. specs may use type = "check", "slider", "dropdown", or
-- "label" and any normal UI.Check/UI.Slider/UI.Dropdown/UI.Label fields.
function UI.BuildOptionRows(parent, specs, opts)
    opts = opts or {}
    local widgets, byKey = {}, {}
    local prev = opts.anchor
    for i = 1, #(specs or {}) do
        local spec = specs[i]
        if spec then
            local rowSpec = {}
            for k, v in pairs(spec) do rowSpec[k] = v end
            rowSpec.parent = rowSpec.parent or parent
            if rowSpec.anchor == nil then rowSpec.anchor = prev end
            local widget
            if rowSpec.type == "check" or rowSpec.type == "checkbox" then
                widget = UI.Check(rowSpec)
            elseif rowSpec.type == "slider" then
                widget = UI.Slider(rowSpec)
            elseif rowSpec.type == "dropdown" then
                widget = UI.Dropdown(rowSpec)
            elseif rowSpec.type == "label" then
                widget = UI.Label(rowSpec)
            end
            if widget then
                widgets[#widgets + 1] = widget
                if rowSpec.key then byKey[rowSpec.key] = widget end
                if rowSpec.name then byKey[rowSpec.name] = widget end
                if rowSpec.nextAnchor ~= false then prev = widget end
            end
        end
    end
    widgets.byKey = byKey
    return widgets, byKey
end

function UI.BindExistingDropdown(dd, spec)
    if not dd or not spec then return dd end
    UIDropDownMenu_Initialize(dd, function() end)
    local function OnSpecClick() DD_Toggle(dd) end
    local peelBtn = dd._msufPeelButton
    if peelBtn then peelBtn:SetScript("OnClick", OnSpecClick) end
    local dname = dd.GetName and dd:GetName()
    local nativeBtn = dd.Button or (dname and _G[dname .. "Button"])
    if nativeBtn then nativeBtn:SetScript("OnClick", OnSpecClick) end
    dd._ddSpec = spec; dd._ddKey = nil
    function dd:SetValue(key)
        self._ddKey = key; local label = tostring(key or "")
        local items = self._ddSpec.items
        if type(items) == "function" then items = items() end
        if type(items) == "table" then
            for _, it in ipairs(items) do
                local itemKey = it.key
                if itemKey == nil then itemKey = it.value end
                if itemKey == key then label = TR(it.label or it.text or label); break end
            end
        end
        if UIDropDownMenu_SetSelectedValue then UIDropDownMenu_SetSelectedValue(self, key) end
        if UIDropDownMenu_SetText then UIDropDownMenu_SetText(self, label) end
    end
    function dd:GetValue() return self._ddKey end
    function dd:Refresh()
        local key = self._ddSpec.get and self._ddSpec.get() or nil; self:SetValue(key)
        if _listOwner == self and _listFrame and _listFrame:IsShown() then DD_Populate(self) end
    end
    dd:HookScript("OnShow", function(self) self:Refresh() end)
    dd:HookScript("OnHide", function(self)
        if _listOwner == self then DD_Close() end
    end)
    dd:Refresh()
    return dd
end

-- ═══════════════════════════════════════════════════════════════════════
-- DB-bound Row Widgets (1 line per widget, auto-sync to MSUF_DB)
-- Usage: local row = UI.DBCheck(body, prevRow, "Enable X", "general.featureX", {apply=Apply})
-- ═══════════════════════════════════════════════════════════════════════

local ROW_H = 26
local SL_W = 160
local DD_W = 140
local math_floor = math.floor

-- DB path reader: "general.enableGradient" → MSUF_DB.general.enableGradient
local function _DBRead(path)
    if not MSUF_DB or type(path) ~= "string" then return nil end
    local t = MSUF_DB
    for token in path:gmatch("[^%.]+") do
        if not t then return nil end
        t = t[token]
    end
    return t
end

-- DB path writer
local function _DBWrite(path, value)
    if not MSUF_DB or type(path) ~= "string" then return end
    local t = MSUF_DB
    local parts = {}
    for token in path:gmatch("[^%.]+") do parts[#parts + 1] = token end
    if #parts == 0 then return end
    for i = 1, #parts - 1 do
        local k = parts[i]
        if type(t[k]) ~= "table" then t[k] = {} end
        t = t[k]
    end
    t[parts[#parts]] = value
end

-- Shared row frame factory
local function _DBRow(parent, prev, topOfs)
    local row = CreateFrame("Frame", nil, parent)
    row:SetSize(parent:GetWidth() - 8, ROW_H)
    if prev then
        row:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, topOfs or -2)
    else
        row:SetPoint("TOPLEFT", parent, "TOPLEFT", 4, topOfs or -6)
    end
    return row
end

local function _DBRowLabel(row, text)
    local fs = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    fs:SetPoint("LEFT", row, "LEFT", 4, 0)
    fs:SetText(TR(text or ""))
    return fs
end

-- ── UI.DBCheck ──────────────────────────────────────────────
-- Returns row frame. opts: {apply, default, tooltip, y}
function UI.DBCheck(parent, prev, label, dbPath, opts)
    opts = opts or {}
    local row = _DBRow(parent, prev, opts.y)
    local cb = CreateFrame("CheckButton", nil, row, "UICheckButtonTemplate")
    cb:SetSize(22, 22)
    cb:SetPoint("LEFT", row, "LEFT", 2, 0)
    local fs = cb.text or cb.Text
    if fs and fs.SetText then
        fs:SetText(TR(label or ""))
        if fs.SetFontObject then fs:SetFontObject("GameFontHighlightSmall") end
    end
    local function Sync()
        local v = _DBRead(dbPath)
        if v == nil then v = (opts.default ~= false) end
        cb:SetChecked(v and true or false)
    end
    Sync()
    cb:SetScript("OnClick", function(self)
        if type(EnsureDB) == "function" then EnsureDB() end
        _DBWrite(dbPath, self:GetChecked() and true or false)
        if opts.apply then opts.apply() end
    end)
    cb:SetScript("OnShow", function() Sync() end)
    if _G.MSUF_StyleCheckmark then _G.MSUF_StyleCheckmark(cb) end
    if _G.MSUF_StyleToggleText then _G.MSUF_StyleToggleText(cb) end
    if opts.tooltip then AttachTooltip(cb, opts.tooltip) end
    row._ctrl = cb
    row._sync = Sync
    return row
end

-- ── UI.DBSlider ─────────────────────────────────────────────
-- Returns row frame. opts: {apply, default, width, y, format, int}
function UI.DBSlider(parent, prev, label, dbPath, lo, hi, step, opts)
    opts = opts or {}
    local def = opts.default or lo
    local isInt = (opts.int ~= false) and (step >= 1)
    local row = _DBRow(parent, prev, opts.y or -2)
    _DBRowLabel(row, label)
    local valFS = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    valFS:SetPoint("RIGHT", row, "RIGHT", -4, 0)
    valFS:SetJustifyH("RIGHT")
    local sl = CreateFrame("Slider", nil, row, "OptionsSliderTemplate")
    sl:SetSize(opts.width or SL_W, 14)
    sl:SetPoint("RIGHT", valFS, "LEFT", -8, 0)
    sl:SetMinMaxValues(lo, hi)
    sl:SetValueStep(step)
    sl:SetObeyStepOnDrag(true)
    if sl.Text then sl.Text:SetText("") end
    if sl.Low  then sl.Low:SetText("")  end
    if sl.High then sl.High:SetText("") end
    local function Fmt(v)
        if opts.format then return opts.format(v) end
        if isInt then return tostring(math_floor(v + 0.5)) end
        return string.format("%.2f", v)
    end
    local function Sync()
        local v = _DBRead(dbPath)
        if v == nil then v = def end
        v = tonumber(v) or def
        sl:SetValue(v)
        valFS:SetText(Fmt(v))
    end
    Sync()
    sl:SetScript("OnValueChanged", function(_, v)
        if isInt then v = math_floor(v + 0.5) end
        valFS:SetText(Fmt(v))
        if type(EnsureDB) == "function" then EnsureDB() end
        _DBWrite(dbPath, v)
        if opts.apply then opts.apply() end
    end)
    sl:SetScript("OnShow", function() Sync() end)
    if StyleSlider then StyleSlider(sl) end
    if opts.tooltip then AttachTooltip(sl, opts.tooltip) end
    row._ctrl = sl
    row._sync = Sync
    return row
end

-- ── UI.DBDropdown ───────────────────────────────────────────
-- items: {{key="X", label="X Label"}, ...}
-- Returns row frame. opts: {apply, default, width, y}
function UI.DBDropdown(parent, prev, label, dbPath, items, opts)
    opts = opts or {}
    local def = opts.default
    local row = _DBRow(parent, prev, opts.y or -2)
    _DBRowLabel(row, label)
    local dd = UI.Dropdown({
        parent = row,
        anchor = row, anchorPoint = "RIGHT", x = -4, y = 0,
        point = "RIGHT",
        width = opts.width or DD_W,
        items = items,
        get = function()
            local v = _DBRead(dbPath)
            return v ~= nil and v or def
        end,
        set = function(key)
            if type(EnsureDB) == "function" then EnsureDB() end
            _DBWrite(dbPath, key)
            if opts.apply then opts.apply() end
        end,
    })
    row._ctrl = dd
    row._sync = function() if dd.Refresh then dd:Refresh() end end
    return row
end

-- ── UI.BuildRows (batch builder) ────────────────────────────
-- specs: array of {type="check"|"slider"|"dropdown", label=..., db=..., ...}
-- Returns array of row frames
function UI.BuildRows(parent, specs, opts)
    opts = opts or {}
    local rows = {}
    local prev = nil
    for _, s in ipairs(specs) do
        local row
        if s.type == "check" then
            row = UI.DBCheck(parent, prev, s.label, s.db, s)
        elseif s.type == "slider" then
            row = UI.DBSlider(parent, prev, s.label, s.db, s.min, s.max, s.step, s)
        elseif s.type == "dropdown" then
            row = UI.DBDropdown(parent, prev, s.label, s.db, s.items, s)
        end
        if row then
            rows[#rows + 1] = row
            prev = row
        end
    end
    return rows
end
