-------------------------------------------------------------------------------
-- FolderMover.lua
--
-- The folder bounding box: while a FOLDER ROW is selected in the By Folder
-- view and its members are previewing, a box is drawn around every previewing
-- element in that folder, and dragging anywhere inside it moves the whole
-- folder's contents together, preserving their relative layout.
--
-- WHY A BOX ON TOP. Element holders are individually draggable while
-- previewing (Tracker.lua's CreateElement), so a box over them takes those
-- clicks. That is the intended trade, and the selection rule is what makes it
-- safe: the box exists ONLY while the folder's own row is selected. Select a
-- member tracker and the box goes away, which is exactly when you want to nudge
-- one element on its own. So "move the folder" and "move one tracker" are the
-- same gesture at two different selections, and neither is ever unreachable.
--
-- POSITIONS ARE PER ELEMENT, not per tracker and not per folder. A folder is
-- navigation only (see the header of CustomView.lua) and nothing in Tracker.lua
-- reads folder data — so this stores nothing of its own. A folder drag is N
-- ordinary element moves applied with one shared delta, written into exactly
-- the same <prefix>elements[i].position that a single-holder drag writes.
-- Nothing downstream can tell the two apart, which is why this needs no
-- migration, no new saved key, and no special case anywhere else.
--
-- DRIVEN BY THE TREE, not by a timer. The box depends on which node is
-- selected and which elements are previewing — and DecorateTreeXs (Options.lua)
-- already re-resolves both on every RefreshTree, which AceGUI calls for every
-- event that can change either: a row click (Button_OnClick -> SetSelected ->
-- RefreshTree), a programmatic Select, expand/collapse, scroll, resize, and the
-- preview eye's own explicit RefreshTree. So the decorator calls Refresh below
-- and there is nothing to poll. Panel close is the one event that produces no
-- further RefreshTree, so its OnHide calls Refresh too, as does SetViewMode.
--
-- The single deferred re-measure after each Refresh is for geometry rather than
-- state: toggling a preview on renders sample buttons whose container can
-- finish laying out a frame later, so the rect measured during the decorator
-- pass can be a frame stale. One C_Timer.After(0) re-measure settles it without
-- reintroducing a timer that runs forever.
-------------------------------------------------------------------------------
local addonName, ns = ...
local HET = ns.addon

local ACD = LibStub("AceConfigDialog-3.0")

-- Tracker.lua publishes this and loads first (see the .toc); the fallback is
-- only so a load-order accident degrades into "checks a couple of extra empty
-- slots" rather than an error.
local MAX_ELEMENTS = ns.MAX_ELEMENTS or 6

-- Breathing room around the outermost holders, so the box reads as a container
-- rather than a tight outline clipping the art it encloses.
local PAD = 10

-------------------------------------------------------------------------------
-- Geometry
-------------------------------------------------------------------------------
-- Everything below works in UIParent units. GetLeft() and friends report in
-- the region's OWN scale, so a holder under a different effective scale would
-- otherwise contribute a rect measured in different units from its neighbour's
-- — multiply into screen units, divide back out by UIParent's.
local function ToUIParent(region)
    local l, r = region:GetLeft(), region:GetRight()
    local b, t = region:GetBottom(), region:GetTop()
    if not (l and r and b and t) then return nil end
    local us = UIParent:GetEffectiveScale()
    if not us or us <= 0 then return nil end
    local hs = region:GetEffectiveScale() or 1
    local k = hs / us
    return l * k, r * k, b * k, t * k
end

-- The cfgKeys rendered under this folder's node, as the LAST options build
-- resolved them. Reusing that list rather than re-deriving membership is what
-- keeps the box agreeing with the tree: it is already spec-filtered and
-- already excludes anything not drawn, so the box can never enclose a tracker
-- the user cannot see in the list.
local function FolderMembers(fid)
    local t = ns._treeNodeMembers and ns._treeNodeMembers["folder" .. fid]
    if type(t) == "table" and #t > 0 then return t end
    return nil
end

-- The folder's elements: the union rect, plus the ones a move should apply to.
--
-- `mode` picks WHICH elements, and the two callers genuinely want different
-- sets:
--   "preview" (default) — elements currently previewing. The box is a handle
--                         on what is visible, so it must enclose exactly that.
--   "all"               — every element with a visual style, previewing or
--                         not. Centring is an edit, not a handle: a holder is
--                         positioned from the moment it is created, so
--                         "centre this folder" can and should mean the whole
--                         folder rather than whichever part happens to be lit
--                         up. This also matches the per-tracker Center
--                         buttons, which have never required a preview.
--
-- Returns nil when there is nothing to act on — no members, nothing in the
-- chosen set, or nothing measurable yet.
local function Collect(fid, mode)
    local members = FolderMembers(fid)
    if not members then return nil end

    local wantAll = (mode == "all")
    local l, r, b, t
    local move

    for m = 1, #members do
        local tracker = ns.trackers and ns.trackers[members[m]]
        if tracker and tracker.elements and tracker.IsElementPreviewActive then
            local els = tracker:GetElementsDB()
            for i = 1, MAX_ELEMENTS do
                local e = tracker.elements[i]
                local cfg = els[i]
                local included
                if wantAll then
                    -- Sound elements have no art and no position; everything
                    -- else with a style is on screen somewhere.
                    included = cfg and cfg.style and cfg.style ~= "sound"
                else
                    included = tracker:IsElementPreviewActive(i)
                end
                if e and e.holder and included then
                    local hl, hr, hb, ht = ToUIParent(e.holder)
                    if hl then
                        if not l or hl < l then l = hl end
                        if not r or hr > r then r = hr end
                        if not b or hb < b then b = hb end
                        if not t or ht > t then t = ht end
                    end
                    -- ENCLOSED BUT NOT MOVED: an icon/bar PAIR element is
                    -- anchored TOPLEFT-to-TOPLEFT on its parent's holder
                    -- (RestoreElementPosition) and has no saved position of its
                    -- own. It rides along when the parent moves, so moving it
                    -- directly would apply the delta twice and then be undone
                    -- by the next restore. It still belongs in the rect — it is
                    -- visibly part of the folder.
                    if cfg and not tracker:IsLockedPairElement(i) then
                        move = move or {}
                        move[#move + 1] = { tracker = tracker, index = i, holder = e.holder }
                    end
                end
            end
        end
    end

    if not l or not move then return nil end
    return l, r, b, t, move
end

-- Shift a collected set by one delta, on screen AND in the profile.
--
-- The single place a folder-wide move is written, shared by the drag's commit
-- and by Center Horizontal / Center Vertical, so the two can never disagree
-- about the anchor shape they store or about which elements they skip.
--
-- The anchor is read from the holder rather than assumed to be CENTER: holders
-- are SetClampedToScreen, so one dropped near an edge reports an edge anchor
-- with offsets measured from that corner (the trap CenterElementPosition
-- documents). Re-using whatever each holder already has makes one delta correct
-- for all of them without normalising anything.
local function ApplyDelta(move, dx, dy)
    if not move or (dx == 0 and dy == 0) then return false end
    local moved = false
    for i = 1, #move do
        local it = move[i]
        local point, _, relPoint, x, y = it.holder:GetPoint()
        if point then
            x, y = (x or 0) + dx, (y or 0) + dy
            it.holder:ClearAllPoints()
            it.holder:SetPoint(point, UIParent, relPoint, x, y)
            local els = it.tracker:GetElementsDB()
            if els[it.index] then
                -- The same four-value shape a single-holder drag stores, read
                -- back by RestoreElementPosition against UIParent.
                els[it.index].position = { point, relPoint, x, y }
            end
            moved = true
        end
    end
    return moved
end

-- Centre the folder's contents on one axis, leaving the other alone — the
-- folder-wide twin of methods:CenterElementPosition.
--
-- Centres the folder AS A UNIT: the bounding box's midpoint goes to the screen
-- midpoint and every element keeps its offset from it. Centring each element
-- individually would instead pile the whole folder into a single stack, which
-- is never what "centre this folder" means.
function ns.CenterFolder(fid, axis)
    if InCombatLockdown() then return false end
    local l, r, b, t, move = Collect(fid, "all")
    if not l or not move then return false end
    local W, H = UIParent:GetWidth(), UIParent:GetHeight()
    if not W or not H then return false end
    local dx = (axis == "h") and (W / 2 - (l + r) / 2) or 0
    local dy = (axis == "v") and (H / 2 - (b + t) / 2) or 0
    local moved = ApplyDelta(move, dx, dy)
    if moved and ns.RefreshFolderMover then ns.RefreshFolderMover() end
    return moved
end

-- Is there anything for Center to act on? Drives the buttons' enabled state,
-- so a folder of sound-only trackers says so by greying out rather than by
-- doing nothing when clicked.
function ns.FolderHasPositionables(fid)
    local _, _, _, _, move = Collect(fid, "all")
    return (move and #move > 0) and true or false
end

-------------------------------------------------------------------------------
-- The box
-------------------------------------------------------------------------------
local box
local drag = { active = false }

local function StopDrag(commit)
    if not drag.active then return end
    drag.active = false
    if box then box:SetScript("OnUpdate", nil) end
    if commit and drag.items then
        for i = 1, #drag.items do
            local it = drag.items[i]
            -- Read back where the holder ACTUALLY ended up rather than
            -- recomputing start + delta. The OnUpdate has already placed it,
            -- and SetClampedToScreen can have re-expressed the anchor on the
            -- way — so the holder is the authority on its own position and the
            -- saved value cannot drift from what is on screen.
            local point, _, relPoint, x, y = it.holder:GetPoint()
            local els = it.tracker:GetElementsDB()
            if point and els[it.index] then
                els[it.index].position = { point, relPoint, x or 0, y or 0 }
            end
        end
    end
    drag.items = nil
end

local function EnsureBox()
    if box then return box end

    box = CreateFrame("Frame", "BuzzardAurasFolderMover", UIParent)
    -- Above the element holders (which inherit UIParent's MEDIUM) so the drag
    -- lands here, and below DIALOG so it never covers the options panel that
    -- is the only reason it is on screen.
    box:SetFrameStrata("HIGH")
    box:EnableMouse(true)
    box:RegisterForDrag("LeftButton")
    box:Hide()

    box.fill = box:CreateTexture(nil, "BACKGROUND")
    box.fill:SetAllPoints()

    -- Four 1px edges rather than a backdrop: no template dependency, and the
    -- border stays 1px at any box size instead of stretching a corner texture.
    box.edges = {}
    for i = 1, 4 do box.edges[i] = box:CreateTexture(nil, "ARTWORK") end
    local e = box.edges
    e[1]:SetPoint("TOPLEFT");     e[1]:SetPoint("TOPRIGHT");    e[1]:SetHeight(1)
    e[2]:SetPoint("BOTTOMLEFT");  e[2]:SetPoint("BOTTOMRIGHT"); e[2]:SetHeight(1)
    e[3]:SetPoint("TOPLEFT");     e[3]:SetPoint("BOTTOMLEFT");  e[3]:SetWidth(1)
    e[4]:SetPoint("TOPRIGHT");    e[4]:SetPoint("BOTTOMRIGHT"); e[4]:SetWidth(1)

    box:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        if InCombatLockdown() then
            GameTooltip:SetText("Locked during combat", 1, 0.3, 0.3)
        else
            GameTooltip:SetText("Drag to move this folder's trackers together",
                1, 1, 1)
            GameTooltip:AddLine("Select a tracker in the list to move it on its own.",
                0.7, 0.7, 0.7)
        end
        GameTooltip:Show()
    end)
    box:SetScript("OnLeave", function() GameTooltip:Hide() end)

    box:SetScript("OnDragStart", function(self)
        -- Same lock every other preview entry point takes. Element holders are
        -- plain UIParent frames, but the whole options panel is control-
        -- disabled in combat and a box that still dragged would be the one
        -- thing that did not.
        if InCombatLockdown() then return end
        local fid = ns.SelectedFolderRowID and ns.SelectedFolderRowID()
        if not fid then return end
        local _, _, _, _, move = Collect(fid)
        if not move or #move == 0 then return end

        local us = UIParent:GetEffectiveScale()
        if not us or us <= 0 then return end
        local cx, cy = GetCursorPosition()

        drag.items = {}
        for i = 1, #move do
            local it = move[i]
            -- SNAPSHOT THE ANCHOR, do not assume CENTER. Holders are
            -- SetClampedToScreen, so one dragged near an edge reports an edge
            -- anchor with offsets measured from that corner (the same trap
            -- CenterElementPosition documents). Re-using whatever anchor the
            -- holder already has means the delta is correct for every one of
            -- them without normalising anything.
            local point, _, relPoint, x, y = it.holder:GetPoint()
            if point then
                drag.items[#drag.items + 1] = {
                    tracker = it.tracker, index = it.index, holder = it.holder,
                    point = point, relPoint = relPoint, x = x or 0, y = y or 0,
                }
            end
        end
        if #drag.items == 0 then drag.items = nil return end

        drag.startX, drag.startY = cx / us, cy / us
        drag.dx, drag.dy = 0, 0
        drag.boxL = self:GetLeft() or 0
        drag.boxB = self:GetBottom() or 0
        drag.active = true

        self:SetScript("OnUpdate", function(f)
            local s = UIParent:GetEffectiveScale()
            if not s or s <= 0 then return end
            local mx, my = GetCursorPosition()
            drag.dx = mx / s - drag.startX
            drag.dy = my / s - drag.startY
            for i = 1, #drag.items do
                local it = drag.items[i]
                it.holder:ClearAllPoints()
                it.holder:SetPoint(it.point, UIParent, it.relPoint,
                    it.x + drag.dx, it.y + drag.dy)
            end
            f:ClearAllPoints()
            f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT",
                drag.boxL + drag.dx, drag.boxB + drag.dy)
        end)
    end)

    box:SetScript("OnDragStop", function() StopDrag(true) end)
    -- A drag interrupted by the box going away (panel closed, preview toggled
    -- off, folder deselected mid-drag) still commits: the holders have already
    -- been moved on screen, so dropping the write would leave the saved
    -- positions disagreeing with what the user can see until the next restore
    -- silently snapped them back.
    box:SetScript("OnHide", function() StopDrag(true) end)

    return box
end

-------------------------------------------------------------------------------
-- Refresh
-------------------------------------------------------------------------------
local function PanelOpen()
    return (ACD.OpenFrames and ACD.OpenFrames[addonName]) and true or false
end

local Refresh
-- One re-measure, one frame later, coalesced. See the header: this settles the
-- rect when a just-shown preview's container finishes laying out after the pass
-- that revealed it. Latched so a burst of Refresh calls (the decorator runs on
-- scroll, and a NotifyChange can fire several in a row) schedules exactly one.
local reMeasureQueued = false
local function QueueReMeasure()
    if reMeasureQueued then return end
    reMeasureQueued = true
    C_Timer.After(0, function()
        reMeasureQueued = false
        Refresh(true)
    end)
end

-- `settling` marks the deferred pass, so it re-measures without queueing
-- another one and the pair can never become a self-sustaining timer.
function Refresh(settling)
    -- A drag owns the geometry until it ends; re-deriving the rect underneath
    -- it would fight the OnUpdate above for the box's anchor.
    if drag.active then return end

    if not (HET and HET.db) or not PanelOpen() then
        if box then box:Hide() end
        return
    end
    local fid = ns.SelectedFolderRowID and ns.SelectedFolderRowID()
    if not fid then
        if box then box:Hide() end
        return
    end
    local l, r, b, t = Collect(fid)
    if not l then
        if box then box:Hide() end
        return
    end

    local f = EnsureBox()
    local cr, cg, cb = 1, 1, 1
    if ns.FolderColor then cr, cg, cb = ns.FolderColor(fid) end
    f.fill:SetColorTexture(cr, cg, cb, 0.07)
    for i = 1, 4 do f.edges[i]:SetColorTexture(cr, cg, cb, 0.55) end

    f:SetSize(math.max(1, (r - l) + PAD * 2), math.max(1, (t - b) + PAD * 2))
    f:ClearAllPoints()
    -- UIParent's BOTTOMLEFT is the screen origin the rect was measured from,
    -- so the UIParent-unit edges ARE the offsets.
    f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l - PAD, b - PAD)
    f:Show()

    if not settling then QueueReMeasure() end
end

-- Every caller that can change what the box should look like:
--   * DecorateTreeXs (Options.lua) — selection, previews, every tree redraw
--   * the panel's OnHide (Options.lua) — the one event with no RefreshTree
--   * ns.SetViewMode (CustomView.lua) — leaving By Folder
ns.RefreshFolderMover = function() Refresh(false) end
