local addonName = ...

-- ============================================================
--  Colours
-- ============================================================
local C = {
    bg         = {0.05, 0.04, 0.07, 0.98},
    sidebar    = {0.03, 0.02, 0.05, 1.00},
    accent     = {0.65, 0.30, 1.00, 1.00},   -- bright purple
    accentDim  = {0.40, 0.15, 0.65, 1.00},
    header     = {0.95, 0.92, 1.00, 1.00},
    sub        = {0.48, 0.42, 0.58, 1.00},
    text       = {0.82, 0.78, 0.90, 1.00},
    inputBg    = {0.08, 0.06, 0.12, 1.00},
    border     = {0.20, 0.14, 0.28, 1.00},
    row1       = {0.10, 0.07, 0.15, 0.70},
}

local FRAME_W   = 1020
local FRAME_H   = 680
local SIDEBAR_W = 240
local PAD       = 20

-- ============================================================
--  Helpers
-- ============================================================
local function Backdrop(f, bg, border)
    f:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
    })
    f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
    if border then
        f:SetBackdropBorderColor(border[1], border[2], border[3], border[4] or 1)
    end
end

local function Tex(parent, r, g, b, a)
    local t = parent:CreateTexture(nil, "ARTWORK")
    t:SetColorTexture(r, g, b, a)
    return t
end

local function Label(parent, fontObj, text, r, g, b, a)
    local fs = parent:CreateFontString(nil, "OVERLAY", fontObj)
    fs:SetText(text or "")
    if r then fs:SetTextColor(r, g, b, a or 1) end
    fs:SetJustifyH("LEFT")
    return fs
end

-- ============================================================
--  Main window
-- ============================================================
local Main = CreateFrame("Frame", "CFLMainFrame", UIParent, "BackdropTemplate")
Main:SetSize(FRAME_W, FRAME_H)
Main:SetPoint("CENTER")
Main:SetFrameStrata("DIALOG")
Main:SetMovable(true)
Main:EnableMouse(true)
Main:RegisterForDrag("LeftButton")
Main:SetScript("OnDragStart", Main.StartMoving)
Main:SetScript("OnDragStop",  Main.StopMovingOrSizing)

-- Register with WoW's Escape key handler
table.insert(UISpecialFrames, "CFLMainFrame")
Main:Hide()
Main:SetBackdrop({
    bgFile   = "Interface\\Buttons\\WHITE8X8",
    edgeFile = "Interface\\Buttons\\WHITE8X8",
    edgeSize = 1,
})
Main:SetBackdropColor(C.bg[1], C.bg[2], C.bg[3], C.bg[4] or 1)
Main:SetBackdropBorderColor(0, 0, 0, 0)

-- border textures drawn directly on Main so they never escape its stacking context
local function AddBorderLine(side)
    local t = Main:CreateTexture(nil, "OVERLAY", nil, 7)
    t:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.9)
    if side == "top" then
        t:SetHeight(2); t:SetPoint("TOPLEFT", Main, "TOPLEFT", 0, 0); t:SetPoint("TOPRIGHT", Main, "TOPRIGHT", 0, 0)
    elseif side == "bottom" then
        t:SetHeight(2); t:SetPoint("BOTTOMLEFT", Main, "BOTTOMLEFT", 0, 0); t:SetPoint("BOTTOMRIGHT", Main, "BOTTOMRIGHT", 0, 0)
    elseif side == "left" then
        t:SetWidth(2); t:SetPoint("TOPLEFT", Main, "TOPLEFT", 0, 0); t:SetPoint("BOTTOMLEFT", Main, "BOTTOMLEFT", 0, 0)
    elseif side == "right" then
        t:SetWidth(2); t:SetPoint("TOPRIGHT", Main, "TOPRIGHT", 0, 0); t:SetPoint("BOTTOMRIGHT", Main, "BOTTOMRIGHT", 0, 0)
    end
end
AddBorderLine("top")
AddBorderLine("bottom")
AddBorderLine("left")
AddBorderLine("right")

-- version label bottom-left


-- clean single-pixel border via backdrop, no overlay frame needed

-- ── CLOSE BUTTON (X) ─────────────────────────────────────────
local CloseBtn = CreateFrame("Button", nil, Main, "BackdropTemplate")
CloseBtn:SetSize(32, 32)
CloseBtn:SetPoint("TOPRIGHT", Main, "TOPRIGHT", -8, -8)
CloseBtn:SetFrameLevel(Main:GetFrameLevel() + 10)
Backdrop(CloseBtn, {0.15, 0.08, 0.04, 1}, C.border)
local closeFS = CloseBtn:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
closeFS:SetText("x")
closeFS:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
closeFS:SetAllPoints(CloseBtn)
closeFS:SetJustifyH("CENTER")
CloseBtn:SetScript("OnEnter", function()
    closeFS:SetTextColor(1, 0.30, 0.20)
end)
CloseBtn:SetScript("OnLeave", function()
    closeFS:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
end)
CloseBtn:SetScript("OnClick", function() Main:Hide() end)

-- ── SIDEBAR ──────────────────────────────────────────────────
local Sidebar = CreateFrame("Frame", nil, Main, "BackdropTemplate")
Sidebar:SetWidth(SIDEBAR_W)
Sidebar:SetPoint("TOPLEFT",    Main, "TOPLEFT",    2, -2)
Sidebar:SetPoint("BOTTOMLEFT", Main, "BOTTOMLEFT", 2,  2)
Backdrop(Sidebar, C.sidebar, C.border)

local sideDiv = Tex(Sidebar, C.accent[1], C.accent[2], C.accent[3], 0.35)
sideDiv:SetWidth(1)
sideDiv:SetPoint("TOPRIGHT",    Sidebar, "TOPRIGHT",    0,  0)
sideDiv:SetPoint("BOTTOMRIGHT", Sidebar, "BOTTOMRIGHT", 0,  0)

local LOGO_H = 80
local logoGlow = Tex(Sidebar, C.accent[1], C.accent[2], C.accent[3], 0.08)
logoGlow:SetPoint("TOPLEFT",  Sidebar, "TOPLEFT",  0, 0)
logoGlow:SetPoint("TOPRIGHT", Sidebar, "TOPRIGHT", 0, 0)
logoGlow:SetHeight(LOGO_H)

local logoTitle = Label(Sidebar, "GameFontNormalHuge", "Custom Font Loader",
    C.accent[1], C.accent[2], C.accent[3])
logoTitle:SetPoint("TOPLEFT", Sidebar, "TOPLEFT", PAD, -14)
logoTitle:SetWidth(SIDEBAR_W - PAD * 2)
logoTitle:SetWordWrap(true)

local logoAuth = Label(Sidebar, "GameFontNormal", "Author: Exxos",
    C.sub[1], C.sub[2], C.sub[3])
logoAuth:SetPoint("TOPLEFT", logoTitle, "BOTTOMLEFT", 0, -4)

local logoDivLine = Tex(Sidebar, C.accent[1], C.accent[2], C.accent[3], 0.25)
logoDivLine:SetHeight(1)
logoDivLine:SetPoint("TOPLEFT",  Sidebar, "TOPLEFT",  0, -LOGO_H)
logoDivLine:SetPoint("TOPRIGHT", Sidebar, "TOPRIGHT", 0, -LOGO_H)

-- version label bottom-left of sidebar
local verLabel = Sidebar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
verLabel:SetPoint("BOTTOMLEFT", Sidebar, "BOTTOMLEFT", 8, 6)
verLabel:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
verLabel:SetJustifyH("LEFT")
verLabel:SetText("v" .. (C_AddOns.GetAddOnMetadata(addonName, "Version") or "?"))



-- ── NAV BUTTONS ───────────────────────────────────────────────
local NAV_Y   = LOGO_H + 40
local NAV_H   = 42
local activePane = nil

local function MakeNavBtn(label, yOffset)
    local btn = CreateFrame("Button", nil, Sidebar, "BackdropTemplate")
    btn:SetHeight(NAV_H)
    btn:SetPoint("TOPLEFT",  Sidebar, "TOPLEFT",  0, -(NAV_Y + yOffset))
    btn:SetPoint("TOPRIGHT", Sidebar, "TOPRIGHT", 0, -(NAV_Y + yOffset))

    local selBG = Tex(btn, C.accent[1], C.accent[2], C.accent[3], 0.00)
    selBG:SetAllPoints(btn)
    btn._selBG = selBG

    local selBar = Tex(btn, C.accent[1], C.accent[2], C.accent[3], 0.00)
    selBar:SetWidth(3)
    selBar:SetPoint("TOPLEFT",    btn, "TOPLEFT",    0, 0)
    selBar:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 0, 0)
    btn._selBar = selBar

    local hl = Tex(btn, 1, 1, 1, 0.00)
    hl:SetAllPoints(btn)
    btn:SetScript("OnEnter", function(self)
        if activePane ~= self then hl:SetColorTexture(1, 1, 1, 0.05) end
    end)
    btn:SetScript("OnLeave", function()
        hl:SetColorTexture(1, 1, 1, 0.00)
    end)

    local lbl = Label(btn, "GameFontNormalLarge", label,
        C.sub[1], C.sub[2], C.sub[3])
    lbl:SetPoint("LEFT", btn, "LEFT", PAD + 10, 0)
    btn._lbl = lbl

    return btn
end

local NavInstructions = MakeNavBtn("Instructions", 0)
local NavFonts        = MakeNavBtn("Font Manager",  NAV_H)
local NavUtilities    = MakeNavBtn("Utilities",     NAV_H * 2)

-- ── RIGHT PANE ────────────────────────────────────────────────
local Pane = CreateFrame("Frame", nil, Main)
Pane:SetPoint("TOPLEFT",     Main, "TOPLEFT",     SIDEBAR_W + 1, -2)
Pane:SetPoint("BOTTOMRIGHT", Main, "BOTTOMRIGHT", -2,             2)

local HEADER_H = 66
local PaneHdr = CreateFrame("Frame", nil, Pane, "BackdropTemplate")
PaneHdr:SetHeight(HEADER_H)
PaneHdr:SetPoint("TOPLEFT",  Pane, "TOPLEFT",  0, 0)
PaneHdr:SetPoint("TOPRIGHT", Pane, "TOPRIGHT", 0, 0)
Backdrop(PaneHdr, {0.07, 0.05, 0.03, 1}, C.border)

local paneTitle = Label(PaneHdr, "GameFontNormalHuge", "",
    C.header[1], C.header[2], C.header[3])
paneTitle:SetPoint("BOTTOMLEFT", PaneHdr, "BOTTOMLEFT", PAD, 22)

local paneSub = Label(PaneHdr, "GameFontNormal", "",
    C.sub[1], C.sub[2], C.sub[3])
paneSub:SetPoint("BOTTOMLEFT", PaneHdr, "BOTTOMLEFT", PAD, 7)

local hdrDiv = Tex(Pane, C.border[1], C.border[2], C.border[3], 1)
hdrDiv:SetHeight(1)
hdrDiv:SetPoint("TOPLEFT",  Pane, "TOPLEFT",  0, -HEADER_H)
hdrDiv:SetPoint("TOPRIGHT", Pane, "TOPRIGHT", 0, -HEADER_H)

-- ============================================================
--  CONTENT FRAMES
-- ============================================================
local CONTENT_TOP = HEADER_H + 1

local function ContentFrame()
    local f = CreateFrame("Frame", nil, Pane)
    f:SetPoint("TOPLEFT",     Pane, "TOPLEFT",     0, -CONTENT_TOP)
    f:SetPoint("BOTTOMRIGHT", Pane, "BOTTOMRIGHT", 0,  1)
    f:Hide()
    return f
end

-- ── INSTRUCTIONS ─────────────────────────────────────────────
local InstrFrame = ContentFrame()
local PANE_TXT_W = FRAME_W - SIDEBAR_W - PAD * 2

local function ILine(text, yOff, font, r, g, b)
    font = font or "GameFontNormal"
    r, g, b = r or C.text[1], g or C.text[2], b or C.text[3]
    local fs = InstrFrame:CreateFontString(nil, "OVERLAY", font)
    fs:SetText(text)
    fs:SetTextColor(r, g, b)
    fs:SetJustifyH("LEFT")
    fs:SetWidth(PANE_TXT_W)
    fs:SetPoint("TOPLEFT", InstrFrame, "TOPLEFT", PAD, -yOff)
    return fs
end

local function ISep(yOff, alpha)
    local t = Tex(InstrFrame, C.border[1], C.border[2], C.border[3], alpha or 0.8)
    t:SetHeight(1)
    t:SetPoint("TOPLEFT",  InstrFrame, "TOPLEFT",  PAD,    -yOff)
    t:SetPoint("TOPRIGHT", InstrFrame, "TOPRIGHT", -PAD,   -yOff)
    return t
end

ILine("How to add a new font", 18, "GameFontNormalLarge",
    C.accent[1], C.accent[2], C.accent[3])
ISep(46, 0.3)

ILine("Step 1 — Place your font file", 58, "GameFontNormalLarge",
    C.header[1], C.header[2], C.header[3])
ILine("Copy your .ttf font file(s) into the addon Fonts folder:", 84)
ILine("   World of Warcraft\\_retail_\\Interface\\AddOns\\CustomFontLoader\\Fonts\\", 106,
    "GameFontNormal", C.accent[1], C.accent[2] * 0.85, C.accent[3] * 0.4)
ILine("Avoid spaces in filenames — use hyphens instead (e.g. My-Font-Bold.ttf).", 132)
ISep(156)

ILine("Step 2 — Reload", 172, "GameFontNormalLarge",
    C.header[1], C.header[2], C.header[3])
ILine("WoW must load the files before they can be registered. Type  /reload  or use the button below:", 198)

-- inline reload button in the instructions panel
local reloadBtn = CreateFrame("Button", nil, InstrFrame, "BackdropTemplate")
reloadBtn:SetSize(110, 26)
reloadBtn:SetPoint("TOPLEFT", InstrFrame, "TOPLEFT", PAD, -216)
Backdrop(reloadBtn, {0.30, 0.10, 0.55, 1.00}, {0.50, 0.20, 0.80, 1.00})
local reloadLbl = reloadBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
reloadLbl:SetText("Reload UI")
reloadLbl:SetTextColor(0.90, 0.80, 1.00)
reloadLbl:SetAllPoints(reloadBtn)
reloadLbl:SetJustifyH("CENTER")
reloadBtn:SetScript("OnEnter", function(self)
    self:SetBackdropColor(C.accent[1], C.accent[2], C.accent[3], 1)
end)
reloadBtn:SetScript("OnLeave", function(self)
    self:SetBackdropColor(0.30, 0.10, 0.55, 1.00)
end)
reloadBtn:SetScript("OnClick", function()
    CFLOpenOnLoad = true
    ReloadUI()
end)

ILine("If you use the button above, this addon will reopen automatically after the reload.", 272)

ISep(292)

ILine("Step 3 — Register in the Font Manager", 308, "GameFontNormalLarge",
    C.header[1], C.header[2], C.header[3])
ILine("Switch to the Font Manager tab, type the filename without the extension, then click Add Font.", 334)
ILine("The font is immediately available in EllesmereUI, Plater, BigWigs, and any other SharedMedia addon.", 360)
ISep(386)

ILine("Notes", 402, "GameFontNormalLarge", C.header[1], C.header[2], C.header[3])
ILine("   -  Fonts are saved automatically and re-registered every time you log in.", 428)
ILine("   -  Only .ttf files are supported — .otf files will not load in WoW.", 452)
ILine("   -  If you delete a font file, the game will not recognize the removal until a full restart.", 476)
ILine("   -  After a full restart, missing fonts are automatically cleaned up on login.", 500)

-- ── FONT MANAGER ─────────────────────────────────────────────
local FontFrame = ContentFrame()

local PANE_INNER = FRAME_W - SIDEBAR_W - 1
local ROW_H      = 30

-- ── PANEL 1: list header (top strip) ─────────────────────────
local ListHeader = CreateFrame("Frame", nil, FontFrame)
ListHeader:SetHeight(44)
ListHeader:SetPoint("TOPLEFT",  FontFrame, "TOPLEFT",  0, 0)
ListHeader:SetPoint("TOPRIGHT", FontFrame, "TOPRIGHT", 0, 0)

local listLabel = Label(ListHeader, "GameFontNormalLarge", "Registered Fonts",
    C.text[1], C.text[2], C.text[3])
listLabel:SetPoint("LEFT", ListHeader, "LEFT", PAD, 0)

local countLabel = Label(ListHeader, "GameFontNormalSmall", "",
    C.sub[1], C.sub[2], C.sub[3])
countLabel:SetPoint("RIGHT", ListHeader, "RIGHT", -PAD, 0)
countLabel:SetJustifyH("RIGHT")

local listDivTop = Tex(FontFrame, C.border[1], C.border[2], C.border[3], 0.8)
listDivTop:SetHeight(1)
listDivTop:SetPoint("TOPLEFT",  FontFrame, "TOPLEFT",  PAD,  -44)
listDivTop:SetPoint("TOPRIGHT", FontFrame, "TOPRIGHT", -PAD, -44)

-- ── PANEL 3: add section (bottom fixed-height panel) ─────────
local ADD_PANEL_H = 170
local AddPanel = CreateFrame("Frame", nil, FontFrame, "BackdropTemplate")
AddPanel:SetHeight(ADD_PANEL_H)
AddPanel:SetPoint("BOTTOMLEFT",  FontFrame, "BOTTOMLEFT",  0, 1)
AddPanel:SetPoint("BOTTOMRIGHT", FontFrame, "BOTTOMRIGHT", 0, 1)
AddPanel:SetBackdrop({bgFile = "Interface\\Buttons\\WHITE8X8"})
AddPanel:SetBackdropColor(0.04, 0.03, 0.07, 1)

local addPanelDiv = Tex(FontFrame, C.accent[1], C.accent[2], C.accent[3], 0.25)
addPanelDiv:SetHeight(1)
addPanelDiv:SetPoint("BOTTOMLEFT",  FontFrame, "BOTTOMLEFT",  PAD,  ADD_PANEL_H)
addPanelDiv:SetPoint("BOTTOMRIGHT", FontFrame, "BOTTOMRIGHT", -PAD, ADD_PANEL_H)

-- add section title — 16px below the divider line
local addTitle = Label(AddPanel, "GameFontNormalLarge", "Add New Font",
    C.header[1], C.header[2], C.header[3])
addTitle:SetPoint("TOPLEFT", AddPanel, "TOPLEFT", PAD, -14)

-- instruction text
local addInstr = Label(AddPanel, "GameFontNormal",
    "Place the .ttf file in the Fonts folder, enter the filename below, then click Add Font.",
    C.sub[1], C.sub[2], C.sub[3])
addInstr:SetPoint("TOPLEFT", addTitle, "BOTTOMLEFT", 0, -6)
addInstr:SetWidth(PANE_INNER - PAD * 2)

-- field label
local lblFile = Label(AddPanel, "GameFontNormal", "Font Filename",
    C.sub[1], C.sub[2], C.sub[3])
lblFile:SetPoint("TOPLEFT", addInstr, "BOTTOMLEFT", 0, -10)

-- sizes
local BTN_W        = 100
local FILE_INPUT_W = PANE_INNER - PAD * 2 - BTN_W - PAD - 8

-- edit box factory
local function MakeEditBox(parent, width, anchorPt, anchorTo, relPt, ox, oy)
    local eb = CreateFrame("EditBox", nil, parent, "BackdropTemplate")
    eb:SetHeight(32)
    eb:SetWidth(width)
    eb:SetPoint(anchorPt, anchorTo, relPt, ox, oy)
    Backdrop(eb, C.inputBg, C.border)
    eb:SetFontObject("GameFontNormal")
    eb:SetTextColor(C.text[1], C.text[2], C.text[3])
    eb:SetTextInsets(10, 10, 0, 0)
    eb:SetAutoFocus(false)
    eb:SetMaxLetters(256)
    eb:SetScript("OnEditFocusGained", function(self)
        self:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.9)
    end)
    eb:SetScript("OnEditFocusLost", function(self)
        self:SetBackdropBorderColor(C.border[1], C.border[2], C.border[3], 1)
    end)
    eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    eb:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    return eb
end

local fileBox = MakeEditBox(AddPanel, FILE_INPUT_W,
    "TOPLEFT", lblFile, "BOTTOMLEFT", 0, -4)

-- Add button
local AddBtn = CreateFrame("Button", nil, AddPanel, "BackdropTemplate")
AddBtn:SetSize(BTN_W, 32)
AddBtn:SetPoint("BOTTOMLEFT", fileBox, "BOTTOMRIGHT", 8, 0)
Backdrop(AddBtn, {0.30, 0.10, 0.55, 1.00}, {0.50, 0.20, 0.80, 1.00})
local addBtnLbl = AddBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
addBtnLbl:SetText("Add Font")
addBtnLbl:SetTextColor(0.90, 0.80, 1.00)
addBtnLbl:SetAllPoints(AddBtn)
addBtnLbl:SetJustifyH("CENTER")
AddBtn:SetScript("OnEnter", function(self)
    self:SetBackdropColor(C.accent[1], C.accent[2], C.accent[3], 1)
end)
AddBtn:SetScript("OnLeave", function(self)
    self:SetBackdropColor(0.30, 0.10, 0.55, 1.00)
end)

-- status line
local statusFS = Label(AddPanel, "GameFontNormal", "",
    C.sub[1], C.sub[2], C.sub[3])
statusFS:SetPoint("TOPLEFT", fileBox, "BOTTOMLEFT", 0, -6)
statusFS:SetWidth(PANE_INNER - PAD * 2)

local function SetStatus(msg, ok)
    statusFS:SetText(msg)
    if ok then
        statusFS:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    else
        statusFS:SetTextColor(1, 0.35, 0.20)
    end
end

-- ── PANEL 2: scroll list (fills between header and add panel) ─
local scrollFrame = CreateFrame("ScrollFrame", nil, FontFrame, "UIPanelScrollFrameTemplate")
scrollFrame:SetPoint("TOPLEFT",     FontFrame, "TOPLEFT",     PAD,       -46)
scrollFrame:SetPoint("BOTTOMRIGHT", FontFrame, "BOTTOMRIGHT", -(PAD + 22), ADD_PANEL_H + 2)

local scrollChild = CreateFrame("Frame", nil, scrollFrame)
scrollChild:SetWidth(1)
scrollChild:SetHeight(1)
scrollFrame:SetScrollChild(scrollChild)

-- ── FONT VALIDATION & SYNC ───────────────────────────────────
local probeString = nil

local function GetProbe()
    if not probeString then
        probeString = UIParent:CreateFontString(nil, "ARTWORK")
        probeString:Hide()
    end
    return probeString
end

local function FontFileExists(path)
    local probe = GetProbe()
    local ok = pcall(function() probe:SetFont(path, 12) end)
    return ok
end

local function SyncFonts()
    local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
    if not LSM then return 0, 0 end
    if not CFLPendingFonts then CFLPendingFonts = {} end

    local removed = 0
    for i = #CFLPendingFonts, 1, -1 do
        local entry = CFLPendingFonts[i]
        if FontFileExists(entry.path) then
            LSM:Register("font", entry.name, entry.path)
        else
            local hash = LSM:HashTable("font")
            if hash[entry.name] == entry.path then
                hash[entry.name] = nil
            end
            table.remove(CFLPendingFonts, i)
            removed = removed + 1
        end
    end

    local total = 0
    for _ in pairs(LSM:HashTable("font")) do total = total + 1 end
    return total, removed
end

local function PopulateFontList()
    for _, c in ipairs({scrollChild:GetChildren()}) do
        c:Hide(); c:SetParent(nil)
    end
    for _, r in ipairs({scrollChild:GetRegions()}) do r:Hide() end

    local LSM   = LibStub and LibStub("LibSharedMedia-3.0", true)
    local fonts = LSM and LSM:HashTable("font") or {}

    local sorted = {}
    for name in pairs(fonts) do sorted[#sorted + 1] = name end
    table.sort(sorted)

    -- update count label
    countLabel:SetText(#sorted .. " font(s) registered")

    local vw = scrollFrame:GetWidth()
    scrollChild:SetWidth(vw)
    scrollChild:SetHeight(math.max(#sorted * ROW_H, 1))

    for i, name in ipairs(sorted) do
        local path    = fonts[name]
        local missing = path and not FontFileExists(path)

        local row = CreateFrame("Frame", nil, scrollChild, "BackdropTemplate")
        row:SetHeight(ROW_H)
        row:SetPoint("TOPLEFT",  scrollChild, "TOPLEFT",  0, -(i - 1) * ROW_H)
        row:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", 0, -(i - 1) * ROW_H)
        if i % 2 == 1 then
            Backdrop(row, C.row1, {0, 0, 0, 0})
        end

        local dot = row:CreateTexture(nil, "ARTWORK")
        dot:SetSize(5, 5)
        if missing then
            dot:SetColorTexture(0.8, 0.2, 0.2, 0.8)
        else
            dot:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.8)
        end
        dot:SetPoint("LEFT", row, "LEFT", 10, 0)

        local nameLbl = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        nameLbl:SetText(name)
        if missing then
            nameLbl:SetTextColor(0.6, 0.2, 0.2)
        else
            nameLbl:SetTextColor(C.text[1], C.text[2], C.text[3])
        end
        nameLbl:SetPoint("LEFT", dot, "RIGHT", 6, 0)
        nameLbl:SetJustifyH("LEFT")

        if missing then
            local missingLbl = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            missingLbl:SetText("file not found")
            missingLbl:SetTextColor(0.6, 0.2, 0.2)
            missingLbl:SetPoint("RIGHT", row, "RIGHT", -8, 0)
            missingLbl:SetJustifyH("RIGHT")
        elseif path then
            local prev = row:CreateFontString(nil, "OVERLAY")
            local ok = pcall(function() prev:SetFont(path, 12) end)
            if ok then
                prev:SetText("Aa Bb Cc 123")
                prev:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
                prev:SetPoint("RIGHT", row, "RIGHT", -8, 0)
                prev:SetJustifyH("RIGHT")
            else
                prev:Hide()
            end
        end
    end

    if #sorted == 0 then
        local empty = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        empty:SetText("No fonts registered yet.  Add one below.")
        empty:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
        empty:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 8, -10)
    end
end



AddBtn:SetScript("OnClick", function()
    local LSM  = LibStub and LibStub("LibSharedMedia-3.0", true)
    local file = fileBox:GetText():match("^%s*(.-)%s*$")

    if not LSM then SetStatus("LibSharedMedia-3.0 not found.", false); return end
    if file == "" then SetStatus("Enter a font filename.", false); return end

    -- check for a wrong extension
    local ext = file:match("%.([^%.]+)$")
    if ext and ext:lower() ~= "ttf" then
        SetStatus('".' .. ext .. '" is not a valid format for WoW. Only .ttf files are supported.', false)
        return
    end

    -- strip .ttf if they included it, then re-append cleanly
    file = file:gsub("%.ttf$", ""):gsub("%.TTF$", "")
    local fileName   = file .. ".ttf"
    local displayName = file  -- use filename (without extension) as display name
    local fullPath   = "Interface\\AddOns\\CustomFontLoader\\Fonts\\" .. fileName

    -- validate the file actually loads
    if not FontFileExists(fullPath) then
        SetStatus('"' .. fileName .. '" not found in the Fonts folder. Check the filename and try again.', false)
        return
    end

    if not CFLPendingFonts then CFLPendingFonts = {} end

    local exists = false
    for _, e in ipairs(CFLPendingFonts) do
        if e.name == displayName then e.path = fullPath; exists = true; break end
    end
    if not exists then
        table.insert(CFLPendingFonts, {name = displayName, path = fullPath})
    end

    LSM:Register("font", displayName, fullPath)

    local total, removed = SyncFonts()

    fileBox:SetText("")
    PopulateFontList()

    local msg = '"' .. displayName .. '" added.'
    if removed > 0 then
        msg = msg .. "  Removed " .. removed .. " missing font(s)."
    end
    SetStatus(msg, true)
    print("|cFFAA44FFCustomFontLoader:|r Registered font: " .. displayName)
end)

-- ============================================================
--  NAV switching
-- ============================================================
local UtilFrame  -- forward-declared; built below

local function SelectTab(navBtn, contentFrame, title, sub)
    InstrFrame:Hide()
    FontFrame:Hide()
    if UtilFrame then UtilFrame:Hide() end

    for _, btn in ipairs({NavInstructions, NavFonts, NavUtilities}) do
        btn._selBG:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.00)
        btn._selBar:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.00)
        btn._lbl:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
    end

    navBtn._selBG:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 0.12)
    navBtn._selBar:SetColorTexture(C.accent[1], C.accent[2], C.accent[3], 1.00)
    navBtn._lbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
    activePane = navBtn

    paneTitle:SetText(title)
    paneSub:SetText(sub)
    contentFrame:Show()
end

NavInstructions:SetScript("OnClick", function()
    SelectTab(NavInstructions, InstrFrame,
        "Instructions", "How to add fonts to CustomFontLoader")
end)

NavFonts:SetScript("OnClick", function()
    SelectTab(NavFonts, FontFrame,
        "Font Manager", "Fonts registered with LibSharedMedia-3.0")
    PopulateFontList()
end)

NavUtilities:SetScript("OnClick", function()
    SelectTab(NavUtilities, UtilFrame,
        "Utilities", "HUD display options for FPS, Latency, and Durability")
end)

Main:SetScript("OnShow", function()
    SelectTab(NavInstructions, InstrFrame,
        "Instructions", "How to add fonts to CustomFontLoader")
end)

-- ============================================================
--  On load: sync (validates files, removes dead entries, re-registers live ones)
-- ============================================================
local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")
loader:RegisterEvent("PLAYER_LOGIN")
loader:SetScript("OnEvent", function(self, event, addon)
    if event == "ADDON_LOADED" then
        if addon ~= addonName then return end
        local total, removed = SyncFonts()
        if removed > 0 then
            print("|cFFAA44FFCustomFontLoader:|r Removed " .. removed
                .. " font(s) with missing files.  " .. total .. " font(s) active.")
        end

        -- Initialise Utilities settings
        if not CFLUtilSettings then CFLUtilSettings = {} end
        local D = CFLUtilSettings
        if D.showFPS   == nil then D.showFPS   = false end
        if D.showMS    == nil then D.showMS    = false end
        if D.showDur   == nil then D.showDur   = false end
        if D.hudLocked == nil then D.hudLocked = false end
        if not D.fps then D.fps = { font="Expressway", size=13, r=0.65, g=0.30, b=1.00, outline="" } end
        if not D.ms  then D.ms  = { font="Expressway", size=13, r=0.65, g=0.30, b=1.00, outline="" } end
        if not D.dur then D.dur = { font="Expressway", size=13, r=0.65, g=0.30, b=1.00, outline="" } end
        -- Backfill / migrate outline field onto cfg tables saved by older addon versions.
        for _, key in ipairs({"fps","ms","dur"}) do
            local cfg = D[key]
            if cfg.outline == nil or type(cfg.outline) == "boolean" then
                -- Older versions stored outline as true/false (+ outlineThick 1-5).
                -- Migrate that into the closest real flag string, default None.
                if cfg.outline == true then
                    cfg.outline = (cfg.outlineThick and cfg.outlineThick >= 3) and "THICKOUTLINE" or "OUTLINE"
                else
                    cfg.outline = ""
                end
            end
            cfg.outlineThick = nil  -- no longer used
        end
        if D.hudX == nil then D.hudX = 0   end
        if D.hudY == nil then D.hudY = -20 end

        -- Refresh UI widgets now (size, color, checkbox state, dropdown label).
        -- Defer the actual HUD font apply to PLAYER_LOGIN so all custom fonts
        -- are fully registered with LSM before we call SetFont.
        CFL_RefreshUtilUI()

        -- if the reload button was used, open the UI once then clear the flag
        if CFLOpenOnLoad then
            CFLOpenOnLoad = nil
            C_Timer.After(0.5, function()
                Main:Show()
            end)
        end

    elseif event == "PLAYER_LOGIN" then
        -- All addons (and their LSM registrations) are complete by now.
        -- Safe to resolve custom font names and apply them to the HUD.
        if CFLUtilSettings then
            CFL_ApplyHUDState()
        end
    end
end)

-- ============================================================
--  UTILITIES — HUD overlay (FPS / MS / Durability)
-- ============================================================

-- HUD frame — draggable, sits on screen independently of the addon window
local HUD = CreateFrame("Frame", "CFLHudFrame", UIParent)
HUD:SetSize(300, 22)
HUD:SetPoint("CENTER", UIParent, "CENTER", 0, -20)
HUD:SetFrameStrata("HIGH")
HUD:SetMovable(true)
HUD:EnableMouse(true)
HUD:RegisterForDrag("LeftButton")
HUD:SetScript("OnDragStart", function(self)
    if CFLUtilSettings and CFLUtilSettings.hudLocked then return end
    self:StartMoving()
end)
HUD:SetScript("OnDragStop", function(self)
    self:StopMovingOrSizing()
    if CFLUtilSettings then
        local x, y = self:GetCenter()
        local ux, uy = UIParent:GetCenter()
        CFLUtilSettings.hudX = x - ux
        CFLUtilSettings.hudY = y - uy
    end
end)
HUD:Hide()

local hudFPS = HUD:CreateFontString(nil, "OVERLAY", "GameFontNormal")
hudFPS:SetJustifyH("LEFT")
local hudMS  = HUD:CreateFontString(nil, "OVERLAY", "GameFontNormal")
hudMS:SetJustifyH("LEFT")
local hudDur = HUD:CreateFontString(nil, "OVERLAY", "GameFontNormal")
hudDur:SetJustifyH("LEFT")

local function ResolveFont(fontName)
    -- Try LSM fetch first, then treat the name as a raw path, then give up.
    local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
    if LSM then
        local path = LSM:Fetch("font", fontName or "")
        if path then return path end
    end
    -- fontName might itself be a usable path (edge case)
    return fontName or ""
end

-- The full set of real WoW SetFont flag combinations, in dropdown order.
-- cfg.outline stores one of these values directly.
local OUTLINE_OPTIONS = {
    { value = "",                          label = "None" },
    { value = "OUTLINE",                   label = "Outline" },
    { value = "THICKOUTLINE",              label = "Thick Outline" },
    { value = "MONOCHROME",                label = "Monochrome" },
    { value = "OUTLINE, MONOCHROME",       label = "Outline + Monochrome" },
    { value = "THICKOUTLINE, MONOCHROME",  label = "Thick Outline + Monochrome" },
}

local function OutlineLabelFor(value)
    for _, opt in ipairs(OUTLINE_OPTIONS) do
        if opt.value == (value or "") then return opt.label end
    end
    return "None"
end

local function ApplyFontToFS(fs, cfg)
    local path = ResolveFont(cfg.font)
    local size = cfg.size or 13
    local flags = cfg.outline or ""
    local ok = pcall(function() fs:SetFont(path, size, flags) end)
    if not ok then fs:SetFontObject("GameFontNormal") end
    fs:SetTextColor(cfg.r or 0.65, cfg.g or 0.30, cfg.b or 1.00)
end

local function LayoutHUD()
    hudFPS:ClearAllPoints(); hudMS:ClearAllPoints(); hudDur:ClearAllPoints()
    local D   = CFLUtilSettings or {}
    local gap = 12
    local prev = nil
    local shown = {}
    -- Always apply font/size/color to all three FontStrings so they are
    -- primed correctly even when their stat is currently hidden.
    for _, item in ipairs({{fs=hudFPS, key="fps"}, {fs=hudMS, key="ms"}, {fs=hudDur, key="dur"}}) do
        local cfg = D[item.key] or {}
        ApplyFontToFS(item.fs, cfg)
    end
    if D.showFPS then shown[#shown+1] = {fs=hudFPS, key="fps"} end
    if D.showMS  then shown[#shown+1] = {fs=hudMS,  key="ms"}  end
    if D.showDur then shown[#shown+1] = {fs=hudDur, key="dur"} end
    for i, item in ipairs(shown) do
        item.fs:Show()
        if i == 1 then
            item.fs:SetPoint("LEFT", HUD, "LEFT", 0, 0)
        else
            item.fs:SetPoint("LEFT", prev, "RIGHT", gap, 0)
        end
        prev = item.fs
    end
    if not D.showFPS then hudFPS:Hide() end
    if not D.showMS  then hudMS:Hide()  end
    if not D.showDur then hudDur:Hide() end
end

-- C_Timer tickers — no per-frame OnUpdate overhead
local hudTickers = {}

local function StopHUDTickers()
    for _, t in ipairs(hudTickers) do t:Cancel() end
    hudTickers = {}
end

local function StartHUDTickers()
    StopHUDTickers()
    local D = CFLUtilSettings or {}
    if D.showFPS then
        hudTickers[#hudTickers+1] = C_Timer.NewTicker(2.0, function()
            hudFPS:SetText(string.format("%.0f fps", GetFramerate()))
        end)
    end
    if D.showMS then
        hudTickers[#hudTickers+1] = C_Timer.NewTicker(2.0, function()
            local _, _, ms = GetNetStats()
            hudMS:SetText(string.format("%d ms", ms))
        end)
    end
    if D.showDur then
        local function UpdateDur()
            local totalCur, totalMax = 0, 0
            for slot = 0, 19 do
                local cur, max = GetInventoryItemDurability(slot)
                if cur and max and max > 0 then
                    totalCur = totalCur + cur
                    totalMax = totalMax + max
                end
            end
            if totalMax > 0 then
                hudDur:SetText(string.format("%d%% dur", math.floor((totalCur/totalMax)*100)))
            else
                hudDur:SetText("-- dur")
            end
        end
        UpdateDur()
        hudTickers[#hudTickers+1] = C_Timer.NewTicker(2.0, UpdateDur)
    end
end

function CFL_ApplyHUDState()
    if not CFLUtilSettings then return end
    local D = CFLUtilSettings
    local any = D.showFPS or D.showMS or D.showDur
    if any then
        HUD:ClearAllPoints()
        HUD:SetPoint("CENTER", UIParent, "CENTER", D.hudX or 0, D.hudY or -20)
        HUD:Show()
        StartHUDTickers()
    else
        HUD:Hide()
        StopHUDTickers()
    end
    LayoutHUD()
end

-- ============================================================
--  UTILITIES — Settings panel (UtilFrame)
-- ============================================================
UtilFrame = ContentFrame()

local UP_U      = PAD
local PANE_TXT_W_U = FRAME_W - SIDEBAR_W - PAD * 2 - 20

local function ULabel(text, yOff, font, r, g, b)
    local fs = UtilFrame:CreateFontString(nil, "OVERLAY", font or "GameFontNormal")
    fs:SetText(text)
    fs:SetTextColor(r or C.text[1], g or C.text[2], b or C.text[3])
    fs:SetJustifyH("LEFT")
    fs:SetPoint("TOPLEFT", UtilFrame, "TOPLEFT", UP_U, -yOff)
    return fs
end

local function USep(yOff, alpha)
    local t = Tex(UtilFrame, C.border[1], C.border[2], C.border[3], alpha or 0.8)
    t:SetHeight(1)
    t:SetPoint("TOPLEFT",  UtilFrame, "TOPLEFT",  UP_U,  -yOff)
    t:SetPoint("TOPRIGHT", UtilFrame, "TOPRIGHT", -UP_U, -yOff)
    return t
end

-- ── Shared font dropdown (one reused panel for all dropdowns) ─
local sharedDD = CreateFrame("Frame", "CFLFontDropdown", UIParent, "BackdropTemplate")
sharedDD:SetFrameStrata("TOOLTIP")
sharedDD:SetWidth(200)
sharedDD:Hide()
sharedDD:EnableMouse(true)
Backdrop(sharedDD, C.sidebar, C.border)

local ddCloseBtn = CreateFrame("Button", nil, UIParent)
ddCloseBtn:SetAllPoints(UIParent)
ddCloseBtn:SetFrameStrata("HIGH")
ddCloseBtn:EnableMouse(true)
ddCloseBtn:Hide()
ddCloseBtn:SetScript("OnClick", function() sharedDD:Hide(); ddCloseBtn:Hide() end)

local ddSF = CreateFrame("ScrollFrame", nil, sharedDD, "UIPanelScrollFrameTemplate")
ddSF:SetPoint("TOPLEFT",     sharedDD, "TOPLEFT",     2, -2)
ddSF:SetPoint("BOTTOMRIGHT", sharedDD, "BOTTOMRIGHT", -20, 2)
local ddSC = CreateFrame("Frame", nil, ddSF)
ddSC:SetWidth(1); ddSC:SetHeight(1)
ddSF:SetScrollChild(ddSC)
sharedDD:SetFrameLevel(ddCloseBtn:GetFrameLevel() + 1)

-- ── Edit box used in util widgets ────────────────────────────
local function MakeUtilEditBox(parent, width, anchorPt, anchorTo, relPt, ox, oy)
    local eb = CreateFrame("EditBox", nil, parent, "BackdropTemplate")
    eb:SetHeight(32)
    eb:SetWidth(width)
    eb:SetPoint(anchorPt, anchorTo, relPt, ox, oy)
    Backdrop(eb, C.inputBg, C.border)
    eb:SetFontObject("GameFontNormal")
    eb:SetTextColor(C.text[1], C.text[2], C.text[3])
    eb:SetTextInsets(10, 10, 0, 0)
    eb:SetAutoFocus(false)
    eb:SetMaxLetters(256)
    eb:SetScript("OnEditFocusGained", function(self)
        self:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.9)
    end)
    eb:SetScript("OnEditFocusLost", function(self)
        self:SetBackdropBorderColor(C.border[1], C.border[2], C.border[3], 1)
    end)
    eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    eb:SetScript("OnEnterPressed",  function(self) self:ClearFocus() end)
    return eb
end

-- statWidgets must exist before any helper below references it.
local statWidgets = {}
local _lockCB_ref = nil

-- ── Helper: refresh a single stat widget's display from SavedVars ──
local function RefreshWidgetDisplay(cfgKey)
    local D = CFLUtilSettings
    if not D then return end
    local w   = statWidgets[cfgKey]
    local cfg = D[cfgKey]
    if not (w and cfg) then return end
    if w.ddBtn and w.ddBtn._lbl then
        w.ddBtn._lbl:SetText(cfg.font or "Expressway")
    end
    if w.sizeEB then
        w.sizeEB:SetTextSafe(tostring(cfg.size or 13))
    end
    if w.swatch and w.swatch._tex then
        w.swatch._tex:SetColorTexture(cfg.r or 0.65, cfg.g or 0.30, cfg.b or 1.00, 1)
    end
    if w.hexBox then
        w.hexBox:SetText(string.format("%02X%02X%02X",
            math.floor((cfg.r or 0.65)*255),
            math.floor((cfg.g or 0.30)*255),
            math.floor((cfg.b or 1.00)*255)))
    end
    if w.outlineDD and w.outlineDD._lbl then
        w.outlineDD._lbl:SetText(OutlineLabelFor(cfg.outline))
    end
end

-- ── Font dropdown ─────────────────────────────────────────────────
-- Each dropdown is permanently tied to one cfgKey ("fps", "ms", or "dur").
-- It reads/writes CFLUtilSettings[cfgKey] directly. No shared state.
local function MakeFontDropdown(parent, cfgKey, onApply, anchorFrame, ax, ay)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(180, 24)
    btn:SetPoint("LEFT", anchorFrame, "RIGHT", ax or 8, ay or 0)
    Backdrop(btn, C.inputBg, C.border)
    local btnLbl = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    btnLbl:SetAllPoints(btn)
    btnLbl:SetJustifyH("CENTER")
    btnLbl:SetTextColor(C.text[1], C.text[2], C.text[3])
    btnLbl:SetText("Expressway")
    btn._lbl = btnLbl
    btn:SetScript("OnClick", function(self)
        if sharedDD:IsShown() and sharedDD._owner == self then
            sharedDD:Hide(); ddCloseBtn:Hide(); return
        end
        local LSM = LibStub and LibStub("LibSharedMedia-3.0", true)
        local fonts = {}; local seen = {}
        for _, n in ipairs({"GameFontNormal","GameFontNormalLarge","GameFontNormalSmall"}) do
            if not seen[n] then seen[n]=true; fonts[#fonts+1]=n end
        end
        if LSM then
            for n in pairs(LSM:HashTable("font")) do
                if not seen[n] then seen[n]=true; fonts[#fonts+1]=n end
            end
        end
        table.sort(fonts)
        for _, c in ipairs({ddSC:GetChildren()}) do c:Hide(); c:SetParent(nil) end
        local ITEM_H = 22
        sharedDD:SetHeight(math.min(#fonts,10)*ITEM_H+4)
        ddSC:SetWidth(sharedDD:GetWidth()-20)
        ddSC:SetHeight(#fonts*ITEM_H)
        local D = CFLUtilSettings
        local currentFont = (D and D[cfgKey] and D[cfgKey].font) or ""
        for i, name in ipairs(fonts) do
            local row = CreateFrame("Button", nil, ddSC)
            row:SetHeight(ITEM_H)
            row:SetPoint("TOPLEFT",  ddSC, "TOPLEFT",  0, -(i-1)*ITEM_H)
            row:SetPoint("TOPRIGHT", ddSC, "TOPRIGHT", 0, -(i-1)*ITEM_H)
            local rl = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            rl:SetText(name)
            rl:SetPoint("LEFT", row, "LEFT", 6, 0)
            local isCurrent = (name == currentFont)
            rl:SetTextColor(
                isCurrent and C.accent[1] or C.text[1],
                isCurrent and C.accent[2] or C.text[2],
                isCurrent and C.accent[3] or C.text[3])
            row:SetScript("OnEnter", function()
                rl:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
            end)
            row:SetScript("OnLeave", function()
                local d2 = CFLUtilSettings and CFLUtilSettings[cfgKey]
                local isC = d2 and (name == d2.font)
                rl:SetTextColor(
                    isC and C.accent[1] or C.text[1],
                    isC and C.accent[2] or C.text[2],
                    isC and C.accent[3] or C.text[3])
            end)
            row:SetScript("OnClick", function()
                if CFLUtilSettings and CFLUtilSettings[cfgKey] then
                    CFLUtilSettings[cfgKey].font = name
                end
                btnLbl:SetText(name)
                if onApply then onApply() end
                sharedDD:Hide(); ddCloseBtn:Hide()
            end)
        end
        sharedDD:ClearAllPoints()
        sharedDD:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)
        sharedDD._owner = self
        sharedDD:Show()
        ddCloseBtn:Show()
        ddSF:SetVerticalScroll(0)
    end)
    return btn
end

-- ── Outline dropdown ──────────────────────────────────────────────
-- Lists every real WoW SetFont flag combination. Stored directly as the
-- flag string in CFLUtilSettings[cfgKey].outline (e.g. "OUTLINE", "").
local function MakeOutlineDropdown(parent, cfgKey, onApply, anchorFrame, ax, ay)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(132, 24)
    btn:SetPoint("LEFT", anchorFrame, "RIGHT", ax or 8, ay or 0)
    Backdrop(btn, C.inputBg, C.border)
    local btnLbl = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    btnLbl:SetAllPoints(btn)
    btnLbl:SetJustifyH("CENTER")
    btnLbl:SetTextColor(C.text[1], C.text[2], C.text[3])
    btnLbl:SetText("None")
    btn._lbl = btnLbl
    btn:SetScript("OnClick", function(self)
        if sharedDD:IsShown() and sharedDD._owner == self then
            sharedDD:Hide(); ddCloseBtn:Hide(); return
        end
        for _, c in ipairs({ddSC:GetChildren()}) do c:Hide(); c:SetParent(nil) end
        local ITEM_H = 22
        local count  = #OUTLINE_OPTIONS
        sharedDD:SetHeight(math.min(count,10)*ITEM_H+4)
        ddSC:SetWidth(sharedDD:GetWidth()-20)
        ddSC:SetHeight(count*ITEM_H)
        local D = CFLUtilSettings
        local currentVal = (D and D[cfgKey] and D[cfgKey].outline) or ""
        for i, opt in ipairs(OUTLINE_OPTIONS) do
            local row = CreateFrame("Button", nil, ddSC)
            row:SetHeight(ITEM_H)
            row:SetPoint("TOPLEFT",  ddSC, "TOPLEFT",  0, -(i-1)*ITEM_H)
            row:SetPoint("TOPRIGHT", ddSC, "TOPRIGHT", 0, -(i-1)*ITEM_H)
            local rl = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            rl:SetText(opt.label)
            rl:SetPoint("LEFT", row, "LEFT", 6, 0)
            local isCurrent = (opt.value == currentVal)
            rl:SetTextColor(
                isCurrent and C.accent[1] or C.text[1],
                isCurrent and C.accent[2] or C.text[2],
                isCurrent and C.accent[3] or C.text[3])
            row:SetScript("OnEnter", function()
                rl:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
            end)
            row:SetScript("OnLeave", function()
                local d2 = CFLUtilSettings and CFLUtilSettings[cfgKey]
                local isC = d2 and (opt.value == (d2.outline or ""))
                rl:SetTextColor(
                    isC and C.accent[1] or C.text[1],
                    isC and C.accent[2] or C.text[2],
                    isC and C.accent[3] or C.text[3])
            end)
            row:SetScript("OnClick", function()
                if CFLUtilSettings and CFLUtilSettings[cfgKey] then
                    CFLUtilSettings[cfgKey].outline = opt.value
                end
                btnLbl:SetText(opt.label)
                if onApply then onApply() end
                sharedDD:Hide(); ddCloseBtn:Hide()
            end)
        end
        sharedDD:ClearAllPoints()
        sharedDD:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2)
        sharedDD._owner = self
        sharedDD:Show()
        ddCloseBtn:Show()
        ddSF:SetVerticalScroll(0)
    end)
    return btn
end


local function MakeSizeSpinner(parent, cfgKey, onApply, anchorFrame, ax, ay)
    local eb = MakeUtilEditBox(parent, 44, "LEFT", anchorFrame, "RIGHT", ax or 8, ay or 0)
    eb:SetText("13")
    eb:SetMaxLetters(3)
    local _suppress = false
    local function Apply()
        if _suppress then return end
        local v = tonumber(eb:GetText())
        local D = CFLUtilSettings
        local cfg = D and D[cfgKey]
        if v and v >= 6 and v <= 48 then
            if cfg then cfg.size = v end
            if onApply then onApply() end
        else
            _suppress = true
            eb:SetText(tostring(cfg and cfg.size or 13))
            _suppress = false
        end
    end
    eb.SetTextSafe = function(self, val)
        _suppress = true
        self:SetText(val)
        _suppress = false
    end
    eb:SetScript("OnEnterPressed", function(self) self:ClearFocus(); Apply() end)
    eb:SetScript("OnEditFocusLost", Apply)
    return eb
end

-- ── Color picker ──────────────────────────────────────────────────
local function MakeColorPicker(parent, cfgKey, onApply, anchorFrame, ax, ay)
    local swatch = CreateFrame("Button", nil, parent, "BackdropTemplate")
    swatch:SetSize(24, 24)
    swatch:SetPoint("LEFT", anchorFrame, "RIGHT", ax or 8, ay or 0)
    Backdrop(swatch, {0,0,0,0}, C.border)
    local swTex = swatch:CreateTexture(nil, "ARTWORK")
    swTex:SetPoint("TOPLEFT", swatch, "TOPLEFT", 2, -2)
    swTex:SetPoint("BOTTOMRIGHT", swatch, "BOTTOMRIGHT", -2, 2)
    swTex:SetColorTexture(0.65, 0.30, 1.00, 1)
    swatch._tex = swTex
    swatch:SetScript("OnEnter", function(self)
        self:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 1)
    end)
    swatch:SetScript("OnLeave", function(self)
        self:SetBackdropBorderColor(C.border[1], C.border[2], C.border[3], 1)
    end)
    local hexBox = MakeUtilEditBox(parent, 72, "LEFT", swatch, "RIGHT", 6, 0)
    hexBox:SetText("A64CFF")
    hexBox:SetMaxLetters(6)
    local function ApplyHex()
        local hex = hexBox:GetText():match("^%s*(.-)%s*$"):upper()
        if #hex == 6 then
            local r = tonumber(hex:sub(1,2),16)
            local g = tonumber(hex:sub(3,4),16)
            local b = tonumber(hex:sub(5,6),16)
            if r and g and b then
                local D = CFLUtilSettings
                if D and D[cfgKey] then
                    D[cfgKey].r=r/255; D[cfgKey].g=g/255; D[cfgKey].b=b/255
                end
                swTex:SetColorTexture(r/255, g/255, b/255, 1)
                if onApply then onApply() end
            end
        end
    end
    hexBox:SetScript("OnEnterPressed", ApplyHex)
    hexBox:SetScript("OnEditFocusLost", ApplyHex)
    swatch:SetScript("OnClick", function()
        local D = CFLUtilSettings
        local cfg = D and D[cfgKey]
        local r0 = cfg and cfg.r or 0.65
        local g0 = cfg and cfg.g or 0.30
        local b0 = cfg and cfg.b or 1.00

        local function OnColorChanged()
            local r, g, b
            if ColorPickerFrame.GetColorRGB then
                r, g, b = ColorPickerFrame:GetColorRGB()
            end
            if not r then return end
            local D2 = CFLUtilSettings
            if D2 and D2[cfgKey] then
                D2[cfgKey].r=r; D2[cfgKey].g=g; D2[cfgKey].b=b
            end
            swTex:SetColorTexture(r, g, b, 1)
            hexBox:SetText(string.format("%02X%02X%02X",
                math.floor(r*255), math.floor(g*255), math.floor(b*255)))
            if onApply then onApply() end
        end

        local function OnColorCancelled(restoreR, restoreG, restoreB)
            -- Some client versions pass the previous color values directly,
            -- others expect us to read PreviousValues; fall back to r0/g0/b0.
            local r, g, b = restoreR, restoreG, restoreB
            if type(r) ~= "number" then r, g, b = r0, g0, b0 end
            local D2 = CFLUtilSettings
            if D2 and D2[cfgKey] then
                D2[cfgKey].r=r; D2[cfgKey].g=g; D2[cfgKey].b=b
            end
            swTex:SetColorTexture(r, g, b, 1)
            hexBox:SetText(string.format("%02X%02X%02X",
                math.floor(r*255), math.floor(g*255), math.floor(b*255)))
            if onApply then onApply() end
        end

        if ColorPickerFrame.SetupColorPickerAndShow then
            -- Modern API (Dragonflight / current retail).
            ColorPickerFrame:SetupColorPickerAndShow({
                r = r0, g = g0, b = b0,
                hasOpacity = false,
                swatchFunc = OnColorChanged,
                opacityFunc = OnColorChanged,
                cancelFunc = function(previousValues)
                    if previousValues then
                        OnColorCancelled(previousValues.r, previousValues.g, previousValues.b)
                    else
                        OnColorCancelled()
                    end
                end,
            })
        else
            -- Legacy API (pre-Dragonflight clients).
            ColorPickerFrame.hasOpacity = false
            ColorPickerFrame.func = OnColorChanged
            ColorPickerFrame.cancelFunc = function() OnColorCancelled() end
            ColorPickerFrame:SetColorRGB(r0, g0, b0)
            ColorPickerFrame:Hide(); ColorPickerFrame:Show()
        end
    end)
    return swatch, hexBox
end

-- ── Checkbox (anchored to TOPLEFT, used for top-level toggles) ──────
local function MakeCheckbox(parent, label, yOff, onToggle)
    local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    cb:SetSize(24, 24)
    cb:SetPoint("TOPLEFT", parent, "TOPLEFT", UP_U, -yOff)
    cb:SetScript("OnClick", function(self)
        self._checked = self:GetChecked()
        if onToggle then onToggle(self._checked) end
    end)
    local lbl = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    lbl:SetText(label)
    lbl:SetTextColor(C.text[1], C.text[2], C.text[3])
    lbl:SetPoint("LEFT", cb, "RIGHT", 4, 0)
    cb.SetCheckedState = function(self, v)
        self._checked = v
        self:SetChecked(v)
    end
    cb._check = cb
    return cb, lbl
end

-- ── Stat block ────────────────────────────────────────────────────
-- Three of these are created (fps, ms, dur). Fully independent: each one
-- reads and writes only its own CFLUtilSettings[cfgKey] table.
local function StatBlock(yBase, label, cfgKey, showKey)
    local function onApply()
        if not CFLUtilSettings then return end
        CFL_ApplyHUDState()
        RefreshWidgetDisplay(cfgKey)
    end

    local cb, cbLbl = MakeCheckbox(UtilFrame, label, yBase, function(checked)
        if CFLUtilSettings then
            CFLUtilSettings[showKey] = checked
        end
        CFL_ApplyHUDState()
    end)
    cb:SetCheckedState(false)

    local fontLbl = UtilFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    fontLbl:SetText("Font:")
    fontLbl:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
    fontLbl:SetPoint("TOPLEFT", UtilFrame, "TOPLEFT", UP_U + 30, -(yBase + 28))

    local ddBtn  = MakeFontDropdown(UtilFrame, cfgKey, onApply, fontLbl, 6, 0)

    local sizeLbl = UtilFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    sizeLbl:SetText("Size:")
    sizeLbl:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
    sizeLbl:SetPoint("LEFT", ddBtn, "RIGHT", 14, 0)

    local sizeEB = MakeSizeSpinner(UtilFrame, cfgKey, onApply, sizeLbl, 6, 0)

    local colorLbl = UtilFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    colorLbl:SetText("Color:")
    colorLbl:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
    colorLbl:SetPoint("LEFT", sizeEB, "RIGHT", 14, 0)

    local swatch, hexBox = MakeColorPicker(UtilFrame, cfgKey, onApply, colorLbl, 6, 0)

    local outlineDD = MakeOutlineDropdown(UtilFrame, cfgKey, onApply, hexBox, 10, 0)

    statWidgets[cfgKey] = {
        cb=cb, cbLbl=cbLbl, fontLbl=fontLbl, ddBtn=ddBtn,
        sizeLbl=sizeLbl, sizeEB=sizeEB, colorLbl=colorLbl,
        swatch=swatch, hexBox=hexBox, showKey=showKey,
        outlineDD=outlineDD,
    }
    return yBase + 62
end

function CFL_RefreshUtilUI()
    local D = CFLUtilSettings
    if not D then return end

    for cfgKey, w in pairs(statWidgets) do
        if cfgKey:sub(1,1) ~= "_" and w then
            RefreshWidgetDisplay(cfgKey)
            if w.cb then w.cb:SetCheckedState(D[w.showKey] or false) end
        end
    end

    if _lockCB_ref then
        _lockCB_ref:SetCheckedState(D.hudLocked or false)
        HUD:EnableMouse(not (D.hudLocked or false))
    end
end

-- ── Layout ────────────────────────────────────────────────────
ULabel("HUD Display", 16, "GameFontNormalLarge", C.accent[1], C.accent[2], C.accent[3])
USep(40, 0.3)
ULabel("Enable items to display them on-screen.  Drag the HUD element to reposition.",
    50, "GameFontNormal", C.sub[1], C.sub[2], C.sub[3])
USep(72, 0.3)

local nextY = 84
nextY = StatBlock(nextY, "FPS",          "fps", "showFPS")
USep(nextY - 8, 0.2)
nextY = StatBlock(nextY, "Latency (MS)", "ms",  "showMS")
USep(nextY - 8, 0.2)
nextY = StatBlock(nextY, "Durability %", "dur", "showDur")

local durNote = UtilFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
durNote:SetText("Durability shows overall gear condition as a percentage.")
durNote:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
durNote:SetPoint("TOPLEFT", UtilFrame, "TOPLEFT", UP_U, -nextY)
durNote:SetWidth(PANE_TXT_W_U)

-- HUD Controls — fixed position below all 3 stat blocks
local CTRL_Y = nextY + 34

local ctrlSep = Tex(UtilFrame, C.border[1], C.border[2], C.border[3], 0.15)
ctrlSep:SetHeight(1)
ctrlSep:SetPoint("TOPLEFT",  UtilFrame, "TOPLEFT",  UP_U, -(CTRL_Y - 10))
ctrlSep:SetPoint("TOPRIGHT", UtilFrame, "TOPRIGHT", -UP_U, -(CTRL_Y - 10))

local ctrlHdr = UtilFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
ctrlHdr:SetText("HUD Controls")
ctrlHdr:SetTextColor(C.sub[1], C.sub[2], C.sub[3])
ctrlHdr:SetPoint("TOPLEFT", UtilFrame, "TOPLEFT", UP_U, -(CTRL_Y))

local resetBtn = CreateFrame("Button", nil, UtilFrame, "BackdropTemplate")
resetBtn:SetSize(160, 26)
resetBtn:SetPoint("TOPLEFT", UtilFrame, "TOPLEFT", UP_U, -(CTRL_Y + 22))
Backdrop(resetBtn, {0.10, 0.06, 0.18, 1}, C.border)
local resetLbl = resetBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
resetLbl:SetText("Reset HUD Position")
resetLbl:SetTextColor(C.accent[1], C.accent[2], C.accent[3])
resetLbl:SetAllPoints(resetBtn)
resetLbl:SetJustifyH("CENTER")
resetBtn:SetScript("OnEnter", function(self) self:SetBackdropColor(0.20, 0.10, 0.32, 1) end)
resetBtn:SetScript("OnLeave", function(self) self:SetBackdropColor(0.10, 0.06, 0.18, 1) end)
resetBtn:SetScript("OnClick", function()
    if CFLUtilSettings then
        CFLUtilSettings.hudX = 0; CFLUtilSettings.hudY = -20
        CFL_ApplyHUDState()
    end
end)

local lockCB = MakeCheckbox(UtilFrame, "Lock Position", 0, function(checked)
    if CFLUtilSettings then
        CFLUtilSettings.hudLocked = checked
        HUD:EnableMouse(not checked)
    end
end)
lockCB:ClearAllPoints()
lockCB:SetPoint("LEFT", resetBtn, "RIGHT", 16, 0)

statWidgets["_lockCB"] = { cb = lockCB, showKey = "_lock" }
_lockCB_ref = lockCB

-- ============================================================
--  Slash command
-- ============================================================
SLASH_CFL1 = "/cfl"
SlashCmdList["CFL"] = function()
    if Main:IsShown() then Main:Hide() else Main:Show() end
end

local Options = CreateFrame("Frame")
Options.name  = addonName
if InterfaceOptions_AddCategory then
    InterfaceOptions_AddCategory(Options)
end

-- ============================================================
--  Minimap Button
-- ============================================================
local MinimapBtn = CreateFrame("Button", "CFLMinimapButton", Minimap)
MinimapBtn:SetSize(32, 32)
MinimapBtn:SetFrameStrata("MEDIUM")
MinimapBtn:SetFrameLevel(8)
MinimapBtn:SetClampedToScreen(true)
MinimapBtn:SetMovable(true)
MinimapBtn:RegisterForClicks("anyUp")
MinimapBtn:RegisterForDrag("LeftButton")

-- black background circle behind the icon
local bg = MinimapBtn:CreateTexture(nil, "BACKGROUND")
bg:SetSize(25, 25)
bg:SetPoint("CENTER", 0, 0)
bg:SetTexture("Interface\\Minimap\\UI-Minimap-Background")
bg:SetVertexColor(0, 0, 0, 1)

-- icon — sized to sit inside the border ring
local icon = MinimapBtn:CreateTexture(nil, "ARTWORK")
icon:SetSize(18, 18)
icon:SetPoint("CENTER", 0, 0)
icon:SetTexture("Interface\\AddOns\\CustomFontLoader\\cfl.png")

-- gold border overlay (standard minimap button look)
local overlay = MinimapBtn:CreateTexture(nil, "OVERLAY")
overlay:SetSize(53, 53)
overlay:SetPoint("TOPLEFT", MinimapBtn, "TOPLEFT", 0, 0)
overlay:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")

-- circular highlight on hover
MinimapBtn:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")

-- tooltip
local isDragging = false
MinimapBtn:SetScript("OnEnter", function(self)
    if isDragging then return end
    GameTooltip:SetOwner(self, "ANCHOR_NONE")
    GameTooltip:SetPoint("TOPRIGHT", self, "TOPLEFT", -2, 0)
    GameTooltip:SetText("Custom Font Loader", 0.65, 0.30, 1.00)
    GameTooltip:AddLine("Left-click: Toggle UI", 0.82, 0.78, 0.90)
    GameTooltip:AddLine("Drag: Reposition", 0.82, 0.78, 0.90)
    GameTooltip:Show()
end)
MinimapBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)

-- click to toggle
MinimapBtn:SetScript("OnClick", function()
    if Main:IsShown() then Main:Hide() else Main:Show() end
end)

-- position on minimap edge
local function UpdateMinimapPos(angle)
    local mw, mh   = Minimap:GetWidth(), Minimap:GetHeight()
    local radius   = (math.max(mw, mh) / 2) + 5
    local rad      = math.rad(angle)
    MinimapBtn:ClearAllPoints()
    MinimapBtn:SetPoint("CENTER", Minimap, "CENTER",
        math.cos(rad) * radius, math.sin(rad) * radius)
end

-- drag to reposition
local function DragOnUpdate()
    local mx, my   = Minimap:GetCenter()
    local cx, cy   = GetCursorPosition()
    local scale    = Minimap:GetEffectiveScale()
    cx, cy         = cx / scale, cy / scale
    CFLMinimapAngle = math.deg(math.atan2(cy - my, cx - mx))
    UpdateMinimapPos(CFLMinimapAngle)
end

MinimapBtn:SetScript("OnDragStart", function(self)
    isDragging = true
    self:LockHighlight()
    self:SetScript("OnUpdate", DragOnUpdate)
    GameTooltip:Hide()
end)

MinimapBtn:SetScript("OnDragStop", function(self)
    self:SetScript("OnUpdate", nil)
    self:UnlockHighlight()
    isDragging = false
    UpdateMinimapPos(CFLMinimapAngle)
end)

-- initialise position from saved angle (default 220 like EllesmereUI)
local initLoader = CreateFrame("Frame")
initLoader:RegisterEvent("ADDON_LOADED")
initLoader:SetScript("OnEvent", function(self, event, addon)
    if addon ~= addonName then return end
    if not CFLMinimapAngle then CFLMinimapAngle = 220 end
    UpdateMinimapPos(CFLMinimapAngle)
end)