-- OldManCursor.lua
-- Displays layered textures anchored to the mouse cursor.
-- Visibility is controlled by the configured mode, unless preview is active.

local ADDON_NAME = "OldManCursor"
local floor      = math.floor
local sort       = table.sort

OldManCursorDB = OldManCursorDB or {}

-- Shared texture list — referenced by the Options file so it stays in one place.
OldManCursor_AvailableTextures = {
    { text = "Single Arrow",   value = "Interface\\AddOns\\OldManCursor\\layers\\1arrow.png" },
    { text = "Single Arrow (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\1arrow_blur.png" },
    { text = "Dual Arrow",   value = "Interface\\AddOns\\OldManCursor\\layers\\2arrows.png" },
    { text = "Dual Arrow (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\2arrows_blur.png" },
    { text = "Ring 2Cut Thin",   value = "Interface\\AddOns\\OldManCursor\\layers\\2cutring_thin.png" },
    { text = "Ring 2Cut Thin (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\2cutring_thin_blur.png" },
    { text = "Ring 2Cut Thick",   value = "Interface\\AddOns\\OldManCursor\\layers\\2cutring_thick.png" },
    { text = "Ring 2Cut Thick (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\2cutring_thick_blur.png" },
    { text = "Ring 3Cut Thin",   value = "Interface\\AddOns\\OldManCursor\\layers\\3cutring_thin.png" },
    { text = "Ring 3Cut Thin (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\3cutring_thin_blur.png" },
    { text = "Ring 3Cut Thick",   value = "Interface\\AddOns\\OldManCursor\\layers\\3cutring_thick.png" },
    { text = "Ring 3Cut Thick (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\3cutring_thick_blur.png" },
    { text = "Ring 4Cut Thin",   value = "Interface\\AddOns\\OldManCursor\\layers\\4cutring_thin.png" },
    { text = "Ring 4Cut Thin (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\4cutring_thin_blur.png" },
    { text = "Ring 4Cut Thick",   value = "Interface\\AddOns\\OldManCursor\\layers\\4cutring_thick.png" },
    { text = "Ring 4Cut Thick (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\4cutring_thick_blur.png" },
    { text = "Ring 8Cut Thin",   value = "Interface\\AddOns\\OldManCursor\\layers\\8cutring_thin.png" },
    { text = "Ring 8Cut Thin (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\8cutring_thin_blur.png" },
    { text = "Ring 8Cut Thick",   value = "Interface\\AddOns\\OldManCursor\\layers\\8cutring_thick.png" },
    { text = "Ring 8Cut Thick (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\8cutring_thick_blur.png" },
    { text = "Circle",   value = "Interface\\AddOns\\OldManCursor\\layers\\circle.png" },
    { text = "Circle (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\circle_blur.png" },
    { text = "Cross",   value = "Interface\\AddOns\\OldManCursor\\layers\\cross.png" },
    { text = "Cross (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\cross_blur.png" },
    { text = "Cross Big",   value = "Interface\\AddOns\\OldManCursor\\layers\\cross_big.png" },
    { text = "Cross Big (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\cross_big_blur.png" },
    { text = "Cross Small",   value = "Interface\\AddOns\\OldManCursor\\layers\\cross_small.png" },
    { text = "Cross Small (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\cross_small_blur.png" },
    { text = "Crosshair",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair.png" },
    { text = "Crosshair (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_blur.png" },
    { text = "Crosshair Long",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_long.png" },
    { text = "Crosshair Long (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_long_blur.png" },
    { text = "Crosshair Short",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_short.png" },
    { text = "Crosshair Short (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_short_blur.png" },
    { text = "Crosshair Rectangular Thin",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_rect_thin.png" },
    { text = "Crosshair Rectangular Thin (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_rect_thin_blur.png" },
    { text = "Crosshair Rectangular Thick",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_rect_thick.png" },
    { text = "Crosshair Rectangular Thick (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_rect_thick_blur.png" },
    { text = "Ring",   value = "Interface\\AddOns\\OldManCursor\\layers\\ring.png" },
    { text = "Ring (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\ring_blur.png" },
    { text = "Ring Thin",   value = "Interface\\AddOns\\OldManCursor\\layers\\ring_thin.png" },
    { text = "Ring Thin (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\ring_thin_blur.png" },
    { text = "Ring Thick",   value = "Interface\\AddOns\\OldManCursor\\layers\\ring_thick.png" },
    { text = "Ring Thick (Blur)",   value = "Interface\\AddOns\\OldManCursor\\layers\\ring_thick_blur.png" },
}

local defaultProfileData = {
    LINES_ENABLED      = true,
    LINE_GAP_SIZE      = 34,
    LINE_THICKNESS     = 1,
    LINE_BLEND         = "ADD",
    LINE_OPACITY       = 0.25,
    LINE_COLOR         = { 1, 1, 1 },
    LINE_LENGTH_MODE   = "SCREEN",
    LINE_EDGE_OFFSET_X = 1,
    LINE_EDGE_OFFSET_Y = 1,
    LAYERS = {
        {
            TEXTURE  = "Interface\\AddOns\\OldManCursor\\layers\\circle.png",
            SIZE     = 6,
            OPACITY  = 0.8,
            COLOR    = { 1, 1, 1 },
            BLEND    = "ADD",
            ROTATION = 0,
        },
        {
            TEXTURE  = "Interface\\AddOns\\OldManCursor\\layers\\crosshair.png",
            SIZE     = 96,
            OPACITY  = 0.25,
            COLOR    = { 1, 0, 0 },
            BLEND    = "ADD",
            ROTATION = 0,
        },
        {
            TEXTURE  = "Interface\\AddOns\\OldManCursor\\layers\\crosshair_short.png",
            SIZE     = 102,
            OPACITY  = 0.25,
            COLOR    = { 1, 1, 1 },
            BLEND    = "ADD",
            ROTATION = 0,
        },
        {
            TEXTURE  = "Interface\\AddOns\\OldManCursor\\layers\\ring.png",
            SIZE     = 100,
            OPACITY  = 0.15,
            COLOR    = { 1, 0, 0 },
            BLEND    = "ADD",
            ROTATION = 0,
        },
        {
            TEXTURE  = "Interface\\AddOns\\OldManCursor\\layers\\ring_blur.png",
            SIZE     = 100,
            OPACITY  = 0.15,
            COLOR    = { 1, 0, 0 },
            BLEND    = "ADD",
            ROTATION = 0,
        },
    },
}

-- Exposed so Options can reference defaults without duplicating them.
OldManCursor_DefaultProfileData = defaultProfileData

-- ─── Utility ──────────────────────────────────────────────────────────────────

-- FIX: setmetatable so copies of tables with metatables aren't silently broken.
local function DeepCopy(value)
    if type(value) ~= "table" then return value end
    local out = {}
    for k, v in pairs(value) do
        out[k] = DeepCopy(v)
    end
    setmetatable(out, getmetatable(value))
    return out
end

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

-- Returns the r, g, b of the currently logged-in character's class color.
-- Prefers user-overridden colors stored in OldManCursorDB.CLASS_COLORS, then
-- falls back to WoW's built-in RAID_CLASS_COLORS table.
local function GetCurrentClassColor()
    local _, classFilename = UnitClass("player")
    if classFilename then
        local ccdb = OldManCursorDB and OldManCursorDB.CLASS_COLORS
        if ccdb and ccdb[classFilename] then
            local c = ccdb[classFilename]
            return c[1] or 1, c[2] or 1, c[3] or 1
        end
        if RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFilename] then
            local c = RAID_CLASS_COLORS[classFilename]
            return c.r or 1, c.g or 1, c.b or 1
        end
    end
    return 1, 1, 1
end

local function Trim(str)
    str = tostring(str or "")
    str = str:gsub("^%s+", "")
    str = str:gsub("%s+$", "")
    return str
end
_G.OldManCursor_Trim = Trim

local function GetCharacterKey()
    local name  = UnitName("player") or "Unknown"
    local realm = GetRealmName()     or "Unknown"
    return name .. "-" .. realm
end

local function CopyDefaults(src, dst)
    for k, v in pairs(src) do
        if dst[k] == nil then
            -- Don't add missing numeric-indexed entries (array elements) to an
            -- existing table — that would silently re-grow arrays like LAYERS
            -- back to the default length after the user has shortened them.
            if type(k) ~= "number" then
                dst[k] = type(v) == "table" and DeepCopy(v) or v
            end
        elseif type(v) == "table" and type(dst[k]) == "table" then
            CopyDefaults(v, dst[k])
        end
    end
end

local function DB()
    return OldManCursorDB
end

-- ─── Database ─────────────────────────────────────────────────────────────────

local dbReady = false

local function EnsureDB()
    if dbReady then return end
    local db = DB()

    db.PROFILES   = db.PROFILES   or {}
    db.CHARACTERS = db.CHARACTERS or {}

    -- One-time migration from the old flat-key SavedVariables format.
    -- If LINE_GAP_SIZE exists we know we're upgrading from a pre-profile build.
    if db.LINE_GAP_SIZE ~= nil then
        db.PROFILES.DEFAULT = {
            LINE_GAP_SIZE      = db.LINE_GAP_SIZE,
            LINE_THICKNESS     = db.LINE_THICKNESS,
            LINE_BLEND         = db.LINE_BLEND,
            LINE_OPACITY       = db.LINE_OPACITY,
            LINE_COLOR         = db.LINE_COLOR and DeepCopy(db.LINE_COLOR) or { 1, 1, 1 },
            LINE_LENGTH_MODE   = db.LINE_LENGTH_MODE,
            LINE_EDGE_OFFSET_X = db.LINE_EDGE_OFFSET_X,
            LINE_EDGE_OFFSET_Y = db.LINE_EDGE_OFFSET_Y,
            LAYERS             = db.LAYERS and DeepCopy(db.LAYERS) or {},
        }
        db.LINE_GAP_SIZE      = nil
        db.LINE_THICKNESS     = nil
        db.LINE_BLEND         = nil
        db.LINE_OPACITY       = nil
        db.LINE_COLOR         = nil
        db.LINE_LENGTH_MODE   = nil
        db.LINE_EDGE_OFFSET_X = nil
        db.LINE_EDGE_OFFSET_Y = nil
        db.LAYERS             = nil
    end

    -- One-time migration from the pre-per-character SavedVariables format.
    -- If VISIBILITY_MODE exists at root we know we're upgrading from that build.
    if db.VISIBILITY_MODE ~= nil then
        local key = GetCharacterKey()
        db.CHARACTERS[key] = db.CHARACTERS[key] or {}
        local c = db.CHARACTERS[key]
        if c.VISIBILITY_MODE        == nil then c.VISIBILITY_MODE        = db.VISIBILITY_MODE        end
        if c.CURRENT_PROFILE        == nil then c.CURRENT_PROFILE        = db.CURRENT_PROFILE        end
        if c.SHOW_IN_RESTED_ZONES   == nil then c.SHOW_IN_RESTED_ZONES   = db.SHOW_IN_RESTED_ZONES   end
        if c.PROFILE_OUTSIDE_COMBAT == nil then c.PROFILE_OUTSIDE_COMBAT = db.PROFILE_OUTSIDE_COMBAT end
        if c.PROFILE_DURING_COMBAT  == nil then c.PROFILE_DURING_COMBAT  = db.PROFILE_DURING_COMBAT  end
        db.VISIBILITY_MODE        = nil
        db.CURRENT_PROFILE        = nil
        db.SHOW_IN_RESTED_ZONES   = nil
        db.PROFILE_OUTSIDE_COMBAT = nil
        db.PROFILE_DURING_COMBAT  = nil
    end

    -- Seed DEFAULT profile from hard-coded defaults if it doesn't exist yet.
    if not db.PROFILES.DEFAULT then
        db.PROFILES.DEFAULT = DeepCopy(defaultProfileData)
    end

    -- Fill any missing keys in every profile.
    for _, profileData in pairs(db.PROFILES) do
        if type(profileData) == "table" then
            CopyDefaults(defaultProfileData, profileData)
        end
    end

    dbReady = true
end

-- Returns (and lazily initialises) the current character's settings table.
-- All per-character state (visibility mode, selected profile, etc.) lives here.
local function CharDB()
    EnsureDB()
    local db  = DB()
    local key = GetCharacterKey()
    if not db.CHARACTERS[key] then
        db.CHARACTERS[key] = {}
    end
    local c = db.CHARACTERS[key]
    if c.VISIBILITY_MODE        == nil then c.VISIBILITY_MODE        = "COMBAT"  end
    if c.SHOW_IN_RESTED_ZONES   == nil then c.SHOW_IN_RESTED_ZONES   = true      end
    if c.CURRENT_PROFILE        == nil then c.CURRENT_PROFILE        = "DEFAULT" end
    if c.PROFILE_OUTSIDE_COMBAT == nil then c.PROFILE_OUTSIDE_COMBAT = "DEFAULT" end
    if c.PROFILE_DURING_COMBAT  == nil then c.PROFILE_DURING_COMBAT  = "DEFAULT" end
    -- Guard against stale profile references.
    if not db.PROFILES[c.CURRENT_PROFILE]        then c.CURRENT_PROFILE        = "DEFAULT" end
    if not db.PROFILES[c.PROFILE_OUTSIDE_COMBAT] then c.PROFILE_OUTSIDE_COMBAT = "DEFAULT" end
    if not db.PROFILES[c.PROFILE_DURING_COMBAT]  then c.PROFILE_DURING_COMBAT  = "DEFAULT" end
    return c
end

-- ─── Profile state ────────────────────────────────────────────────────────────

local currentAppliedProfileName = nil
local isPreviewing              = false
local previewProfileName        = nil

-- FIX: All rendering now reads directly from the profile table instead of a
-- flat DB mirror.  This eliminates the divergence risk that existed before.
local function GetActiveProfileData()
    local db   = DB()
    local name = currentAppliedProfileName or "DEFAULT"
    if not db.PROFILES or not db.PROFILES[name] then name = "DEFAULT" end
    if not db.PROFILES[name] then
        db.PROFILES[name] = DeepCopy(defaultProfileData)
    end
    return db.PROFILES[name]
end

local function NotifyProfileChanged()
    if _G.OldManCursor_ProfileChanged then
        _G.OldManCursor_ProfileChanged()
    end
end

local function GetValidProfileName(profileName)
    local db = DB()
    profileName = Trim(profileName)
    if profileName == "" or not db.PROFILES[profileName] then return nil end
    return profileName
end

-- FIX: ApplyNamedProfile no longer copies profile data into flat keys.
-- It simply records which profile is active.
local function ApplyNamedProfile(profileName, updateCurrentProfileField)
    EnsureDB()
    profileName = GetValidProfileName(profileName)
    if not profileName then return false, "Profile not found." end

    currentAppliedProfileName = profileName
    if updateCurrentProfileField then
        CharDB().CURRENT_PROFILE = profileName
    end
    return true
end

local function GetStateProfileNameForCurrentState()
    local c = CharDB()
    if c.VISIBILITY_MODE == "ADVANCED" then
        if UnitAffectingCombat("player") then
            return c.PROFILE_DURING_COMBAT or "DEFAULT"
        end
        return c.PROFILE_OUTSIDE_COMBAT or "DEFAULT"
    end
    return c.CURRENT_PROFILE or "DEFAULT"
end

local function GetDesiredAppliedProfileName()
    EnsureDB()
    local db = DB()
    if isPreviewing and previewProfileName and db.PROFILES[previewProfileName] then
        return previewProfileName
    end
    local desired = GetStateProfileNameForCurrentState()
    if not db.PROFILES[desired] then desired = "DEFAULT" end
    return desired
end

local function GetEditingProfileName()
    EnsureDB()
    local db = DB()
    if isPreviewing and previewProfileName and db.PROFILES[previewProfileName] then
        return previewProfileName
    end
    if currentAppliedProfileName and db.PROFILES[currentAppliedProfileName] then
        return currentAppliedProfileName
    end
    return GetDesiredAppliedProfileName()
end

local function GetSortedProfileNames()
    EnsureDB()
    local db    = DB()
    local names = {}
    for name in pairs(db.PROFILES) do names[#names + 1] = name end
    sort(names, function(a, b)
        if a == "DEFAULT" then return true  end
        if b == "DEFAULT" then return false end
        return string.lower(a) < string.lower(b)
    end)
    return names
end

local function SyncProfileForCurrentState()
    local desired = GetDesiredAppliedProfileName()
    if currentAppliedProfileName == desired then return false end
    ApplyNamedProfile(desired, (not isPreviewing) and (CharDB().VISIBILITY_MODE ~= "ADVANCED"))
    NotifyProfileChanged()
    return true
end

local function LoadProfile(profileName)
    local ok, message = ApplyNamedProfile(profileName, true)
    if not ok then return false, message end
    ApplySettings()
    NotifyProfileChanged()
    return true
end

local function CreateProfileFromCurrent(profileName)
    EnsureDB()
    local db = DB()
    profileName = Trim(profileName)
    if profileName == ""           then return false, "Please enter a profile name."             end
    if db.PROFILES[profileName]    then return false, "A profile with that name already exists." end

    db.PROFILES[profileName] = DeepCopy(defaultProfileData)
    NotifyProfileChanged()
    return true, "Profile created."
end

local function CopyProfileIntoProfile(sourceName, targetName)
    EnsureDB()
    local db = DB()
    sourceName = Trim(sourceName)
    targetName = Trim(targetName)
    if sourceName == "" or targetName == ""  then return false, "Invalid profile name."                    end
    if not db.PROFILES[sourceName]           then return false, "Source profile not found."                end
    if not db.PROFILES[targetName]           then return false, "Target profile not found."                end
    if sourceName == targetName              then return false, "Source and target are the same profile."  end

    db.PROFILES[targetName] = DeepCopy(db.PROFILES[sourceName])
    CopyDefaults(defaultProfileData, db.PROFILES[targetName])

    ApplySettings()
    NotifyProfileChanged()
    return true, "Profile overwritten."
end

local function DeleteProfile(profileName)
    EnsureDB()
    local db = DB()
    profileName = Trim(profileName)
    if profileName == "" or not db.PROFILES[profileName] then return false, "Profile not found."                      end
    if profileName == "DEFAULT"                          then return false, "The default profile cannot be deleted."   end

    db.PROFILES[profileName] = nil

    if previewProfileName == profileName then
        previewProfileName = nil
        isPreviewing       = false
    end

    -- Fix up every character that referenced the deleted profile.
    for _, charData in pairs(db.CHARACTERS or {}) do
        if type(charData) == "table" then
            if charData.CURRENT_PROFILE        == profileName then charData.CURRENT_PROFILE        = "DEFAULT" end
            if charData.PROFILE_OUTSIDE_COMBAT == profileName then charData.PROFILE_OUTSIDE_COMBAT = "DEFAULT" end
            if charData.PROFILE_DURING_COMBAT  == profileName then charData.PROFILE_DURING_COMBAT  = "DEFAULT" end
        end
    end

    SyncProfileForCurrentState()
    ApplySettings()
    NotifyProfileChanged()
    return true, "Profile deleted."
end

-- FIX: Takes an explicit profileName so the Options file can reset whichever
-- profile is selected in the UI dropdown, not just the currently applied one.
local function ResetProfile(profileName)
    EnsureDB()
    local db = DB()
    profileName = GetValidProfileName(profileName) or GetEditingProfileName()
    db.PROFILES[profileName] = DeepCopy(defaultProfileData)
    ApplySettings()
    NotifyProfileChanged()
    return true, "Profile reset to defaults."
end

-- SaveActiveProfile is now a no-op.  Writes go directly to the profile table,
-- so there is no secondary flat-key store that needs flushing.
local function SaveActiveProfile()
    return true
end

-- ─── Cursor frame ─────────────────────────────────────────────────────────────

local LINE_TEXTURE = "Interface\\AddOns\\OldManCursor\\white1x1.png"

local cursorFrame = CreateFrame("Frame", ADDON_NAME .. "Frame", UIParent)
cursorFrame:SetSize(1, 1)
cursorFrame:SetPoint("CENTER", UIParent, "CENTER")
cursorFrame:SetFrameStrata("TOOLTIP")
cursorFrame:SetClampedToScreen(false)
cursorFrame:Hide()

local layerTextures = {}

local hLineLeft  = cursorFrame:CreateTexture(nil, "OVERLAY")
local hLineRight = cursorFrame:CreateTexture(nil, "OVERLAY")
local vLineUp    = cursorFrame:CreateTexture(nil, "OVERLAY")
local vLineDown  = cursorFrame:CreateTexture(nil, "OVERLAY")

hLineLeft :SetTexture(LINE_TEXTURE)
hLineRight:SetTexture(LINE_TEXTURE)
vLineUp   :SetTexture(LINE_TEXTURE)
vLineDown :SetTexture(LINE_TEXTURE)

local UpdateVisibility

local function EnsureLayerTexture(index)
    if not layerTextures[index] then
        local tex = cursorFrame:CreateTexture(nil, "OVERLAY")
        tex:SetPoint("CENTER", cursorFrame, "TOPLEFT", 0, 0)
        layerTextures[index] = tex
    end
    return layerTextures[index]
end

local function HideUnusedLayerTextures(usedCount)
    for i = usedCount + 1, #layerTextures do
        layerTextures[i]:Hide()
    end
end

-- ─── Rendering ────────────────────────────────────────────────────────────────

local function ApplyLayerSettings()
    local profile    = GetActiveProfileData()
    local usedLayers = 0

    if profile.LAYERS then
        for i, layer in ipairs(profile.LAYERS) do
            if layer and layer.TEXTURE then
                local tex     = EnsureLayerTexture(i)
                local size    = Clamp(tonumber(layer.SIZE)    or 64, 1, 9999)
                local opacity = Clamp(tonumber(layer.OPACITY) or 1,  0, 1)
                local color
                if layer.COLOR_CURRENT_CLASS then
                    local r, g, b = GetCurrentClassColor()
                    color = { r, g, b }
                else
                    color = layer.COLOR or { 1, 1, 1 }
                end

                tex:SetTexture(layer.TEXTURE)
                tex:SetBlendMode(layer.BLEND or "BLEND")
                tex:SetSize(size, size)
                tex:SetVertexColor(color[1] or 1, color[2] or 1, color[3] or 1, opacity)
                tex:SetRotation(math.rad(tonumber(layer.ROTATION) or 0))
                tex:ClearAllPoints()
                tex:SetPoint("CENTER", cursorFrame, "TOPLEFT", 0, 0)
                tex:Show()
                usedLayers = i
            end
        end
    end

    HideUnusedLayerTextures(usedLayers)
end

-- FIX: UpdateLineGeometry is no longer called every frame from OnUpdate.
-- It is only called from ApplySettings (which fires on relevant events).
local function UpdateLineGeometry()
    local profile  = GetActiveProfileData()

    if profile.LINES_ENABLED == false then
        hLineLeft:Hide()
        hLineRight:Hide()
        vLineUp:Hide()
        vLineDown:Hide()
        return
    end

    hLineLeft:Show()
    hLineRight:Show()
    vLineUp:Show()
    vLineDown:Show()

    local uiWidth  = UIParent:GetWidth()  or 0
    local uiHeight = UIParent:GetHeight() or 0

    local gap       = Clamp(floor((tonumber(profile.LINE_GAP_SIZE)  or 42) + 0.5), 0, 512)
    local thickness = Clamp(floor((tonumber(profile.LINE_THICKNESS) or 1)  + 0.5), 1, 10)
    local opacity   = Clamp(tonumber(profile.LINE_OPACITY) or 0.25, 0, 1)
    local blend     = profile.LINE_BLEND or "BLEND"
    local color
    if profile.LINE_COLOR_CURRENT_CLASS then
        local r, g, b = GetCurrentClassColor()
        color = { r, g, b }
    else
        color = profile.LINE_COLOR or { 1, 1, 1 }
    end
    local r, g, b   = color[1] or 1, color[2] or 1, color[3] or 1
    local mode      = profile.LINE_LENGTH_MODE or "SCREEN"

    local hReduction, vReduction = 0, 0
    if mode == "OFFSET" then
        hReduction = math.max(1, floor((tonumber(profile.LINE_EDGE_OFFSET_X) or 1) + 0.5))
        vReduction = math.max(1, floor((tonumber(profile.LINE_EDGE_OFFSET_Y) or 1) + 0.5))
    end

    local hLen = math.max(0, uiWidth  - gap - hReduction)
    local vLen = math.max(0, uiHeight - gap - vReduction)

    hLineLeft:SetBlendMode(blend)
    hLineLeft:SetSize(hLen, thickness)
    hLineLeft:SetVertexColor(r, g, b, opacity)
    hLineLeft:ClearAllPoints()
    hLineLeft:SetPoint("RIGHT", cursorFrame, "TOPLEFT", -gap, 0)

    hLineRight:SetBlendMode(blend)
    hLineRight:SetSize(hLen, thickness)
    hLineRight:SetVertexColor(r, g, b, opacity)
    hLineRight:ClearAllPoints()
    hLineRight:SetPoint("LEFT", cursorFrame, "TOPLEFT", gap, 0)

    vLineUp:SetBlendMode(blend)
    vLineUp:SetSize(thickness, vLen)
    vLineUp:SetVertexColor(r, g, b, opacity)
    vLineUp:ClearAllPoints()
    vLineUp:SetPoint("BOTTOM", cursorFrame, "TOPLEFT", 0, gap)

    vLineDown:SetBlendMode(blend)
    vLineDown:SetSize(thickness, vLen)
    vLineDown:SetVertexColor(r, g, b, opacity)
    vLineDown:ClearAllPoints()
    vLineDown:SetPoint("TOP", cursorFrame, "TOPLEFT", 0, -gap)
end

local function ApplySettings()
    SyncProfileForCurrentState()
    ApplyLayerSettings()
    UpdateLineGeometry()
    UpdateVisibility()
end

UpdateVisibility = function()
    local c = CharDB()

    if isPreviewing then
        cursorFrame:Show()
        return
    end

    if not c.SHOW_IN_RESTED_ZONES and IsResting() then
        cursorFrame:Hide()
        return
    end

    local shouldShow = false
    if     c.VISIBILITY_MODE == "ALWAYS"   then shouldShow = true
    elseif c.VISIBILITY_MODE == "COMBAT"   then shouldShow = UnitAffectingCombat("player") and true or false
    elseif c.VISIBILITY_MODE == "ADVANCED" then shouldShow = true
    end

    if shouldShow then cursorFrame:Show() else cursorFrame:Hide() end
end

-- ─── Global API ───────────────────────────────────────────────────────────────

_G.OldManCursor_ApplySettings        = ApplySettings
_G.OldManCursor_GetActiveProfileData = GetActiveProfileData

-- Forces a full re-render by clearing the cached profile name so
-- SyncProfileForCurrentState can't bail out early.
_G.OldManCursor_ForceApplySettings = function()
    currentAppliedProfileName = nil
    ApplySettings()
end

_G.OldManCursor_SetPreview = function(enabled, profileName)
    EnsureDB()
    if enabled then
        local target = GetValidProfileName(profileName) or GetDesiredAppliedProfileName()
        isPreviewing      = true
        previewProfileName = target
        ApplySettings()
        NotifyProfileChanged()
        return true
    end
    isPreviewing       = false
    previewProfileName = nil
    ApplySettings()
    NotifyProfileChanged()
    return true
end

_G.OldManCursor_GetPreviewState        = function() return isPreviewing and true or false end
_G.OldManCursor_GetActiveProfileName   = function() return CharDB().CURRENT_PROFILE or "DEFAULT" end
_G.OldManCursor_GetEditingProfileName  = function() return GetEditingProfileName() end
_G.OldManCursor_GetProfileNames        = function() return GetSortedProfileNames() end

_G.OldManCursor_GetProfileEntries = function(excludeName)
    local names   = GetSortedProfileNames()
    local entries = {}
    excludeName   = Trim(excludeName or "")
    for _, name in ipairs(names) do
        if name ~= excludeName then
            entries[#entries + 1] = { text = name, value = name }
        end
    end
    return entries
end

_G.OldManCursor_CreateProfile          = function(n)        return CreateProfileFromCurrent(n)     end
_G.OldManCursor_LoadProfile            = function(n)        return LoadProfile(n)                   end
_G.OldManCursor_CopyProfileIntoProfile = function(src, tgt) return CopyProfileIntoProfile(src, tgt) end
_G.OldManCursor_DeleteProfile          = function(n)        return DeleteProfile(n)                 end
_G.OldManCursor_SaveActiveProfile      = function()         return SaveActiveProfile()              end

-- FIX: Explicit-name reset so Options can target the dropdown-selected profile.
_G.OldManCursor_ResetProfile = function(n)
    return ResetProfile(n)
end

-- Exposes the current character's settings table to the Options UI.
_G.OldManCursor_GetCharacterDB = function()
    return CharDB()
end

-- Backward-compatible alias.
_G.OldManCursor_ResetCurrentProfileToDefaults = function()
    return ResetProfile(GetEditingProfileName())
end

-- ─── OnUpdate — frame position only ──────────────────────────────────────────
-- FIX: UpdateLineGeometry() has been removed from here.  It was recalculating
-- line lengths (derived from UIParent dimensions and profile settings) 60+ times
-- per second despite those values never changing with cursor movement.

local lastCursorX, lastCursorY = nil, nil

cursorFrame:SetScript("OnUpdate", function(self)
    local x, y = GetCursorPosition()
    if x == lastCursorX and y == lastCursorY then return end
    lastCursorX, lastCursorY = x, y
    local scale = UIParent:GetEffectiveScale()
    self:ClearAllPoints()
    self:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x / scale, y / scale)
end)

-- ─── Events ───────────────────────────────────────────────────────────────────

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
eventFrame:RegisterEvent("DISPLAY_SIZE_CHANGED")
eventFrame:RegisterEvent("UI_SCALE_CHANGED")
eventFrame:RegisterEvent("PLAYER_UPDATE_RESTING")

eventFrame:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_ENTERING_WORLD"
    or event == "PLAYER_REGEN_DISABLED"
    or event == "PLAYER_REGEN_ENABLED"
    or event == "PLAYER_UPDATE_RESTING"
    or event == "DISPLAY_SIZE_CHANGED"
    or event == "UI_SCALE_CHANGED" then
        ApplySettings()  -- already calls UpdateVisibility() internally
    end
end)

print("|cff00ff00" .. ADDON_NAME .. "|r loaded.")