--------------------------------------------------------------------------------
-- Nanami-UI: Layout Mode (Mover System)
--------------------------------------------------------------------------------

SFrames.Movers = {}

local M = SFrames.Movers
local registry = {}
local moverFrames = {}
local gridFrame = nil
local controlBar = nil
local overlayFrame = nil
local isLayoutMode = false

local GRID_SPACING = 50
local SNAP_THRESHOLD = 10

local MOVER_BACKDROP = {
    bgFile   = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 1,
    insets   = { left = 1, right = 1, top = 1, bottom = 1 },
}
local MOVER_BACKDROP_2PX = {
    bgFile   = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 2,
    insets   = { left = 2, right = 2, top = 2, bottom = 2 },
}

--------------------------------------------------------------------------------
-- Theme helper
--------------------------------------------------------------------------------
local function T()
    return SFrames.ActiveTheme or {}
end

local function Font()
    return (SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
end

--------------------------------------------------------------------------------
-- Config helpers
--------------------------------------------------------------------------------
local function GetLayoutCfg()
    if not SFramesDB then SFramesDB = {} end
    if type(SFramesDB.layoutMode) ~= "table" then
        SFramesDB.layoutMode = {
            snapEnabled = true,
            snapThreshold = SNAP_THRESHOLD,
            showGrid = true,
        }
    end
    return SFramesDB.layoutMode
end

local function EnsurePositions()
    if not SFramesDB then SFramesDB = {} end
    if not SFramesDB.Positions then SFramesDB.Positions = {} end
    return SFramesDB.Positions
end

--------------------------------------------------------------------------------
-- Snap logic
--------------------------------------------------------------------------------
local function GetFrameEdges(frame)
    local l = frame:GetLeft() or 0
    local r = frame:GetRight() or 0
    local tp = frame:GetTop() or 0
    local b = frame:GetBottom() or 0
    return l, r, tp, b, (l + r) / 2, (tp + b) / 2
end

local function ApplySnap(mover)
    local cfg = GetLayoutCfg()
    if not cfg.snapEnabled then return end
    if IsShiftKeyDown() then return end

    local threshold = cfg.snapThreshold or SNAP_THRESHOLD
    local ml, mr, mt, mb, mcx, mcy = GetFrameEdges(mover)
    local mw, mh = mr - ml, mt - mb

    local screenW = UIParent:GetRight() or UIParent:GetWidth()
    local screenH = UIParent:GetTop() or UIParent:GetHeight()
    local screenCX, screenCY = screenW / 2, screenH / 2

    local snapX, snapY = nil, nil
    local bestDX, bestDY = threshold + 1, threshold + 1

    if math.abs(ml) < bestDX then bestDX = math.abs(ml); snapX = 0 end
    if math.abs(mr - screenW) < bestDX then bestDX = math.abs(mr - screenW); snapX = screenW - mw end
    if math.abs(mt - screenH) < bestDY then bestDY = math.abs(mt - screenH); snapY = screenH - mh end
    if math.abs(mb) < bestDY then bestDY = math.abs(mb); snapY = 0 end

    if math.abs(mcx - screenCX) < bestDX then bestDX = math.abs(mcx - screenCX); snapX = screenCX - mw / 2 end
    if math.abs(mcy - screenCY) < bestDY then bestDY = math.abs(mcy - screenCY); snapY = screenCY - mh / 2 end

    for name, _ in pairs(registry) do
        local other = moverFrames[name]
        if other and other ~= mover and other:IsShown() then
            local ol, or2, ot, ob, ocx, ocy = GetFrameEdges(other)
            local px = {
                { ml, or2, 0 }, { mr, ol, -mw }, { ml, ol, 0 },
                { mr, or2, -mw }, { mcx, ocx, -mw / 2 },
            }
            for _, p in ipairs(px) do
                local d = math.abs(p[1] - p[2])
                if d < bestDX then bestDX = d; snapX = p[2] + p[3] end
            end
            local py = {
                { math.abs(mb - ot), ot }, { math.abs(mt - ob), ob - mh },
                { math.abs(mt - ot), ot - mh }, { math.abs(mb - ob), ob },
                { math.abs(mcy - ocy), ocy - mh / 2 },
            }
            for _, p in ipairs(py) do
                if p[1] < bestDY then bestDY = p[1]; snapY = p[2] end
            end
        end
    end

    if bestDX <= threshold and snapX then
        mover:ClearAllPoints()
        local curB = (bestDY <= threshold and snapY) or (mover:GetBottom() or 0)
        mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", snapX, curB)
        return
    end
    if bestDY <= threshold and snapY then
        mover:ClearAllPoints()
        mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", mover:GetLeft() or 0, snapY)
    end
end

--------------------------------------------------------------------------------
-- Position save / load
--------------------------------------------------------------------------------
local function SaveMoverPosition(name, mover)
    local positions = EnsurePositions()
    local l = mover:GetLeft()
    local b = mover:GetBottom()
    local screenW = UIParent:GetRight() or UIParent:GetWidth()
    local screenH = UIParent:GetTop() or UIParent:GetHeight()
    if not l or not b then return end

    local cx = l + (mover:GetWidth() or 0) / 2
    local cy = b + (mover:GetHeight() or 0) / 2

    local point, relPoint, xOfs, yOfs
    if cx < screenW / 3 then
        if cy > screenH * 2 / 3 then
            point = "TOPLEFT"; relPoint = "TOPLEFT"
            xOfs = l; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
        elseif cy < screenH / 3 then
            point = "BOTTOMLEFT"; relPoint = "BOTTOMLEFT"
            xOfs = l; yOfs = b
        else
            point = "LEFT"; relPoint = "LEFT"
            xOfs = l; yOfs = cy - screenH / 2
        end
    elseif cx > screenW * 2 / 3 then
        local r = mover:GetRight() or 0
        if cy > screenH * 2 / 3 then
            point = "TOPRIGHT"; relPoint = "TOPRIGHT"
            xOfs = r - screenW; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
        elseif cy < screenH / 3 then
            point = "BOTTOMRIGHT"; relPoint = "BOTTOMRIGHT"
            xOfs = r - screenW; yOfs = b
        else
            point = "RIGHT"; relPoint = "RIGHT"
            xOfs = r - screenW; yOfs = cy - screenH / 2
        end
    else
        if cy > screenH * 2 / 3 then
            point = "TOP"; relPoint = "TOP"
            xOfs = cx - screenW / 2; yOfs = -(screenH - (b + (mover:GetHeight() or 0)))
        elseif cy < screenH / 3 then
            point = "BOTTOM"; relPoint = "BOTTOM"
            xOfs = cx - screenW / 2; yOfs = b
        else
            point = "CENTER"; relPoint = "CENTER"
            xOfs = cx - screenW / 2; yOfs = cy - screenH / 2
        end
    end

    positions[name] = { point = point, relativePoint = relPoint, xOfs = xOfs, yOfs = yOfs }
end

--------------------------------------------------------------------------------
-- Sync mover <-> actual frame
--------------------------------------------------------------------------------
local function UpdateCoordText(mover)
    if not mover or not mover._coordText then return end
    local l = mover:GetLeft()
    local b = mover:GetBottom()
    if l and b then
        mover._coordText:SetText(string.format("%.0f, %.0f", l, b))
    end
end

local function SyncMoverToFrame(name)
    local entry = registry[name]
    local mover = moverFrames[name]
    if not entry or not mover then return end
    local frame = entry.frame
    if not frame then return end

    local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
    local w = (frame:GetWidth() or 100) * scale
    local h = (frame:GetHeight() or 50) * scale

    mover:SetWidth(math.max(w, 72))
    mover:SetHeight(math.max(h, 40))

    local l = frame:GetLeft()
    local b = frame:GetBottom()
    if l and b then
        mover:ClearAllPoints()
        mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l * scale, b * scale)
    else
        local positions = EnsurePositions()
        local pos = positions[name]
        if pos and pos.point and pos.relativePoint then
            local tempF = CreateFrame("Frame", nil, UIParent)
            tempF:SetWidth(w)
            tempF:SetHeight(h)
            tempF:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
            local tl = tempF:GetLeft()
            local tb = tempF:GetBottom()
            if tl and tb then
                mover:ClearAllPoints()
                mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", tl, tb)
            end
            tempF:Hide()
        else
            mover:ClearAllPoints()
            local relTo = _G[entry.defaultRelativeTo] or UIParent
            mover:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
                entry.defaultX or 0, entry.defaultY or 0)
        end
    end
    UpdateCoordText(mover)
end

local function SyncFrameToMover(name)
    local entry = registry[name]
    local mover = moverFrames[name]
    if not entry or not mover then return end
    local frame = entry.frame
    if not frame then return end

    local moverL = mover:GetLeft() or 0
    local moverB = mover:GetBottom() or 0

    SaveMoverPosition(name, mover)

    local positions = EnsurePositions()
    local pos = positions[name]
    if pos then
        frame:ClearAllPoints()
        frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)

        local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
        local newL = frame:GetLeft() or 0
        local newB = frame:GetBottom() or 0
        local dL = newL * scale - moverL
        local dB = newB * scale - moverB
        if math.abs(dL) > 2 or math.abs(dB) > 2 then
            SFrames:Print(string.format(
                "|cffff6666[位置偏差]|r |cffaaddff%s|r  anchor=%s ofs=(%.1f,%.1f)  dL=%.1f dB=%.1f scale=%.2f",
                entry.label or name, pos.point, pos.xOfs or 0, pos.yOfs or 0, dL, dB, scale))
        end
    end

    if entry.onMoved then entry.onMoved() end
    UpdateCoordText(mover)
end

--------------------------------------------------------------------------------
-- Nudge helper
--------------------------------------------------------------------------------
local function NudgeMover(name, dx, dy)
    local mover = moverFrames[name]
    if not mover then return end
    local l = mover:GetLeft()
    local b = mover:GetBottom()
    if not l or not b then return end
    mover:ClearAllPoints()
    mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l + dx, b + dy)
    SyncFrameToMover(name)
end

--------------------------------------------------------------------------------
-- Pixel-art triangle arrow helper
-- Draws a triangle using 4 horizontal/vertical strips for a clean geometric look
--------------------------------------------------------------------------------
local function CreateTriangle(parent, direction, r, g, b, a)
    local strips = {}

    if direction == "UP" then
        local widths  = { 2, 5, 8, 11 }
        local heights = { 2, 2, 2, 2 }
        local yOfs    = { 3, 1, -1, -3 }
        for i = 1, 4 do
            local s = parent:CreateTexture(nil, "OVERLAY")
            s:SetTexture("Interface\\Buttons\\WHITE8x8")
            s:SetWidth(widths[i])
            s:SetHeight(heights[i])
            s:SetPoint("CENTER", parent, "CENTER", 0, yOfs[i])
            s:SetVertexColor(r, g, b, a)
            table.insert(strips, s)
        end
    elseif direction == "DOWN" then
        local widths  = { 11, 8, 5, 2 }
        local heights = { 2, 2, 2, 2 }
        local yOfs    = { 3, 1, -1, -3 }
        for i = 1, 4 do
            local s = parent:CreateTexture(nil, "OVERLAY")
            s:SetTexture("Interface\\Buttons\\WHITE8x8")
            s:SetWidth(widths[i])
            s:SetHeight(heights[i])
            s:SetPoint("CENTER", parent, "CENTER", 0, yOfs[i])
            s:SetVertexColor(r, g, b, a)
            table.insert(strips, s)
        end
    elseif direction == "LEFT" then
        local widths   = { 2, 2, 2, 2 }
        local heights  = { 2, 5, 8, 11 }
        local xOfs     = { -3, -1, 1, 3 }
        for i = 1, 4 do
            local s = parent:CreateTexture(nil, "OVERLAY")
            s:SetTexture("Interface\\Buttons\\WHITE8x8")
            s:SetWidth(widths[i])
            s:SetHeight(heights[i])
            s:SetPoint("CENTER", parent, "CENTER", xOfs[i], 0)
            s:SetVertexColor(r, g, b, a)
            table.insert(strips, s)
        end
    elseif direction == "RIGHT" then
        local widths   = { 2, 2, 2, 2 }
        local heights  = { 11, 8, 5, 2 }
        local xOfs     = { -3, -1, 1, 3 }
        for i = 1, 4 do
            local s = parent:CreateTexture(nil, "OVERLAY")
            s:SetTexture("Interface\\Buttons\\WHITE8x8")
            s:SetWidth(widths[i])
            s:SetHeight(heights[i])
            s:SetPoint("CENTER", parent, "CENTER", xOfs[i], 0)
            s:SetVertexColor(r, g, b, a)
            table.insert(strips, s)
        end
    end

    return strips
end

local function SetTriangleColor(strips, r, g, b, a)
    for _, s in ipairs(strips) do
        s:SetVertexColor(r, g, b, a)
    end
end

--------------------------------------------------------------------------------
-- Arrow nudge button (triangle-based)
--------------------------------------------------------------------------------
local ARROW_SIZE = 16
local ARROW_REPEAT_DELAY = 0.45
local ARROW_REPEAT_RATE  = 0.04

local function CreateArrowButton(parent, moverName, anchor, ofsX, ofsY, direction, dx, dy)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetWidth(ARROW_SIZE)
    btn:SetHeight(ARROW_SIZE)
    btn:SetPoint(anchor, parent, anchor, ofsX, ofsY)
    btn:SetFrameLevel(parent:GetFrameLevel() + 5)
    btn:SetMovable(true)
    btn:RegisterForDrag("LeftButton")

    local th = T()
    local dimC = th.dimText or { 0.5, 0.5, 0.55 }
    local accentC = th.accent or { 1, 0.5, 0.8 }

    local bgTex = btn:CreateTexture(nil, "BACKGROUND")
    bgTex:SetTexture("Interface\\Buttons\\WHITE8x8")
    bgTex:SetAllPoints(btn)
    bgTex:SetVertexColor(0, 0, 0, 0)
    btn._bg = bgTex

    local tri = CreateTriangle(btn, direction, dimC[1], dimC[2], dimC[3], 0.55)
    btn._tri = tri

    btn._elapsed = 0
    btn._held = false
    btn._dragging = false
    btn._delay = ARROW_REPEAT_DELAY

    btn:SetScript("OnClick", function()
        if not this._dragging then
            NudgeMover(moverName, dx, dy)
        end
        this._dragging = false
    end)
    btn:SetScript("OnMouseDown", function()
        this._held = true
        this._dragging = false
        this._elapsed = 0
        this._delay = ARROW_REPEAT_DELAY
    end)
    btn:SetScript("OnMouseUp", function()
        this._held = false
    end)
    btn:SetScript("OnDragStart", function()
        this._held = false
        this._dragging = true
        parent:StartMoving()
    end)
    btn:SetScript("OnDragStop", function()
        parent:StopMovingOrSizing()
        ApplySnap(parent)
        SyncFrameToMover(moverName)
        this._dragging = false
    end)
    btn:SetScript("OnUpdate", function()
        if not this._held then return end
        this._elapsed = this._elapsed + arg1
        if this._elapsed >= this._delay then
            this._elapsed = 0
            this._delay = ARROW_REPEAT_RATE
            NudgeMover(moverName, dx, dy)
        end
    end)
    btn:SetScript("OnEnter", function()
        this._bg:SetVertexColor(accentC[1], accentC[2], accentC[3], 0.18)
        SetTriangleColor(this._tri, 1, 1, 1, 1)
        if parent._SetHover then parent:_SetHover(true) end
    end)
    btn:SetScript("OnLeave", function()
        this._held = false
        this._bg:SetVertexColor(0, 0, 0, 0)
        SetTriangleColor(this._tri, dimC[1], dimC[2], dimC[3], 0.55)
        if parent._SetHover then parent:_SetHover(false) end
    end)

    return btn
end

--------------------------------------------------------------------------------
-- Create individual mover frame
--------------------------------------------------------------------------------
local function CreateMoverFrame(name, entry)
    if moverFrames[name] then return moverFrames[name] end

    local mover = CreateFrame("Button", "SFramesMover_" .. name, UIParent)
    mover:SetFrameStrata("FULLSCREEN")
    mover:SetFrameLevel(100)
    mover:SetWidth(100)
    mover:SetHeight(40)
    mover:SetClampedToScreen(true)
    mover:SetMovable(true)
    mover:EnableMouse(true)
    mover:RegisterForDrag("LeftButton")
    mover:RegisterForClicks("RightButtonUp")

    mover:SetBackdrop(MOVER_BACKDROP_2PX)

    local th = T()
    local secBg = th.sectionBg or { 0.12, 0.10, 0.18, 0.82 }
    local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
    local accentLine = th.accentLine or { 0.6, 0.9, 1, 0.9 }

    mover:SetBackdropColor(secBg[1], secBg[2], secBg[3], secBg[4] or 0.82)
    mover:SetBackdropBorderColor(accent[1], accent[2], accent[3], 0.6)

    -- Top accent stripe
    local stripe = mover:CreateTexture(nil, "ARTWORK")
    stripe:SetTexture("Interface\\Buttons\\WHITE8x8")
    stripe:SetHeight(2)
    stripe:SetPoint("TOPLEFT", mover, "TOPLEFT", 2, -2)
    stripe:SetPoint("TOPRIGHT", mover, "TOPRIGHT", -2, -2)
    stripe:SetVertexColor(accentLine[1], accentLine[2], accentLine[3], accentLine[4] or 0.9)
    mover._stripe = stripe

    -- Label
    local label = mover:CreateFontString(nil, "OVERLAY")
    label:SetFont(Font(), 11, "OUTLINE")
    label:SetPoint("CENTER", mover, "CENTER", 0, 5)
    label:SetText(entry.label or name)
    local titleC = th.title or { 1, 0.9, 1 }
    label:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
    mover._label = label

    -- Coordinate readout
    local coord = mover:CreateFontString(nil, "OVERLAY")
    coord:SetFont(Font(), 9, "OUTLINE")
    coord:SetPoint("CENTER", mover, "CENTER", 0, -7)
    local dimC = th.dimText or { 0.5, 0.5, 0.55 }
    coord:SetTextColor(dimC[1], dimC[2], dimC[3], 0.9)
    coord:SetText("")
    mover._coordText = coord

    -- Triangle arrow buttons
    CreateArrowButton(mover, name, "TOP",    0,  -1, "UP",    0,  1)
    CreateArrowButton(mover, name, "BOTTOM", 0,   1, "DOWN",  0, -1)
    CreateArrowButton(mover, name, "LEFT",   1,   0, "LEFT", -1,  0)
    CreateArrowButton(mover, name, "RIGHT", -1,   0, "RIGHT", 1,  0)

    -- Hover state manager (tracks child enter/leave)
    local hoverCount = 0
    function mover:_SetHover(isEnter)
        if isEnter then
            hoverCount = hoverCount + 1
        else
            hoverCount = hoverCount - 1
            if hoverCount < 0 then hoverCount = 0 end
        end
        if hoverCount > 0 then
            self:SetBackdropBorderColor(1, 1, 1, 0.95)
            stripe:SetVertexColor(1, 1, 1, 1)
        else
            self:SetBackdropBorderColor(accent[1], accent[2], accent[3], 0.6)
            stripe:SetVertexColor(accentLine[1], accentLine[2], accentLine[3], accentLine[4] or 0.9)
        end
    end

    mover:SetScript("OnDragStart", function()
        this:StartMoving()
    end)

    mover:SetScript("OnDragStop", function()
        this:StopMovingOrSizing()
        ApplySnap(this)
        SyncFrameToMover(name)
    end)

    mover:SetScript("OnClick", function()
        if arg1 == "RightButton" then
            M:ResetMover(name)
        end
    end)

    mover:SetScript("OnEnter", function()
        this:_SetHover(true)
        GameTooltip:SetOwner(this, "ANCHOR_TOP")
        GameTooltip:ClearLines()
        GameTooltip:AddLine(entry.label or name, 1, 0.84, 0.94)
        GameTooltip:AddDoubleLine("拖拽", "移动位置", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
        GameTooltip:AddDoubleLine("箭头", "微调 1 像素", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
        GameTooltip:AddDoubleLine("右键", "重置位置", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
        GameTooltip:AddDoubleLine("Shift+拖拽", "禁用磁吸", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)

    mover:SetScript("OnLeave", function()
        this:_SetHover(false)
        GameTooltip:Hide()
    end)

    mover:Hide()
    moverFrames[name] = mover
    return mover
end

--------------------------------------------------------------------------------
-- Dark overlay
--------------------------------------------------------------------------------
local function CreateOverlay()
    if overlayFrame then return overlayFrame end

    overlayFrame = CreateFrame("Frame", "SFramesLayoutOverlay", UIParent)
    overlayFrame:SetFrameStrata("DIALOG")
    overlayFrame:SetFrameLevel(0)
    overlayFrame:SetAllPoints(UIParent)
    overlayFrame:EnableMouse(false)

    local bg = overlayFrame:CreateTexture(nil, "BACKGROUND")
    bg:SetTexture("Interface\\Buttons\\WHITE8x8")
    bg:SetAllPoints(overlayFrame)
    bg:SetVertexColor(0, 0, 0, 0.45)
    overlayFrame._bg = bg

    overlayFrame:Hide()
    return overlayFrame
end

--------------------------------------------------------------------------------
-- Grid overlay
--------------------------------------------------------------------------------
local function CreateGrid()
    if gridFrame then return gridFrame end

    gridFrame = CreateFrame("Frame", "SFramesLayoutGrid", UIParent)
    gridFrame:SetFrameStrata("DIALOG")
    gridFrame:SetFrameLevel(1)
    gridFrame:SetAllPoints(UIParent)
    gridFrame:EnableMouse(false)
    gridFrame._lines = {}

    local th = T()
    local axisColor = th.accent or { 1, 0.5, 0.8 }
    local lineColor = th.dimText or { 0.5, 0.5, 0.55 }

    local function MakeLine(r, g, b, a)
        local tex = gridFrame:CreateTexture(nil, "BACKGROUND")
        tex:SetTexture("Interface\\Buttons\\WHITE8x8")
        tex:SetVertexColor(r, g, b, a)
        table.insert(gridFrame._lines, tex)
        return tex
    end

    local screenW = UIParent:GetRight() or UIParent:GetWidth()
    local screenH = UIParent:GetTop() or UIParent:GetHeight()
    local halfW = math.floor(screenW / 2)
    local halfH = math.floor(screenH / 2)

    local cv = MakeLine(axisColor[1], axisColor[2], axisColor[3], 0.45)
    cv:SetWidth(1)
    cv:SetPoint("TOP", gridFrame, "TOP", 0, 0)
    cv:SetPoint("BOTTOM", gridFrame, "BOTTOM", 0, 0)

    local ch = MakeLine(axisColor[1], axisColor[2], axisColor[3], 0.45)
    ch:SetHeight(1)
    ch:SetPoint("LEFT", gridFrame, "LEFT", 0, 0)
    ch:SetPoint("RIGHT", gridFrame, "RIGHT", 0, 0)

    local i = GRID_SPACING
    while i < halfW do
        local vl = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
        vl:SetWidth(1)
        vl:SetPoint("TOP", gridFrame, "TOP", -i, 0)
        vl:SetPoint("BOTTOM", gridFrame, "BOTTOM", -i, 0)

        local vr = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
        vr:SetWidth(1)
        vr:SetPoint("TOP", gridFrame, "TOP", i, 0)
        vr:SetPoint("BOTTOM", gridFrame, "BOTTOM", i, 0)
        i = i + GRID_SPACING
    end

    i = GRID_SPACING
    while i < halfH do
        local ha = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
        ha:SetHeight(1)
        ha:SetPoint("LEFT", gridFrame, "LEFT", 0, i)
        ha:SetPoint("RIGHT", gridFrame, "RIGHT", 0, i)

        local hb = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10)
        hb:SetHeight(1)
        hb:SetPoint("LEFT", gridFrame, "LEFT", 0, -i)
        hb:SetPoint("RIGHT", gridFrame, "RIGHT", 0, -i)
        i = i + GRID_SPACING
    end

    gridFrame:Hide()
    return gridFrame
end

--------------------------------------------------------------------------------
-- Rounded button helper (for control bar) – matches UI-wide rounded style
--------------------------------------------------------------------------------
local ROUND_BACKDROP = {
    bgFile   = "Interface\\Tooltips\\UI-Tooltip-Background",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    tile = true, tileSize = 16, edgeSize = 14,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
}

local function MakeControlButton(parent, text, width, xOfs, onClick)
    local th = T()
    local btnBg   = th.buttonBg   or { 0.16, 0.12, 0.22, 0.94 }
    local btnBd   = th.buttonBorder or { 0.35, 0.30, 0.50, 0.90 }
    local btnHBg  = th.buttonHoverBg or { 0.22, 0.18, 0.30, 0.96 }
    local btnDnBg = th.buttonDownBg or { 0.08, 0.04, 0.06, 0.95 }
    local btnText = th.buttonText or { 0.85, 0.82, 0.92 }
    local btnHTxt = th.buttonActiveText or { 1, 0.92, 0.96 }

    local btn = CreateFrame("Button", nil, parent)
    btn:SetWidth(width)
    btn:SetHeight(26)
    btn:SetPoint("LEFT", parent, "LEFT", xOfs, 0)
    btn:SetBackdrop(ROUND_BACKDROP)
    btn:SetBackdropColor(btnBg[1], btnBg[2], btnBg[3], btnBg[4] or 0.94)
    btn:SetBackdropBorderColor(btnBd[1], btnBd[2], btnBd[3], btnBd[4] or 0.90)

    local fs = btn:CreateFontString(nil, "OVERLAY")
    fs:SetFont(Font(), 10, "OUTLINE")
    fs:SetPoint("CENTER", 0, 0)
    fs:SetText(text)
    fs:SetTextColor(btnText[1], btnText[2], btnText[3], 1)
    btn._text = fs

    btn:SetScript("OnClick", onClick)
    btn:SetScript("OnEnter", function()
        this:SetBackdropColor(btnHBg[1], btnHBg[2], btnHBg[3], btnHBg[4] or 0.96)
        local a = th.accent or { 1, 0.5, 0.8 }
        this:SetBackdropBorderColor(a[1], a[2], a[3], 0.95)
        this._text:SetTextColor(btnHTxt[1], btnHTxt[2], btnHTxt[3], 1)
    end)
    btn:SetScript("OnLeave", function()
        this:SetBackdropColor(btnBg[1], btnBg[2], btnBg[3], btnBg[4] or 0.94)
        this:SetBackdropBorderColor(btnBd[1], btnBd[2], btnBd[3], btnBd[4] or 0.90)
        this._text:SetTextColor(btnText[1], btnText[2], btnText[3], 1)
    end)
    btn:SetScript("OnMouseDown", function()
        this:SetBackdropColor(btnDnBg[1], btnDnBg[2], btnDnBg[3], btnDnBg[4] or 0.95)
    end)
    btn:SetScript("OnMouseUp", function()
        this:SetBackdropColor(btnHBg[1], btnHBg[2], btnHBg[3], btnHBg[4] or 0.96)
    end)

    return btn
end

--------------------------------------------------------------------------------
-- Control bar
--------------------------------------------------------------------------------
local function CreateControlBar()
    if controlBar then return controlBar end

    local th = T()
    local panelBg = th.headerBg or th.panelBg or { 0.08, 0.06, 0.12, 0.98 }
    local panelBd = th.panelBorder or { 0.35, 0.30, 0.50, 0.90 }
    local accent  = th.accent or { 1, 0.5, 0.8, 0.98 }
    local titleC  = th.title or { 1, 0.88, 1 }

    controlBar = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent)
    controlBar:SetFrameStrata("FULLSCREEN_DIALOG")
    controlBar:SetFrameLevel(200)
    controlBar:SetWidth(480)
    controlBar:SetHeight(44)
    controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8)
    controlBar:SetClampedToScreen(true)
    controlBar:SetMovable(true)
    controlBar:EnableMouse(true)
    controlBar:RegisterForDrag("LeftButton")
    controlBar:SetScript("OnDragStart", function() this:StartMoving() end)
    controlBar:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)

    controlBar:SetBackdrop(ROUND_BACKDROP)
    controlBar:SetBackdropColor(panelBg[1], panelBg[2], panelBg[3], panelBg[4] or 0.98)
    controlBar:SetBackdropBorderColor(panelBd[1], panelBd[2], panelBd[3], panelBd[4] or 0.90)

    local title = controlBar:CreateFontString(nil, "OVERLAY")
    title:SetFont(Font(), 13, "OUTLINE")
    title:SetPoint("LEFT", controlBar, "LEFT", 14, 0)
    title:SetText("Nanami 布局")
    title:SetTextColor(titleC[1], titleC[2], titleC[3], 1)

    local sep = controlBar:CreateTexture(nil, "ARTWORK")
    sep:SetTexture("Interface\\Buttons\\WHITE8x8")
    sep:SetWidth(1)
    sep:SetHeight(24)
    sep:SetPoint("LEFT", controlBar, "LEFT", 118, 0)
    sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5)

    local bx = 128

    -- Snap toggle
    local snapBtn = MakeControlButton(controlBar, "", 76, bx, function()
        local cfg = GetLayoutCfg()
        cfg.snapEnabled = not cfg.snapEnabled
        if controlBar._updateSnap then controlBar._updateSnap() end
    end)
    controlBar.snapBtn = snapBtn

    local function UpdateSnapBtnText()
        local cfg = GetLayoutCfg()
        local a2 = T().accent or { 1, 0.5, 0.8 }
        if cfg.snapEnabled then
            snapBtn._text:SetText("|cff66ee66开|r 磁吸")
            snapBtn:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.7)
        else
            snapBtn._text:SetText("|cffee6666关|r 磁吸")
            snapBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.7)
        end
    end
    controlBar._updateSnap = UpdateSnapBtnText

    -- Grid toggle
    local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function()
        local cfg = GetLayoutCfg()
        cfg.showGrid = not cfg.showGrid
        if controlBar._updateGrid then controlBar._updateGrid() end
        if gridFrame then
            if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end
        end
    end)
    controlBar.gridBtn = gridBtn

    local function UpdateGridBtnText()
        local cfg = GetLayoutCfg()
        local a2 = T().accent or { 1, 0.5, 0.8 }
        if cfg.showGrid then
            gridBtn._text:SetText("|cff66ee66开|r 网格")
            gridBtn:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.7)
        else
            gridBtn._text:SetText("|cffee6666关|r 网格")
            gridBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.7)
        end
    end
    controlBar._updateGrid = UpdateGridBtnText

    -- Reset all
    local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function()
        M:ResetAllMovers()
    end)
    local wbGold = th.wbGold or { 1, 0.88, 0.55 }
    resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1)

    -- Close
    local closeBtn = CreateFrame("Button", nil, controlBar)
    closeBtn:SetWidth(60)
    closeBtn:SetHeight(26)
    closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
    closeBtn:SetBackdrop(ROUND_BACKDROP)
    closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
    closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
    local closeText = closeBtn:CreateFontString(nil, "OVERLAY")
    closeText:SetFont(Font(), 10, "OUTLINE")
    closeText:SetPoint("CENTER", 0, 0)
    closeText:SetText("关闭")
    closeText:SetTextColor(1, 0.65, 0.65, 1)

    closeBtn:SetScript("OnClick", function() M:ExitLayoutMode() end)
    closeBtn:SetScript("OnEnter", function()
        this:SetBackdropColor(0.50, 0.12, 0.15, 0.98)
        this:SetBackdropBorderColor(1, 0.35, 0.40, 1)
        closeText:SetTextColor(1, 1, 1, 1)
    end)
    closeBtn:SetScript("OnLeave", function()
        this:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
        this:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
        closeText:SetTextColor(1, 0.65, 0.65, 1)
    end)
    closeBtn:SetScript("OnMouseDown", function()
        this:SetBackdropColor(0.25, 0.04, 0.06, 0.98)
    end)
    closeBtn:SetScript("OnMouseUp", function()
        this:SetBackdropColor(0.50, 0.12, 0.15, 0.98)
    end)
    controlBar.closeBtn = closeBtn

    controlBar:Hide()
    return controlBar
end

--------------------------------------------------------------------------------
-- Register mover
--------------------------------------------------------------------------------
function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved)
    if not name or not frame then return end

    registry[name] = {
        frame = frame,
        label = label or name,
        defaultPoint = defaultPoint or "CENTER",
        defaultRelativeTo = defaultRelativeTo or "UIParent",
        defaultRelPoint = defaultRelPoint or "CENTER",
        defaultX = defaultX or 0,
        defaultY = defaultY or 0,
        onMoved = onMoved,
    }

    CreateMoverFrame(name, registry[name])
end

--------------------------------------------------------------------------------
-- Enter / Exit layout mode
--------------------------------------------------------------------------------
function M:EnterLayoutMode()
    if isLayoutMode then return end
    isLayoutMode = true

    if SFrames.ConfigUI and SFramesConfigPanel and SFramesConfigPanel:IsShown() then
        SFramesConfigPanel:Hide()
    end

    CreateOverlay()
    CreateGrid()
    CreateControlBar()

    overlayFrame:Show()

    local cfg = GetLayoutCfg()
    if cfg.showGrid then gridFrame:Show() end
    if controlBar._updateSnap then controlBar._updateSnap() end
    if controlBar._updateGrid then controlBar._updateGrid() end
    controlBar:Show()

    SFrames:Print(string.format(
        "|cff66eeff[布局]|r UIScale=%.2f  screenW=%.0f screenH=%.0f (GetRight=%.0f GetTop=%.0f)",
        UIParent:GetEffectiveScale(),
        UIParent:GetWidth(), UIParent:GetHeight(),
        UIParent:GetRight() or 0, UIParent:GetTop() or 0))

    for name, _ in pairs(registry) do
        SyncMoverToFrame(name)
        local mover = moverFrames[name]
        if mover then mover:Show() end
    end

    SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸")
end

function M:ExitLayoutMode()
    if not isLayoutMode then return end
    isLayoutMode = false

    for name, _ in pairs(registry) do
        SyncFrameToMover(name)
        local mover = moverFrames[name]
        if mover then mover:Hide() end
    end

    if gridFrame then gridFrame:Hide() end
    if controlBar then controlBar:Hide() end
    if overlayFrame then overlayFrame:Hide() end

    SFrames:Print("布局模式已关闭 - 所有位置已保存")
end

function M:ToggleLayoutMode()
    if isLayoutMode then self:ExitLayoutMode() else self:EnterLayoutMode() end
end

function M:IsLayoutMode()
    return isLayoutMode
end

--------------------------------------------------------------------------------
-- Reset movers
--------------------------------------------------------------------------------
function M:ResetMover(name)
    local entry = registry[name]
    if not entry then return end

    local positions = EnsurePositions()
    positions[name] = nil

    local frame = entry.frame
    if frame then
        frame:ClearAllPoints()
        local relTo = _G[entry.defaultRelativeTo] or UIParent
        frame:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
            entry.defaultX, entry.defaultY)
    end

    if entry.onMoved then entry.onMoved() end
    if isLayoutMode then SyncMoverToFrame(name) end

    SFrames:Print((entry.label or name) .. " 位置已重置")
end

function M:ResetAllMovers()
    for name, _ in pairs(registry) do
        local entry = registry[name]
        local positions = EnsurePositions()
        positions[name] = nil

        local frame = entry.frame
        if frame then
            frame:ClearAllPoints()
            local relTo = _G[entry.defaultRelativeTo] or UIParent
            frame:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint,
                entry.defaultX, entry.defaultY)
        end
        if entry.onMoved then entry.onMoved() end
    end

    if isLayoutMode then
        for name, _ in pairs(registry) do SyncMoverToFrame(name) end
    end

    SFrames:Print("所有位置已重置为默认")
end

--------------------------------------------------------------------------------
-- Apply saved position to a frame (for modules to call on init)
--------------------------------------------------------------------------------
function M:ApplyPosition(name, frame, defaultPoint, defaultRelTo, defaultRelPoint, defaultX, defaultY)
    local positions = EnsurePositions()
    local pos = positions[name]
    if pos and pos.point and pos.relativePoint then
        frame:ClearAllPoints()
        frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
        return true
    else
        frame:ClearAllPoints()
        local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent
        frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER",
            defaultX or 0, defaultY or 0)
        return false
    end
end

--------------------------------------------------------------------------------
-- Utility
--------------------------------------------------------------------------------
function M:GetRegistry()
    return registry
end
