local _, namespace = ...

local WTUI = namespace.WildThingsUI
local Theme = namespace.Theme

--[[
CREATE-ONCE CONTRACT (ROADMAP SC5):
Every WTUI:Add* call in this file must run exactly once per frame, at that frame's
construction time (inside its Create* function). NEVER call one of these from a
per-refresh/per-render/rebind callback (Refresh*/Update*/SetData*-style methods) —
doing so builds an additional, orphaned AnimationGroup that competes with the
original for the same target's alpha/scale/rotation/position, causing visible
flicker/snapping and an ever-growing group count on reused frames (e.g. list rows).
]]

-- Guard helper: ensure a frame has a given script registered before HookScript-ing it
-- (mirrors WTUI:AddTooltip in UI/Functions.lua — some frame types don't have every
-- script slot available until something has been assigned to it at least once).
local function EnsureScript(frame, scriptName)
    if not frame:HasScript(scriptName) then
        frame:SetScript(scriptName, function() end)
    end
end

-- WTUI:AddHoverFade(frame, targetTexture, opts)
-- Create-once hover fade on targetTexture, self-wired via
-- frame:HookScript("OnEnter"/"OnLeave", ...) — additive, never reassigns the widget's own
-- .OnEnter/.OnLeave stub-field trampoline. Internals drive Blizzard's shared FADEFRAMES
-- queue via UIFrameFadeIn/UIFrameFadeOut rather than a per-widget AnimationGroup pair.
-- opts.alphaOnly = true skips color application entirely, so the target may be a FRAME
-- rather than a texture — the caller has already colored its own child textures and just
-- wants them faded together as a group (see Button.lua's multi-texture hover ring).
-- Everything else (fade timing, interrupted-hover handling, wiring) is identical.
function WTUI:AddHoverFade(frame, targetTexture, opts)
    opts = opts or {}
    local color = opts.color or Theme.ACCENT_A13
    local durIn = opts.durIn or Theme.ANIMATION.HOVER_IN
    local durOut = opts.durOut or Theme.ANIMATION.HOVER_OUT

    -- Bake the token's real alpha into the color (NOT a hardcoded 1 — this is the
    -- CR-01 fix from 09-REVIEW.md; regressing to a literal 1 here would reintroduce a
    -- fully-opaque hover wash instead of a subtle tint).
    if not opts.alphaOnly then
        targetTexture:SetColorTexture(color.r, color.g, color.b, color.a)
    end
    targetTexture:SetAlpha(0)

    EnsureScript(frame, "OnEnter")
    EnsureScript(frame, "OnLeave")

    frame:HookScript("OnEnter", function()
        if frame.IsEnabled and not frame:IsEnabled() then return end
        -- Passing targetTexture:GetAlpha() as the explicit startAlpha is mandatory —
        -- UIFrameFade snaps to its default startAlpha (0 for fade-in) before its
        -- already-fading dedupe check, so omitting it reintroduces the
        -- interrupted-hover alpha snap the old AnimationGroup path avoided (Pitfall 1).
        UIFrameFadeIn(targetTexture, durIn, targetTexture:GetAlpha(), 1)
    end)

    frame:HookScript("OnLeave", function()
        if frame.IsEnabled and not frame:IsEnabled() then return end
        UIFrameFadeOut(targetTexture, durOut, targetTexture:GetAlpha(), 0)
    end)
end

-- WTUI:AddPressDim(frame, opts)
-- Lightest press feedback: instantly dim the whole frame to opts.pressAlpha on OnMouseDown
-- and restore it to 1.0 on OnMouseUp / OnLeave. No scaling, no animation — this REPLACES
-- the former AddPressScale, whose Scale animation read as janky on button frames across
-- three attempts (G-9-2e: scaling a frame also scales its font/child textures, which jitter
-- at fractional scale). Dimming the frame (label + border included) reads as a physical
-- "press". Additive HookScript wiring, mirrors the other helpers. OnLeave also restores so
-- a press that drags off the button doesn't leave it stuck dim.
function WTUI:AddPressDim(frame, opts)
    opts = opts or {}
    local pressAlpha = opts.pressAlpha or 0.85

    EnsureScript(frame, "OnMouseDown")
    EnsureScript(frame, "OnMouseUp")
    EnsureScript(frame, "OnLeave")

    frame:HookScript("OnMouseDown", function()
        if frame.IsEnabled and not frame:IsEnabled() then return end
        frame:SetAlpha(pressAlpha)
    end)

    frame:HookScript("OnMouseUp", function()
        frame:SetAlpha(1)
    end)

    frame:HookScript("OnLeave", function()
        frame:SetAlpha(1)
    end)
end

-- WTUI:AddToggleAnimation(frame, targetTexture, animKind, opts)
-- Create-once single AnimationGroup whose one child Animation's type/setter is chosen by
-- animKind ("Scale" | "Rotation" | "Alpha" | "Translation"). Wires NO event handler and
-- does NOT call Play — returns the raw group so the caller drives Play()/Play(true)/Stop()
-- from its own state-change logic (e.g. SetChecked, menu open/close, toggle-on/off).
--
-- Stop() vs Play(true) on uncheck: Scale/Alpha kinds use absolute from/to values (every
-- forward Play() restarts at the declared "from" regardless of prior state), so Stop() is
-- safe there (see CheckButton.lua's SetChecked). Rotation/Translation kinds are RELATIVE
-- deltas from the target's current position/angle — they only return to origin via an
-- exact Play(true) reverse; calling Stop() on them leaves residual drift instead of
-- snapping back (see Toggle.lua/Dropdown.lua's SetChecked/menu handlers). Don't copy
-- CheckButton's Stop() idiom onto a Rotation/Translation toggle.
function WTUI:AddToggleAnimation(frame, targetTexture, animKind, opts)
    opts = opts or {}
    local duration = opts.duration or Theme.DUR_IN

    local group = frame:CreateAnimationGroup()
    local anim = group:CreateAnimation(animKind)
    anim:SetTarget(targetTexture)
    anim:SetDuration(duration)

    if animKind == "Scale" then
        anim:SetOrigin("CENTER", 0, 0)
        anim:SetScaleFrom(opts.from or 0.1, opts.from or 0.1)
        anim:SetScaleTo(opts.to or 1, opts.to or 1)
    elseif animKind == "Rotation" then
        anim:SetOrigin("CENTER", 0, 0)
        anim:SetDegrees(opts.to or 180) -- relative delta; Play(true) reverses exactly
    elseif animKind == "Alpha" then
        anim:SetFromAlpha(opts.from or 0)
        anim:SetToAlpha(opts.to or 1)
    elseif animKind == "Translation" then
        -- offset is a relative pixel delta from the target's current position, not an
        -- absolute coordinate; Play(true) reverses it exactly, same delta contract as
        -- Rotation (see 10-RESEARCH.md Pitfall 3).
        local toX = (opts.to and opts.to.x) or 0
        local toY = (opts.to and opts.to.y) or 0
        anim:SetOffset(toX, toY)
    end

    return group
end
