-- OldManCursor_Options.lua

local ADDON_NAME = "OldManCursor"
local floor = math.floor
local format = string.format
local tremove = table.remove
local tinsert = table.insert
local tconcat = table.concat

local panel = CreateFrame("Frame", ADDON_NAME .. "OptionsPanel")
panel.name = ADDON_NAME

local State = {
    built = false,
    previewActive = false,
    currentTabKey = "GENERAL",
}

local UI = {
    previewButton = nil,
    UpdatePreviewButtonState = nil,

    layerContent = nil,
    layerScrollFrame = nil,

    lineLengthModeWidgets = nil,
    lineThicknessWidgets = nil,
    lineGapWidgets = nil,
    lineOpacityWidgets = nil,
    lineBlendDropdown = nil,
    lineColorWidgets = nil,

    exportEditBox = nil,
    importEditBox = nil,
    exportStatusText = nil,
    importStatusText = nil,

    profilesStatusText = nil,
    newProfileEditBox = nil,
    existingProfilesDropdown = nil,
    generalVisibilityWidgets = nil,

    lineHeadingText = nil,
    layersHeadingText = nil,
    lineLockOverlay = nil,
    layersLockOverlay = nil,
}

local Lists = {
    layerRows = {},
    layerRowWidgets = {},
    tabs = {},
}

local PROFILE_PREFIX = "OMC:"
local PROFILE_VERSION = "1"

local imexportBoxWidth = 370

local availableTextures = OldManCursor_AvailableTextures

local availableBlendModes = {
    { text = "Blend (Default)", value = "BLEND" },
    { text = "Add (Glow)",      value = "ADD" },
    { text = "Disable (Crisp)", value = "DISABLE" },
    { text = "Mod (Darken)",    value = "MOD" },
    { text = "Mod2x (Strong)",  value = "MOD2X" },
}

-- Returns the current character's per-character settings table.
local function GetCharDB()
    if _G.OldManCursor_GetCharacterDB then
        return _G.OldManCursor_GetCharacterDB()
    end
    -- Fallback: should not be reached if load order is correct.
    return OldManCursorDB
end

-- Returns the root SavedVariables table.  Only global (non-profile) keys
-- should be read or written via this function.
local function GetDB()
    OldManCursorDB = OldManCursorDB or {}
    OldManCursorDB.PROFILES = OldManCursorDB.PROFILES or {}

    if OldManCursorDB.NEW_PROFILE_NAME        == nil then OldManCursorDB.NEW_PROFILE_NAME        = ""        end
    if OldManCursorDB.COPY_FROM_PROFILE       == nil then OldManCursorDB.COPY_FROM_PROFILE       = ""        end
    if OldManCursorDB.DELETE_PROFILE          == nil then OldManCursorDB.DELETE_PROFILE          = ""        end
    if OldManCursorDB.EXISTING_PROFILE_SELECT == nil or OldManCursorDB.EXISTING_PROFILE_SELECT == "" or
       not (OldManCursorDB.PROFILES and OldManCursorDB.PROFILES[OldManCursorDB.EXISTING_PROFILE_SELECT]) then
        OldManCursorDB.EXISTING_PROFILE_SELECT = GetCharDB().CURRENT_PROFILE or "DEFAULT"
    end

    return OldManCursorDB
end

-- Returns the per-profile data table currently being edited/previewed.
-- All line settings and layer data should be read and written via this.
local function GetProfileDB()
    if _G.OldManCursor_GetActiveProfileData then
        return _G.OldManCursor_GetActiveProfileData()
    end
    -- Fallback: should not be reached if load order is correct.
    return OldManCursorDB
end

local Trim = _G.OldManCursor_Trim

local function GetEditingProfileName()
    local db = GetDB()
    return db.EXISTING_PROFILE_SELECT or GetCharDB().CURRENT_PROFILE or "DEFAULT"
end

local function GetPreviewTargetProfileName()
    local db = GetDB()
    return db.EXISTING_PROFILE_SELECT or GetCharDB().CURRENT_PROFILE or "DEFAULT"
end

local function IsPreviewActive()
    if _G.OldManCursor_GetPreviewState then
        return _G.OldManCursor_GetPreviewState() and true or false
    end
    return State.previewActive and true or false
end

local function CanApplyLiveEdit()
    return IsPreviewActive()
end

local function UpdateEditingProfileTexts()
    if UI.lineHeadingText then
        UI.lineHeadingText:SetText("Line Settings")
    end
    if UI.layersHeadingText then
        local layerCount = #((GetProfileDB and GetProfileDB() or {}).LAYERS or {})
        UI.layersHeadingText:SetText("Layers (" .. layerCount .. ")")
    end
end

local function RefreshReticle()
    if _G.OldManCursor_ApplySettings then
        _G.OldManCursor_ApplySettings()
    end
end

local function SaveActiveProfile()
    if _G.OldManCursor_SaveActiveProfile then
        _G.OldManCursor_SaveActiveProfile()
    end
end

local function SetPreview(enabled, profileName)
    if _G.OldManCursor_SetPreview then
        _G.OldManCursor_SetPreview(enabled, profileName)
    end
    State.previewActive = IsPreviewActive()
end

local function UpdatePreviewButton()
    if not UI.previewButton then
        return
    end

    State.previewActive = IsPreviewActive()

    if State.previewActive then
        UI.previewButton:SetText("Stop Edit & Preview:")
    else
        UI.previewButton:SetText("Edit & Preview:")
    end
end

UI.UpdatePreviewButtonState = function()
    if not UI.previewButton then
        return
    end

    UI.previewButton:Enable()

    if UI.previewButton.tooltipOverlay then
        UI.previewButton.tooltipOverlay:EnableMouse(false)
        GameTooltip:Hide()
    end

    UpdatePreviewButton()
end

local function UpdateEditModeUI()
    local unlocked = IsPreviewActive()
    -- UpdateEditingProfileTexts() owns the text content; we only set colour here.
    UpdateEditingProfileTexts()

    if UI.lineHeadingText then
        if unlocked then
            UI.lineHeadingText:SetTextColor(1, 0.82, 0, 1)
        else
            UI.lineHeadingText:SetTextColor(1, 0.2, 0.2, 1)
        end
    end

    if UI.layersHeadingText then
        if unlocked then
            UI.layersHeadingText:SetTextColor(1, 0.82, 0, 1)
        else
            UI.layersHeadingText:SetTextColor(1, 0.2, 0.2, 1)
        end
    end

    if UI.lineLockOverlay then
        if unlocked then
            UI.lineLockOverlay:Hide()
        else
            UI.lineLockOverlay:Show()
        end
    end

    if UI.layersLockOverlay then
        if unlocked then
            UI.layersLockOverlay:Hide()
        else
            UI.layersLockOverlay:Show()
        end
    end
end

local function FindEntryByValue(entries, value)
    for _, entry in ipairs(entries) do
        if entry.value == value then
            return entry
        end
    end
    return entries[1]
end

local function Clamp(value, minValue, maxValue)
    if value < minValue then
        return minValue
    end
    if value > maxValue then
        return maxValue
    end
    return value
end

local function SplitString(str, sep)
    local out = {}
    if str == nil or str == "" then
        return out
    end

    local start = 1

    while true do
        local pos = string.find(str, sep, start, true)
        if not pos then
            tinsert(out, string.sub(str, start))
            break
        end
        tinsert(out, string.sub(str, start, pos - 1))
        start = pos + 1
    end

    return out
end

local function EncodeBase64(data)
    local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    return ((data:gsub(".", function(x)
        local r, byte = "", x:byte()
        for i = 8, 1, -1 do
            r = r .. ((byte % 2 ^ i - byte % 2 ^ (i - 1) > 0) and "1" or "0")
        end
        return r
    end) .. "0000"):gsub("%d%d%d?%d?%d?%d?", function(x)
        if #x < 6 then
            return ""
        end
        local c = 0
        for i = 1, 6 do
            if x:sub(i, i) == "1" then
                c = c + 2 ^ (6 - i)
            end
        end
        return b:sub(c + 1, c + 1)
    end) .. ({ "", "==", "=" })[#data % 3 + 1])
end

local function DecodeBase64(data)
    local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    data = string.gsub(data or "", "[^" .. b .. "=]", "")

    return (data:gsub(".", function(x)
        if x == "=" then
            return ""
        end

        local r, f = "", (b:find(x, 1, true) or 1) - 1
        for i = 6, 1, -1 do
            r = r .. ((f % 2 ^ i - f % 2 ^ (i - 1) > 0) and "1" or "0")
        end
        return r
    end):gsub("%d%d%d?%d?%d?%d?%d?%d?", function(x)
        if #x ~= 8 then
            return ""
        end

        local c = 0
        for i = 1, 8 do
            if x:sub(i, i) == "1" then
                c = c + 2 ^ (8 - i)
            end
        end
        return string.char(c)
    end))
end

local function SerializeColor(color)
    color = color or {1, 1, 1}
    local r = Clamp(tonumber(color[1]) or 1, 0, 1)
    local g = Clamp(tonumber(color[2]) or 1, 0, 1)
    local b = Clamp(tonumber(color[3]) or 1, 0, 1)
    return format("%.3f,%.3f,%.3f", r, g, b)
end

local function ParseColor(str)
    local parts = SplitString(str or "", ",")
    local r = Clamp(tonumber(parts[1] or "") or 1, 0, 1)
    local g = Clamp(tonumber(parts[2] or "") or 1, 0, 1)
    local b = Clamp(tonumber(parts[3] or "") or 1, 0, 1)
    return { r, g, b }
end

local function SerializeLayer(layer)
    return tconcat({
        layer.TEXTURE or "",
        tostring(Clamp(floor((tonumber(layer.SIZE) or 64) + 0.5), 1, 9999)),
        format("%.2f", Clamp(tonumber(layer.OPACITY) or 1, 0, 1)),
        SerializeColor(layer.COLOR),
        layer.BLEND or "BLEND",
        tostring(Clamp(floor((tonumber(layer.ROTATION) or 0) + 0.5), 0, 999))
    }, "~")
end

local function ParseLayer(str)
    local parts = SplitString(str or "", "~")
    if #parts < 6 or parts[1] == "" then
        return nil
    end

    return {
        TEXTURE = parts[1],
        SIZE = Clamp(floor((tonumber(parts[2]) or 64) + 0.5), 1, 9999),
        OPACITY = Clamp(tonumber(parts[3]) or 1, 0, 1),
        COLOR = ParseColor(parts[4]),
        BLEND = parts[5] ~= "" and parts[5] or "BLEND",
        ROTATION = Clamp(floor((tonumber(parts[6]) or 0) + 0.5), 0, 999),
    }
end

local function GenerateProfileString()
    local profile = GetProfileDB()
    local layerStrings = {}

    for _, layer in ipairs(profile.LAYERS or {}) do
        tinsert(layerStrings, SerializeLayer(layer))
    end

    local raw = tconcat({
        "OMC",
        PROFILE_VERSION,
        tostring(Clamp(floor((tonumber(profile.LINE_GAP_SIZE) or 42) + 0.5), 0, 512)),
        profile.LINE_BLEND or "BLEND",
        format("%.2f", Clamp(tonumber(profile.LINE_OPACITY) or 0.25, 0, 1)),
        tostring(Clamp(floor((tonumber(profile.LINE_THICKNESS) or 1) + 0.5), 1, 10)),
        SerializeColor(profile.LINE_COLOR),
        profile.LINE_LENGTH_MODE or "SCREEN",
        tostring(math.max(1, floor((tonumber(profile.LINE_EDGE_OFFSET_X) or 1) + 0.5))),
        tostring(math.max(1, floor((tonumber(profile.LINE_EDGE_OFFSET_Y) or 1) + 0.5))),
        tconcat(layerStrings, "^")
    }, "|")

    return PROFILE_PREFIX .. EncodeBase64(raw)
end

local function ApplyProfileString(profileString)
    if not profileString or profileString == "" then
        return false, "Import box is empty."
    end

    local cleaned = profileString:gsub("%s+", "")
    if string.sub(cleaned, 1, #PROFILE_PREFIX) ~= PROFILE_PREFIX then
        return false, "Invalid prefix."
    end

    local encoded = string.sub(cleaned, #PROFILE_PREFIX + 1)
    local raw = DecodeBase64(encoded)
    if not raw or raw == "" then
        return false, "Could not decode profile string."
    end

    local fields = SplitString(raw, "|")
    if #fields < 10 then
        return false, "Profile string is incomplete."
    end

    if fields[1] ~= "OMC" then
        return false, "Unknown profile format."
    end

    if fields[2] ~= PROFILE_VERSION then
        return false, "Unsupported profile version."
    end

    local db = GetDB()
    local profileName = GetEditingProfileName()
    local profile = db.PROFILES and db.PROFILES[profileName]
    if not profile then
        return false, "Target profile not found."
    end

    profile.LINE_GAP_SIZE      = Clamp(floor((tonumber(fields[3]) or 42) + 0.5), 0, 512)
    profile.LINE_BLEND         = (fields[4] ~= "" and fields[4]) or "BLEND"
    profile.LINE_OPACITY       = Clamp(tonumber(fields[5]) or 0.25, 0, 1)
    profile.LINE_THICKNESS     = Clamp(floor((tonumber(fields[6]) or 1) + 0.5), 1, 10)
    profile.LINE_COLOR         = ParseColor(fields[7])
    profile.LINE_LENGTH_MODE   = (fields[8] == "OFFSET") and "OFFSET" or "SCREEN"
    profile.LINE_EDGE_OFFSET_X = math.max(1, floor((tonumber(fields[9])  or 1) + 0.5))
    profile.LINE_EDGE_OFFSET_Y = math.max(1, floor((tonumber(fields[10]) or 1) + 0.5))

    profile.LAYERS = {}

    local layerBlob = fields[11] or ""
    if layerBlob ~= "" then
        local rawLayers = SplitString(layerBlob, "^")
        for _, rawLayer in ipairs(rawLayers) do
            local parsedLayer = ParseLayer(rawLayer)
            if parsedLayer then
                tinsert(profile.LAYERS, parsedLayer)
            end
        end
    end

    if #profile.LAYERS == 0 then
        tinsert(profile.LAYERS, {
            TEXTURE = availableTextures[1].value,
            SIZE = 64,
            OPACITY = 1.0,
            COLOR = {1, 1, 1},
            BLEND = "BLEND",
            ROTATION = 0,
        })
    end

    return true, "Profile imported."
end

local function SetProfileStatus(target, message, isError)
    local textWidget = nil

    if target == "EXPORT" then
        textWidget = UI.exportStatusText
    elseif target == "IMPORT" then
        textWidget = UI.importStatusText
    elseif target == "PROFILES" then
        textWidget = UI.profilesStatusText
    end

    if textWidget then
        textWidget:SetText(message or "")
        if isError then
            textWidget:SetTextColor(1, 0.2, 0.2)
        else
            textWidget:SetTextColor(0.2, 1, 0.2)
        end
    end
end

local function CreateMultiLineBox(parent, name, width, height)
    local holder = CreateFrame("Frame", name .. "Holder", parent, "BackdropTemplate")
    holder:SetSize(width, height)
    holder:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 }
    })
    holder:SetBackdropColor(0, 0, 0, 0.6)
    holder:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)

    local scrollFrame = CreateFrame("ScrollFrame", name .. "ScrollFrame", holder, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", 4, -4)
    scrollFrame:SetPoint("BOTTOMRIGHT", -26, 4)

    local editBox = CreateFrame("EditBox", name, scrollFrame)
    editBox:SetMultiLine(true)
    editBox:SetFontObject("ChatFontNormal")
    editBox:SetWidth(width - 34)
    editBox:SetAutoFocus(false)
    editBox:SetTextInsets(4, 4, 4, 4)

    editBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)

    editBox:SetScript("OnTextChanged", function(self)
        local textHeight = self:GetStringHeight()
        local minHeight = height - 8
        if textHeight < minHeight then
            textHeight = minHeight
        end
        self:SetHeight(textHeight + 8)
    end)

    scrollFrame:SetScrollChild(editBox)
    editBox:SetHeight(height - 8)

    local clickCatcher = CreateFrame("Button", nil, holder)
    clickCatcher:SetPoint("TOPLEFT", 4, -4)
    clickCatcher:SetPoint("BOTTOMRIGHT", -26, 4)
    clickCatcher:SetFrameLevel(scrollFrame:GetFrameLevel() - 1)
    clickCatcher:SetScript("OnMouseDown", function()
        editBox:SetFocus()
    end)

    editBox:SetFrameLevel(scrollFrame:GetFrameLevel() + 1)

    holder.scrollFrame = scrollFrame

    return holder, editBox
end

local function SetWidgetEnabled(widget, enabled)
    if not widget then
        return
    end

    if widget.SetEnabled then
        widget:SetEnabled(enabled)
    end

    if widget.Enable then
        if enabled then
            widget:Enable()
        else
            widget:Disable()
        end
    end

    if widget.SetAlpha then
        widget:SetAlpha(enabled and 1 or 0.45)
    end
end

local function CreateEditLockOverlay(parent)
    local overlay = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    overlay:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -28)
    overlay:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)
    overlay:SetFrameStrata("FULLSCREEN_DIALOG")
    overlay:SetFrameLevel(parent:GetFrameLevel() + 50)
    overlay:EnableMouse(true)
    overlay:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        tile = true,
        tileSize = 16,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    overlay:SetBackdropColor(0, 0, 0, 0.85)

    local text = overlay:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    text:SetPoint("CENTER", overlay, "CENTER", 0, 10)
    text:SetText("Edit & Preview is required")
    text:SetTextColor(1, 0.2, 0.2, 1)

    local subText = overlay:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
    subText:SetPoint("TOP", text, "BOTTOM", 0, -10)
    subText:SetText("Use the button at the top-right to unlock live editing.")
    subText:SetTextColor(1, 1, 1, 1)

    overlay:SetScript("OnMouseDown", function()
    end)

    overlay:Hide()
    return overlay
end

local function GetSavedProfileEntries(excludeName)
    if _G.OldManCursor_GetProfileEntries then
        return _G.OldManCursor_GetProfileEntries(excludeName)
    end

    local db = GetDB()
    local out = {}

    for name in pairs(db.PROFILES or {}) do
        if name ~= excludeName then
            out[#out + 1] = { text = name, value = name }
        end
    end

    table.sort(out, function(a, b)
        if a.value == "DEFAULT" then
            return true
        end
        if b.value == "DEFAULT" then
            return false
        end
        return string.lower(a.text) < string.lower(b.text)
    end)

    return out
end

local function RefreshDropdownSelection(dropdown, entries, selectedValue, fallbackText)
    if not dropdown then
        return
    end

    if not entries or #entries == 0 then
        UIDropDownMenu_SetSelectedValue(dropdown, nil)
        UIDropDownMenu_SetText(dropdown, fallbackText or "")
        return
    end

    local selected = FindEntryByValue(entries, selectedValue or entries[1].value)
    UIDropDownMenu_SetSelectedValue(dropdown, selected.value)
    UIDropDownMenu_SetText(dropdown, selected.text)
end

local function RefreshLineWidgets()
    if UI.lineThicknessWidgets and UI.lineThicknessWidgets.RefreshFromDB then
        UI.lineThicknessWidgets:RefreshFromDB()
    end

    if UI.lineGapWidgets and UI.lineGapWidgets.RefreshFromDB then
        UI.lineGapWidgets:RefreshFromDB()
    end

    if UI.lineOpacityWidgets and UI.lineOpacityWidgets.RefreshFromDB then
        UI.lineOpacityWidgets:RefreshFromDB()
    end

    if UI.lineBlendDropdown and UI.lineBlendDropdown.RefreshFromDB then
        UI.lineBlendDropdown:RefreshFromDB()
    end

    if UI.lineColorWidgets and UI.lineColorWidgets.RefreshFromDB then
        UI.lineColorWidgets:RefreshFromDB()
    end

    if UI.lineLengthModeWidgets then
        if UI.lineLengthModeWidgets.RefreshLimits then
            UI.lineLengthModeWidgets.RefreshLimits()
        end
        if UI.lineLengthModeWidgets.UpdateModeUI then
            UI.lineLengthModeWidgets.UpdateModeUI()
        end
    end

    UpdateEditingProfileTexts()
    UpdatePreviewButton()
    UpdateEditModeUI()
end

local function RefreshAllProfileWidgets()
    local db = GetDB()

    if not db.EXISTING_PROFILE_SELECT or db.EXISTING_PROFILE_SELECT == "" then
        db.EXISTING_PROFILE_SELECT = GetCharDB().CURRENT_PROFILE or "DEFAULT"
    end

    if UI.existingProfilesDropdown and UI.existingProfilesDropdown.RefreshList then
        UI.existingProfilesDropdown:RefreshList()
    end

    if UI.generalVisibilityWidgets and UI.generalVisibilityWidgets.RefreshProfileDropdowns then
        UI.generalVisibilityWidgets:RefreshProfileDropdowns()
    end

    if UI.newProfileEditBox then
        UI.newProfileEditBox:SetText(db.NEW_PROFILE_NAME or "")
    end

    if UI.copyDropdown and UI.copyDropdown.RefreshList then UI.copyDropdown:RefreshList() end
    if UI.deleteDropdown and UI.deleteDropdown.RefreshList then UI.deleteDropdown:RefreshList() end

    State.previewActive = IsPreviewActive()
    RefreshLineWidgets()
    UpdateEditingProfileTexts()
    UpdatePreviewButton()
    UpdateEditModeUI()
end

-- dbGetter is an optional function returning the table that owns `key`.
-- Pass GetProfileDB for profile-level keys, or omit for root-DB keys.
local function CreateValueDropdown(parent, key, labelText, entries, xOffset, yOffset, dbGetter)
    dbGetter = dbGetter or GetDB

    local label = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    label:SetPoint("TOPLEFT", xOffset, yOffset)
    label:SetText(labelText)

    local frameName = ADDON_NAME .. key .. "Dropdown"
    local dropdown = CreateFrame("Frame", frameName, parent, "UIDropDownMenuTemplate")
    dropdown:SetPoint("TOPLEFT", label, "BOTTOMLEFT", -16, -6)
    UIDropDownMenu_SetWidth(dropdown, 160)

    UIDropDownMenu_Initialize(dropdown, function(self, level)
        local db = dbGetter()
        for _, entry in ipairs(entries) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = entry.text
            info.value = entry.value
            info.func = function()
                if not CanApplyLiveEdit() then
                    dropdown:RefreshFromDB()
                    return
                end

                dbGetter()[key] = entry.value
                UIDropDownMenu_SetSelectedValue(dropdown, entry.value)
                UIDropDownMenu_SetText(dropdown, entry.text)
                RefreshReticle()
                UpdateEditingProfileTexts()
                UpdatePreviewButton()
            end
            info.checked = (db[key] == entry.value)
            UIDropDownMenu_AddButton(info, level)
        end
    end)

    dropdown.RefreshFromDB = function(self)
        local db = dbGetter()
        local entry = FindEntryByValue(entries, db[key] or entries[1].value)
        db[key] = entry.value
        UIDropDownMenu_SetSelectedValue(self, entry.value)
        UIDropDownMenu_SetText(self, entry.text)
    end

    dropdown:RefreshFromDB()

    return dropdown
end

local function CreateCompactNumberBox(parent, width, maxLetters, initialValue, formatter)
    local editBox = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
    editBox:SetSize(width, 20)
    editBox:SetAutoFocus(false)
    editBox:SetNumeric(false)
    editBox:SetMaxLetters(maxLetters)
    editBox:SetAltArrowKeyMode(false)
    editBox:SetMultiLine(false)

    local currentValue = initialValue

    local function SetDisplayValue(value)
        currentValue = value
        if formatter then
            editBox:SetText(formatter(value))
        else
            editBox:SetText(tostring(value or ""))
        end
    end

    editBox:SetScript("OnEscapePressed", function(self)
        SetDisplayValue(currentValue)
        self:ClearFocus()
    end)

    editBox:SetScript("OnEnterPressed", function(self)
        self:ClearFocus()
    end)

    editBox:SetScript("OnEditFocusLost", function(self)
    end)

    SetDisplayValue(initialValue)
    editBox.SetDisplayValue = SetDisplayValue
    return editBox
end

local function CreateColorButton(parent, getColor, setColor)
    local swatch = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    swatch:SetSize(40, 20)

    local preview = swatch:CreateTexture(nil, "ARTWORK")
    preview:SetSize(12, 12)
    preview:SetPoint("CENTER", swatch, "CENTER", 0, 0)
    preview:SetTexture("Interface\\Buttons\\WHITE8X8")

    local function UpdatePreview()
        local r, g, b = getColor()
        preview:SetVertexColor(r, g, b, 1)
    end

    swatch:SetScript("OnClick", function()
        if not CanApplyLiveEdit() then
            return
        end

        local r, g, b = getColor()

        ColorPickerFrame:SetupColorPickerAndShow({
            r = r,
            g = g,
            b = b,
            opacity = 0,
            hasOpacity = false,
            swatchFunc = function()
                if not CanApplyLiveEdit() then
                    return
                end

                local nr, ng, nb = ColorPickerFrame:GetColorRGB()
                setColor(nr, ng, nb)
                UpdatePreview()
                RefreshReticle()
                UpdateEditingProfileTexts()
                UpdatePreviewButton()
            end,
            cancelFunc = function(previousValues)
                if previousValues and CanApplyLiveEdit() then
                    setColor(previousValues.r, previousValues.g, previousValues.b)
                    UpdatePreview()
                    RefreshReticle()
                    UpdateEditingProfileTexts()
                    UpdatePreviewButton()
                end
            end,
        })
    end)

    UpdatePreview()
    swatch.UpdatePreview = UpdatePreview
    return swatch
end

local function CreateLayerTextureDropdown(parent, layerIndex)
    local dropdown = CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate")
    UIDropDownMenu_SetWidth(dropdown, 140)
    UIDropDownMenu_SetText(dropdown, "")

    UIDropDownMenu_Initialize(dropdown, function(self, level)
        local layers = GetProfileDB().LAYERS
        for _, entry in ipairs(availableTextures) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = entry.text
            info.value = entry.value
            info.func = function()
                if not CanApplyLiveEdit() then return end
                local l = GetProfileDB().LAYERS
                if l[layerIndex] then
                    l[layerIndex].TEXTURE = entry.value
                    UIDropDownMenu_SetSelectedValue(dropdown, entry.value)
                    UIDropDownMenu_SetText(dropdown, entry.text)
                    RefreshReticle()
                    UpdateEditingProfileTexts()
                    UpdatePreviewButton()
                end
            end
            info.checked = (layers[layerIndex] and layers[layerIndex].TEXTURE == entry.value)
            UIDropDownMenu_AddButton(info, level)
        end
    end)

    local layers = GetProfileDB().LAYERS
    if layers[layerIndex] then
        local selected = FindEntryByValue(availableTextures, layers[layerIndex].TEXTURE)
        layers[layerIndex].TEXTURE = selected.value
        UIDropDownMenu_SetSelectedValue(dropdown, selected.value)
        UIDropDownMenu_SetText(dropdown, selected.text)
    end

    return dropdown
end

local function CreateLayerBlendDropdown(parent, layerIndex)
    local dropdown = CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate")
    UIDropDownMenu_SetWidth(dropdown, 100)
    UIDropDownMenu_SetText(dropdown, "")

    UIDropDownMenu_Initialize(dropdown, function(self, level)
        local layers = GetProfileDB().LAYERS
        for _, entry in ipairs(availableBlendModes) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = entry.text
            info.value = entry.value
            info.func = function()
                if not CanApplyLiveEdit() then return end
                local l = GetProfileDB().LAYERS
                if l[layerIndex] then
                    l[layerIndex].BLEND = entry.value
                    UIDropDownMenu_SetSelectedValue(dropdown, entry.value)
                    UIDropDownMenu_SetText(dropdown, entry.text)
                    RefreshReticle()
                    UpdateEditingProfileTexts()
                    UpdatePreviewButton()
                end
            end
            info.checked = (layers[layerIndex] and layers[layerIndex].BLEND == entry.value)
            UIDropDownMenu_AddButton(info, level)
        end
    end)

    local layers = GetProfileDB().LAYERS
    if layers[layerIndex] then
        local selected = FindEntryByValue(availableBlendModes, layers[layerIndex].BLEND or "BLEND")
        layers[layerIndex].BLEND = selected.value
        UIDropDownMenu_SetSelectedValue(dropdown, selected.value)
        UIDropDownMenu_SetText(dropdown, selected.text)
    end

    return dropdown
end

local function ClearLayerRows()
    for _, row in ipairs(Lists.layerRows) do
        row:Hide()
    end
    wipe(Lists.layerRows)
    wipe(Lists.layerRowWidgets)
end

local function MoveLayerUp(index)
    local layers = GetProfileDB().LAYERS
    if index <= 1 then return end
    layers[index], layers[index - 1] = layers[index - 1], layers[index]
end

local function MoveLayerDown(index)
    local layers = GetProfileDB().LAYERS
    if index >= #layers then return end
    layers[index], layers[index + 1] = layers[index + 1], layers[index]
end


local function CreateStyledLayerSlider(parent, width, minVal, maxVal, step, lowText, highText)
    -- Anonymous frame: no global name, so no stale _G child pointers across rebuilds.
    -- OptionsSliderTemplate in WoW 10+ exposes Low/High/Text as parentKey fields directly
    -- on the slider frame, so we can set them without _G lookups.
    local slider = CreateFrame("Slider", nil, parent, "OptionsSliderTemplate")
    slider:SetWidth(width)
    slider:SetMinMaxValues(minVal, maxVal)
    slider:SetValueStep(step)
    slider:SetObeyStepOnDrag(true)
    if slider.Low  then
        slider.Low:SetText(tostring(lowText))
        slider.Low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 3, 3)
    end
    if slider.High then
        slider.High:SetText(tostring(highText))
        slider.High:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", -3, 3)
    end
    if slider.Text then slider.Text:SetText("") end
    return slider
end

local RebuildLayerList

RebuildLayerList = function()
    if not UI.layerContent then
        return
    end

    local profile = GetProfileDB()
    ClearLayerRows()

    local rowHeight = 130
    local startY = -6

    for i, layer in ipairs(profile.LAYERS or {}) do
        local row = CreateFrame("Frame", nil, UI.layerContent, "InsetFrameTemplate")
        row:SetSize(350, rowHeight)
        row:SetPoint("TOPLEFT", 6, startY - ((i - 1) * rowHeight))
        Lists.layerRows[i] = row

        if i % 2 == 0 then
            local highlight = row:CreateTexture(nil, "BACKGROUND")
            highlight:SetAllPoints(row)
            highlight:SetColorTexture(1, 1, 1, 0.06)
        end

        -- Sub-row 1: number, up/down, color button, dupe button, delete button
        local numberText = row:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        numberText:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -8)
        numberText:SetText("#" .. i)

        local upButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
        upButton:SetSize(20, 20)
        upButton:SetText("")
        upButton:SetPoint("TOPLEFT", numberText, "TOPRIGHT", 4, 2)
        upButton:SetScript("OnClick", function()
            if not CanApplyLiveEdit() then return end
            MoveLayerUp(i)
            RebuildLayerList()
            RefreshReticle()
            UpdateEditingProfileTexts()
            UpdatePreviewButton()
        end)

        local upTex = upButton:CreateTexture(nil, "ARTWORK")
        upTex:SetSize(36, 36)
        upTex:SetPoint("CENTER", upButton, "CENTER", 0, 0)
        upTex:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up")

        local downButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
        downButton:SetSize(20, 20)
        downButton:SetText("")
        downButton:SetPoint("LEFT", upButton, "RIGHT", 2, 0)
        downButton:SetScript("OnClick", function()
            if not CanApplyLiveEdit() then return end
            MoveLayerDown(i)
            RebuildLayerList()
            RefreshReticle()
            UpdateEditingProfileTexts()
            UpdatePreviewButton()
        end)

        local downTex = downButton:CreateTexture(nil, "ARTWORK")
        downTex:SetSize(36, 36)
        downTex:SetPoint("CENTER", downButton, "CENTER", 0, 0)
        downTex:SetTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up")

        local colorButton = CreateColorButton(
            row,
            function()
                local layers = GetProfileDB().LAYERS
                local color = layers[i] and layers[i].COLOR or {1, 1, 1}
                return color[1] or 1, color[2] or 1, color[3] or 1
            end,
            function(r, g, b)
                local layers = GetProfileDB().LAYERS
                if layers[i] then
                    layers[i].COLOR = layers[i].COLOR or {}
                    layers[i].COLOR[1] = r
                    layers[i].COLOR[2] = g
                    layers[i].COLOR[3] = b
                end
            end
        )
        colorButton:SetPoint("LEFT", downButton, "RIGHT", 10, 1)

        local deleteButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
        deleteButton:SetSize(64, 20)
        deleteButton:SetText("Delete")
        deleteButton:SetPoint("TOPRIGHT", row, "TOPRIGHT", -4, -5)
        deleteButton:SetScript("OnClick", function()
            if not CanApplyLiveEdit() then return end
            tremove(GetProfileDB().LAYERS, i)
            RebuildLayerList()
            RefreshReticle()
            UpdateEditingProfileTexts()
            UpdatePreviewButton()
        end)

        local dupeButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate")
        dupeButton:SetSize(48, 20)
        dupeButton:SetText("Dupe")
        dupeButton:SetPoint("RIGHT", deleteButton, "LEFT", -8, 0)
        dupeButton:SetScript("OnClick", function()
            if not CanApplyLiveEdit() then return end
            local layers = GetProfileDB().LAYERS
            if layers[i] then
                local src = layers[i]
                local copy = {
                    TEXTURE  = src.TEXTURE,
                    SIZE     = src.SIZE,
                    OPACITY  = src.OPACITY,
                    COLOR    = { (src.COLOR or {})[1] or 1, (src.COLOR or {})[2] or 1, (src.COLOR or {})[3] or 1 },
                    BLEND    = src.BLEND,
                    ROTATION = src.ROTATION,
                }
                tinsert(layers, i + 1, copy)
                RebuildLayerList()
                RefreshReticle()
                UpdateEditingProfileTexts()
                UpdatePreviewButton()
            end
        end)

        -- Sub-row 2: texture dropdown (left, widened) | blend dropdown (right)
        -- Blend frame width ≈ SetWidth+32 = 132; texture fills from x=8 to 8px before blend.
        local textureDropdown = CreateLayerTextureDropdown(row, i)
        textureDropdown:SetPoint("TOPLEFT", row, "TOPLEFT", 8, -34)
        UIDropDownMenu_SetWidth(textureDropdown, 164)

        local blendDropdown = CreateLayerBlendDropdown(row, i)
        blendDropdown:SetPoint("TOPRIGHT", row, "TOPRIGHT", -6, -34)

        -- Sub-row 3: size label + box + slider (fills remainder of row)
        local sizeLabel = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        sizeLabel:SetPoint("TOPLEFT", row, "TOPLEFT", 10, -68)
        sizeLabel:SetText("Size")

        local sizeBox = CreateCompactNumberBox(row, 42, 4, layer.SIZE or 64)
        sizeBox:SetPoint("LEFT", sizeLabel, "RIGHT", 8, 0)

        local sizeSlider = CreateStyledLayerSlider(row, 230, 2, 2048, 1, "2", "2048")
        sizeSlider:SetPoint("LEFT", sizeBox, "RIGHT", 15, 0)

        local resettingSize = false
        local function SetSizeSlider(v)
            resettingSize = true; sizeSlider:SetValue(v); resettingSize = false
        end
        local function GetLayerSize() return (GetProfileDB().LAYERS[i] and GetProfileDB().LAYERS[i].SIZE) or 64 end

        sizeSlider:SetScript("OnValueChanged", function(self, value)
            if resettingSize then return end
            if not CanApplyLiveEdit() then
                local sz = GetLayerSize()
                SetSizeSlider(sz); sizeBox:SetText(tostring(sz)); return
            end
            value = Clamp(floor(value + 0.5), 2, 2048)
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].SIZE = value end
            sizeBox:SetText(tostring(value))
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        sizeBox:SetScript("OnEnterPressed", function(self)
            if not CanApplyLiveEdit() then
                self:SetText(tostring(GetLayerSize())); self:ClearFocus(); return
            end
            local v = Clamp(floor((tonumber(self:GetText()) or GetLayerSize()) + 0.5), 2, 2048)
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].SIZE = v end
            SetSizeSlider(v); self:SetText(tostring(v)); self:ClearFocus()
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        sizeBox:SetScript("OnEscapePressed", function(self)
            self:SetText(tostring(GetLayerSize())); self:ClearFocus()
        end)
        sizeBox:SetScript("OnEditFocusLost", function(self)
            if not CanApplyLiveEdit() then
                self:SetText(tostring(GetLayerSize())); return
            end
            local v = Clamp(floor((tonumber(self:GetText()) or GetLayerSize()) + 0.5), 2, 2048)
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].SIZE = v end
            SetSizeSlider(v); self:SetText(tostring(v))
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        do
            local sz = Clamp(floor(((layer.SIZE) or 64) + 0.5), 2, 2048)
            SetSizeSlider(sz); sizeBox:SetText(tostring(sz))
        end

        -- Sub-row 4: opacity (left half) | rotation (right half)
        local opacityLabel = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        opacityLabel:SetPoint("TOPLEFT", row, "TOPLEFT", 10, -102)
        opacityLabel:SetText("Opac")

        local opacityBox = CreateCompactNumberBox(row, 42, 5, layer.OPACITY or 1, function(v) return format("%.2f", v or 1) end)
        opacityBox:SetPoint("LEFT", opacityLabel, "RIGHT", 8, 0)

        local opacSlider = CreateStyledLayerSlider(row, 70, 0, 1, 0.01, "0", "1")
        opacSlider:SetPoint("LEFT", opacityBox, "RIGHT", 15, 0)

        local resettingOpac = false
        local function SetOpacSlider(v)
            resettingOpac = true; opacSlider:SetValue(v); resettingOpac = false
        end
        local function GetLayerOpac() return (GetProfileDB().LAYERS[i] and GetProfileDB().LAYERS[i].OPACITY) or 1 end
        local function NormalizeOpac(v) return Clamp(tonumber(format("%.2f", tonumber(v) or 1)) or 1, 0, 1) end

        opacSlider:SetScript("OnValueChanged", function(self, value)
            if resettingOpac then return end
            if not CanApplyLiveEdit() then
                local op = GetLayerOpac()
                SetOpacSlider(op); opacityBox:SetText(format("%.2f", op)); return
            end
            value = NormalizeOpac(value)
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].OPACITY = value end
            opacityBox:SetText(format("%.2f", value))
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        opacityBox:SetScript("OnEnterPressed", function(self)
            if not CanApplyLiveEdit() then
                self:SetText(format("%.2f", GetLayerOpac())); self:ClearFocus(); return
            end
            local v = NormalizeOpac(self:GetText())
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].OPACITY = v end
            SetOpacSlider(v); self:SetText(format("%.2f", v)); self:ClearFocus()
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        opacityBox:SetScript("OnEscapePressed", function(self)
            self:SetText(format("%.2f", GetLayerOpac())); self:ClearFocus()
        end)
        opacityBox:SetScript("OnEditFocusLost", function(self)
            if not CanApplyLiveEdit() then
                self:SetText(format("%.2f", GetLayerOpac())); return
            end
            local v = NormalizeOpac(self:GetText())
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].OPACITY = v end
            SetOpacSlider(v); self:SetText(format("%.2f", v))
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        do
            local op = NormalizeOpac((layer.OPACITY) or 1)
            SetOpacSlider(op); opacityBox:SetText(format("%.2f", op))
        end

        local rotationLabel = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        rotationLabel:SetPoint("LEFT", opacSlider, "RIGHT", 8, 0)
        rotationLabel:SetText("Rota")

        local rotationBox = CreateCompactNumberBox(row, 36, 3, layer.ROTATION or 0)
        rotationBox:SetPoint("LEFT", rotationLabel, "RIGHT", 8, 0)

        local rotaSlider = CreateStyledLayerSlider(row, 70, 0, 359, 1, "0", "359")
        rotaSlider:SetPoint("LEFT", rotationBox, "RIGHT", 15, 0)

        local resettingRota = false
        local function SetRotaSlider(v)
            resettingRota = true; rotaSlider:SetValue(v); resettingRota = false
        end
        local function GetLayerRota() return (GetProfileDB().LAYERS[i] and GetProfileDB().LAYERS[i].ROTATION) or 0 end

        rotaSlider:SetScript("OnValueChanged", function(self, value)
            if resettingRota then return end
            if not CanApplyLiveEdit() then
                local rot = GetLayerRota()
                SetRotaSlider(rot); rotationBox:SetText(tostring(rot)); return
            end
            value = Clamp(floor(value + 0.5), 0, 359)
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].ROTATION = value end
            rotationBox:SetText(tostring(value))
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        rotationBox:SetScript("OnEnterPressed", function(self)
            if not CanApplyLiveEdit() then
                self:SetText(tostring(GetLayerRota())); self:ClearFocus(); return
            end
            local v = Clamp(floor((tonumber(self:GetText()) or GetLayerRota()) + 0.5), 0, 359)
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].ROTATION = v end
            SetRotaSlider(v); self:SetText(tostring(v)); self:ClearFocus()
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        rotationBox:SetScript("OnEscapePressed", function(self)
            self:SetText(tostring(GetLayerRota())); self:ClearFocus()
        end)
        rotationBox:SetScript("OnEditFocusLost", function(self)
            if not CanApplyLiveEdit() then
                self:SetText(tostring(GetLayerRota())); return
            end
            local v = Clamp(floor((tonumber(self:GetText()) or GetLayerRota()) + 0.5), 0, 359)
            local layers = GetProfileDB().LAYERS
            if layers[i] then layers[i].ROTATION = v end
            SetRotaSlider(v); self:SetText(tostring(v))
            RefreshReticle(); UpdateEditingProfileTexts(); UpdatePreviewButton()
        end)
        do
            local rot = Clamp(floor(((layer.ROTATION) or 0) + 0.5), 0, 359)
            SetRotaSlider(rot); rotationBox:SetText(tostring(rot))
        end

        Lists.layerRowWidgets[i] = {
            sizeBox = sizeBox,
            opacityBox = opacityBox,
            rotationBox = rotationBox,
        }
    end

    local contentHeight = math.max(230, (#(profile.LAYERS or {}) * rowHeight) + 12)
    UI.layerContent:SetSize(350, contentHeight)
end

local function AddLayer()
    if not CanApplyLiveEdit() then return end

    local profile = GetProfileDB()
    tinsert(profile.LAYERS, {
        TEXTURE = availableTextures[1].value,
        SIZE = 64,
        OPACITY = 1.0,
        COLOR = {1, 1, 1},
        BLEND = "BLEND",
        ROTATION = 0,
    })

    RebuildLayerList()
    RefreshReticle()
    UpdateEditingProfileTexts()
    UpdatePreviewButton()
end

local function CreateLineThicknessControl(parent, xOffset, yOffset)
    local label = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    label:SetPoint("TOPLEFT", xOffset, yOffset)
    label:SetText("Line Thickness")

    local editBox = CreateFrame("EditBox", ADDON_NAME .. "LineThicknessEditBox", parent, "InputBoxTemplate")
    editBox:SetSize(42, 20)
    editBox:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 6, -6)
    editBox:SetAutoFocus(false)
    editBox:SetNumeric(false)
    editBox:SetMaxLetters(2)
    editBox:SetAltArrowKeyMode(false)
    editBox:SetPropagateKeyboardInput(false)

    local sliderName = ADDON_NAME .. "LineThicknessSlider"
    local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate")
    slider:SetPoint("LEFT", editBox, "RIGHT", 15, 0)
    slider:SetWidth(240)
    slider:SetMinMaxValues(1, 10)
    slider:SetValueStep(1)
    slider:SetObeyStepOnDrag(true)

    _G[sliderName .. "Low"]:SetText("1")
    _G[sliderName .. "Low"]:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 3, 3)
    _G[sliderName .. "High"]:SetText("10")
    _G[sliderName .. "High"]:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", -3, 3)
    _G[sliderName .. "Text"]:SetText("")

    local resetting = false
    local function SetSliderValue(v)
        resetting = true
        slider:SetValue(v)
        resetting = false
    end

    local function ApplyValue(value)
        local profile = GetProfileDB()
        value = Clamp(floor((tonumber(value) or profile.LINE_THICKNESS or 1) + 0.5), 1, 10)
        profile.LINE_THICKNESS = value
        SetSliderValue(value)
        editBox:SetText(tostring(value))
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end

    slider:SetScript("OnValueChanged", function(self, value)
        if resetting then return end
        if not CanApplyLiveEdit() then
            local profile = GetProfileDB()
            SetSliderValue(profile.LINE_THICKNESS or 1)
            editBox:SetText(tostring(profile.LINE_THICKNESS or 1))
            return
        end
        value = floor(value + 0.5)
        GetProfileDB().LINE_THICKNESS = value
        editBox:SetText(tostring(value))
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end)

    editBox:SetScript("OnEnterPressed", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(tostring(GetProfileDB().LINE_THICKNESS or 1))
            self:ClearFocus()
            return
        end
        ApplyValue(self:GetText())
        self:ClearFocus()
    end)

    editBox:SetScript("OnEscapePressed", function(self)
        self:SetText(tostring(GetProfileDB().LINE_THICKNESS or 1))
        self:ClearFocus()
    end)

    editBox:SetScript("OnEditFocusLost", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(tostring(GetProfileDB().LINE_THICKNESS or 1))
            return
        end
        ApplyValue(self:GetText())
    end)

    local widgets = {
        label = label,
        editBox = editBox,
        slider = slider,
    }

    function widgets:RefreshFromDB()
        local profile = GetProfileDB()
        local value = Clamp(floor((tonumber(profile.LINE_THICKNESS) or 1) + 0.5), 1, 10)
        profile.LINE_THICKNESS = value
        SetSliderValue(value)
        self.editBox:SetText(tostring(value))
    end

    widgets:RefreshFromDB()
    return widgets
end

local function CreateLineGapControl(parent, xOffset, yOffset)
    local label = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    label:SetPoint("TOPLEFT", xOffset, yOffset)
    label:SetText("Center Gap Size")

    local editBox = CreateFrame("EditBox", ADDON_NAME .. "LineGapEditBox", parent, "InputBoxTemplate")
    editBox:SetSize(42, 20)
    editBox:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 6, -6)
    editBox:SetAutoFocus(false)
    editBox:SetNumeric(false)
    editBox:SetMaxLetters(4)
    editBox:SetAltArrowKeyMode(false)
    editBox:SetPropagateKeyboardInput(false)

    local sliderName = ADDON_NAME .. "LineGapSlider"
    local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate")
    slider:SetPoint("LEFT", editBox, "RIGHT", 15, 0)
    slider:SetWidth(240)
    slider:SetMinMaxValues(0, 512)
    slider:SetValueStep(1)
    slider:SetObeyStepOnDrag(true)

    _G[sliderName .. "Low"]:SetText("0")
    _G[sliderName .. "Low"]:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 3, 3)
    _G[sliderName .. "High"]:SetText("512")
    _G[sliderName .. "High"]:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", -3, 3)
    _G[sliderName .. "Text"]:SetText("")

    local function RefreshOffsetLimits()
        if UI.lineLengthModeWidgets and UI.lineLengthModeWidgets.RefreshLimits then
            UI.lineLengthModeWidgets.RefreshLimits()
        end
    end

    local resetting = false
    local function SetSliderValue(v)
        resetting = true
        slider:SetValue(v)
        resetting = false
    end

    local function ApplyValue(value)
        local profile = GetProfileDB()
        value = Clamp(floor((tonumber(value) or profile.LINE_GAP_SIZE or 42) + 0.5), 0, 512)
        profile.LINE_GAP_SIZE = value
        SetSliderValue(value)
        editBox:SetText(tostring(value))
        RefreshOffsetLimits()
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end

    slider:SetScript("OnValueChanged", function(self, value)
        if resetting then return end
        if not CanApplyLiveEdit() then
            local profile = GetProfileDB()
            SetSliderValue(profile.LINE_GAP_SIZE or 42)
            editBox:SetText(tostring(profile.LINE_GAP_SIZE or 42))
            return
        end
        value = floor(value + 0.5)
        GetProfileDB().LINE_GAP_SIZE = value
        editBox:SetText(tostring(value))
        RefreshOffsetLimits()
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end)

    editBox:SetScript("OnEnterPressed", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(tostring(GetProfileDB().LINE_GAP_SIZE or 42))
            self:ClearFocus()
            return
        end
        ApplyValue(self:GetText())
        self:ClearFocus()
    end)

    editBox:SetScript("OnEscapePressed", function(self)
        self:SetText(tostring(GetProfileDB().LINE_GAP_SIZE or 42))
        self:ClearFocus()
    end)

    editBox:SetScript("OnEditFocusLost", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(tostring(GetProfileDB().LINE_GAP_SIZE or 42))
            return
        end
        ApplyValue(self:GetText())
    end)

    local widgets = {
        label = label,
        editBox = editBox,
        slider = slider,
    }

    function widgets:RefreshFromDB()
        local profile = GetProfileDB()
        local value = Clamp(floor((tonumber(profile.LINE_GAP_SIZE) or 42) + 0.5), 0, 512)
        profile.LINE_GAP_SIZE = value
        SetSliderValue(value)
        self.editBox:SetText(tostring(value))
        if UI.lineLengthModeWidgets and UI.lineLengthModeWidgets.RefreshLimits then
            UI.lineLengthModeWidgets.RefreshLimits()
        end
    end

    widgets:RefreshFromDB()
    return widgets
end

local function CreateLineOpacityControl(parent, xOffset, yOffset)
    local label = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    label:SetPoint("TOPLEFT", xOffset, yOffset)
    label:SetText("Opacity")

    local editBox = CreateFrame("EditBox", ADDON_NAME .. "LineOpacityEditBox", parent, "InputBoxTemplate")
    editBox:SetSize(42, 20)
    editBox:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 6, -6)
    editBox:SetAutoFocus(false)
    editBox:SetNumeric(false)
    editBox:SetMaxLetters(4)
    editBox:SetAltArrowKeyMode(false)
    editBox:SetPropagateKeyboardInput(false)

    local sliderName = ADDON_NAME .. "LineOpacitySlider"
    local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate")
    slider:SetPoint("LEFT", editBox, "RIGHT", 15, 0)
    slider:SetWidth(240)
    slider:SetMinMaxValues(0, 1)
    slider:SetValueStep(0.01)
    slider:SetObeyStepOnDrag(true)

    _G[sliderName .. "Low"]:SetText("0")
    _G[sliderName .. "Low"]:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 3, 3)
    _G[sliderName .. "High"]:SetText("1")
    _G[sliderName .. "High"]:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", -3, 3)
    _G[sliderName .. "Text"]:SetText("")

    local resetting = false
    local function SetSliderValue(v)
        resetting = true
        slider:SetValue(v)
        resetting = false
    end

    local function NormalizeOpacity(value)
        return Clamp(tonumber(format("%.2f", tonumber(value) or 0.25)) or 0.25, 0, 1)
    end

    local function ApplyValue(value)
        local profile = GetProfileDB()
        value = NormalizeOpacity(value)
        profile.LINE_OPACITY = value
        SetSliderValue(value)
        editBox:SetText(format("%.2f", value))
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end

    slider:SetScript("OnValueChanged", function(self, value)
        if resetting then return end
        if not CanApplyLiveEdit() then
            local profile = GetProfileDB()
            SetSliderValue(profile.LINE_OPACITY or 0.25)
            editBox:SetText(format("%.2f", profile.LINE_OPACITY or 0.25))
            return
        end
        value = NormalizeOpacity(value)
        GetProfileDB().LINE_OPACITY = value
        editBox:SetText(format("%.2f", value))
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end)

    editBox:SetScript("OnEnterPressed", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(format("%.2f", GetProfileDB().LINE_OPACITY or 0.25))
            self:ClearFocus()
            return
        end
        ApplyValue(self:GetText())
        self:ClearFocus()
    end)

    editBox:SetScript("OnEscapePressed", function(self)
        self:SetText(format("%.2f", GetProfileDB().LINE_OPACITY or 0.25))
        self:ClearFocus()
    end)

    editBox:SetScript("OnEditFocusLost", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(format("%.2f", GetProfileDB().LINE_OPACITY or 0.25))
            return
        end
        ApplyValue(self:GetText())
    end)

    local widgets = {
        label = label,
        editBox = editBox,
        slider = slider,
    }

    function widgets:RefreshFromDB()
        local profile = GetProfileDB()
        local value = NormalizeOpacity(profile.LINE_OPACITY or 0.25)
        profile.LINE_OPACITY = value
        SetSliderValue(value)
        self.editBox:SetText(format("%.2f", value))
    end

    widgets:RefreshFromDB()
    return widgets
end

local function CreateLineColorControl(parent, xOffset, yOffset)
    local label = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    label:SetPoint("TOPLEFT", xOffset, yOffset)
    label:SetText("Color")

    local colorButton = CreateColorButton(
        parent,
        function()
            local color = GetProfileDB().LINE_COLOR or {1, 1, 1}
            return color[1] or 1, color[2] or 1, color[3] or 1
        end,
        function(r, g, b)
            local profile = GetProfileDB()
            profile.LINE_COLOR = profile.LINE_COLOR or {}
            profile.LINE_COLOR[1] = r
            profile.LINE_COLOR[2] = g
            profile.LINE_COLOR[3] = b
        end
    )
    colorButton:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 6, -6)

    local widgets = {
        label = label,
        button = colorButton,
    }

    function widgets:RefreshFromDB()
        local profile = GetProfileDB()
        profile.LINE_COLOR = profile.LINE_COLOR or {1, 1, 1}
        if self.button and self.button.UpdatePreview then
            self.button:UpdatePreview()
        end
    end

    widgets:RefreshFromDB()
    return widgets
end

local function CreateLineLengthModeControls(parent, xOffset, yOffset)
    local screenRadio = CreateFrame("CheckButton", nil, parent, "UIRadioButtonTemplate")
    screenRadio:SetPoint("TOPLEFT", xOffset, yOffset)

    local screenText = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    screenText:SetPoint("LEFT", screenRadio, "RIGHT", 4, 0)
    screenText:SetText("Line Length: Screen Size")

    local offsetRadio = CreateFrame("CheckButton", nil, parent, "UIRadioButtonTemplate")
    offsetRadio:SetPoint("TOPLEFT", xOffset, yOffset - 28)

    local offsetText = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    offsetText:SetPoint("LEFT", offsetRadio, "RIGHT", 4, 0)
    offsetText:SetText("Line Length: Reduction")

    local hLabel = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    hLabel:SetPoint("TOPLEFT", xOffset + 24, yOffset - 56)
    hLabel:SetText("Horizontal Reduction")

    local hEditBox = CreateFrame("EditBox", ADDON_NAME .. "LineEdgeOffsetXEditBox", parent, "InputBoxTemplate")
    hEditBox:SetSize(42, 20)
    hEditBox:SetPoint("TOPLEFT", hLabel, "BOTTOMLEFT", 6, -6)
    hEditBox:SetAutoFocus(false)
    hEditBox:SetNumeric(false)
    hEditBox:SetMaxLetters(5)
    hEditBox:SetAltArrowKeyMode(false)
    hEditBox:SetPropagateKeyboardInput(false)

    local hSlider = CreateFrame("Slider", nil, parent, "OptionsSliderTemplate")
    hSlider:SetPoint("LEFT", hEditBox, "RIGHT", 15, 0)
    hSlider:SetWidth(240)
    hSlider:SetMinMaxValues(1, UIParent:GetWidth() * 2)
    hSlider:SetValueStep(1)
    hSlider:SetObeyStepOnDrag(true)
    if hSlider.Low  then hSlider.Low:SetText("1"); hSlider.Low:SetPoint("TOPLEFT", hSlider, "BOTTOMLEFT", 3, 3) end
    if hSlider.High then hSlider.High:SetPoint("TOPRIGHT", hSlider, "BOTTOMRIGHT", -3, 3) end
    if hSlider.Text then hSlider.Text:SetText("") end

    local vLabel = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    vLabel:SetPoint("TOPLEFT", xOffset + 24, yOffset - 114)
    vLabel:SetText("Vertical Reduction")

    local vEditBox = CreateFrame("EditBox", ADDON_NAME .. "LineEdgeOffsetYEditBox", parent, "InputBoxTemplate")
    vEditBox:SetSize(42, 20)
    vEditBox:SetPoint("TOPLEFT", vLabel, "BOTTOMLEFT", 6, -6)
    vEditBox:SetAutoFocus(false)
    vEditBox:SetNumeric(false)
    vEditBox:SetMaxLetters(5)
    vEditBox:SetAltArrowKeyMode(false)
    vEditBox:SetPropagateKeyboardInput(false)

    local vSlider = CreateFrame("Slider", nil, parent, "OptionsSliderTemplate")
    vSlider:SetPoint("LEFT", vEditBox, "RIGHT", 15, 0)
    vSlider:SetWidth(240)
    vSlider:SetMinMaxValues(1, UIParent:GetHeight() * 2)
    vSlider:SetValueStep(1)
    vSlider:SetObeyStepOnDrag(true)
    if vSlider.Low  then vSlider.Low:SetText("1"); vSlider.Low:SetPoint("TOPLEFT", vSlider, "BOTTOMLEFT", 3, 3) end
    if vSlider.High then vSlider.High:SetPoint("TOPRIGHT", vSlider, "BOTTOMRIGHT", -3, 3) end
    if vSlider.Text then vSlider.Text:SetText("") end

    local resettingH = false
    local function SetHSliderValue(v)
        resettingH = true
        hSlider:SetValue(v)
        resettingH = false
    end

    local resettingV = false
    local function SetVSliderValue(v)
        resettingV = true
        vSlider:SetValue(v)
        resettingV = false
    end

    local function ApplyHorizontal(value)
        local profile = GetProfileDB()
        local maxValue = floor(UIParent:GetWidth() * 2)
        value = Clamp(floor((tonumber(value) or profile.LINE_EDGE_OFFSET_X or 1) + 0.5), 1, maxValue)
        profile.LINE_EDGE_OFFSET_X = value
        SetHSliderValue(value)
        hEditBox:SetText(tostring(value))
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end

    local function ApplyVertical(value)
        local profile = GetProfileDB()
        local maxValue = floor(UIParent:GetHeight() * 2)
        value = Clamp(floor((tonumber(value) or profile.LINE_EDGE_OFFSET_Y or 1) + 0.5), 1, maxValue)
        profile.LINE_EDGE_OFFSET_Y = value
        SetVSliderValue(value)
        vEditBox:SetText(tostring(value))
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end

    hSlider:SetScript("OnValueChanged", function(self, value)
        if resettingH then return end
        if not CanApplyLiveEdit() then
            local profile = GetProfileDB()
            SetHSliderValue(profile.LINE_EDGE_OFFSET_X or 1)
            hEditBox:SetText(tostring(profile.LINE_EDGE_OFFSET_X or 1))
            return
        end
        value = floor(value + 0.5)
        GetProfileDB().LINE_EDGE_OFFSET_X = value
        hEditBox:SetText(tostring(value))
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end)

    vSlider:SetScript("OnValueChanged", function(self, value)
        if resettingV then return end
        if not CanApplyLiveEdit() then
            local profile = GetProfileDB()
            SetVSliderValue(profile.LINE_EDGE_OFFSET_Y or 1)
            vEditBox:SetText(tostring(profile.LINE_EDGE_OFFSET_Y or 1))
            return
        end
        value = floor(value + 0.5)
        GetProfileDB().LINE_EDGE_OFFSET_Y = value
        vEditBox:SetText(tostring(value))
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end)

    hEditBox:SetScript("OnEnterPressed", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(tostring(GetProfileDB().LINE_EDGE_OFFSET_X or 1))
            self:ClearFocus()
            return
        end
        ApplyHorizontal(self:GetText())
        self:ClearFocus()
    end)

    hEditBox:SetScript("OnEscapePressed", function(self)
        self:SetText(tostring(GetProfileDB().LINE_EDGE_OFFSET_X or 1))
        self:ClearFocus()
    end)

    hEditBox:SetScript("OnEditFocusLost", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(tostring(GetProfileDB().LINE_EDGE_OFFSET_X or 1))
            return
        end
        ApplyHorizontal(self:GetText())
    end)

    vEditBox:SetScript("OnEnterPressed", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(tostring(GetProfileDB().LINE_EDGE_OFFSET_Y or 1))
            self:ClearFocus()
            return
        end
        ApplyVertical(self:GetText())
        self:ClearFocus()
    end)

    vEditBox:SetScript("OnEscapePressed", function(self)
        self:SetText(tostring(GetProfileDB().LINE_EDGE_OFFSET_Y or 1))
        self:ClearFocus()
    end)

    vEditBox:SetScript("OnEditFocusLost", function(self)
        if not CanApplyLiveEdit() then
            self:SetText(tostring(GetProfileDB().LINE_EDGE_OFFSET_Y or 1))
            return
        end
        ApplyVertical(self:GetText())
    end)

    local function UpdateModeUI()
        local profile = GetProfileDB()
        local useOffset = (profile.LINE_LENGTH_MODE == "OFFSET")

        screenRadio:SetChecked(profile.LINE_LENGTH_MODE == "SCREEN")
        offsetRadio:SetChecked(useOffset)

        SetWidgetEnabled(hLabel, useOffset)
        SetWidgetEnabled(hEditBox, useOffset)
        SetWidgetEnabled(hSlider, useOffset)

        SetWidgetEnabled(vLabel, useOffset)
        SetWidgetEnabled(vEditBox, useOffset)
        SetWidgetEnabled(vSlider, useOffset)
    end

    local function RefreshLimits()
        local profile = GetProfileDB()
        -- *1.01 because *1 somehow bugs out if the reduction is maxed out, with additional line thickness it forms a nice cross
        local hMax = floor(UIParent:GetWidth() * 1.01) 
        local vMax = floor(UIParent:GetHeight() * 1.01)

        hSlider:SetMinMaxValues(1, hMax)
        vSlider:SetMinMaxValues(1, vMax)

        if hSlider.High then hSlider.High:SetText(tostring(hMax)) end
        if vSlider.High then vSlider.High:SetText(tostring(vMax)) end

        if (profile.LINE_EDGE_OFFSET_X or 1) > hMax then profile.LINE_EDGE_OFFSET_X = hMax end
        if (profile.LINE_EDGE_OFFSET_Y or 1) > vMax then profile.LINE_EDGE_OFFSET_Y = vMax end

        SetHSliderValue(profile.LINE_EDGE_OFFSET_X or 1)
        SetVSliderValue(profile.LINE_EDGE_OFFSET_Y or 1)

        hEditBox:SetText(tostring(profile.LINE_EDGE_OFFSET_X or 1))
        vEditBox:SetText(tostring(profile.LINE_EDGE_OFFSET_Y or 1))

        screenRadio:SetChecked(profile.LINE_LENGTH_MODE == "SCREEN")
        offsetRadio:SetChecked(profile.LINE_LENGTH_MODE == "OFFSET")
    end

    screenRadio:SetScript("OnClick", function()
        if not CanApplyLiveEdit() then
            UpdateModeUI()
            return
        end
        GetProfileDB().LINE_LENGTH_MODE = "SCREEN"
        UpdateModeUI()
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end)

    offsetRadio:SetScript("OnClick", function()
        if not CanApplyLiveEdit() then
            UpdateModeUI()
            return
        end
        GetProfileDB().LINE_LENGTH_MODE = "OFFSET"
        UpdateModeUI()
        RefreshLimits()
        RefreshReticle()
        UpdateEditingProfileTexts()
        UpdatePreviewButton()
    end)

    RefreshLimits()
    UpdateModeUI()

    return {
        screenRadio = screenRadio,
        offsetRadio = offsetRadio,
        hLabel = hLabel,
        hEditBox = hEditBox,
        hSlider = hSlider,
        vLabel = vLabel,
        vEditBox = vEditBox,
        vSlider = vSlider,
        RefreshLimits = RefreshLimits,
        UpdateModeUI = UpdateModeUI,
    }
end

local function CreateDynamicProfileDropdown(parent, width, xOffset, yOffset, selectedValueKey, excludeCurrentProfile, onSelect)
    local db = GetCharDB()
    local dropdown = CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate")
    dropdown:SetPoint("TOPLEFT", xOffset, yOffset)
    UIDropDownMenu_SetWidth(dropdown, width or 180)

    dropdown.RefreshList = function(self)
        local excludeName = nil
        if excludeCurrentProfile then
            excludeName = (_G.OldManCursor_GetActiveProfileName and _G.OldManCursor_GetActiveProfileName()) or db.CURRENT_PROFILE
        end

        local entries = GetSavedProfileEntries(excludeName)
        if #entries == 0 then
            UIDropDownMenu_SetSelectedValue(self, nil)
            UIDropDownMenu_SetText(self, "")
            return
        end

        local selectedValue = db[selectedValueKey]
        local selected = FindEntryByValue(entries, selectedValue or entries[1].value)
        db[selectedValueKey] = selected.value
        UIDropDownMenu_SetSelectedValue(self, selected.value)
        UIDropDownMenu_SetText(self, selected.text)
    end

    UIDropDownMenu_Initialize(dropdown, function(self, level)
        local excludeName = nil
        if excludeCurrentProfile then
            excludeName = (_G.OldManCursor_GetActiveProfileName and _G.OldManCursor_GetActiveProfileName()) or db.CURRENT_PROFILE
        end

        local entries = GetSavedProfileEntries(excludeName)

        for _, entry in ipairs(entries) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = entry.text
            info.value = entry.value
            info.func = function()
                db[selectedValueKey] = entry.value
                UIDropDownMenu_SetSelectedValue(dropdown, entry.value)
                UIDropDownMenu_SetText(dropdown, entry.text)
                if onSelect then
                    onSelect(entry.value, entry.text)
                end
                UpdateEditingProfileTexts()
                UpdatePreviewButton()
            end
            info.checked = (db[selectedValueKey] == entry.value)
            UIDropDownMenu_AddButton(info, level)
        end
    end)

    dropdown:RefreshList()
    return dropdown
end

local function CreateActionProfileDropdown(parent, width, anchorTo, anchorPoint, relativePoint, xOffset, yOffset, excludeCurrentProfile, excludeProfileName, onSelect)
    local dropdown = CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate")
    dropdown:SetPoint(anchorPoint, anchorTo, relativePoint, xOffset, yOffset)
    UIDropDownMenu_SetWidth(dropdown, width or 180)
    UIDropDownMenu_SetText(dropdown, "")

    dropdown.RefreshList = function(self)
        UIDropDownMenu_SetSelectedValue(self, nil)
        UIDropDownMenu_SetText(self, "")
    end

    UIDropDownMenu_Initialize(dropdown, function(self, level)
        local entries = GetSavedProfileEntries(nil)
        local filteredEntries = {}
        local currentProfile = (_G.OldManCursor_GetActiveProfileName and _G.OldManCursor_GetActiveProfileName()) or GetCharDB().CURRENT_PROFILE

        for _, entry in ipairs(entries) do
            local keep = true

            if excludeCurrentProfile and entry.value == currentProfile then
                keep = false
            end

            if type(excludeProfileName) == "function" and excludeProfileName(entry.value) then
                keep = false
            elseif excludeProfileName and entry.value == excludeProfileName then
                keep = false
            end

            if keep then
                tinsert(filteredEntries, entry)
            end
        end

        for _, entry in ipairs(filteredEntries) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = entry.text
            info.value = entry.value
            info.func = function()
                if onSelect then
                    onSelect(entry.value, entry.text)
                end
                UIDropDownMenu_SetSelectedValue(dropdown, nil)
                UIDropDownMenu_SetText(dropdown, "")
            end
            info.checked = false
            UIDropDownMenu_AddButton(info, level)
        end
    end)

    return dropdown
end

local function CreateGeneralVisibilityControls(parent, xOffset, yOffset)
    local db = GetCharDB()

    local modeBox = CreateFrame("Frame", nil, parent, "InsetFrameTemplate")
    modeBox:SetPoint("TOPLEFT", xOffset, yOffset)
    modeBox:SetSize(400, 130)

    local onlyCombatRadio = CreateFrame("CheckButton", nil, parent, "UIRadioButtonTemplate")
    onlyCombatRadio:SetPoint("TOPLEFT", modeBox, "TOPLEFT", 12, -14)

    local onlyCombatText = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    onlyCombatText:SetPoint("LEFT", onlyCombatRadio, "RIGHT", 4, 0)
    onlyCombatText:SetText("Only show during combat")

    local alwaysShowRadio = CreateFrame("CheckButton", nil, parent, "UIRadioButtonTemplate")
    alwaysShowRadio:SetPoint("TOPLEFT", modeBox, "TOPLEFT", 12, -42)

    local alwaysShowText = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    alwaysShowText:SetPoint("LEFT", alwaysShowRadio, "RIGHT", 4, 0)
    alwaysShowText:SetText("Always show")

    local sharedProfileLabel = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    sharedProfileLabel:SetPoint("TOPLEFT", modeBox, "TOPLEFT", 36, -74)
    sharedProfileLabel:SetText("Profile")

    local sharedProfileDropdown = CreateDynamicProfileDropdown(
        parent,
        180,
        xOffset + 18,
        yOffset - 90,
        "CURRENT_PROFILE",
        false,
        function(value)
            db.CURRENT_PROFILE = value
            RefreshReticle()
            UpdateEditingProfileTexts()
            UpdatePreviewButton()
        end
    )

    local advancedBox = CreateFrame("Frame", nil, parent, "InsetFrameTemplate")
    advancedBox:SetPoint("TOPLEFT", modeBox, "BOTTOMLEFT", 0, -10)
    advancedBox:SetSize(400, 168)

    local advancedRadio = CreateFrame("CheckButton", nil, parent, "UIRadioButtonTemplate")
    advancedRadio:SetPoint("TOPLEFT", advancedBox, "TOPLEFT", 12, -14)

    local advancedText = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    advancedText:SetPoint("LEFT", advancedRadio, "RIGHT", 4, 0)
    advancedText:SetText("Advanced")

    local outsideLabel = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    outsideLabel:SetPoint("TOPLEFT", advancedBox, "TOPLEFT", 36, -50)
    outsideLabel:SetText("Profile outside of combat")

    local outsideDropdown = CreateDynamicProfileDropdown(
        parent,
        180,
        xOffset + 18,
        yOffset - 206,
        "PROFILE_OUTSIDE_COMBAT",
        false,
        function(value)
            db.PROFILE_OUTSIDE_COMBAT = value
            RefreshReticle()
            UpdateEditingProfileTexts()
            UpdatePreviewButton()
        end
    )

    local duringLabel = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    duringLabel:SetPoint("TOPLEFT", advancedBox, "TOPLEFT", 36, -108)
    duringLabel:SetText("Profile during combat")

    local duringDropdown = CreateDynamicProfileDropdown(
        parent,
        180,
        xOffset + 18,
        yOffset - 260,
        "PROFILE_DURING_COMBAT",
        false,
        function(value)
            db.PROFILE_DURING_COMBAT = value
            RefreshReticle()
            UpdateEditingProfileTexts()
            UpdatePreviewButton()
        end
    )

    local restedBox = CreateFrame("Frame", nil, parent, "InsetFrameTemplate")
    restedBox:SetPoint("TOPLEFT", advancedBox, "BOTTOMLEFT", 0, -10)
    restedBox:SetSize(400, 54)

    local restedCheck = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    restedCheck:SetPoint("TOPLEFT", restedBox, "TOPLEFT", 12, -12)
    restedCheck:SetChecked(db.SHOW_IN_RESTED_ZONES and true or false)

    local restedText = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    restedText:SetPoint("LEFT", restedCheck, "RIGHT", 4, 0)
    restedText:SetText("Show in Rested Zones")

    restedCheck:SetScript("OnClick", function(self)
        db.SHOW_IN_RESTED_ZONES = self:GetChecked() and true or false
        RefreshReticle()
        UpdateEditingProfileTexts()
        if UI.UpdatePreviewButtonState then
            UI.UpdatePreviewButtonState()
        end
        UpdatePreviewButton()
    end)

    local function UpdateModeUI()
        local isAdvanced = (db.VISIBILITY_MODE == "ADVANCED")
        local isRegular = not isAdvanced

        onlyCombatRadio:SetChecked(db.VISIBILITY_MODE == "COMBAT")
        alwaysShowRadio:SetChecked(db.VISIBILITY_MODE == "ALWAYS")
        advancedRadio:SetChecked(isAdvanced)

        SetWidgetEnabled(sharedProfileLabel, isRegular)
        SetWidgetEnabled(sharedProfileDropdown, isRegular)

        SetWidgetEnabled(outsideLabel, isAdvanced)
        SetWidgetEnabled(outsideDropdown, isAdvanced)

        SetWidgetEnabled(duringLabel, isAdvanced)
        SetWidgetEnabled(duringDropdown, isAdvanced)

        SetWidgetEnabled(restedCheck, true)
        SetWidgetEnabled(restedText, true)
    end

    onlyCombatRadio:SetScript("OnClick", function()
        db.VISIBILITY_MODE = "COMBAT"
        UpdateModeUI()
        RefreshReticle()
        UpdateEditingProfileTexts()
        if UI.UpdatePreviewButtonState then
            UI.UpdatePreviewButtonState()
        end
        UpdatePreviewButton()
    end)

    alwaysShowRadio:SetScript("OnClick", function()
        db.VISIBILITY_MODE = "ALWAYS"
        UpdateModeUI()
        RefreshReticle()
        UpdateEditingProfileTexts()
        if UI.UpdatePreviewButtonState then
            UI.UpdatePreviewButtonState()
        end
        UpdatePreviewButton()
    end)

    advancedRadio:SetScript("OnClick", function()
        db.VISIBILITY_MODE = "ADVANCED"
        UpdateModeUI()
        RefreshReticle()
        UpdateEditingProfileTexts()
        if UI.UpdatePreviewButtonState then
            UI.UpdatePreviewButtonState()
        end
        UpdatePreviewButton()
    end)

    local function RefreshProfileDropdowns()
        sharedProfileDropdown:RefreshList()
        outsideDropdown:RefreshList()
        duringDropdown:RefreshList()
        restedCheck:SetChecked(db.SHOW_IN_RESTED_ZONES and true or false)
    end

    RefreshProfileDropdowns()
    UpdateModeUI()

    return {
        modeBox = modeBox,
        advancedBox = advancedBox,
        restedBox = restedBox,
        onlyCombatRadio = onlyCombatRadio,
        alwaysShowRadio = alwaysShowRadio,
        sharedProfileLabel = sharedProfileLabel,
        sharedProfileDropdown = sharedProfileDropdown,
        advancedRadio = advancedRadio,
        outsideLabel = outsideLabel,
        outsideDropdown = outsideDropdown,
        duringLabel = duringLabel,
        duringDropdown = duringDropdown,
        restedCheck = restedCheck,
        restedText = restedText,
        UpdateModeUI = UpdateModeUI,
        RefreshProfileDropdowns = RefreshProfileDropdowns,
    }
end

local function ResetProfilesTabSelections()
    local db = GetDB()
    db.COPY_FROM_PROFILE = ""
    db.DELETE_PROFILE = ""
end

local function SelectTab(key)
    local previousTabKey = State.currentTabKey
    State.currentTabKey = key

    if previousTabKey == "LINE" and key ~= "LINE" then
        SaveActiveProfile()
    end

    if previousTabKey == "LAYERS" and key ~= "LAYERS" then
        SaveActiveProfile()
    end

    if previousTabKey == "IMPORT_EXPORT" and key ~= "IMPORT_EXPORT" then
        SaveActiveProfile()
    end

    if previousTabKey == "IMPORT_EXPORT" and key ~= "IMPORT_EXPORT" then
        SetProfileStatus("EXPORT", "", false)
        SetProfileStatus("IMPORT", "", false)

        if UI.exportEditBox then
            UI.exportEditBox:SetText("")
        end
        if UI.importEditBox then
            UI.importEditBox:SetText("")
        end
    end

    if previousTabKey == "PROFILES" and key ~= "PROFILES" then
        ResetProfilesTabSelections()
    end

    for _, tab in pairs(Lists.tabs) do
        if tab.frame then
            tab.frame:Hide()
        end
        if tab.button then
            tab.button:UnlockHighlight()
        end
    end

    local tab = Lists.tabs[key]
    if not tab then
        return
    end

    tab.frame:Show()
    tab.button:LockHighlight()

    if key == "LAYERS" then
        RebuildLayerList()
    elseif key == "LINE" then
        RefreshLineWidgets()
    elseif key == "PROFILES" or key == "GENERAL" then
        RefreshAllProfileWidgets()
    end

    UpdateEditingProfileTexts()
    UpdatePreviewButton()
    UpdateEditModeUI()
end

-- Custom confirm dialog — replaces StaticPopupDialogs for full hide reliability.
local OMCConfirm
do
    local dialog = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    dialog:SetSize(340, 130)
    dialog:SetPoint("CENTER")
    dialog:SetFrameStrata("FULLSCREEN_DIALOG")
    dialog:SetFrameLevel(100)
    dialog:EnableMouse(true)
    dialog:SetMovable(true)
    dialog:RegisterForDrag("LeftButton")
    dialog:SetScript("OnDragStart", dialog.StartMoving)
    dialog:SetScript("OnDragStop",  dialog.StopMovingOrSizing)
    dialog:SetBackdrop({
        bgFile   = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true, tileSize = 16, edgeSize = 16,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    dialog:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
    dialog:SetBackdropBorderColor(0.6, 0.6, 0.6, 1)

    local msgText = dialog:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    msgText:SetPoint("TOP", 0, -22)
    msgText:SetWidth(300)
    msgText:SetJustifyH("CENTER")

    local confirmBtn = CreateFrame("Button", nil, dialog, "UIPanelButtonTemplate")
    confirmBtn:SetSize(100, 24)
    confirmBtn:SetPoint("BOTTOMLEFT", 50, 16)

    local cancelBtn = CreateFrame("Button", nil, dialog, "UIPanelButtonTemplate")
    cancelBtn:SetSize(100, 24)
    cancelBtn:SetPoint("BOTTOMRIGHT", -50, 16)
    cancelBtn:SetText("Cancel")
    cancelBtn:SetScript("OnClick", function() dialog:Hide() end)

    dialog:SetScript("OnKeyDown", function(self, key)
        if key == "ESCAPE" then dialog:Hide() end
    end)

    dialog:Hide()

    OMCConfirm = function(text, confirmLabel, onConfirm)
        msgText:SetText(text or "")
        confirmBtn:SetText(confirmLabel or "OK")
        confirmBtn:SetScript("OnClick", function()
            dialog:Hide()
            if onConfirm then onConfirm() end
        end)
        dialog:Show()
    end
end

local function BuildPanel()
    if State.built then
        return
    end
    State.built = true

    local title = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -16)
    title:SetText("Old Man Cursor")

    UI.previewButton = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
    UI.previewButton:SetSize(175, 24)
    UI.previewButton:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -166, -12)
    UI.previewButton:SetText("Edit & Preview")

    UI.existingProfilesDropdown = CreateFrame("Frame", nil, panel, "UIDropDownMenuTemplate")
    UI.existingProfilesDropdown:SetPoint("LEFT", UI.previewButton, "RIGHT", -10, -2)
    UIDropDownMenu_SetWidth(UI.existingProfilesDropdown, 120)

    UI.existingProfilesDropdown.RefreshList = function(self)
        local db = GetDB()
        local entries = GetSavedProfileEntries(nil)

        if not db.EXISTING_PROFILE_SELECT or db.EXISTING_PROFILE_SELECT == "" then
            db.EXISTING_PROFILE_SELECT = GetCharDB().CURRENT_PROFILE or "DEFAULT"
        end

        RefreshDropdownSelection(self, entries, db.EXISTING_PROFILE_SELECT, "")
    end

    UIDropDownMenu_Initialize(UI.existingProfilesDropdown, function(self, level)
        local db = GetDB()
        local entries = GetSavedProfileEntries(nil)

        for _, entry in ipairs(entries) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = entry.text
            info.value = entry.value
            info.func = function()
                db.EXISTING_PROFILE_SELECT = entry.value
                UIDropDownMenu_SetSelectedValue(UI.existingProfilesDropdown, entry.value)
                UIDropDownMenu_SetText(UI.existingProfilesDropdown, entry.text)

                if IsPreviewActive() then
                    SaveActiveProfile()
                    SetPreview(true, entry.value)
                    RefreshReticle()
                    RefreshLineWidgets()
                    RebuildLayerList()
                end

                SetProfileStatus("PROFILES", "Profile selected for editing: " .. entry.text, false)
                UpdateEditingProfileTexts()
                UpdatePreviewButton()
                RefreshAllProfileWidgets()
            end
            info.checked = (db.EXISTING_PROFILE_SELECT == entry.value)
            UIDropDownMenu_AddButton(info, level)
        end
    end)

    UI.existingProfilesDropdown:RefreshList()

    UI.previewButton:SetScript("OnClick", function()
        State.previewActive = IsPreviewActive()

        if State.previewActive then
            SaveActiveProfile()
            SetPreview(false)
        else
            SetPreview(true, GetPreviewTargetProfileName())
        end

        State.previewActive = IsPreviewActive()
        if UI.UpdatePreviewButtonState then
            UI.UpdatePreviewButtonState()
        end
        UpdatePreviewButton()
        UpdateEditingProfileTexts()
        UpdateEditModeUI()
        RefreshAllProfileWidgets()
        RebuildLayerList()
    end)

    local previewTooltipOverlay = CreateFrame("Frame", nil, UI.previewButton)
    previewTooltipOverlay:SetAllPoints(UI.previewButton)
    previewTooltipOverlay:EnableMouse(false)
    previewTooltipOverlay:SetFrameLevel(UI.previewButton:GetFrameLevel() + 10)

    previewTooltipOverlay:SetScript("OnEnter", function()
    end)

    previewTooltipOverlay:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    UI.previewButton.tooltipOverlay = previewTooltipOverlay

    UpdatePreviewButton()
    if UI.UpdatePreviewButtonState then
        UI.UpdatePreviewButtonState()
    end

    local nav = CreateFrame("Frame", nil, panel, "BackdropTemplate")
    nav:SetPoint("TOPLEFT", 16, -48)
    nav:SetPoint("BOTTOMLEFT", 16, 16)
    nav:SetWidth(190)
    nav:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 14,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    nav:SetBackdropColor(0, 0, 0, 0.35)

    local content = CreateFrame("Frame", nil, panel, "BackdropTemplate")
    content:SetPoint("TOPLEFT", nav, "TOPRIGHT", 14, 0)
    content:SetPoint("BOTTOMRIGHT", -20, 16)
    content:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 14,
        insets = { left = 6, right = 10, top = 6, bottom = 6 },
    })
    content:SetBackdropColor(0, 0, 0, 0.25)

    local tabOrder = {
        { key = "GENERAL", text = "General" },
        { key = "LINE",     text = "Line Settings" },
        { key = "LAYERS",   text = "Layers" },
        { key = "IMPORT_EXPORT", text = "Import & Export" },
        { key = "PROFILES", text = "Profiles" },
    }

    local btnY = -8
    for i = 1, #tabOrder do
        local info = tabOrder[i]

        local button = CreateFrame("Button", nil, nav, "UIPanelButtonTemplate")
        button:SetSize(170, 22)
        button:SetPoint("TOPLEFT", 10, btnY)
        button:SetText(info.text)
        btnY = btnY - 26

        local frame = CreateFrame("Frame", nil, content)
        frame:SetAllPoints(content)
        frame:Hide()

        button:SetScript("OnClick", function()
            SelectTab(info.key)
        end)

        Lists.tabs[info.key] = {
            button = button,
            frame = frame,
        }
    end

    local versionLabel = nav:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    versionLabel:SetPoint("BOTTOM", nav, "BOTTOM", 0, 10)
    versionLabel:SetText("v1.044")

    do
        local tf = Lists.tabs.GENERAL.frame

        local heading = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        heading:SetPoint("TOPLEFT", 16, -16)
        heading:SetText("General")

        local helper = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        helper:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -10)
        helper:SetJustifyH("LEFT")
        helper:SetJustifyV("TOP")
        helper:SetWidth(tf:GetWidth() - 40)
        helper:SetText("Choose which profiles are used by each visibility mode. Use Profiles to manage profiles, and Edit & Preview to edit the selected profile.")

        UI.generalVisibilityWidgets = CreateGeneralVisibilityControls(tf, 12, -72)

        local resetDefaultsButton = CreateFrame("Button", nil, tf, "UIPanelButtonTemplate")
        resetDefaultsButton:SetSize(130, 24)
        resetDefaultsButton:SetPoint("BOTTOMLEFT", tf, "BOTTOMLEFT", 16, 16)
        resetDefaultsButton:SetText("Reset to Defaults")
        resetDefaultsButton:SetScript("OnClick", function()
            local profileName = GetEditingProfileName()
            -- Stop preview immediately in the hardware-event context (same as the preview button).
            State.previewActive = IsPreviewActive()
            if State.previewActive then
                SaveActiveProfile()
                SetPreview(false)
                State.previewActive = IsPreviewActive()
                if UI.UpdatePreviewButtonState then
                    UI.UpdatePreviewButtonState()
                end
                UpdatePreviewButton()
                UpdateEditingProfileTexts()
                UpdateEditModeUI()
                RefreshAllProfileWidgets()
                RebuildLayerList()
            end
            OMCConfirm(
                format("Reset profile \"%s\" to default settings?", profileName),
                "Reset",
                function()
                    if _G.OldManCursor_ResetProfile then
                        local ok, message = _G.OldManCursor_ResetProfile(profileName)
                        if ok then
                            SetProfileStatus("PROFILES", message or "Current profile reset.", false)
                            RefreshAllProfileWidgets()
                            RebuildLayerList()
                        else
                            SetProfileStatus("PROFILES", message or "Could not reset profile.", true)
                        end
                    end
                end
            )
        end)

        local resetInfoText = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        resetInfoText:SetPoint("LEFT", resetDefaultsButton, "RIGHT", 10, 0)
        resetInfoText:SetJustifyH("LEFT")
        resetInfoText:SetText("Resets the profile currently being edited.")
    end

    do
        local tf = Lists.tabs.LINE.frame

        UI.lineHeadingText = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        UI.lineHeadingText:SetPoint("TOPLEFT", 16, -16)
        UI.lineHeadingText:SetText("Line Settings")

        local helper = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        helper:SetPoint("TOPLEFT", UI.lineHeadingText, "BOTTOMLEFT", 0, -10)
        helper:SetJustifyH("LEFT")
        helper:SetText("These settings affect the cursor guide lines behind the layered textures.")

        UI.lineThicknessWidgets = CreateLineThicknessControl(tf, 16, -72)
        UI.lineLengthModeWidgets = CreateLineLengthModeControls(tf, 16, -132)
        UI.lineGapWidgets = CreateLineGapControl(tf, 16, -300)
        UI.lineOpacityWidgets = CreateLineOpacityControl(tf, 16, -350)
        UI.lineBlendDropdown = CreateValueDropdown(tf, "LINE_BLEND", "Blend Mode", availableBlendModes, 16, -400, GetProfileDB)
        UI.lineColorWidgets = CreateLineColorControl(tf, 250, -400)

        UI.lineLockOverlay = CreateEditLockOverlay(tf)
    end

    do
        local tf = Lists.tabs.LAYERS.frame

        UI.layersHeadingText = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        UI.layersHeadingText:SetPoint("TOPLEFT", 16, -16)
        UI.layersHeadingText:SetText("Layers (0)")

        local border = CreateFrame("Frame", nil, tf, "InsetFrameTemplate")
        border:SetPoint("TOPLEFT", UI.layersHeadingText, "BOTTOMLEFT", 4, -8)
        border:SetSize(390, 450)

        UI.layerScrollFrame = CreateFrame("ScrollFrame", ADDON_NAME .. "LayerScrollFrame", border, "UIPanelScrollFrameTemplate")
        UI.layerScrollFrame:SetPoint("TOPLEFT", 4, -4)
        UI.layerScrollFrame:SetPoint("BOTTOMRIGHT", -28, 4)

        UI.layerContent = CreateFrame("Frame", nil, UI.layerScrollFrame)
        UI.layerContent:SetSize(290, 290)
        UI.layerScrollFrame:SetScrollChild(UI.layerContent)

        local addLayerButton = CreateFrame("Button", nil, tf, "UIPanelButtonTemplate")
        addLayerButton:SetSize(90, 24)
        addLayerButton:SetPoint("TOPLEFT", border, "BOTTOMLEFT", 0, -10)
        addLayerButton:SetText("Add Layer")
        addLayerButton:SetScript("OnClick", function()
            AddLayer()
        end)


        UI.layersLockOverlay = CreateEditLockOverlay(tf)
    end

    do
        local tf = Lists.tabs.IMPORT_EXPORT.frame

        local heading = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        heading:SetPoint("TOPLEFT", 16, -16)
        heading:SetText("Import & Export")

        local helper = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        helper:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -10)
        helper:SetJustifyH("LEFT")
        helper:SetText("Export your current cursor settings to a single string, or import them later.")

        local exportLabel = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        exportLabel:SetPoint("TOPLEFT", helper, "BOTTOMLEFT", 0, -18)
        exportLabel:SetText("Export")

        local exportScroll
        exportScroll, UI.exportEditBox = CreateMultiLineBox(tf, ADDON_NAME .. "ExportEditBox", imexportBoxWidth, 90)
        exportScroll:SetPoint("TOPLEFT", exportLabel, "BOTTOMLEFT", 0, -8)

        local exportButton = CreateFrame("Button", nil, tf, "UIPanelButtonTemplate")
        exportButton:SetSize(140, 24)
        exportButton:SetPoint("TOPLEFT", exportScroll, "BOTTOMLEFT", 0, -10)
        exportButton:SetText("Export Current")
        exportButton:SetScript("OnClick", function()
            local exportString = GenerateProfileString()
            UI.exportEditBox:SetText(exportString)
            UI.exportEditBox:SetCursorPosition(0)
            if exportScroll and exportScroll.scrollFrame then
                exportScroll.scrollFrame:SetVerticalScroll(0)
            end
            UI.exportEditBox:HighlightText()
            SetProfileStatus("EXPORT", "Current settings exported.", false)
            SetProfileStatus("IMPORT", "", false)
        end)

        local importLabel = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        importLabel:SetPoint("TOPLEFT", exportButton, "BOTTOMLEFT", 0, -22)
        importLabel:SetText("Import")

        local importScroll
        importScroll, UI.importEditBox = CreateMultiLineBox(tf, ADDON_NAME .. "ImportEditBox", imexportBoxWidth, 90)
        importScroll:SetPoint("TOPLEFT", importLabel, "BOTTOMLEFT", 0, -8)

        local importButton = CreateFrame("Button", nil, tf, "UIPanelButtonTemplate")
        importButton:SetSize(140, 24)
        importButton:SetPoint("TOPLEFT", importScroll, "BOTTOMLEFT", 0, -10)
        importButton:SetText("Import String")
        importButton:SetScript("OnClick", function()
            local ok, message = ApplyProfileString(UI.importEditBox:GetText() or "")

            if importScroll and importScroll.scrollFrame then
                importScroll.scrollFrame:SetVerticalScroll(0)
            end
            UI.importEditBox:SetCursorPosition(0)

            if ok then
                RefreshReticle()
                SaveActiveProfile()
                RefreshLineWidgets()
                RebuildLayerList()
                SetProfileStatus("IMPORT", message, false)
                SetProfileStatus("EXPORT", "", false)
            else
                SetProfileStatus("IMPORT", message, true)
                SetProfileStatus("EXPORT", "", false)
            end
        end)

        UI.exportStatusText = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        UI.exportStatusText:SetPoint("LEFT", exportButton, "RIGHT", 14, 0)
        UI.exportStatusText:SetJustifyH("LEFT")
        UI.exportStatusText:SetText("")

        UI.importStatusText = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        UI.importStatusText:SetPoint("LEFT", importButton, "RIGHT", 14, 0)
        UI.importStatusText:SetJustifyH("LEFT")
        UI.importStatusText:SetText("")
    end

    do
        local tf = Lists.tabs.PROFILES.frame
        local db = GetDB()

        local heading = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        heading:SetPoint("TOPLEFT", 16, -16)
        heading:SetText("Profiles")

        local helper = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        helper:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -10)
        helper:SetJustifyH("LEFT")
        helper:SetWidth(tf:GetWidth() - 40)
        helper:SetText("Manage profiles here. The profile on the top right is used for Edit & Preview.")

        local newLegend = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        newLegend:SetPoint("TOPLEFT", helper, "BOTTOMLEFT", 0, -18)
        newLegend:SetText("Create New Profile")

        UI.newProfileEditBox = CreateFrame("EditBox", ADDON_NAME .. "NewProfileEditBox", tf, "InputBoxTemplate")
        UI.newProfileEditBox:SetSize(180, 20)
        UI.newProfileEditBox:SetPoint("TOPLEFT", newLegend, "BOTTOMLEFT", 10, -8)
        UI.newProfileEditBox:SetAutoFocus(false)
        UI.newProfileEditBox:SetMaxLetters(40)
        UI.newProfileEditBox:SetText(db.NEW_PROFILE_NAME or "")
        UI.newProfileEditBox:SetScript("OnTextChanged", function(self)
            db.NEW_PROFILE_NAME = self:GetText() or ""
        end)

        local createProfileButton = CreateFrame("Button", nil, tf, "UIPanelButtonTemplate")
        createProfileButton:SetSize(110, 22)
        createProfileButton:SetPoint("LEFT", UI.newProfileEditBox, "RIGHT", 10, 0)
        createProfileButton:SetText("Create")
        createProfileButton:SetScript("OnClick", function()
            local name = Trim(UI.newProfileEditBox:GetText() or "")
            if _G.OldManCursor_CreateProfile then
                local ok, message = _G.OldManCursor_CreateProfile(name)
                if ok then
                    db.NEW_PROFILE_NAME = ""
                    UI.newProfileEditBox:SetText("")
                    if not db.EXISTING_PROFILE_SELECT or db.EXISTING_PROFILE_SELECT == "" then
                        db.EXISTING_PROFILE_SELECT = name
                    end
                    SetProfileStatus("PROFILES", message or "Profile created.", false)
                    RefreshAllProfileWidgets()
                else
                    SetProfileStatus("PROFILES", message or "Could not create profile.", true)
                end
            end
        end)

        local copyHelper = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        copyHelper:SetPoint("TOPLEFT", UI.newProfileEditBox, "BOTTOMLEFT", -10, -24)
        copyHelper:SetJustifyH("LEFT")
        copyHelper:SetText("Copy settings from an existing profile into the selected profile to edit.")

        local copyLabel = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        copyLabel:SetPoint("TOPLEFT", copyHelper, "BOTTOMLEFT", 0, -14)
        copyLabel:SetText("Copy from:")

        local copyDropdown = CreateActionProfileDropdown(
            tf,
            180,
            copyLabel,
            "LEFT",
            "RIGHT",
            -10,
            -2,
            false,
            function(value) return value == "DEFAULT" and #GetSavedProfileEntries(nil) <= 1 end,
            function(profileValue, profileText)
                local targetProfile = GetDB().EXISTING_PROFILE_SELECT or "DEFAULT"

                if profileValue == targetProfile then
                    SetProfileStatus("PROFILES", "Source and target are the same profile.", true)
                    return
                end

                local capturedSource = profileValue
                local capturedTarget = targetProfile
                OMCConfirm(
                    format("Selected profile to edit: \"%s\"\nOverwrite it with \"%s\"?", targetProfile, profileText),
                    "Yes",
                    function()
                        if _G.OldManCursor_CopyProfileIntoProfile then
                            local ok, message = _G.OldManCursor_CopyProfileIntoProfile(capturedSource, capturedTarget)
                            if ok then
                                SetProfileStatus("PROFILES", message or "Profile overwritten.", false)
                                RefreshAllProfileWidgets()
                                RefreshReticle()
                                RebuildLayerList()
                            else
                                SetProfileStatus("PROFILES", message or "Could not overwrite profile.", true)
                            end
                        end
                    end
                )
            end
        )

        local deleteHelper = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        deleteHelper:SetPoint("TOPLEFT", copyLabel, "BOTTOMLEFT", 0, -20)
        deleteHelper:SetJustifyH("LEFT")
        deleteHelper:SetText("Delete an existing profile.")

        local deleteLabel = tf:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        deleteLabel:SetPoint("TOPLEFT", deleteHelper, "BOTTOMLEFT", 0, -14)
        deleteLabel:SetText("Delete:")

        local deleteDropdown = CreateActionProfileDropdown(
            tf,
            180,
            deleteLabel,
            "LEFT",
            "RIGHT",
            -10,
            -2,
            false,
            "DEFAULT",
            function(profileValue, profileText)
                local capturedProfile = profileValue
                if IsPreviewActive() then
                    SaveActiveProfile()
                    SetPreview(false)
                    if UI.UpdatePreviewButtonState then UI.UpdatePreviewButtonState() end
                    UpdatePreviewButton()
                    UpdateEditingProfileTexts()
                    UpdateEditModeUI()
                    RefreshAllProfileWidgets()
                    RebuildLayerList()
                end
                OMCConfirm(
                    format("Delete profile \"%s\"?", profileText),
                    "Delete",
                    function()
                        if _G.OldManCursor_DeleteProfile then
                            local ok, message = _G.OldManCursor_DeleteProfile(capturedProfile)
                            if ok then
                                local db = GetDB()
                                if db.EXISTING_PROFILE_SELECT == capturedProfile then
                                    db.EXISTING_PROFILE_SELECT = GetCharDB().CURRENT_PROFILE or "DEFAULT"
                                end
                                SetProfileStatus("PROFILES", message or "Profile deleted.", false)
                                RefreshAllProfileWidgets()
                                RefreshReticle()
                                RebuildLayerList()
                            else
                                SetProfileStatus("PROFILES", message or "Could not delete profile.", true)
                            end
                        end
                    end
                )
            end
        )

        UI.profilesStatusText = tf:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        UI.profilesStatusText:SetPoint("TOPLEFT", deleteLabel, "BOTTOMLEFT", 0, -40)
        UI.profilesStatusText:SetJustifyH("LEFT")
        UI.profilesStatusText:SetText("")

        UI.copyDropdown = copyDropdown
        UI.copyLabel = copyLabel
        UI.deleteDropdown = deleteDropdown
        UI.deleteLabel = deleteLabel

        copyDropdown:RefreshList()
        deleteDropdown:RefreshList()
    end

    SelectTab(State.currentTabKey or "GENERAL")
    RebuildLayerList()
    RefreshAllProfileWidgets()
    UpdateEditingProfileTexts()
    UpdatePreviewButton()
    UpdateEditModeUI()
end

_G.OldManCursor_ProfileChanged = function()
    if not State.built then
        return
    end

    State.previewActive = IsPreviewActive()
    RefreshAllProfileWidgets()

    if UI.generalVisibilityWidgets and UI.generalVisibilityWidgets.UpdateModeUI then
        UI.generalVisibilityWidgets.UpdateModeUI()
    end

    RebuildLayerList()
    UpdateEditingProfileTexts()
    UpdatePreviewButton()
    UpdateEditModeUI()
end

panel:SetScript("OnShow", function()
    BuildPanel()
    State.previewActive = IsPreviewActive()
    UpdatePreviewButton()
    if UI.UpdatePreviewButtonState then
        UI.UpdatePreviewButtonState()
    end
    SelectTab(State.currentTabKey or "GENERAL")
    RebuildLayerList()
    RefreshAllProfileWidgets()
    UpdateEditingProfileTexts()
    UpdatePreviewButton()
    UpdateEditModeUI()
end)

panel:SetScript("OnHide", function()
    SaveActiveProfile()

    State.previewActive = false
    SetPreview(false)
    UpdatePreviewButton()
    UpdateEditModeUI()
    GameTooltip:Hide()
    ResetProfilesTabSelections()
end)

local category = Settings.RegisterCanvasLayoutCategory(panel, ADDON_NAME)
Settings.RegisterAddOnCategory(category)

SLASH_OLDMANCURSOR1 = "/omc"
SlashCmdList["OLDMANCURSOR"] = function()
    Settings.OpenToCategory(category:GetID())
end