local addOnName, namespace = ...

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

local HOVER_RING_THICKNESS = 1

local DEFAULT_HEIGHT = Theme.H_BTN

-- Per-variant chrome token map (Area 1). "wash" is an optional second overlay texture
-- layered above the base fill (used by danger's "DANGER_A13 wash over a dark base").
-- hoverFill/hoverBorder/hoverText are only set where a matching *_HOVER counterpart of the
-- variant's own base token exists in Theme (BTN_BG_HOVER/BORDER_BTN_HOVER/TEXT_BTN_HOVER
-- pair with BTN_BG/BORDER_BTN/TEXT_BTN). primary/danger have no such counterparts in Theme
-- (no ACCENT_HOVER/DANGER_HOVER background token) -- per 09-REVIEW.md WR-05, we do not
-- invent new tokens, so those variants keep only the generic ACCENT_A13 hover wash.
local VARIANTS = {
    default = {
        fill = Theme.BTN_BG,
        border = Theme.BORDER_BTN,
        text = Theme.TEXT_BTN,
        hoverFill = Theme.BTN_BG_HOVER,
        hoverBorder = Theme.BORDER_BTN_HOVER,
        hoverText = Theme.TEXT_BTN_HOVER,
    },
    primary = {
        fill = Theme.ACCENT_A08,
        border = Theme.ACCENT_A70,
        text = Theme.ACCENT,
        hoverBorder = Theme.ACCENT,
    },
    danger = {
        fill = Theme.BTN_BG,
        wash = Theme.DANGER_A13,
        border = Theme.DANGER,
        text = Theme.DANGER,
    },
    ghost = {
        fill = nil, -- transparent, no background texture color
        border = Theme.BORDER_BTN,
        text = Theme.TEXT_BTN,
        hoverBorder = Theme.BORDER_BTN_HOVER,
        hoverText = Theme.TEXT_BTN_HOVER,
    },
}

function namespace:CreateButton(parent, title, OnClick, opts)
    opts = opts or {}

    -- TOOLKIT-F5: condition and events for automatic enable/disable based on game state.
    -- Stored on the button for the shared condition frame to consume.
    local condition = opts.condition
    local conditionEvents = opts.events

    local variantDef = VARIANTS[opts.variant] or VARIANTS.default

    local button = CreateFrame("Button", nil, parent)
    local fixedWidth = false

    button.OnEnter = function() end
    button.OnLeave = function() end

    button:SetScript("OnEnter", function(_self) _self.OnEnter() end)
    button:SetScript("OnLeave", function(_self) _self.OnLeave() end)

    button.OnClick = OnClick

    -- If no title is present, or the title is an empty string, no font string is created
    button:SetText(" ") -- Force font string creation
    button:SetText(title) -- Set text equal to title (even if it's nil or an empty string)
    button:SetNormalFontObject(WildThingsFont13)
    button:SetHighlightFontObject(WildThingsFont13)
    button:SetDisabledFontObject(WildThingsFont13)
    button:SetScript("OnClick", button.OnClick)

    button.textColor = variantDef.text
    button:GetFontString():SetTextColor(variantDef.text.r, variantDef.text.g, variantDef.text.b, variantDef.text.a)

    local function UpdateWidth()
        local width = button:GetFontString():GetUnboundedStringWidth() + 20
        if button.leadingIcon then
            width = width + (button._leadingIconSize or Theme.ICON_SM) + 6
        end
        button:SetSize(width, DEFAULT_HEIGHT)
    end

    function button:SetFixedWidth(isFixed)
        fixedWidth = isFixed
    end

    hooksecurefunc(
        button,
        "SetText",
        function()
            if not fixedWidth then
                UpdateWidth()
            end
        end
    )

    hooksecurefunc(
        button,
        "SetNormalFontObject",
        function()
            if not fixedWidth then
                UpdateWidth()
            end
        end
    )

    -- Background (base fill)
    button.tex = button:CreateTexture(nil, "BACKGROUND")
    button.tex:SetAllPoints()
    if variantDef.fill then
        button.tex:SetColorTexture(variantDef.fill.r, variantDef.fill.g, variantDef.fill.b, variantDef.fill.a)
    else
        button.tex:SetColorTexture(0, 0, 0, 0)
    end
    button.tex:SetSnapToPixelGrid(false)
    button.tex:SetTexelSnappingBias(0)

    -- Variant wash (danger's DANGER_A13 wash layered over the dark base fill)
    if variantDef.wash then
        button.variantWash = button:CreateTexture(nil, "ARTWORK")
        button.variantWash:SetAllPoints()
        button.variantWash:SetColorTexture(variantDef.wash.r, variantDef.wash.g, variantDef.wash.b, variantDef.wash.a)
        button.variantWash:SetSnapToPixelGrid(false)
        button.variantWash:SetTexelSnappingBias(0)
    end

    -- Static border: hidden by default (fills only, carried-forward Phase 27/28 lock), but
    -- built as a real rounded stroke rather than the old square one, because it is NOT dead
    -- weight -- WTUI:BorderPulse drives it through SetBorderColor to pulse a button for
    -- attention (RosterScreen's import-pending and pending-apply affordances), and the
    -- caller shows it for the duration of the glow.
    WTUI:AddRoundedBorder(button, Theme.RADIUS_SM, 1)
    button:SetBorderColor(variantDef.border.r, variantDef.border.g, variantDef.border.b, variantDef.border.a)
    button:SetBorderShown(false)

    -- No AddHoverHighlight here: its 4 HIGHLIGHT-layer edge textures are not clipped by the
    -- rounded mask below, so they drew a square outline across the 4px corners, and being
    -- engine-driven they snapped on/off against the hover wash's 0.12s/0.20s fade. The
    -- accent wash + text brighten carry the hover state on their own.

    -- Selected wash — persistent accent overlay, created once, toggled (no animation)
    button.selectedOverlay = button:CreateTexture(nil, "ARTWORK")
    button.selectedOverlay:SetAllPoints()
    button.selectedOverlay:SetColorTexture(Theme.SELECTED_OVERLAY.r, Theme.SELECTED_OVERLAY.g, Theme.SELECTED_OVERLAY.b, Theme.SELECTED_OVERLAY.a)
    button.selectedOverlay:Hide()

    function button:SetSelected(isSelected)
        button.selected = isSelected and true or false
        if button.selected then
            button.selectedOverlay:Show()
        else
            button.selectedOverlay:Hide()
        end
    end

    -- Hover fade: shared motion helper (WIDGET-12) — create-once AnimationGroup pair,
    -- bakes Theme.ACCENT_A13's real alpha into the overlay (CR-01 fix), self-wires via
    -- HookScript OnEnter/OnLeave.
    button.hoverOverlay = button:CreateTexture(nil, "OVERLAY")
    button.hoverOverlay:SetAllPoints()

    WTUI:AddHoverFade(button, button.hoverOverlay, {color = Theme.ACCENT_A13})

    -- Rounded overlay-clip primitive (ROUND-04/05, spike-002): one shared mask clips the
    -- base fill AND every overlay/wash layer to the same 4px rounded shape at once.
    WTUI:AddRoundedMask(button, Theme.RADIUS_SM)
    button.tex:AddMaskTexture(button.roundedMask)
    if button.variantWash then button.variantWash:AddMaskTexture(button.roundedMask) end
    button.selectedOverlay:AddMaskTexture(button.roundedMask)
    button.hoverOverlay:AddMaskTexture(button.roundedMask)

    -- Hover ring: a rounded 1px stroke, drawn on top of every wash.
    --
    -- Built as 4 corner arcs + 4 straight strips, NOT as one nine-sliced texture. Nine-slice
    -- draws the arc at RADIUS_SM source texels = 4x4 pixels, which is too few to be anything
    -- but blurry (with AA) or stair-stepped (without) — measured, both were rejected in UAT.
    -- Sizing a corner texture independently of its source resolution is what lets the GPU
    -- downsample high-res art instead, and it is the technique every other addon uses:
    -- Details! Framework (rounded_panel.lua:113-134, vendored in several addons and
    -- WorldQuestTracker) drives a corner quadrant down to a 3px radius this way, and
    -- WTUI:AddRoundedFill (Functions.lua:97-118) already does exactly this for FILLS here —
    -- spike-001 validated it in-game. The ring just brings the stroke in line with it.
    --
    -- Only the 4 corners sample a texture; the straight runs are plain color textures, which
    -- stay perfectly crisp because nothing resamples them.
    --
    -- A separate stroke from the static border above, not a reuse of it: that one is driven
    -- by WTUI:BorderPulse through SetBorderColor for the attention glow, so the hover state
    -- needs its own layer to fade independently. Uses the variant's hoverBorder token, which
    -- was declared but dead until now.
    do
        local c = variantDef.hoverBorder or variantDef.border

        button.hoverRing = WTUI:AddRoundedStroke(button, Theme.RADIUS_SM, HOVER_RING_THICKNESS)
        button.hoverRing:SetStrokeColor(c.r, c.g, c.b, c.a)

        -- alphaOnly: the stroke is already colored; the helper just fades the group, on the
        -- same 0.12s-in / 0.20s-out timing as the wash.
        WTUI:AddHoverFade(button, button.hoverRing, {alphaOnly = true})
    end

    button.OnEnter = function()
        if not button:IsEnabled() then return end

        if variantDef.hoverFill then
            local c = variantDef.hoverFill
            button.tex:SetColorTexture(c.r, c.g, c.b, c.a)
        end
        if variantDef.hoverText then
            local c = variantDef.hoverText
            button:GetFontString():SetTextColor(c.r, c.g, c.b, c.a)
        end
    end

    button.OnLeave = function()
        if not button:IsEnabled() then return end

        if variantDef.hoverFill then
            local c = variantDef.fill
            button.tex:SetColorTexture(c.r, c.g, c.b, c.a)
        end
        if variantDef.hoverText then
            local c = variantDef.text
            button:GetFontString():SetTextColor(c.r, c.g, c.b, c.a)
        end
    end

    -- Press feedback: instant opacity dip via the shared motion helper (WIDGET-12). Replaced
    -- the former press-scale, which read as janky on button frames across 3 attempts (G-9-2e).
    WTUI:AddPressDim(button, {pressAlpha = 0.85})

    -- Leading icon (plain texture, left of the label; label re-anchored to sit right of it).
    -- Accepts three forms:
    --   number   – raw Blizzard texture fileID (EJ creature portraits). A center-crop
    --              texCoord is applied automatically: crops 17% from each horizontal edge
    --              to turn the typically-wide portrait into a square (1:1).
    --   string   – Lucide icon stem (resolved via Theme.GetIconPath), OR a full texture
    --              path like "Interface\\Icons\\..." (e.g. achievement icons) — detected
    --              by the presence of a backslash.
    -- Numeric icons and full-path textures render in their original colors (no text-color
    -- tint); Lucide stems keep the existing tint behaviour so they match the label color.
    -- Optional `size` parameter overrides the default Theme.ICON_SM (e.g. larger boss avatars).
    function button:SetLeadingIcon(stem, size)
        if not button.leadingIcon then
            button.leadingIcon = button:CreateTexture(nil, "ARTWORK")
        end

        local iconSize = size or Theme.ICON_SM
        button._leadingIconSize = iconSize

        local isNumericIcon = type(stem) == "number"
        local isFullPath = type(stem) == "string" and stem:find("\\")
        button._leadingIconUntinted = (isNumericIcon or isFullPath) or nil

        if isNumericIcon then
            button.leadingIcon:SetTexture(stem)
            -- Center-crop wide EJ creature portrait to a 1:1 square.
            -- Most EJ portraits are ~1.5:1; cropping 17% from each side
            -- leaves the middle 66%, yielding ~1:1 aspect ratio.
            button.leadingIcon:SetTexCoord(0.17, 0.83, 0, 1)
        elseif isFullPath then
            button.leadingIcon:SetTexture(stem)
        else
            button.leadingIcon:SetTexture(Theme.GetIconPath(stem))
        end
        button.leadingIcon:SetSize(iconSize, iconSize)
        button.leadingIcon:ClearAllPoints()
        button.leadingIcon:SetPoint("LEFT", button, "LEFT", Theme.PAD_BTN_X, 0)
        button.leadingIcon:Show()
        if not button._leadingIconUntinted then
            local t = button.textColor
            button.leadingIcon:SetVertexColor(t.r, t.g, t.b, t.a)
        end

        local fontString = button:GetFontString()
        fontString:ClearAllPoints()
        fontString:SetPoint("LEFT", button.leadingIcon, "RIGHT", 6, 0)

        if not fixedWidth then
            UpdateWidth()
        end
    end

    -- Disabled: native Enable(false) path + TEXT_DISABLED label color.
    -- Numeric leading icons (EJ boss portraits) keep their original colors -- only
    -- Lucide-stem icons are tinted to match the label.
    local function UpdateLabelColor()
        if button:IsEnabled() then
            button:GetFontString():SetTextColor(button.textColor.r, button.textColor.g, button.textColor.b, button.textColor.a)
            if button.leadingIcon and not button._leadingIconUntinted then
                local c = button.textColor
                button.leadingIcon:SetVertexColor(c.r, c.g, c.b, c.a)
            end
        else
            local c = Theme.TEXT_DISABLED
            button:GetFontString():SetTextColor(c.r, c.g, c.b, c.a)
            if button.leadingIcon and not button._leadingIconUntinted then
                button.leadingIcon:SetVertexColor(c.r, c.g, c.b, c.a)
            end
        end
    end

    hooksecurefunc(button, "Enable", UpdateLabelColor)
    hooksecurefunc(button, "Disable", UpdateLabelColor)

    if opts.icon then
        button:SetLeadingIcon(opts.icon, opts.iconSize)
    end

    button:SetSelected(opts.selected)

    UpdateWidth()

    -- TOOLKIT-F5: register with the shared condition frame for event-driven
    -- enable/disable. If condition and events are both present, the button is
    -- evaluated whenever any of its declared events fire (plus a 3s ticker fallback).
    -- OnHide hook ensures cleanup when the button is hidden or destroyed.
    button.condition = condition
    button.conditionEvents = conditionEvents
    if button.condition and button.conditionEvents then
        WTUI:RegisterConditionButton(button)
        -- Evaluate immediately to set the correct initial enabled/disabled state.
        WTUI:EvaluateConditionButton(button)
    end
    button:HookScript("OnHide", function()
        if button._conditionRegistered then
            WTUI:UnregisterConditionButton(button)
        end
    end)

    if opts.fixedWidth then
        button:SetFixedWidth(true)
    end

    return button
end

--------------------------------------------------------------------------------
-- Button Condition Polling (TOOLKIT-F5)
--
-- Contract: Buttons declare `condition` (fn -> bool) and `conditionEvents` (string
-- array of WoW event names). The shared condition frame registers each unique event
-- once and routes it to all matching buttons on the active screen.
--
-- Architecture:
--   Primary path: Event-driven — the shared frame's OnEvent handler re-evaluates
--     every button registered for the fired event.
--   Fallback: A slow C_Timer.NewTicker(3, ...) catches state changes with no
--     dedicated WoW event (D-07).
--   Active-screen gate: EvaluateConditionButton checks button:IsVisible(), which
--     returns false when the parent screen is hidden by ShowScreen (D-09).
--   Lifecycle: Registered at CreateButton time, unregistered on OnHide via
--     HookScript, cleaned up on last event removal (ticker cancelled when empty).
--
-- The BuildWidgets button builder passes def.condition and def.events through the
-- opts table (Plan 49-01 / BuildWidgets.lua).
--------------------------------------------------------------------------------

-- Shared condition frame and routing table, initialized once at file load.
WTUI._conditionFrame = WTUI._conditionFrame or CreateFrame("Frame")
WTUI._conditionButtons = WTUI._conditionButtons or {} -- [eventName] = {button1, ...}
WTUI._conditionTicker = nil

--- Registers a button for condition evaluation on its declared events.
--- Idempotent guard: sets _conditionRegistered to prevent double registration.
function WTUI:RegisterConditionButton(button)
    if button._conditionRegistered then return end
    if not button.conditionEvents then return end

    button._conditionRegistered = true

    for _, event in ipairs(button.conditionEvents) do
        if not WTUI._conditionButtons[event] then
            WTUI._conditionButtons[event] = {}
            WTUI._conditionFrame:RegisterEvent(event)
        end
        WTUI._conditionButtons[event][#WTUI._conditionButtons[event] + 1] = button
    end

    -- Start the 3-second fallback ticker on first registration (D-07).
    if not WTUI._conditionTicker then
        WTUI._conditionTicker = C_Timer.NewTicker(3, function()
            WTUI:EvaluateAllConditions()
        end)
    end
end

--- Unregisters a button from condition evaluation. Cleans up orphaned event
--- registrations and cancels the ticker when no buttons remain.
function WTUI:UnregisterConditionButton(button)
    if not button._conditionRegistered then return end
    if not button.conditionEvents then return end

    button._conditionRegistered = false

    for _, event in ipairs(button.conditionEvents) do
        local list = WTUI._conditionButtons[event]
        if list then
            for i = #list, 1, -1 do
                if list[i] == button then
                    table.remove(list, i)
                end
            end
            -- Remove orphaned event registration when no buttons remain for this event.
            if #list == 0 then
                WTUI._conditionButtons[event] = nil
                WTUI._conditionFrame:UnregisterEvent(event)
            end
        end
    end

    -- Cancel ticker when no buttons are registered (no orphaned ticker).
    if not next(WTUI._conditionButtons) then
        if WTUI._conditionTicker then
            WTUI._conditionTicker:Cancel()
            WTUI._conditionTicker = nil
        end
    end
end

--- Evaluates a single button's condition and enables/disables accordingly.
--- Safe: pcall-wrapped condition, destroyed/hidden/IsVisible guards.
function WTUI:EvaluateConditionButton(button)
    -- Guard: destroyed or invalid button reference.
    if not button or button:IsDestroyed() then return end
    -- Guard: only evaluate buttons on the active screen (D-09).
    -- When ShowScreen hides inactive screens, their descendants' IsVisible() returns false.
    if not button:IsVisible() then return end
    -- Guard: no condition means nothing to evaluate.
    if not button.condition then return end

    local ok, result = pcall(button.condition)
    if ok and result then
        button:Enable()
    else
        button:Disable()
    end
end

--- Evaluates all registered condition buttons (3s ticker fallback).
--- Each button's IsVisible() gate prevents wasted work on hidden screens.
function WTUI:EvaluateAllConditions()
    for _, buttons in pairs(WTUI._conditionButtons or {}) do
        for _, button in ipairs(buttons) do
            WTUI:EvaluateConditionButton(button)
        end
    end
end

-- Shared frame OnEvent script: routes events to matching buttons.
-- Guarded against double-assignment (Button.lua loads once, but /reload re-runs it).
if not WTUI._conditionFrame._scriptSet then
    WTUI._conditionFrame._scriptSet = true
    WTUI._conditionFrame:SetScript("OnEvent", function(_, event, ...)
        local buttons = WTUI._conditionButtons[event]
        if buttons then
            for _, button in ipairs(buttons) do
                WTUI:EvaluateConditionButton(button)
            end
        end
    end)
end
