-- TOG Tools — Shared UI helpers
--
-- Loaded BEFORE MainWindow.lua in every TOC. All helpers here are addon-
-- global so any tab (existing or future) can use them without duplicating
-- the boilerplate.
--
-- Globals exposed:
--   addon.Tooltip.Owner(frame, [budget])      auto-flipping tooltip anchor
--   addon.UI.Brand(text)                      wrap in brand colour
--   addon.UI.AttachTooltip(frame, t, body)    HookScript-based tooltip
--   addon.UI.MakeIcon(parent, opts)           bottom-row icon button factory
--   addon.UI.DetachChildren(host, keys)       release helper for OnClose
--   addon.UI.StyleSearchBox(editbox, keepLbl) magnifying-glass search styling
--   addon.UI.OpenColumnMenu(anchor, spec)     column-header sort + filter menu

local _, addon = ...

-- ---------------------------------------------------------------------------
-- Tooltip — auto-flipping anchor
-- ---------------------------------------------------------------------------
-- Two entry points share the same "flip if frame is near the bottom of
-- the screen" logic via a single internal placement computation:
--
--   addon.Tooltip.Owner(frame, [budget])
--     Use at TOOLTIP ATTACHMENT TIME — calls GameTooltip:SetOwner with
--     ANCHOR_TOPRIGHT or ANCHOR_BOTTOMLEFT. SetOwner CLEARS the tooltip
--     content, so you populate (SetText / AddLine / Show) after.
--
--   addon.Tooltip.AnchorTo(tooltipFrame, ownerFrame, [budget])
--     Use AFTER the tooltip has been populated by SOMEBODY ELSE (e.g.,
--     AceConfigDialog) when you want to re-position without clearing
--     content. Calls ClearAllPoints + SetPoint on the tooltip — content
--     survives. Used by the Settings panel re-anchor hook to make
--     AceConfigDialog tooltips obey our flip rules while OUR panel is
--     active.
--
-- Both call `_computePlacement` so the flip threshold (250 px below
-- the owner frame's top edge) stays consistent between them.
addon.Tooltip = addon.Tooltip or {}

-- Room-below check used by both Owner and AnchorTo. Uses the frame's
-- BOTTOM edge against the screen origin (Y=0 at the bottom of the
-- screen in WoW's coordinate system) — earlier versions used
-- `screenH - frame:GetTop()` which measures distance from the frame
-- to the TOP of the screen (the wrong direction) and flipped the
-- tooltip's anchor opposite of what the user expected.
local function _computePlacement(frame, budget)
    local bottom = frame and frame.GetBottom and frame:GetBottom()
    budget = budget or 250
    return bottom and bottom > budget
end

-- Narrow-owner detection. Owners thinner than this (checkboxes ~22 px,
-- icon buttons 20-24 px) get the tooltip offset rightward by the
-- owner's width + a small gap so the tooltip's left edge aligns with
-- where the LABEL text starts — not the icon's left edge. For wider
-- owners (header buttons, dropdowns, etc.) the offset is 0 and the
-- tooltip drops straight down aligned with the owner.
local NARROW_OWNER_THRESHOLD = 40

local function _xOffsetForLabel(ownerFrame)
    local width = ownerFrame and ownerFrame.GetWidth and ownerFrame:GetWidth() or 0
    if width > 0 and width < NARROW_OWNER_THRESHOLD then
        return width + 8   -- small gap past the icon/checkbox
    end
    return 0
end

function addon.Tooltip.Owner(frame, budget)
    local anchor = _computePlacement(frame, budget)
        and "ANCHOR_TOPRIGHT"
        or  "ANCHOR_BOTTOMLEFT"
    GameTooltip:SetOwner(frame, anchor)
end

-- ALWAYS tear a tooltip down through this, never a bare GameTooltip:Hide().
--
-- `_G.GameTooltip` is ONE frame shared with Blizzard and every other addon, and
-- a minimum width set on it is NOT cleared when it hides -- `GameTooltip_OnHide`
-- clears money frames, status bars and the backdrop, and not this. So an addon
-- that sets a floor and only hides pins EVERY tooltip in the game to that width
-- until the player logs out.
--
-- The reset pair is Blizzard's own, not invented here: `Blizzard_TalentUI.lua`
-- sets `SetMinimumWidth(300, true)` in `SpecButton_OnEnter` and clears it with
-- `SetMinimumWidth(0, false)` in `SpecButton_OnLeave`. Resetting unconditionally
-- is safe because it only ever runs on our own frames' OnLeave.
function addon.Tooltip.Release()
    GameTooltip:SetMinimumWidth(0, false)
    GameTooltip:Hide()
end

function addon.Tooltip.AnchorTo(tooltipFrame, ownerFrame, budget)
    if not tooltipFrame or not ownerFrame then return end
    tooltipFrame:ClearAllPoints()
    local xOff = _xOffsetForLabel(ownerFrame)
    if _computePlacement(ownerFrame, budget) then
        -- Plenty of room below the owner → drop tooltip down, top-left
        -- corner of tooltip aligned to bottom-left corner of owner
        -- (with optional offset right so narrow owners' tooltips don't
        -- start under the icon).
        tooltipFrame:SetPoint("TOPLEFT", ownerFrame, "BOTTOMLEFT", xOff, -4)
    else
        -- Near bottom of screen → flip up so the tooltip doesn't clip.
        tooltipFrame:SetPoint("BOTTOMLEFT", ownerFrame, "TOPLEFT", xOff, 4)
    end
end

-- (An always-on global `hooksecurefunc(GameTooltip, "SetOwner", ...)`
-- lived here briefly in v0.5.2. It re-anchored every tooltip in the
-- entire UI — including Blizzard's native corpse / unit / item
-- tooltips — and made them visually wonky. User reverted; the scoped
-- version lives in `GUI/Settings.lua` instead, gated by the
-- `_blizPanel:OnShow/OnHide` flag so the re-anchor only runs while
-- the TOG Tools settings panel is the active page.)

-- ---------------------------------------------------------------------------
-- UI helpers
-- ---------------------------------------------------------------------------
addon.UI = addon.UI or {}
local UI = addon.UI

--- Wrap `text` in the addon brand colour. Safe to embed in fontstrings,
--- tooltips, and chat output.
function UI.Brand(text)
    local c = addon.BrandColor or "ffFF8000"
    return "|c" .. c .. tostring(text or "") .. "|r"
end

--- Canonical tooltip-attach helper. Accepts either an AceGUI widget or
--- a raw `CreateFrame` frame and picks the correct binding mechanism
--- automatically:
---
---   * AceGUI widget (table with a `SetCallback` method): uses
---     `widget:SetCallback("OnEnter"/"OnLeave", ...)`. AceGUI clears
---     widget callbacks on Release, so the handler is properly
---     released when the widget returns to the pool. For Dropdown /
---     EditBox the label area sits outside the interactive body and
---     never fires the SetCallback OnEnter — for those types we also
---     wire `widget.frame` via `addon.AceGUIFrameScripts` (defined in
---     Compat.lua), which saves the prior script and restores it on
---     Release so the recycled widget's next owner gets a clean frame.
---
---   * Raw frame (no SetCallback): uses `HookScript` because raw
---     `CreateFrame` frames aren't pool-shared, so the never-unhookable
---     HookScript chain is safe — the handler lives as long as the
---     frame does.
---
--- |cffff5555Never call this with `widget.frame` for an AceGUI widget|r —
--- the AceGUI widget pool will recycle that frame into AceConfigDialog
--- option toggles (and any other addon that uses AceGUI), where the
--- residual HookScript fires alongside the new owner's handlers and
--- renders tooltip content twice. This was the cause of the v0.5.1
--- Settings>Modules tooltip doubling. Pass the WIDGET, not the frame.
---
--- For back-compat, `addon.GUI.AttachTooltip` is aliased to this same
--- function at the bottom of this file, so callers using either
--- namespace get identical behaviour.
function UI.AttachTooltip(target, title, body)
    if not target then return end

    local function show(anchorFrame)
        addon.Tooltip.Owner(anchorFrame)
        -- SetText's wrap flag is the SIXTH argument (text, r, g, b, alpha, wrap)
        -- per FrameAPITooltipDocumentation, where it defaults to false -- so the
        -- alpha has to be passed to reach it. AddLine's is the fifth.
        if title and title ~= "" then GameTooltip:SetText(title, 1, 1, 1, 1, true) end
        if body  and body  ~= "" then GameTooltip:AddLine(body, nil, nil, nil, true) end
        GameTooltip:Show()
    end
    local function hide() addon.Tooltip.Release() end

    -- AceGUI widget: tables with a SetCallback method. AceGUI clears
    -- callbacks on Release, so this is the leak-safe path.
    if type(target) == "table" and type(target.SetCallback) == "function" then
        target:SetCallback("OnEnter", function(w) show(w.frame) end)
        target:SetCallback("OnLeave", hide)

        -- Dropdown / EditBox label-area fallback. The label text sits
        -- outside the interactive body and SetCallback OnEnter doesn't
        -- fire for hovers on the label. Wire the wrapper frame via
        -- AceGUIFrameScripts, which records the prior script and
        -- restores it on Release — leaving the pooled widget clean.
        if (target.type == "Dropdown" or target.type == "EditBox")
           and target.frame and addon.AceGUIFrameScripts then
            if target.frame.EnableMouse then
                target.frame:EnableMouse(true)
            end
            addon.AceGUIFrameScripts(target, {
                OnEnter = function(f) show(f) end,
                OnLeave = hide,
            })
        end
        return
    end

    -- Raw CreateFrame frame. HookScript is fine here because the frame
    -- isn't pool-shared.
    if target.HookScript then
        target:HookScript("OnEnter", function(self_) show(self_) end)
        target:HookScript("OnLeave", hide)
    end
end

--- Build a bottom-row icon button (gear, help-i, future icons).
---
--- opts:
---   size           number  button size (default 20)
---   texture        string  normal/pushed texture path (required)
---   texCoordCrop   bool    apply 0.08-0.92 crop. Interface\Icons\* have
---                          ~8% transparent border padding so the visible
---                          icon doesn't fill the button; crop fixes that.
---                          Default false (Interface\Common\* etc. don't
---                          need it).
---   hitRectInsets  number  hit-rect expansion in px (negative widens);
---                          default -2 — gives 2 px slop on every side
---                          inside the row's standard 3 px gap.
---   onClick        func    optional click handler.
---   onEnter        func    optional OnEnter; overrides tooltipTitle/Body.
---   onLeave        func    optional OnLeave; defaults to GameTooltip:Hide.
---   tooltipTitle   string  if set (and no onEnter), installs an auto
---   tooltipBody    string  tooltip via addon.Tooltip.Owner.
---
--- Returns the Button frame. Caller is responsible for SetPoint and for
--- storing the frame somewhere DetachChildren can find it on close.
function UI.MakeIcon(parent, opts)
    opts = opts or {}
    local size = opts.size or 20
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(size, size)
    btn:EnableMouse(true)
    if opts.texture then
        btn:SetNormalTexture(opts.texture)
        btn:SetPushedTexture(opts.texture)
    end
    if opts.texCoordCrop then
        local n = btn:GetNormalTexture()
        if n then n:SetTexCoord(0.08, 0.92, 0.08, 0.92) end
        local p = btn:GetPushedTexture()
        if p then
            p:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            p:SetVertexColor(0.7, 0.7, 0.7)
        end
    end
    btn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
    local slop = opts.hitRectInsets or -2
    btn:SetHitRectInsets(slop, slop, slop, slop)

    if opts.onEnter then
        btn:SetScript("OnEnter", opts.onEnter)
        -- A caller-supplied onLeave still gets the minimum-width reset first: an
        -- onEnter is free to set a floor (the help icon does), and forgetting the
        -- reset leaks it onto every tooltip in the game. Doing it here means no
        -- caller has to remember, which is the only version of this that stays
        -- true as icons get added.
        if opts.onLeave then
            btn:SetScript("OnLeave", function(self_, ...)
                GameTooltip:SetMinimumWidth(0, false)
                return opts.onLeave(self_, ...)
            end)
        else
            btn:SetScript("OnLeave", addon.Tooltip.Release)
        end
    elseif opts.tooltipTitle or opts.tooltipBody then
        btn:SetScript("OnEnter", function(self_)
            addon.Tooltip.Owner(self_)
            if opts.tooltipTitle then
                GameTooltip:SetText(opts.tooltipTitle, 1, 1, 1, 1, true)
            end
            if opts.tooltipBody then
                GameTooltip:AddLine(opts.tooltipBody, nil, nil, nil, true)
            end
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", addon.Tooltip.Release)
    end

    if opts.onClick then btn:SetScript("OnClick", opts.onClick) end

    return btn
end

--- Detach child frames that were manually attached to a pooled AceGUI
--- widget's underlying WoW frame. Without this, the next AceGUI:Create
--- of the same widget type returns the same pooled instance with our
--- frames still parented — they stack on top of each other across
--- Open/Close cycles, and can leak into other addons' UIs since the
--- AceGUI pool is global.
---
--- `host` is the table holding the references (typically MainWindow self
--- or a tab module's self). `keys` is a list of self-keys to detach.
--- Hides each frame, reparents to UIParent, clears its points, and nils
--- the reference so a fresh icon can take its place on next Open.
function UI.DetachChildren(host, keys)
    if not (host and keys) then return end
    for _, k in ipairs(keys) do
        local frame = host[k]
        if frame then
            if frame.Hide          then frame:Hide()          end
            if frame.SetParent     then frame:SetParent(UIParent) end
            if frame.ClearAllPoints then frame:ClearAllPoints() end
            host[k] = nil
        end
    end
end

--- Style an AceGUI EditBox as a search box: a magnifying-glass icon at the left
--- edge with the text inset past it (the TOGPM pattern). `keepLabelSpace = true`
--- keeps a blank label so the box lines up vertically with labelled widgets in
--- the same row. Cleans the icon + text insets up on the widget's OnRelease so
--- the pooled EditBox returns clean.
function UI.StyleSearchBox(widget, keepLabelSpace)
    if not (widget and widget.type == "EditBox" and widget.editbox) then return widget end
    widget:SetLabel(keepLabelSpace and " " or "")
    local eb = widget.editbox

    local icon = widget._searchIcon
    if not icon then
        icon = widget.frame:CreateTexture(nil, "OVERLAY")
        icon:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
        icon:SetVertexColor(0.7, 0.7, 0.7)
        widget._searchIcon = icon
    end
    icon:SetParent(widget.frame)
    icon:SetSize(14, 14)
    icon:ClearAllPoints()
    icon:SetPoint("LEFT", eb, "LEFT", 1, -2)
    icon:Show()

    if eb.SetTextInsets and eb.GetTextInsets then
        if not widget._origTextInsets then
            widget._origTextInsets = { eb:GetTextInsets() }
        end
        eb:SetTextInsets(18, 0, 0, 0)
    end

    local prevOnRelease = widget.events and widget.events.OnRelease
    widget:SetCallback("OnRelease", function(self_)
        local i = self_._searchIcon
        if i then
            i:Hide(); i:SetParent(nil); i:ClearAllPoints()
            self_._searchIcon = nil
        end
        if self_.editbox and self_.editbox.SetTextInsets and self_._origTextInsets then
            self_.editbox:SetTextInsets(unpack(self_._origTextInsets))
            self_._origTextInsets = nil
        end
        if prevOnRelease then prevOnRelease(self_) end
    end)
    return widget
end

-- ---------------------------------------------------------------------------
-- Column header menu (sort + multi-select filter) — shared scaffolding
-- ---------------------------------------------------------------------------
-- The dropdown FRAME mechanics live here (global, write-once); callers supply
-- the per-column DATA (distinct values, sort type, callbacks). Cross-version:
-- MenuUtil on Retail 11.0+, UIDropDownMenu/EasyMenu on Classic.
--
--   addon.UI.OpenColumnMenu(anchor, spec) where spec = {
--     title  = "Type",
--     sort   = { type = "text"|"number"|"date", onSort = function(desc) end } | nil,
--     filter = {                                 -- nil to omit the filter block
--       values     = { "Mining", "Fishing", ... },        -- flat list, OR
--       groups     = { { label = "Kalimdor", values = {...} }, ... },  -- nested
--       truncated  = N | nil,                              -- flat lists only
--       isChecked  = function(v) return bool end,
--       setChecked = function(v, checked) end,             -- mutate only
--       commit     = function() end,                       -- apply (refresh)
--     } | nil,
--   }
-- Select all / Clear all (top-level and per-group) are synthesised by the menu
-- from setChecked + commit, so callers don't supply them. A grouped filter
-- renders each group as a submenu of value checkboxes.
local SORT_LABELS = {
    text   = { asc = "Sort A > Z",           desc = "Sort Z > A" },
    number = { asc = "Sort Low > High",      desc = "Sort High > Low" },
    date   = { asc = "Sort Oldest > Newest", desc = "Sort Newest > Oldest" },
}

local _classicColMenu   -- reusable UIDropDownMenu frame for the Classic path

function UI.OpenColumnMenu(anchor, spec)
    spec = spec or {}
    local sl = spec.sort and (SORT_LABELS[spec.sort.type] or SORT_LABELS.text)
    local f  = spec.filter
    local groups    = f and f.groups
    local flat      = f and f.values
    local hasFilter = f and ((groups and #groups > 0) or (flat and #flat > 0))

    -- Every leaf value (for the top-level Select all / Clear all).
    local function allValues()
        if groups then
            local t = {}
            for _, g in ipairs(groups) do
                for _, v in ipairs(g.values) do t[#t + 1] = v end
            end
            return t
        end
        return flat or {}
    end
    -- Set a batch of values then refresh once.
    local function setMany(vals, checked)
        for _, v in ipairs(vals) do f.setChecked(v, checked) end
        f.commit()
    end

    -- Retail: MenuUtil. Checkboxes keep the menu open; Select/Clear all return
    -- MenuResponse.Refresh so the ticks update without the menu closing.
    if MenuUtil and MenuUtil.CreateContextMenu then
        local refresh = MenuResponse and MenuResponse.Refresh or nil
        local function addCheckbox(parent, v)
            parent:CreateCheckbox(v,
                function() return f.isChecked(v) end,
                function() f.setChecked(v, not f.isChecked(v)); f.commit() end)
        end
        MenuUtil.CreateContextMenu(anchor or UIParent, function(_, root)
            if spec.title then root:CreateTitle(spec.title) end
            if sl then
                root:CreateButton(sl.asc,  function() spec.sort.onSort(false) end)
                root:CreateButton(sl.desc, function() spec.sort.onSort(true)  end)
            end
            if hasFilter then
                root:CreateDivider()
                root:CreateButton("Select all", function() setMany(allValues(), true);  return refresh end)
                root:CreateButton("Clear all",  function() setMany(allValues(), false); return refresh end)
                root:CreateDivider()
                if groups then
                    for _, g in ipairs(groups) do
                        local sub = root:CreateButton(g.label)   -- children → submenu
                        sub:CreateButton("Select all", function() setMany(g.values, true);  return refresh end)
                        sub:CreateButton("Clear all",  function() setMany(g.values, false); return refresh end)
                        sub:CreateDivider()
                        for _, v in ipairs(g.values) do addCheckbox(sub, v) end
                    end
                else
                    for _, v in ipairs(flat) do addCheckbox(root, v) end
                    if f.truncated and f.truncated > 0 then
                        root:CreateTitle("… " .. f.truncated .. " more (not shown)")
                    end
                end
            end
        end)
        return
    end

    -- Classic: EasyMenu / UIDropDownMenu. Checkboxes use keepShownOnClick + a
    -- `checked` function so multi-select stays open and reflects live state.
    -- Select/Clear all close the menu (Classic can't cleanly live-refresh the
    -- ticks); reopen to see the result.
    _classicColMenu = _classicColMenu
        or CreateFrame("Frame", "TOGToolsColumnMenu", UIParent, "UIDropDownMenuTemplate")
    local function checkboxEntry(v)
        return {
            text = v, isNotRadio = true, keepShownOnClick = true,
            checked = function() return f.isChecked(v) end,
            func    = function() f.setChecked(v, not f.isChecked(v)); f.commit() end,
        }
    end
    local menu = {}
    if spec.title then
        tinsert(menu, { text = spec.title, isTitle = true, notCheckable = true })
    end
    if sl then
        tinsert(menu, { text = sl.asc,  notCheckable = true, func = function() spec.sort.onSort(false) end })
        tinsert(menu, { text = sl.desc, notCheckable = true, func = function() spec.sort.onSort(true)  end })
    end
    if hasFilter then
        tinsert(menu, { text = "Select all", notCheckable = true, func = function() setMany(allValues(), true)  end })
        tinsert(menu, { text = "Clear all",  notCheckable = true, func = function() setMany(allValues(), false) end })
        if groups then
            for _, g in ipairs(groups) do
                local sublist = {
                    { text = "Select all", notCheckable = true, func = function() setMany(g.values, true)  end },
                    { text = "Clear all",  notCheckable = true, func = function() setMany(g.values, false) end },
                }
                for _, v in ipairs(g.values) do tinsert(sublist, checkboxEntry(v)) end
                tinsert(menu, { text = g.label, hasArrow = true, notCheckable = true, menuList = sublist })
            end
        else
            for _, v in ipairs(flat) do tinsert(menu, checkboxEntry(v)) end
            if f.truncated and f.truncated > 0 then
                tinsert(menu, { text = "… " .. f.truncated .. " more (not shown)", notCheckable = true, disabled = true })
            end
        end
    end
    if EasyMenu then EasyMenu(menu, _classicColMenu, anchor or "cursor", 0, 0, "MENU") end
end

-- ---------------------------------------------------------------------------
-- Cross-namespace alias.
--
-- Compat.lua used to expose a separate `addon.GUI.AttachTooltip` for
-- AceGUI widgets. Now there's a single canonical implementation in
-- `addon.UI.AttachTooltip` that handles both AceGUI widgets and raw
-- frames — point the GUI namespace alias at it so existing call sites
-- in either namespace get the same leak-safe behaviour. UI.lua loads
-- after Compat.lua per the TOC ordering, so this override is final.
-- ---------------------------------------------------------------------------
addon.GUI = addon.GUI or {}
addon.GUI.AttachTooltip = UI.AttachTooltip
