-- zSettingsFrame/zSF_Primitives.lua
-- Lightweight frame construction and layout helpers.
-- Pure functions that return WoW frame objects.  No visual style decisions here —
-- colours, fonts, and sizes all live in zSF_Widgets and zSF_Panel.
-- No external library dependencies; uses raw WoW frame APIs only.
local ZSF = _G.ZSF  -- namespace for style constants and easing functions

--- Create a plain Frame.
---@param name   string|nil  Global frame name. Pass nil for anonymous frames.
---@param parent Frame|nil
---@return Frame
function zSF_Frame(name, parent)
    return CreateFrame("Frame", name, parent)
end

--- Create a FontString attached to parent.
--- The caller is responsible for calling :SetText() afterwards.
---@param parent     Region
---@param fontObject string|Font|nil  Font object or template name (e.g. "GameFontNormal").
---@param justifyH   string|nil       "LEFT" | "CENTER" | "RIGHT". Defaults to "LEFT".
---@return FontString
function zSF_Text(parent, fontObject, justifyH)
    local fs = parent:CreateFontString(nil, "OVERLAY")
    local fo = fontObject
    -- Replace WoW's default font objects with the per-addon virtual ones.
    -- During build passes, _buildCtx holds the active per-addon context.
    local ctx = zSettingsFrame and zSettingsFrame._buildCtx
    local foNormal = (ctx and ctx.FONTOBJ_NORMAL) or ZSF.FONTOBJ_NORMAL
    local foSmall  = (ctx and ctx.FONTOBJ_SMALL)  or ZSF.FONTOBJ_SMALL
    local foLarge  = (ctx and ctx.FONTOBJ_LARGE)  or ZSF.FONTOBJ_LARGE
    if foNormal then
        if fo == "GameFontNormal" then
            fo = foNormal
        elseif fo == "GameFontHighlightSmall" then
            fo = foSmall
        elseif fo == "GameFontNormalLarge" and foLarge then
            fo = foLarge
        end
    end
    if fo then
        fs:SetFontObject(fo)
    end
    fs:SetJustifyH(justifyH or "LEFT")
    return fs
end

--- Create a Texture attached to parent.
--- When r/g/b are supplied the texture is filled with that solid colour.
---@param parent Region
---@param layer  string|nil  Draw layer. Defaults to "BACKGROUND".
---@param r      number|nil  Red   (0–1).
---@param g      number|nil  Green (0–1).
---@param b      number|nil  Blue  (0–1).
---@param a      number|nil  Alpha (0–1). Defaults to 1 when r/g/b are provided.
---@return Texture
function zSF_Texture(parent, layer, r, g, b, a)
    local t = parent:CreateTexture(nil, layer or "BACKGROUND")
    if r then
        t:SetColorTexture(r, g, b, a or 1)
    end
    return t
end

--- Stack child frames vertically inside a parent, top-to-bottom.
--- Anchors each child's TOPLEFT to the previous child's BOTTOMLEFT.
--- The parent frame's height is NOT modified — callers must set it after if needed.
--- Returns the total pixel height consumed by all children plus gaps.
---@param parent   Frame
---@param children Frame[]  Ordered list of frames. Each must have a defined height.
---@param gapY     number|nil  Pixels between consecutive children. Defaults to 0.
---@param padTop   number|nil  Pixels before the first child. Defaults to 0.
---@return number totalHeight  Sum of all child heights, all gaps, and padTop.
function zSF_LayoutVertical(parent, children, gapY, padTop)
    gapY   = gapY   or 0
    padTop = padTop or 0
    local total = padTop
    local prev  = nil
    for _, child in ipairs(children) do
        child:ClearAllPoints()
        if prev then
            child:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gapY)
            total = total + gapY
        else
            child:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -padTop)
        end
        total = total + child:GetHeight()
        prev  = child
    end
    return total
end

--- Anchor child frames horizontally inside a parent, left-to-right.
--- The parent frame's width is NOT modified — callers must set it if needed.
---@param parent   Frame
---@param children Frame[]  Ordered list of frames.
---@param gapX     number|nil  Pixels between consecutive children. Defaults to 0.
---@param padLeft  number|nil  Pixels before the first child. Defaults to 0.
function zSF_LayoutHorizontal(parent, children, gapX, padLeft)
    gapX    = gapX    or 0
    padLeft = padLeft or 0
    local prev = nil
    for _, child in ipairs(children) do
        child:ClearAllPoints()
        if prev then
            child:SetPoint("LEFT", prev, "RIGHT", gapX, 0)
        else
            child:SetPoint("LEFT", parent, "LEFT", padLeft, 0)
        end
        prev = child
    end
end

--- Create a ScrollFrame with a managed content child.
--- Mousewheel scrolling is wired automatically.
--- The content child stretches to the full width of the scroll frame via two anchor
--- points (TOPLEFT / TOPRIGHT).  Its height must be updated by the caller after
--- widgets are laid out — call content:SetHeight(totalHeight).
---@param parent      Frame
---@param scrollSpeed number|nil  Pixels scrolled per mousewheel tick. Defaults to 20.
---@return ScrollFrame scrollFrame
---@return Frame       content     Parent all scrollable content frames to this.
function zSF_ScrollFrame(parent, scrollSpeed)
    scrollSpeed = scrollSpeed or 20

    local sf      = CreateFrame("ScrollFrame", nil, parent)
    local content = CreateFrame("Frame", nil, sf)

    -- Stretch content width to match the scroll frame; height is managed dynamically.
    content:SetPoint("TOPLEFT")
    content:SetPoint("TOPRIGHT")
    content:SetHeight(1)  -- placeholder; caller must set after layout

    sf:SetScrollChild(content)
    sf:EnableMouseWheel(true)
    sf:SetScript("OnMouseWheel", function(self, delta)
        local current   = self:GetVerticalScroll()
        local maxScroll = self:GetVerticalScrollRange()
        local new       = math.max(0, math.min(current - (delta * scrollSpeed), maxScroll))
        self:SetVerticalScroll(new)
    end)

    return sf, content
end

--- Fill a frame with a solid-colour BACKGROUND texture.
--- Returns the texture so the caller can recolour it later.
---@param frame Frame
---@param r     number  Red   (0–1).
---@param g     number  Green (0–1).
---@param b     number  Blue  (0–1).
---@param a     number  Alpha (0–1).
---@return Texture
function zSF_FillBG(frame, r, g, b, a)
    local t = frame:CreateTexture(nil, "BACKGROUND")
    t:SetAllPoints(frame)
    t:SetColorTexture(r or 0, g or 0, b or 0, a or 1)
    return t
end

--- Disable WoW's built-in texel-snapping on a texture or StatusBar.
--- Call this AFTER SetColorTexture / SetTexture / SetGradient — WoW re-enables
--- snapping whenever the color or texture is changed.
---@param t table  Texture or StatusBar.
function zSF_DisablePixelSnap(t)
    if t and t.SetSnapToPixelGrid then
        t:SetSnapToPixelGrid(false)
        t:SetTexelSnappingBias(0)
    end
end

--- Draw a 1-pixel solid border around a frame using four edge textures.
--- PixelUtil is used for all size and position calls so edges render at
--- exactly 1 physical pixel at every UI scale.
--- On repeated calls the existing textures are reused — only the colour/alpha
--- is updated, so hover-state changes never leak new texture allocations.
---@param frame Frame
---@param r     number|nil  Red   (0–1). Defaults to ZSF.COLOR_BORDER[1].
---@param g     number|nil  Green (0–1). Defaults to ZSF.COLOR_BORDER[2].
---@param b     number|nil  Blue  (0–1). Defaults to ZSF.COLOR_BORDER[3].
---@param a     number|nil  Alpha (0–1). Defaults to ZSF.COLOR_BORDER[4].
function zSF_DrawBorder(frame, r, g, b, a)
    r = r or ZSF.COLOR_BORDER[1]
    g = g or ZSF.COLOR_BORDER[2]
    b = b or ZSF.COLOR_BORDER[3]
    a = a or ZSF.COLOR_BORDER[4]

    -- Reuse cached edges when available; only update colour and alpha.
    if frame._zsfBorder then
        for _, t in ipairs(frame._zsfBorder) do
            t:SetVertexColor(r, g, b)
            t:SetAlpha(a)
        end
        return
    end

    local function MkEdge()
        local t = frame:CreateTexture(nil, "OVERLAY", nil, 7)
        t:SetTexture(zSF_AssetPath("1px.png"))
        t:SetVertexColor(r, g, b)
        t:SetAlpha(a)
        return t
    end

    local eT = MkEdge()
    PixelUtil.SetPoint(eT, "TOPLEFT",  frame, "TOPLEFT",  0, 0)
    PixelUtil.SetPoint(eT, "TOPRIGHT", frame, "TOPRIGHT", 0, 0)
    PixelUtil.SetHeight(eT, 1, 1)

    local eB = MkEdge()
    PixelUtil.SetPoint(eB, "BOTTOMLEFT",  frame, "BOTTOMLEFT",  0, 0)
    PixelUtil.SetPoint(eB, "BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
    PixelUtil.SetHeight(eB, 1, 1)

    -- Vertical edges inset 1px top/bottom to avoid doubling corner pixels.
    local eL = MkEdge()
    PixelUtil.SetPoint(eL, "TOPLEFT",    eT, "BOTTOMLEFT",  0, 0)
    PixelUtil.SetPoint(eL, "BOTTOMLEFT", eB, "TOPLEFT",     0, 0)
    PixelUtil.SetWidth(eL, 1, 1)

    local eR = MkEdge()
    PixelUtil.SetPoint(eR, "TOPRIGHT",    eT, "BOTTOMRIGHT",  0, 0)
    PixelUtil.SetPoint(eR, "BOTTOMRIGHT", eB, "TOPRIGHT",     0, 0)
    PixelUtil.SetWidth(eR, 1, 1)

    frame._zsfBorder = { eT, eB, eL, eR }
end

-- ── Tween engine ─────────────────────────────────────────────────────────────
-- Minimal numeric tween driver for smooth open/close, nav sweep, and tab flash animations.

-- Easing functions — all take t in [0,1] and return an eased value in [0,1].
ZSF.EASE_LINEAR    = function(t) return t end
ZSF.EASE_QUAD_IN   = function(t) return t * t end
ZSF.EASE_QUAD_OUT  = function(t) local u = 1 - t; return 1 - u * u end
ZSF.EASE_CUBIC_IN  = function(t) return t * t * t end
ZSF.EASE_CUBIC_OUT = function(t) local u = 1 - t; return 1 - u * u * u end
ZSF.EASE_SINE_OUT  = function(t) return math.sin(t * math.pi * 0.5) end

local _tweens     = {}
local _tweenCount = 0
local _tweenFrame = nil

local function _TweenOnUpdate(_, elapsed)
    local i = 1
    while i <= _tweenCount do
        local tw = _tweens[i]
        if tw.cancelled then
            _tweens[i]           = _tweens[_tweenCount]
            _tweens[_tweenCount] = nil
            _tweenCount          = _tweenCount - 1
        else
            tw.elapsed = tw.elapsed + elapsed
            local p = tw.elapsed / tw.duration
            if p > 1 then p = 1 end
            local val = tw.from + (tw.to - tw.from) * tw.ease(p)
            tw.apply(val)
            if p >= 1 then
                if tw.onFinish then tw.onFinish() end
                _tweens[i]           = _tweens[_tweenCount]
                _tweens[_tweenCount] = nil
                _tweenCount          = _tweenCount - 1
            else
                i = i + 1
            end
        end
    end
    if _tweenCount == 0 and _tweenFrame then
        _tweenFrame:SetScript("OnUpdate", nil)
    end
end

--- Start a numeric tween. Returns a handle; call handle:Cancel() to abort early.
---@param from     number
---@param to       number
---@param duration number    Seconds.
---@param ease     function  Easing function (use ZSF.EASE_* constants).
---@param apply    function  Called each frame with the current interpolated value.
---@param onFinish function|nil  Called once when the tween completes.
---@return table handle
function zSF_Tween(from, to, duration, ease, apply, onFinish)
    if not _tweenFrame then _tweenFrame = CreateFrame("Frame") end
    local tw = {
        from      = from,
        to        = to,
        duration  = math.max(duration, 0.001),
        ease      = ease or ZSF.EASE_QUAD_OUT,
        apply     = apply,
        onFinish  = onFinish,
        elapsed   = 0,
        cancelled = false,
    }
    _tweenCount          = _tweenCount + 1
    _tweens[_tweenCount] = tw
    if _tweenCount == 1 then
        _tweenFrame:SetScript("OnUpdate", _TweenOnUpdate)
    end
    local handle = { _tw = tw }
    function handle:Cancel() self._tw.cancelled = true end
    return handle
end
