local ADDON_NAME, ns = ...
local WF = ns.WF
local Theme = WF.UI.Theme

local Widgets = WF.UI.Widgets or {}
WF.UI.Widgets = Widgets

local _G = _G
local IsAddOnLoaded = IsAddOnLoaded
local C_AddOns = C_AddOns

-- -------------------------------------------------------
-- Lock grip label helpers
--   Goal: "Frame" = blue, state = gray (no white)
-- -------------------------------------------------------
local function Clamp01(x)
  x = tonumber(x) or 0
  if x < 0 then return 0 end
  if x > 1 then return 1 end
  return x
end

local function RGBToHex(r, g, b)
  r = math.floor(Clamp01(r) * 255 + 0.5)
  g = math.floor(Clamp01(g) * 255 + 0.5)
  b = math.floor(Clamp01(b) * 255 + 0.5)
  return string.format("%02x%02x%02x", r, g, b)
end

local _WF_LOCK_LABEL_BLUE_HEX = RGBToHex(
  Theme and Theme.accent and Theme.accent[1] or 0.35,
  Theme and Theme.accent and Theme.accent[2] or 0.78,
  Theme and Theme.accent and Theme.accent[3] or 1.00
)

local function StyleFrameLockText(rawText)
  rawText = tostring(rawText or "")
  local rest = rawText:gsub("^%s*[Ff]rame", "", 1)
  if rest == rawText then
    return rawText
  end
  return ("|cff%sFrame|r%s"):format(_WF_LOCK_LABEL_BLUE_HEX, rest)
end

local function ApplyBackdrop(f, bg, border)
  f:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  })
  f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
  f:SetBackdropBorderColor(border[1], border[2], border[3], border[4])
end

local function ApplySolidBackdrop(f, bg, border, edgeSize, insets)
  if not f or not f.SetBackdrop then return end
  f:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = edgeSize or 12,
    insets = insets or { left = 3, right = 3, top = 3, bottom = 3 },
  })
  f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
  f:SetBackdropBorderColor(border[1], border[2], border[3], border[4])
end

local function WF_IsElvUIEnabled()
  if _G and _G.ElvUI ~= nil then
    return true
  end
  if type(C_AddOns) == "table" and type(C_AddOns.IsAddOnLoaded) == "function" then
    local ok, loaded = pcall(C_AddOns.IsAddOnLoaded, "ElvUI")
    if ok and loaded then
      return true
    end
  end
  if type(IsAddOnLoaded) == "function" then
    local ok, loaded = pcall(IsAddOnLoaded, "ElvUI")
    if ok and loaded then
      return true
    end
  end
  return false
end

local function WF_ShouldUseBundledFonts()
  if WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.UseBundledFontsOnly then
    return WF.UI.Fonts.UseBundledFontsOnly()
  end
  return not WF_IsElvUIEnabled()
end

local function WF_FontTemplate(wfName, blizzardName)
  if WF_ShouldUseBundledFonts() and wfName and _G[wfName] then
    return wfName
  end
  if blizzardName and _G[blizzardName] then
    return blizzardName
  end
  return wfName or blizzardName
end

local function WF_FontPath(kind)
  if WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.ResolveFontPath then
    return WF.UI.Fonts.ResolveFontPath(kind)
  end
  local base = "Interface\\AddOns\\" .. tostring(ADDON_NAME) .. "\\Media\\Fonts\\"
  if kind == "display" then
    return base .. "Expressway.ttf"
  end
  return base .. "PTSansNarrow.ttf"
end

local function _WF_SetVerticalGradient(tex, sr, sg, sb, sa, er, eg, eb, ea)
  if not tex then return end
  tex:SetTexture("Interface/Buttons/WHITE8x8")
  if tex.SetGradientAlpha then
    tex:SetGradientAlpha("VERTICAL", sr, sg, sb, sa, er, eg, eb, ea)
  else
    tex:SetVertexColor(sr or 1, sg or 1, sb or 1, sa or 1)
  end
end

-- -------------------------------------------------------
-- Returns the smallest edgeSize that survives WoW's pixel rounding at the current UI scale.
-- SetBackdrop edgeSize is in logical pixels; WoW multiplies by effectiveScale to get physical
-- pixels, then rounds. At scales like 0.9, 1*0.9=0.9 rounds to 1 — fine. But backdrop
-- rendering has a minimum: if the result < 0.5 physical pixels it vanishes. Using max(1, ceil)
-- of 1/effectiveScale ensures the border always covers at least 1 physical pixel.
local function WF_PixelEdge(ref)
  local s = 1
  if ref and ref.GetEffectiveScale then
    local ok, v = pcall(ref.GetEffectiveScale, ref)
    if ok and type(v) == "number" and v > 0 then s = v end
  end
  if s >= 1 then return 1 end
  -- s < 1 means we're scaled down; need ceil(1/s) logical px to get 1 physical px
  return math.ceil(1 / s)
end
-- -------------------------------------------------------
local function _WF_EffectiveScaleFor(ref)
  if ref and ref.GetEffectiveScale then
    local ok, s = pcall(ref.GetEffectiveScale, ref)
    if ok and type(s) == "number" and s > 0 then
      return s
    end
  end
  if UIParent and UIParent.GetEffectiveScale then
    local ok, s = pcall(UIParent.GetEffectiveScale, UIParent)
    if ok and type(s) == "number" and s > 0 then
      return s
    end
  end
  return 1
end

function Widgets:PixelEdge(ref)
  return WF_PixelEdge(ref)
end

function Widgets:PixelSize(ref)
  local s = _WF_EffectiveScaleFor(ref)
  return 1 / s
end

function Widgets:Snap(ref, v)
  local s = _WF_EffectiveScaleFor(ref)
  v = tonumber(v) or 0
  return math.floor(v * s + 0.5) / s
end

function Widgets:SnapSize(ref, w, h)
  local s = _WF_EffectiveScaleFor(ref)
  local px = 1 / s
  w = tonumber(w) or 0
  h = tonumber(h) or 0
  w = math.floor(w / px + 0.5) * px
  h = math.floor(h / px + 0.5) * px
  return w, h
end

local _WF_PixelPerfectRegistry = setmetatable({}, { __mode = "k" })

local function _WF_CapturePixelPoints(obj)
  if not obj or not obj.GetNumPoints or not obj.GetPoint then return nil end
  local n = obj:GetNumPoints() or 0
  if n <= 0 or n > 2 then return nil end

  local pts = {}
  for i = 1, n do
    local point, relativeTo, relativePoint, xOfs, yOfs = obj:GetPoint(i)
    if not point then return nil end
    pts[#pts + 1] = {
      point = point,
      relativeTo = relativeTo,
      relativePoint = relativePoint or point,
      xOfs = tonumber(xOfs) or 0,
      yOfs = tonumber(yOfs) or 0,
    }
  end
  return pts
end

local function _WF_ResolvePixelMode(obj, opts)
  if type(opts) == "table" and (opts.mode == "h" or opts.mode == "v") then
    return opts.mode
  end
  if not obj then return nil end
  local w = obj.GetWidth and tonumber(obj:GetWidth()) or nil
  local h = obj.GetHeight and tonumber(obj:GetHeight()) or nil
  if h and h > 0 and h <= 2 then return "h" end
  if w and w > 0 and w <= 2 then return "v" end
  return nil
end

local function _WF_ApplyPixelPerfect(obj, meta)
  if not obj or not meta then return end

  if obj.SetSnapToPixelGrid then
    pcall(obj.SetSnapToPixelGrid, obj, true)
  end
  if obj.SetTexelSnappingBias then
    pcall(obj.SetTexelSnappingBias, obj, 0)
  end
  -- Do NOT resize the texture here. SetSnapToPixelGrid tells the GPU renderer
  -- to align texture edges to physical pixel boundaries without changing the
  -- declared logical size. Resizing to 1/effectiveScale causes visible thickness
  -- changes as scale shifts.
end

function Widgets:RegisterPixelPerfect(obj, ref, opts)
  if not obj then return end
  local meta = _WF_PixelPerfectRegistry[obj] or {}
  meta.ref = ref or meta.ref or (obj.GetParent and obj:GetParent()) or UIParent
  meta.mode = _WF_ResolvePixelMode(obj, opts) or meta.mode
  if meta.points == nil then
    meta.points = _WF_CapturePixelPoints(obj)
  end
  _WF_PixelPerfectRegistry[obj] = meta
  _WF_ApplyPixelPerfect(obj, meta)
end

local function _WF_IsDescendantOf(obj, ancestor)
  if not obj or not ancestor or obj == ancestor then
    return obj == ancestor
  end

  local cur = obj
  local safety = 0
  while cur and cur.GetParent and safety < 100 do
    if cur == ancestor then
      return true
    end
    cur = cur:GetParent()
    safety = safety + 1
  end

  return false
end

function Widgets:RefreshPixelPerfect(filterRef)
  for obj, meta in pairs(_WF_PixelPerfectRegistry) do
    if obj and meta then
      local ref = meta.ref
      local matches = (not filterRef)
        or (obj == filterRef)
        or (ref == filterRef)
        or _WF_IsDescendantOf(obj, filterRef)
        or _WF_IsDescendantOf(ref, filterRef)

      if matches then
        _WF_ApplyPixelPerfect(obj, meta)
      end
    end
  end
end

function Widgets:MakePixelPerfect(tex, ref, opts)
  if not tex then return end
  self:RegisterPixelPerfect(tex, ref, opts)
end

function Widgets:CreatePixelBorder(frame, ref, opts)
  if not frame then return nil end

  opts = (type(opts) == "table") and opts or {}
  ref = ref or frame

  if frame._wfPixelBorder then
    if opts.color and frame._wfPixelBorder.SetColor then
      frame._wfPixelBorder:SetColor(opts.color[1], opts.color[2], opts.color[3], opts.color[4])
    end
    if frame._wfPixelBorder.Refresh then
      frame._wfPixelBorder:Refresh()
    end
    return frame._wfPixelBorder
  end

  local layer = opts.layer or "BORDER"
  local subLevel = opts.subLevel
  local r = opts.r or (opts.color and opts.color[1]) or 1
  local g = opts.g or (opts.color and opts.color[2]) or 1
  local b = opts.b or (opts.color and opts.color[3]) or 1
  local a = opts.a or (opts.color and opts.color[4]) or 1

  local border = {}
  border.top = frame:CreateTexture(nil, layer, nil, subLevel)
  border.bottom = frame:CreateTexture(nil, layer, nil, subLevel)
  border.left = frame:CreateTexture(nil, layer, nil, subLevel)
  border.right = frame:CreateTexture(nil, layer, nil, subLevel)

  local textures = { border.top, border.bottom, border.left, border.right }
  for i = 1, #textures do
    textures[i]:SetTexture("Interface/Buttons/WHITE8x8")
    textures[i]:SetVertexColor(r, g, b, a)
  end

  border.top:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
  border.top:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)

  border.bottom:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
  border.bottom:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)

  border.left:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
  border.left:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)

  border.right:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
  border.right:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)

  function border:SetColor(cr, cg, cb, ca)
    cr = cr or r
    cg = cg or g
    cb = cb or b
    if ca == nil then ca = a end
    for i = 1, #textures do
      textures[i]:SetVertexColor(cr, cg, cb, ca)
    end
  end

  function border:Refresh()
    -- SetHeight/Width stay at 1. SetSnapToPixelGrid handles physical alignment.
    for i = 1, #textures do
      if textures[i].SetSnapToPixelGrid then
        pcall(textures[i].SetSnapToPixelGrid, textures[i], true)
        pcall(textures[i].SetTexelSnappingBias, textures[i], 0)
      end
    end
  end

  border:Refresh()

  Widgets:MakePixelPerfect(border.top, ref, { mode = "h" })
  Widgets:MakePixelPerfect(border.bottom, ref, { mode = "h" })
  Widgets:MakePixelPerfect(border.left, ref, { mode = "v" })
  Widgets:MakePixelPerfect(border.right, ref, { mode = "v" })

  frame._wfPixelBorder = border

  if frame.HookScript then
    frame:HookScript("OnShow", function(self)
      if self._wfPixelBorder and self._wfPixelBorder.Refresh then
        self._wfPixelBorder:Refresh()
      end
    end)
    frame:HookScript("OnSizeChanged", function(self)
      if self._wfPixelBorder and self._wfPixelBorder.Refresh then
        self._wfPixelBorder:Refresh()
      end
    end)
  end

  return border
end

function Widgets:SetPixelBorderColor(frame, r, g, b, a)
  if frame and frame._wfPixelBorder and frame._wfPixelBorder.SetColor then
    frame._wfPixelBorder:SetColor(r, g, b, a)
  end
end

function Widgets:ShowResizeOverlay(frame, text)
  if not frame then return end

  local overlay = frame._wfResizeOverlay
  if not overlay then
    overlay = CreateFrame("Frame", nil, frame)
    overlay:SetAllPoints(frame)
    overlay:EnableMouse(false)
    overlay:SetFrameStrata(frame:GetFrameStrata() or "DIALOG")
    overlay:SetFrameLevel((frame.GetFrameLevel and frame:GetFrameLevel() or 0) + 40)

    local bg = overlay:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(overlay)
    bg:SetColorTexture(0, 0, 0, 0.72)
    overlay.bg = bg

    local label = overlay:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontNormalLarge", "GameFontNormalLarge"))
    label:SetPoint("CENTER", overlay, "CENTER", 0, 0)
    label:SetJustifyH("CENTER")
    label:SetJustifyV("MIDDLE")
    label:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
    overlay.label = label

    frame._wfResizeOverlay = overlay
  end

  overlay:SetFrameLevel((frame.GetFrameLevel and frame:GetFrameLevel() or 0) + 40)
  overlay.label:SetText(tostring(text or "FRAME BEING RESIZED"))
  overlay:Show()
end

function Widgets:HideResizeOverlay(frame)
  if frame and frame._wfResizeOverlay then
    frame._wfResizeOverlay:Hide()
  end
end


function Widgets:GetTooltipPalette()
  local theme = Theme or {}
  return {
    bg = { 0.06, 0.06, 0.06, 1.00 },
    border = { 0.18, 0.18, 0.18, 1.00 },
    title = {
      (theme.accent and theme.accent[1]) or 0.35,
      (theme.accent and theme.accent[2]) or 0.78,
      (theme.accent and theme.accent[3]) or 1.00,
      1.00,
    },
    body = {
      (theme.text and theme.text[1]) or 0.92,
      (theme.text and theme.text[2]) or 0.92,
      (theme.text and theme.text[3]) or 0.92,
      1.00,
    },
    muted = {
      (theme.muted and theme.muted[1]) or 0.65,
      (theme.muted and theme.muted[2]) or 0.65,
      (theme.muted and theme.muted[3]) or 0.65,
      1.00,
    },
  }
end

local _WF_TOOLTIP_NINESLICE_KEYS = {
  "TopEdge",
  "BottomEdge",
  "LeftEdge",
  "RightEdge",
  "TopLeftCorner",
  "TopRightCorner",
  "BottomLeftCorner",
  "BottomRightCorner",
  "Center",
  "Background",
  "Bg",
}

local function _WF_CopyTable(src)
  if type(src) ~= "table" then return src end
  local out = {}
  for k, v in pairs(src) do
    if type(v) == "table" then
      local inner = {}
      for kk, vv in pairs(v) do
        inner[kk] = vv
      end
      out[k] = inner
    else
      out[k] = v
    end
  end
  return out
end

local function _WF_SnapshotTooltipState(tip)
  if not tip or tip._wfTooltipOriginalState then return end

  local state = {}

  if tip.GetBackdrop then
    local ok, backdrop = pcall(tip.GetBackdrop, tip)
    if ok and type(backdrop) == "table" then
      state.backdrop = _WF_CopyTable(backdrop)
    end
  end

  if tip.GetBackdropColor then
    local ok, r, g, b, a = pcall(tip.GetBackdropColor, tip)
    if ok then
      state.bg = { r, g, b, a }
    end
  end

  if tip.GetBackdropBorderColor then
    local ok, r, g, b, a = pcall(tip.GetBackdropBorderColor, tip)
    if ok then
      state.border = { r, g, b, a }
    end
  end

  if tip.NineSlice then
    state.nineSliceAlpha = {}
    for i = 1, #_WF_TOOLTIP_NINESLICE_KEYS do
      local key = _WF_TOOLTIP_NINESLICE_KEYS[i]
      local region = tip.NineSlice[key]
      if region and region.GetAlpha then
        local ok, alpha = pcall(region.GetAlpha, region)
        if ok then
          state.nineSliceAlpha[key] = alpha
        end
      end
    end
  end

  tip._wfTooltipOriginalState = state
end

local function _WF_TooltipCanAdjustClamp(tip)
  if not tip or not tip.SetClampedToScreen then return false end

  if tip == EmbeddedItemTooltip or (_G and tip == _G.EmbeddedItemTooltip) then
    return false
  end

  if tip.IsForbidden then
    local ok, forbidden = pcall(tip.IsForbidden, tip)
    if ok and forbidden then
      return false
    end
  end

  if tip.IsProtected then
    local ok, protected = pcall(tip.IsProtected, tip)
    if ok and protected then
      return false
    end
  end

  return true
end

local function _WF_RestoreTooltipState(tip)
  if not tip then return end

  if tip._wfOpaqueFill and tip._wfOpaqueFill.Hide then
    tip._wfOpaqueFill:Hide()
  end

  local state = tip._wfTooltipOriginalState
  if not state then return end

  if state.backdrop and tip.SetBackdrop then
    pcall(tip.SetBackdrop, tip, state.backdrop)
  end

  if state.bg and tip.SetBackdropColor then
    pcall(tip.SetBackdropColor, tip, state.bg[1], state.bg[2], state.bg[3], state.bg[4])
  end

  if state.border and tip.SetBackdropBorderColor then
    pcall(tip.SetBackdropBorderColor, tip, state.border[1], state.border[2], state.border[3], state.border[4])
  end

  if state.nineSliceAlpha and tip.NineSlice then
    for i = 1, #_WF_TOOLTIP_NINESLICE_KEYS do
      local key = _WF_TOOLTIP_NINESLICE_KEYS[i]
      local region = tip.NineSlice[key]
      local alpha = state.nineSliceAlpha[key]
      if region and region.SetAlpha and alpha ~= nil then
        pcall(region.SetAlpha, region, alpha)
      end
    end
  end
end

function Widgets:StyleTooltip(tip)
  tip = tip or GameTooltip
  if not tip then return end

  local elvuiLoaded = WF_IsElvUIEnabled()
  if elvuiLoaded then
    if tip._wfOpaqueFill and tip._wfOpaqueFill.Hide then
      tip._wfOpaqueFill:Hide()
    end

    if tip.SetAlpha then
      tip:SetAlpha(1)
    end

    if tip.GetBackdropColor and tip.SetBackdropColor then
      local ok, r, g, b, a = pcall(tip.GetBackdropColor, tip)
      if ok then
        tip:SetBackdropColor(r or 0, g or 0, b or 0, 1)
      end
    end

    if tip.NineSlice then
      local center = tip.NineSlice.Center
      if center and center.SetAlpha then
        center:SetAlpha(1)
      end
      local background = tip.NineSlice.Background or tip.NineSlice.Bg
      if background and background.SetAlpha then
        background:SetAlpha(1)
      end
    end

    if _WF_TooltipCanAdjustClamp(tip) then
      tip:SetClampedToScreen(true)
    end
    return
  end

  _WF_SnapshotTooltipState(tip)

  local pal = self:GetTooltipPalette()

  if tip.SetBackdrop then
    tip:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = WF_PixelEdge(tip),
      insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
  end

  if tip.NineSlice then
    local center = tip.NineSlice.Center
    if center and center.SetAlpha then
      center:SetAlpha(0)
    end
    local background = tip.NineSlice.Background or tip.NineSlice.Bg
    if background and background.SetAlpha then
      background:SetAlpha(0)
    end
  end

  if not tip._wfOpaqueFill and tip.CreateTexture then
    local fill = tip:CreateTexture(nil, "ARTWORK", nil, -7)
    fill:SetTexture("Interface/Buttons/WHITE8x8")
    fill:SetPoint("TOPLEFT", tip, "TOPLEFT", 1, -1)
    fill:SetPoint("BOTTOMRIGHT", tip, "BOTTOMRIGHT", -1, 1)
    tip._wfOpaqueFill = fill
  end
  if tip._wfOpaqueFill then
    tip._wfOpaqueFill:SetColorTexture(pal.bg[1], pal.bg[2], pal.bg[3], pal.bg[4])
    tip._wfOpaqueFill:Show()
  end

  if tip.SetBackdropColor then
    tip:SetBackdropColor(pal.bg[1], pal.bg[2], pal.bg[3], pal.bg[4])
  end
  if tip.SetBackdropBorderColor then
    tip:SetBackdropBorderColor(pal.border[1], pal.border[2], pal.border[3], pal.border[4])
  end
  if tip.SetAlpha then
    tip:SetAlpha(1)
  end

  local statusBar = tip.StatusBar
  if (not statusBar) and tip.GetName then
    statusBar = _G[tostring(tip:GetName() or "") .. "StatusBar"]
  end
  if statusBar and statusBar.SetStatusBarTexture then
    statusBar:SetStatusBarTexture("Interface/Buttons/WHITE8x8")
    if statusBar.SetStatusBarColor then
      statusBar:SetStatusBarColor(0.10, 0.10, 0.10, 1.00)
    end
  end

  if _WF_TooltipCanAdjustClamp(tip) then
    tip:SetClampedToScreen(true)
  end
end

function Widgets:EnsureTooltipSkin(tip)
  -- Do not globally hook shared Blizzard tooltips.
  -- What Farm only styles the tooltip currently being shown by What Farm, then
  -- restores the original backdrop on hide. Global OnShow hooks were causing
  -- every tooltip in the game to inherit What Farm's solid black backdrop.
  tip = tip or GameTooltip
  if not tip then return end

  self._wfTooltipSkinFrames = self._wfTooltipSkinFrames or setmetatable({}, { __mode = "k" })

  if not self._wfTooltipSkinFrames[tip] then
    self._wfTooltipSkinFrames[tip] = true
    if tip.HookScript then
      tip:HookScript("OnHide", function(selfTip)
        _WF_RestoreTooltipState(selfTip)
      end)
      tip:HookScript("OnTooltipCleared", function(selfTip)
        _WF_RestoreTooltipState(selfTip)
      end)
    end
  end

  if Widgets and Widgets.StyleTooltip then
    Widgets:StyleTooltip(tip)
  end
end

function Widgets:AddTooltipBodyLine(text, style)
  if not GameTooltip then return end
  local pal = self:GetTooltipPalette()
  text = tostring(text or "")
  if text == "" then
    GameTooltip:AddLine(" ", 1, 1, 1)
    return
  end

  local color = pal.body
  if style == "muted" or style == "gray" or style == "grey" or style == "hint" then
    color = pal.muted
  elseif style == "title" or style == "header" then
    color = pal.title
  end

  GameTooltip:AddLine(text, color[1], color[2], color[3], true)
end

function Widgets:ShowSimpleTooltip(owner, title, body, anchor)
  if not owner or not GameTooltip then return end
  self:EnsureTooltipSkin()

  GameTooltip:SetOwner(owner, anchor or "ANCHOR_RIGHT")
  GameTooltip:ClearLines()

  local pal = self:GetTooltipPalette()
  if title and tostring(title) ~= "" then
    GameTooltip:AddLine(tostring(title), pal.title[1], pal.title[2], pal.title[3], true)
  end

  if type(body) == "table" then
    for i = 1, #body do
      local line = body[i]
      if type(line) == "table" then
        self:AddTooltipBodyLine(line.text or line[1] or "", line.style or line.color)
      else
        self:AddTooltipBodyLine(line, (i == 1) and "body" or "muted")
      end
    end
  elseif body ~= nil and tostring(body) ~= "" then
    local idx = 1
    for line in tostring(body):gmatch("[^\n]+") do
      if line ~= "" then
        self:AddTooltipBodyLine(line, (idx == 1) and "body" or "muted")
        idx = idx + 1
      end
    end
  end

  GameTooltip:Show()
end

-- -------------------------------------------------------
-- Shared styling helpers (match MainWindow look)
-- -------------------------------------------------------
function Widgets:SkinCloseButton(btn)
  if not btn or btn._wfSkinnedClose then return end
  btn._wfSkinnedClose = true

  if btn.SetText then
    btn:SetText("")
  end
  if btn.label and btn.label.SetText then
    btn.label:SetText("")
    if btn.label.Hide then btn.label:Hide() end
  end
  local fs = btn.GetFontString and btn:GetFontString()
  if fs and fs.SetText then
    fs:SetText("")
    if fs.Hide then fs:Hide() end
  end

  local nt = btn.GetNormalTexture and btn:GetNormalTexture()
  if nt then nt:SetTexture(nil) end
  local pt = btn.GetPushedTexture and btn:GetPushedTexture()
  if pt then pt:SetTexture(nil) end
  local ht = btn.GetHighlightTexture and btn:GetHighlightTexture()
  if ht then ht:SetTexture(nil) end
  local dt = btn.GetDisabledTexture and btn:GetDisabledTexture()
  if dt then dt:SetTexture(nil) end

  btn:SetSize(24, 24)
  btn:SetHitRectInsets(-10, -10, -10, -10)

  if not btn._wfX then
    btn._wfX = btn:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontNormalLarge", "GameFontNormalLarge"))
    btn._wfX:SetPoint("CENTER", 0, 0)
    local fnt, sz, fl = btn._wfX:GetFont()
    if not fnt then
      fnt = WF_FontPath("display")
    end
    sz = tonumber(sz) or 16
    btn._wfX:SetFont(fnt, sz + 7, fl)
    btn._wfX:SetText("×")
  end
  btn._wfX:SetTextColor(1, 1, 1, 0.92)

  btn:HookScript("OnEnter", function()
    if btn._wfX then
      btn._wfX:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
    end
  end)
  btn:HookScript("OnLeave", function()
    if btn._wfX then
      btn._wfX:SetTextColor(1, 1, 1, 0.92)
    end
  end)
end

function Widgets:SkinScrollBar(sb)
  if not sb or sb._wfSkinnedScroll then return end
  sb._wfSkinnedScroll = true

  sb:SetWidth(7)

  local thumb = sb.GetThumbTexture and sb:GetThumbTexture()
  if thumb then
    thumb:SetTexture("Interface/Buttons/WHITE8x8")
    thumb:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.85)
    thumb:SetWidth(7)
  end

  if not sb._wfBG then
    sb._wfBG = sb:CreateTexture(nil, "BACKGROUND")
    sb._wfBG:SetColorTexture(0, 0, 0, 0.22)
  end

  local function IsButton(x)
    return x and x.GetObjectType and x:GetObjectType() == "Button"
  end

  local up = sb.ScrollUpButton or sb.UpButton or sb.Back or sb.DecrementButton or sb.ScrollUp or sb.Up
  local down = sb.ScrollDownButton or sb.DownButton or sb.Forward or sb.IncrementButton or sb.ScrollDown or sb.Down

  local sbName = sb.GetName and sb:GetName()
  if sbName and _G then
    up = up or _G[sbName .. "ScrollUpButton"] or _G[sbName .. "UpButton"] or _G[sbName .. "BackButton"] or _G[sbName .. "DecrementButton"]
    down = down or _G[sbName .. "ScrollDownButton"] or _G[sbName .. "DownButton"] or _G[sbName .. "ForwardButton"] or _G[sbName .. "IncrementButton"]
  end

  if not IsButton(up) then up = nil end
  if not IsButton(down) then down = nil end

  if (not up or not down) and sb.GetChildren then
    for _, c in ipairs({ sb:GetChildren() }) do
      if IsButton(c) then
        local _, relTo, relPoint = c:GetPoint(1)
        if relTo == sb and type(relPoint) == "string" then
          if not up and relPoint:find("TOP") then up = c end
          if not down and relPoint:find("BOTTOM") then down = c end
        end
      end
    end
  end

  if (not up or not down) and sb.GetChildren then
    for _, c in ipairs({ sb:GetChildren() }) do
      if IsButton(c) then
        local n = c.GetName and c:GetName()
        if type(n) == "string" then
          local ln = n:lower()
          if not up and ln:find("up") then up = c end
          if not down and ln:find("down") then down = c end
        end
      end
    end
  end

  local function NeutralizeButton(b, isUp)
    if not IsButton(b) then return end

    local t = b.GetNormalTexture and b:GetNormalTexture()
    if t then t:SetTexture(nil) end
    t = b.GetPushedTexture and b:GetPushedTexture()
    if t then t:SetTexture(nil) end
    t = b.GetHighlightTexture and b:GetHighlightTexture()
    if t then t:SetTexture(nil) end
    t = b.GetDisabledTexture and b:GetDisabledTexture()
    if t then t:SetTexture(nil) end

    b:EnableMouse(false)
    b:SetAlpha(0.0)

    b:ClearAllPoints()
    if isUp then
      b:SetPoint("TOP", sb, "TOP", 0, 0)
    else
      b:SetPoint("BOTTOM", sb, "BOTTOM", 0, 0)
    end
    b:SetHeight(Widgets:PixelSize(sb))
    b:SetWidth(sb:GetWidth() or 7)

    if b.HookScript then
      b:HookScript("OnShow", function(self) self:SetAlpha(0.0) end)
    else
      b:SetScript("OnShow", function(self) self:SetAlpha(0.0) end)
    end
    if b.Show then b:Show() end
  end

  NeutralizeButton(up, true)
  NeutralizeButton(down, false)

  sb._wfBG:ClearAllPoints()
  if IsButton(up) and IsButton(down) then
    sb._wfBG:SetPoint("TOPLEFT", up, "BOTTOMLEFT", 0, 0)
    sb._wfBG:SetPoint("BOTTOMRIGHT", down, "TOPRIGHT", 0, 0)
  else
    sb._wfBG:SetAllPoints()
  end

  if sb.GetValue and sb.SetValue then
    local v = sb:GetValue()
    if v then pcall(sb.SetValue, sb, v) end
  end

  if sb.Hide then sb:Hide() end
  if sb._wfBG and sb._wfBG.Hide then sb._wfBG:Hide() end

  if not sb._wfAutoHide then
    sb._wfAutoHide = true

    local sf = sb.GetParent and sb:GetParent() or nil

    local function _Safe(fn, ...)
      if not fn then return end
      local ok = pcall(fn, ...)
      return ok
    end

    local function _Update()
      if not sb then return end
      if (not sf) and sb.GetParent then sf = sb:GetParent() end
      if not sf then return end

      local range = 0
      if sf.GetVerticalScrollRange then
        range = tonumber(sf:GetVerticalScrollRange()) or 0
      end

      local show = range > 0.5
      if show then
        if sb.Show then sb:Show() end
        if sb._wfBG and sb._wfBG.Show then sb._wfBG:Show() end
      else
        if sf.SetVerticalScroll then _Safe(sf.SetVerticalScroll, sf, 0) end
        if sb.SetValue then _Safe(sb.SetValue, sb, 0) end

        if sb.Hide then sb:Hide() end
        if sb._wfBG and sb._wfBG.Hide then sb._wfBG:Hide() end
      end
    end

    local function _Defer()
      if C_Timer and C_Timer.After then
        C_Timer.After(0, _Update)
      else
        _Update()
      end
    end

    _Defer()

    if sb.HookScript then
      sb:HookScript("OnMinMaxChanged", _Defer)
      sb:HookScript("OnShow", _Defer)
    end

    if sf and sf.HookScript then
      sf:HookScript("OnShow", _Defer)
      sf:HookScript("OnSizeChanged", _Defer)
    end

    local child = sf and sf.GetScrollChild and sf:GetScrollChild() or nil
    if child and child.HookScript then
      child:HookScript("OnShow", _Defer)
      child:HookScript("OnSizeChanged", _Defer)
    end
  end
end

function Widgets:SkinSlider(sl)
  if not sl or sl._wfSkinnedSlider then return end
  sl._wfSkinnedSlider = true

  local name = sl.GetName and sl:GetName()

  if name and _G then
    for _, suffix in ipairs({
      "BG", "Background", "Backdrop", "Border", "Track", "Bar",
      "Left", "Middle", "Right", "Disabled",
      "Thumb",
    }) do
      local r = _G[name .. suffix]
      if r and r.SetTexture then r:SetTexture(nil) end
      if r and r.Hide then r:Hide() end
    end
  end

  if sl.GetHeight and sl.SetHeight then
    local h = sl:GetHeight() or 0
    if h < 20 then sl:SetHeight(20) end
  end

  local function ApplyFlatBackdrop(f, bgR, bgG, bgB, bgA, brR, brG, brB, brA)
    if not f or not f.SetBackdrop then return end
    f:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = WF_PixelEdge(f),
      insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    f:SetBackdropColor(bgR, bgG, bgB, bgA)
    f:SetBackdropBorderColor(brR, brG, brB, brA)
  end

  if not sl._wfTrackFrame then
    sl._wfTrackFrame = CreateFrame("Frame", nil, sl, "BackdropTemplate")
    sl._wfTrackFrame:EnableMouse(false)
  end
  sl._wfTrackFrame:ClearAllPoints()
  sl._wfTrackFrame:SetPoint("LEFT", sl, "LEFT", 6, 0)
  sl._wfTrackFrame:SetPoint("RIGHT", sl, "RIGHT", -6, 0)
  sl._wfTrackFrame:SetHeight(8)

  sl._wfTrackFrame:SetBackdrop({ bgFile = "Interface/Buttons/WHITE8x8" })
  sl._wfTrackFrame:SetBackdropColor(0, 0, 0, 0.28)

  if Widgets.CreatePixelBorder then
    sl._wfTrackBorder = Widgets:CreatePixelBorder(sl._wfTrackFrame, sl._wfTrackFrame, { color = { 1, 1, 1, 0.06 }, layer = "BORDER", subLevel = 3 })
  end

  if not sl._wfFill then
    sl._wfFill = sl._wfTrackFrame:CreateTexture(nil, "ARTWORK")
    sl._wfFill:SetTexture("Interface/Buttons/WHITE8x8")
  end
  sl._wfFill:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.90)
  sl._wfFill:ClearAllPoints()
  sl._wfFill:SetPoint("TOPLEFT", sl._wfTrackFrame, "TOPLEFT", 0, 0)
  sl._wfFill:SetPoint("BOTTOMLEFT", sl._wfTrackFrame, "BOTTOMLEFT", 0, 0)

  local thumb = sl.GetThumbTexture and sl:GetThumbTexture()
  if thumb then
    thumb:SetTexture("Interface/Buttons/WHITE8x8")
    thumb:SetVertexColor(1, 1, 1, 0)
    thumb:SetSize(18, 18)
    thumb:SetDrawLayer("BACKGROUND", 0)
  end

  if thumb and not sl._wfKnob then
    sl._wfKnobShadow = sl:CreateTexture(nil, "BACKGROUND")
    sl._wfKnobShadow:SetTexture("Interface/Buttons/WHITE8x8")
    sl._wfKnobShadow:SetColorTexture(0, 0, 0, 0.55)
    sl._wfKnobShadow:SetPoint("CENTER", thumb, "CENTER", 1, -1)
    sl._wfKnobShadow:SetSize(22, 22)

    sl._wfKnob = CreateFrame("Frame", nil, sl, "BackdropTemplate")
    sl._wfKnob:EnableMouse(false)
    sl._wfKnob:SetPoint("CENTER", thumb, "CENTER", 0, 0)
    sl._wfKnob:SetSize(20, 20)
    ApplyFlatBackdrop(sl._wfKnob, 0.07, 0.07, 0.07, 0.95, 0, 0, 0, 0.95)

    sl._wfKnobFill = sl._wfKnob:CreateTexture(nil, "ARTWORK")
    sl._wfKnobFill:SetTexture("Interface/Buttons/WHITE8x8")
    sl._wfKnobFill:SetPoint("TOPLEFT", 2, -2)
    sl._wfKnobFill:SetPoint("BOTTOMRIGHT", -2, 2)
    sl._wfKnobFill:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.95)

    sl._wfGrip1 = sl._wfKnob:CreateTexture(nil, "OVERLAY")
    sl._wfGrip2 = sl._wfKnob:CreateTexture(nil, "OVERLAY")
    sl._wfGrip3 = sl._wfKnob:CreateTexture(nil, "OVERLAY")
    for _, g in ipairs({ sl._wfGrip1, sl._wfGrip2, sl._wfGrip3 }) do
      g:SetTexture("Interface/Buttons/WHITE8x8")
      g:SetSize(1, 10)
      g:SetVertexColor(1, 1, 1, 0.20)
      g:ClearAllPoints()
    end
    sl._wfGrip1:SetPoint("CENTER", sl._wfKnob, "CENTER", -3, 0)
    sl._wfGrip2:SetPoint("CENTER", sl._wfKnob, "CENTER", 0, 0)
    sl._wfGrip3:SetPoint("CENTER", sl._wfKnob, "CENTER", 3, 0)
  end

  local function Clamp(v, a, b)
    if v < a then return a end
    if v > b then return b end
    return v
  end

  local function UpdateFill()
    if not sl._wfFill then return end

    local minV, maxV = sl:GetMinMaxValues()
    local v = sl:GetValue() or minV or 0
    minV = tonumber(minV) or 0
    maxV = tonumber(maxV) or (minV + 1)

    local pct = 0
    if maxV > minV then
      pct = ((tonumber(v) or minV) - minV) / (maxV - minV)
    end
    pct = Clamp(pct, 0, 1)

    local trackW = (sl._wfTrackFrame and sl._wfTrackFrame:GetWidth()) or ((sl:GetWidth() or 0) - 12)
    trackW = (trackW or 0)
    if trackW < 0 then trackW = 0 end

    local w = trackW * pct
    if pct > 0 and w < 1 then w = 1 end
    if w > trackW then w = trackW end
    sl._wfFill:SetWidth(w)
  end

  sl:HookScript("OnValueChanged", UpdateFill)
  sl:HookScript("OnSizeChanged", UpdateFill)
  sl:HookScript("OnShow", UpdateFill)

  sl:HookScript("OnEnter", function()
    if sl._wfKnobFill then
      sl._wfKnobFill:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1.0)
    end
    if sl._wfTrackBorder and sl._wfTrackBorder.SetColor then
      sl._wfTrackBorder:SetColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.35)
    elseif sl._wfTrackFrame and sl._wfTrackFrame.SetBackdropBorderColor then
      sl._wfTrackFrame:SetBackdropBorderColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.35)
    end
  end)
  sl:HookScript("OnLeave", function()
    if sl._wfKnobFill then
      sl._wfKnobFill:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.95)
    end
    if sl._wfTrackBorder and sl._wfTrackBorder.SetColor then
      sl._wfTrackBorder:SetColor(1, 1, 1, 0.06)
    elseif sl._wfTrackFrame and sl._wfTrackFrame.SetBackdropBorderColor then
      sl._wfTrackFrame:SetBackdropBorderColor(1, 1, 1, 0.06)
    end
  end)

  UpdateFill()
end

function Widgets:Panel(parent)
  local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  ApplyBackdrop(f, Theme.panel, Theme.border)
  return f
end

function Widgets:Header(parent, title, subtitle)
  local bar = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  ApplyBackdrop(bar, Theme.panel2, Theme.border)

  bar.title = bar:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontNormalLarge", "GameFontNormalLarge"))
  bar.title:SetPoint("LEFT", 12, 0)
  bar.title:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
  bar.title:SetText(title or "WHAT FARM?")

  bar.sub = bar:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisable", "GameFontDisable"))
  bar.sub:SetPoint("LEFT", bar.title, "RIGHT", 10, -1)
  bar.sub:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4])
  bar.sub:SetText(subtitle or "farming intelligence")

  bar.close = CreateFrame("Button", nil, bar, "UIPanelCloseButton")
  bar.close:SetPoint("RIGHT", -6, 0)

  if Widgets and Widgets.SkinCloseButton then
    Widgets:SkinCloseButton(bar.close)
  end

  return bar
end

-- -------------------------------------------------------
-- Button (keep tooltip border SHAPE, fix highlight)
--   Old behavior: border turns neon blue on hover.
--   New behavior: subtle inner tint + subtle pressed darken.
-- -------------------------------------------------------
function Widgets:Button(parent, text, w, h)
  local b = CreateFrame("Button", nil, parent, "BackdropTemplate")
  ApplyBackdrop(b, Theme.panel2, Theme.border)
  b:SetSize(w or 90, h or 22)

  local usingElvUI = WF_IsElvUIEnabled()
  local labelTemplate = WF_FontTemplate("WF_GameFontNormal", "GameFontNormal")
  if not usingElvUI then
    if _G["WF_GameFontHighlightSmall"] then
      labelTemplate = "WF_GameFontHighlightSmall"
    elseif _G["WF_GameFontHighlight"] then
      labelTemplate = "WF_GameFontHighlight"
    else
      labelTemplate = WF_FontTemplate("WF_GameFontHighlight", "GameFontHighlight")
    end
  end

  b.label = b:CreateFontString(nil, "OVERLAY", labelTemplate)
  b.label:SetPoint("CENTER", 0, 0)
  b.label:SetText(text or "Button")
  b.label:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])

  if not usingElvUI then
    local fontPath, fontSize, fontFlags = b.label:GetFont()
    if fontPath then
      local newSize = math.max(13, math.floor((tonumber(fontSize) or 12) + 0.5))
      b.label:SetFont(fontPath, newSize, fontFlags or "")
    end
    if b.label.SetShadowOffset then
      b.label:SetShadowOffset(0, -1)
    end
    if b.label.SetShadowColor then
      b.label:SetShadowColor(0, 0, 0, 0.62)
    end
    b.label:SetTextColor(0.96, 0.97, 0.99, 1)
  end

  function b:SetText(value)
    if self.label and self.label.SetText then
      self.label:SetText(value or "")
    end
  end

  function b:GetText()
    if self.label and self.label.GetText then
      return self.label:GetText()
    end
    return nil
  end

  function b:SetLabelTextColor(r, g, b2, a)
    if self.label and self.label.SetTextColor then
      self.label:SetTextColor(r or 1, g or 1, b2 or 1, a or 1)
    end
  end

  if not usingElvUI then
    local px = Widgets:PixelSize(b)
    local insetX = Widgets:Snap(b, 5)
    local topY = Widgets:Snap(b, -2)
    local underTopY = Widgets:Snap(b, topY - px)
    local underShadowY = Widgets:Snap(b, topY - (px * 2))
    local bottomY = Widgets:Snap(b, 4)

    local topLine = b:CreateTexture(nil, "BORDER", nil, 3)
    topLine:SetPoint("TOPLEFT", b, "TOPLEFT", insetX, topY)
    topLine:SetPoint("TOPRIGHT", b, "TOPRIGHT", -insetX, topY)
    topLine:SetHeight(px)
    topLine:SetTexture("Interface/Buttons/WHITE8x8")
    topLine:SetVertexColor(1, 1, 1, 0.105)
    b._wfTopLine = topLine

    local topLift = b:CreateTexture(nil, "BORDER", nil, 2)
    topLift:SetPoint("TOPLEFT", b, "TOPLEFT", insetX, underTopY)
    topLift:SetPoint("TOPRIGHT", b, "TOPRIGHT", -insetX, underTopY)
    topLift:SetHeight(px * 2)
    _WF_SetVerticalGradient(topLift, 1, 1, 1, 0.028, 1, 1, 1, 0.000)
    b._wfTopStreak = topLift

    local topShadow = b:CreateTexture(nil, "BORDER", nil, 1)
    topShadow:SetPoint("TOPLEFT", b, "TOPLEFT", insetX, underShadowY)
    topShadow:SetPoint("TOPRIGHT", b, "TOPRIGHT", -insetX, underShadowY)
    topShadow:SetHeight(px)
    _WF_SetVerticalGradient(topShadow, 0, 0, 0, 0.018, 0, 0, 0, 0.000)
    b._wfTopShadow = topShadow

    local bottomShade = b:CreateTexture(nil, "BORDER", nil, 0)
    bottomShade:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", insetX, bottomY)
    bottomShade:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -insetX, bottomY)
    bottomShade:SetHeight(px * 3)
    _WF_SetVerticalGradient(bottomShade, 0, 0, 0, 0.00, 0, 0, 0, 0.045)
    b._wfBottomShade = bottomShade
  end

  -- Hover highlight: inner tint (does NOT change border, keeps exact shape)
  b:SetHighlightTexture("Interface/Buttons/WHITE8x8")
  local ht = b.GetHighlightTexture and b:GetHighlightTexture()
  if ht then
    ht:SetBlendMode("BLEND") -- softer than ADD
    ht:ClearAllPoints()
    ht:SetPoint("TOPLEFT", b, "TOPLEFT", 4, -4)
    ht:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -4, 4)
    ht:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.10)
  end

  -- Pressed: subtle darken inside
  b:SetPushedTexture("Interface/Buttons/WHITE8x8")
  local pt = b.GetPushedTexture and b:GetPushedTexture()
  if pt then
    pt:ClearAllPoints()
    pt:SetPoint("TOPLEFT", b, "TOPLEFT", 4, -4)
    pt:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -4, 4)
    pt:SetVertexColor(0, 0, 0, 0.18)
  end

  return b
end



-- -------------------------------------------------------
-- Reusable text prompt modal
--   Used for rename/add-note/profile-name prompts so they
--   all share the same focused modal treatment.
-- -------------------------------------------------------
local function _WFPromptGetColor(t, fallback)
  if type(t) == "table" then
    return t[1] or fallback[1], t[2] or fallback[2], t[3] or fallback[3], t[4] or fallback[4]
  end
  return fallback[1], fallback[2], fallback[3], fallback[4]
end

local function _WFPromptStyleInput(eb)
  if not eb then return end

  eb:SetAutoFocus(false)

  if eb.SetFontObject then
    eb:SetFontObject(WF_FontTemplate("WF_GameFontHighlight", "GameFontHighlight"))
  end

  local br, bg, bb, ba = _WFPromptGetColor(Theme and Theme.border, { 1, 1, 1, 0.14 })
  if eb.SetBackdrop then
    ApplySolidBackdrop(eb, { 0.045, 0.045, 0.045, 1.00 }, { br, bg, bb, ba }, 10, { left = 6, right = 6, top = 4, bottom = 4 })
  end

  local tr, tg, tb, ta = _WFPromptGetColor(Theme and Theme.text, { 1, 1, 1, 1 })

  if eb.SetBackdropColor then
    eb:SetBackdropColor(0.045, 0.045, 0.045, 1.00)
  end
  if eb.SetBackdropBorderColor then
    eb:SetBackdropBorderColor(br, bg, bb, ba)
  end
  if eb.SetTextInsets then
    eb:SetTextInsets(6, 6, 2, 2)
  end
  if eb.SetTextColor then
    eb:SetTextColor(tr, tg, tb, ta)
  end
end

function Widgets:EnsureTextPrompt(owner, key, opts)
  if not owner then return nil end

  key = tostring(key or "_wfTextPrompt")
  if owner[key] and owner[key].box and owner[key].blocker then
    return owner[key]
  end

  opts = opts or {}

  local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  blocker:SetAllPoints(UIParent)
  blocker:EnableMouse(true)
  blocker:SetFrameStrata(opts.frameStrata or "FULLSCREEN_DIALOG")
  blocker:SetBackdrop({ bgFile = "Interface/Buttons/WHITE8x8" })
  blocker:SetBackdropColor(0, 0, 0, tonumber(opts.blockerAlpha) or 0.60)
  blocker:Hide()

  local box = CreateFrame("Frame", nil, blocker, "BackdropTemplate")
  box:SetSize(opts.width or 440, opts.height or 188)
  box:SetPoint("CENTER", UIParent, "CENTER", opts.x or 0, opts.y or 40)
  box:SetFrameStrata(opts.frameStrata or "FULLSCREEN_DIALOG")
  box:SetFrameLevel((blocker:GetFrameLevel() or 0) + 60)
  box:EnableMouse(true)
  box:SetScript("OnMouseDown", function() end)
  do
    local br, bg, bb, ba = _WFPromptGetColor(Theme and Theme.border, { 1, 1, 1, 0.14 })
    ApplySolidBackdrop(box, { 0.015, 0.015, 0.015, 1.00 }, { br, bg, bb, ba }, 12, { left = 3, right = 3, top = 3, bottom = 3 })
  end

  local title = box:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
  title:SetPoint("TOPLEFT", 14, -12)
  do
    local tr, tg, tb, ta = _WFPromptGetColor(Theme and Theme.text, { 1, 1, 1, 1 })
    title:SetTextColor(tr, tg, tb, ta)
  end
  title:SetText("Edit")

  local body = box:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  body:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
  body:SetPoint("TOPRIGHT", box, "TOPRIGHT", -14, -40)
  do
    local mr, mg, mb, ma = _WFPromptGetColor(Theme and Theme.muted, { 0.72, 0.72, 0.72, 1 })
    body:SetTextColor(mr, mg, mb, ma)
  end
  body:SetJustifyH("LEFT")
  body:SetJustifyV("TOP")
  body:SetText("Enter a value:")

  local input = CreateFrame("EditBox", nil, box, "BackdropTemplate")
  input:SetSize(opts.inputWidth or ((opts.width or 440) - 28), opts.inputHeight or 24)
  input:SetPoint("TOPLEFT", 14, opts.inputY or -80)
  _WFPromptStyleInput(input)

  local bCancel = self:Button(box, opts.cancelText or "Cancel", opts.cancelWidth or 92, opts.buttonHeight or 24)
  bCancel:SetPoint("BOTTOMRIGHT", -14, 12)

  local bOK = self:Button(box, opts.okText or "OK", opts.okWidth or 92, opts.buttonHeight or 24)
  bOK:SetPoint("RIGHT", bCancel, "LEFT", -10, 0)

  local prompt = {
    blocker = blocker,
    box = box,
    title = title,
    body = body,
    input = input,
    ok = bOK,
    cancel = bCancel,
  }

  local function Close()
    blocker:Hide()
    box:Hide()
    if input.ClearFocus then input:ClearFocus() end
    if input.SetText then input:SetText("") end
    if input.SetCursorPosition then input:SetCursorPosition(0) end
    box._onOK = nil
  end

  local function Commit()
    local cb = box._onOK
    local txt = tostring((input.GetText and input:GetText()) or "")
    Close()
    if type(cb) == "function" then
      cb(txt)
    end
  end

  blocker:SetScript("OnMouseDown", Close)
  bCancel:SetScript("OnClick", Close)
  bOK:SetScript("OnClick", Commit)
  input:SetScript("OnEnterPressed", Commit)
  input:SetScript("OnEscapePressed", Close)

  prompt.close = Close
  prompt.commit = Commit

  owner[key] = prompt
  return prompt
end

function Widgets:OpenTextPrompt(owner, key, opts)
  opts = opts or {}
  local prompt = self:EnsureTextPrompt(owner, key, opts)
  if not prompt then return nil end

  local width = opts.width or (prompt.box.GetWidth and prompt.box:GetWidth()) or 440
  local height = opts.height or (prompt.box.GetHeight and prompt.box:GetHeight()) or 188

  prompt.box:SetSize(width, height)
  prompt.box:ClearAllPoints()
  prompt.box:SetPoint("CENTER", UIParent, "CENTER", opts.x or 0, opts.y or 40)

  if prompt.input and prompt.input.SetSize then
    prompt.input:SetSize(opts.inputWidth or (width - 28), opts.inputHeight or 24)
  end
  if prompt.input and prompt.input.ClearAllPoints then
    prompt.input:ClearAllPoints()
    prompt.input:SetPoint("TOPLEFT", 14, opts.inputY or -80)
  end
  if prompt.input and prompt.input.SetMaxLetters then
    prompt.input:SetMaxLetters(tonumber(opts.maxLetters) or 0)
  end

  if prompt.title and prompt.title.SetText then
    prompt.title:SetText(opts.title or "Edit")
  end
  if prompt.body and prompt.body.SetText then
    prompt.body:SetText(opts.body or "Enter a value:")
  end
  if prompt.ok and prompt.ok.SetText then
    prompt.ok:SetText(opts.okText or "OK")
  end
  if prompt.cancel and prompt.cancel.SetText then
    prompt.cancel:SetText(opts.cancelText or "Cancel")
  end

  prompt.box._onOK = opts.onOK

  if prompt.input and prompt.input.SetText then
    prompt.input:SetText(opts.defaultText or "")
  end

  prompt.blocker:Show()
  prompt.box:Show()

  if prompt.input and prompt.input.SetFocus then
    prompt.input:SetFocus()
  end
  if opts.highlightText ~= false and prompt.input and prompt.input.HighlightText then
    prompt.input:HighlightText()
  end

  return prompt
end

function Widgets:MetricChip(parent, labelText)
  local chip = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  ApplyBackdrop(chip, Theme.panel2, Theme.border)
  chip:SetSize(160, 26)
  if chip.SetClipsChildren then
    chip:SetClipsChildren(true)
  end

  local labelFont = WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall")
  local valueFont = WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlight")

  chip.label = chip:CreateFontString(nil, "OVERLAY", labelFont)
  chip.label:SetPoint("LEFT", 10, 0)
  chip.label:SetJustifyH("LEFT")
  chip.label:SetJustifyV("MIDDLE")
  chip.label:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4])
  if chip.label.SetMaxLines then
    chip.label:SetMaxLines(1)
  end
  if chip.label.SetWordWrap then
    chip.label:SetWordWrap(false)
  end
  if chip.label.SetNonSpaceWrap then
    chip.label:SetNonSpaceWrap(false)
  end
  chip.label:SetText(labelText or "Metric")

  chip.value = chip:CreateFontString(nil, "OVERLAY", valueFont)
  chip.value:SetPoint("RIGHT", -10, 0)
  chip.value:SetJustifyH("RIGHT")
  chip.value:SetJustifyV("MIDDLE")
  chip.value:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
  if chip.value.SetMaxLines then
    chip.value:SetMaxLines(1)
  end
  if chip.value.SetWordWrap then
    chip.value:SetWordWrap(false)
  end
  if chip.value.SetNonSpaceWrap then
    chip.value:SetNonSpaceWrap(false)
  end
  chip.value:SetText("--")

  return chip
end

-- -------------------------------------------------------
-- UI lock grips (corner grippers)
-- Drag = resize, click = lock (PER-FRAME)
-- When locked: shows a lock icon.
-- -------------------------------------------------------
local _WF_LOCK_TEX = "Interface/Buttons/UI-Panel-LockButton-Up"
local _WF_UNLOCK_TEX = "Interface/ChatFrame/UI-ChatIM-SizeGrabber-Up"

function Widgets:IsLocked(lockKey)
  lockKey = tostring(lockKey or "uiLocked")
  if not (WF and WF.DB and WF.DB.GetSetting) then return false end
  return WF.DB:GetSetting(lockKey) and true or false
end

function Widgets:SetLocked(lockKey, v)
  lockKey = tostring(lockKey or "uiLocked")
  if not (WF and WF.DB and WF.DB.SetSetting) then return end
  WF.DB:SetSetting(lockKey, v and true or false)
  self:UpdateLockGrips()
end

function Widgets:IsUILocked()
  return self:IsLocked("uiLocked")
end

function Widgets:SetUILocked(v)
  self:SetLocked("uiLocked", v)
end

function Widgets:UpdateLockGrips()
  if not self._wfLockGrips then return end

  for i = 1, #self._wfLockGrips do
    local entry = self._wfLockGrips[i]
    local g = entry and entry.grip or nil
    local t = entry and entry.tex or nil
    local alpha = entry and entry.alpha or 0.60
    local lockKey = entry and entry.lockKey or "uiLocked"
    local unlockedTex = entry and entry.unlockedTex or _WF_UNLOCK_TEX
    local lockedTex = entry and entry.lockedTex or _WF_LOCK_TEX
    local lockedAlpha = entry and entry.lockedAlpha or 0.85

    local locked = self:IsLocked(lockKey)

    if g and g.Show then
      g:Show()
      if g.EnableMouse then g:EnableMouse(true) end
    end

    if t and t.SetTexture then
      t:SetTexture(locked and lockedTex or unlockedTex)
      t:SetAlpha(locked and lockedAlpha or alpha)
    end

    local label = g and g._wfLockLabel or nil
    if label and label.SetText then
      local lockedText = StyleFrameLockText((g and g._wfLockTextLocked) or "Frame: Locked")
      local unlockedText = StyleFrameLockText((g and g._wfLockTextUnlocked) or "Frame: Unlocked")
      label:SetText(locked and lockedText or unlockedText)

      if Theme and Theme.muted and label.SetTextColor then
        label:SetTextColor(Theme.muted[1] or 0.65, Theme.muted[2] or 0.65, Theme.muted[3] or 0.65, Theme.muted[4] or 1)
      elseif label.SetTextColor then
        label:SetTextColor(0.65, 0.65, 0.65, 1)
      end
    end
  end
end

function Widgets:RegisterLockGrip(grip, tex, alpha, lockKey, unlockedTex, lockedTex, lockedAlpha)
  if not grip then return end
  self._wfLockGrips = self._wfLockGrips or {}
  self._wfLockGrips[#self._wfLockGrips + 1] = {
    grip = grip,
    tex = tex,
    alpha = alpha,
    lockKey = lockKey,
    unlockedTex = unlockedTex,
    lockedTex = lockedTex,
    lockedAlpha = lockedAlpha,
  }
  self:UpdateLockGrips()
end

function Widgets:AttachCornerLockGrip(frame, opts)
  if not frame then return nil end
  opts = opts or {}

  local size = opts.size or 18
  local x = opts.x or -6
  local y = opts.y or 6
  local alpha = opts.alpha or 0.60

  local lockKey = opts.lockKey or "uiLocked"
  local unlockedTex = opts.unlockedTexture or opts.texture or _WF_UNLOCK_TEX
  local lockedTex = opts.lockedTexture or _WF_LOCK_TEX
  local lockedAlpha = opts.lockedAlpha or 0.85

  local noResize = opts.noResize and true or false
  local onResizeStop = opts.onResizeStop
  local onResizeStart = opts.onResizeStart
  local onResizeUpdate = opts.onResizeUpdate or opts.onResize

  local grip = CreateFrame("Button", nil, frame)
  frame._wfLockGrip = grip
  grip:SetSize(size, size)
  grip:SetPoint("BOTTOMRIGHT", x, y)
  grip:SetFrameLevel((frame.GetFrameLevel and frame:GetFrameLevel() or 0) + 60)
  grip:EnableMouse(true)

  local t = grip:CreateTexture(nil, "OVERLAY")
  t:SetAllPoints(grip)
  t:SetTexture(unlockedTex)
  t:SetAlpha(alpha)

  if not opts.noLockText then
    local font = opts.lockTextFont or WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall")
    local fs = grip:CreateFontString(nil, "OVERLAY", font)
    fs:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", x - size - 6, 2)
    fs:SetJustifyH("RIGHT")
    fs:SetText(StyleFrameLockText(opts.unlockedText or "Frame: Unlocked"))
    if Theme and Theme.muted then
      fs:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
    end

    grip._wfLockLabel = fs
    grip._wfLockTextLocked = StyleFrameLockText(opts.lockedText or "Frame: Locked")
    grip._wfLockTextUnlocked = StyleFrameLockText(opts.unlockedText or "Frame: Unlocked")
    grip._wfLockLabelColorLocked = opts.lockedTextColor
    grip._wfLockLabelColorUnlocked = opts.unlockedTextColor
  end

  grip._wfActive = false
  grip._wfSizingStarted = false
  grip._wfDownW = nil
  grip._wfDownH = nil
  grip._wfDownX = nil
  grip._wfDownY = nil
  grip._wfMinW = nil
  grip._wfMinH = nil
  grip._wfMaxW = nil
  grip._wfMaxH = nil

  local dragThreshold = tonumber(opts.dragThreshold or 3) or 3
  if dragThreshold < 1 then
    dragThreshold = 1
  end

  local function GetScaledCursorPosition()
    local xPos, yPos = GetCursorPosition()
    local scale = 1
    if UIParent and UIParent.GetEffectiveScale then
      scale = UIParent:GetEffectiveScale() or 1
    end
    if scale == 0 then
      scale = 1
    end
    return (xPos or 0) / scale, (yPos or 0) / scale
  end

  local function Clamp(v, minV, maxV)
    v = tonumber(v) or 0
    if minV and v < minV then v = minV end
    if maxV and maxV > 0 and v > maxV then v = maxV end
    return v
  end

  local function ReadResizeBounds()
    local minW = tonumber(opts.minWidth or opts.minW) or 120
    local minH = tonumber(opts.minHeight or opts.minH) or 80
    local maxW = tonumber(opts.maxWidth or opts.maxW)
    local maxH = tonumber(opts.maxHeight or opts.maxH)

    if frame and frame.GetResizeBounds then
      local ok, a, b, c, d = pcall(frame.GetResizeBounds, frame)
      if ok then
        minW = tonumber(a) or minW
        minH = tonumber(b) or minH
        maxW = tonumber(c) or maxW
        maxH = tonumber(d) or maxH
      end
    elseif frame and frame.GetMinResize then
      local ok, a, b = pcall(frame.GetMinResize, frame)
      if ok then
        minW = tonumber(a) or minW
        minH = tonumber(b) or minH
      end
      if frame.GetMaxResize then
        local okMax, c, d = pcall(frame.GetMaxResize, frame)
        if okMax then
          maxW = tonumber(c) or maxW
          maxH = tonumber(d) or maxH
        end
      end
    end

    if not maxW and UIParent and UIParent.GetWidth then
      maxW = math.max(minW, math.floor((UIParent:GetWidth() or 0) - 20))
    end
    if not maxH and UIParent and UIParent.GetHeight then
      maxH = math.max(minH, math.floor((UIParent:GetHeight() or 0) - 20))
    end

    if maxW and maxW > 0 and maxW < minW then maxW = minW end
    if maxH and maxH > 0 and maxH < minH then maxH = minH end

    return minW, minH, maxW, maxH
  end

  local function ClearGripResizeState(selfBtn)
    if selfBtn and selfBtn.SetScript then
      selfBtn:SetScript("OnUpdate", nil)
    end
    if frame then
      frame._wfSizing = nil
    end
    if Widgets and Widgets.HideResizeOverlay then
      Widgets:HideResizeOverlay(frame)
    end
    if selfBtn then
      selfBtn._wfActive = false
      selfBtn._wfSizingStarted = false
      selfBtn._wfDownW = nil
      selfBtn._wfDownH = nil
      selfBtn._wfDownX = nil
      selfBtn._wfDownY = nil
      selfBtn._wfMinW = nil
      selfBtn._wfMinH = nil
      selfBtn._wfMaxW = nil
      selfBtn._wfMaxH = nil
    end
  end

  local function BeginGripResize(selfBtn)
    if not selfBtn then return end
    if selfBtn._wfSizingStarted then return end
    if noResize then return end
    if not (frame and frame.SetSize) then return end

    if frame.StopMovingOrSizing then
      frame:StopMovingOrSizing()
    end

    selfBtn._wfSizingStarted = true
    frame._wfSizing = true

    if onResizeStart then
      pcall(onResizeStart, frame)
    end

    selfBtn._wfMinW, selfBtn._wfMinH, selfBtn._wfMaxW, selfBtn._wfMaxH = ReadResizeBounds()

    if Widgets and Widgets.ShowResizeOverlay then
      Widgets:ShowResizeOverlay(frame, opts.resizeText or "FRAME BEING RESIZED")
    end
  end

  local function ApplyGripResize(selfBtn)
    if not (selfBtn and selfBtn._wfActive) then return end
    if not (frame and frame.SetSize) then return end

    local xNow, yNow = GetScaledCursorPosition()
    local dx = (xNow or 0) - (selfBtn._wfDownX or 0)
    local dy = (yNow or 0) - (selfBtn._wfDownY or 0)

    if not selfBtn._wfSizingStarted then
      if math.abs(dx) < dragThreshold and math.abs(dy) < dragThreshold then
        return
      end
      BeginGripResize(selfBtn)
    end

    if not selfBtn._wfSizingStarted then return end

    local minW = selfBtn._wfMinW or 120
    local minH = selfBtn._wfMinH or 80
    local maxW = selfBtn._wfMaxW
    local maxH = selfBtn._wfMaxH

    local nextW = Clamp((selfBtn._wfDownW or frame:GetWidth() or minW) + dx, minW, maxW)
    local nextH = Clamp((selfBtn._wfDownH or frame:GetHeight() or minH) - dy, minH, maxH)

    nextW = math.floor(nextW + 0.5)
    nextH = math.floor(nextH + 0.5)

    local curW, curH = frame:GetSize()
    if math.abs((curW or 0) - nextW) >= 1 or math.abs((curH or 0) - nextH) >= 1 then
      frame:SetSize(nextW, nextH)
      if onResizeUpdate then
        pcall(onResizeUpdate, frame, nextW, nextH)
      end
    end
  end

  local function FinishGripResize(selfBtn, lockIfClick)
    if not selfBtn then return end
    if not selfBtn._wfActive then return end

    local didResize = selfBtn._wfSizingStarted and true or false
    if selfBtn.SetScript then
      selfBtn:SetScript("OnUpdate", nil)
    end

    if frame and frame.StopMovingOrSizing then
      frame:StopMovingOrSizing()
    end
    if frame then
      frame._wfSizing = nil
    end

    if not didResize then
      ClearGripResizeState(selfBtn)
      if lockIfClick then
        Widgets:SetLocked(lockKey, true)
      end
      return
    end

    local w, h = frame:GetSize()
    if Widgets and Widgets.SnapSize then
      local sw, sh = Widgets:SnapSize(frame, w, h)
      if sw ~= w or sh ~= h then
        frame:SetSize(sw, sh)
        w, h = sw, sh
      end
    end
    if Widgets and Widgets.RefreshPixelPerfect then
      Widgets:RefreshPixelPerfect(frame)
    end
    if Widgets and Widgets.HideResizeOverlay then
      Widgets:HideResizeOverlay(frame)
    end

    if onResizeStop then
      pcall(onResizeStop, frame, w, h)
    end

    ClearGripResizeState(selfBtn)
  end

  if frame and frame.HookScript and not frame._wfPixelRefreshHooked then
    frame._wfPixelRefreshHooked = true
    frame:HookScript("OnSizeChanged", function(selfFrame)
      if selfFrame and selfFrame._wfSizing then return end
      if Widgets and Widgets.RefreshPixelPerfect then
        Widgets:RefreshPixelPerfect(selfFrame)
      end
    end)
  end

  grip:SetScript("OnMouseDown", function(selfBtn, button)
    if button ~= "LeftButton" then return end

    if Widgets:IsLocked(lockKey) then
      Widgets:SetLocked(lockKey, false)
      ClearGripResizeState(selfBtn)
      return
    end

    if frame and frame.StopMovingOrSizing then
      frame:StopMovingOrSizing()
    end

    selfBtn._wfActive = true
    selfBtn._wfSizingStarted = false
    selfBtn._wfDownW, selfBtn._wfDownH = frame:GetSize()
    selfBtn._wfDownX, selfBtn._wfDownY = GetScaledCursorPosition()
    selfBtn._wfMinW, selfBtn._wfMinH, selfBtn._wfMaxW, selfBtn._wfMaxH = ReadResizeBounds()

    selfBtn:SetScript("OnUpdate", function(btn)
      if not (btn and btn._wfActive) then return end

      if IsMouseButtonDown and not IsMouseButtonDown("LeftButton") then
        FinishGripResize(btn, true)
        return
      end

      if noResize then return end
      ApplyGripResize(btn)
    end)
  end)

  grip:SetScript("OnMouseUp", function(selfBtn, button)
    if button ~= "LeftButton" then return end
    FinishGripResize(selfBtn, true)
  end)

  grip:SetScript("OnHide", function()
    if frame and frame.StopMovingOrSizing then
      frame:StopMovingOrSizing()
    end
    ClearGripResizeState(grip)
  end)

  self:RegisterLockGrip(grip, t, alpha, lockKey, unlockedTex, lockedTex, lockedAlpha)
  return grip, t
end
