---@diagnostic disable: undefined-field, undefined-global
local _, namespace = ...

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

--[[
Declarative descriptor -> widget builder (WTUI-04). Turns a `{type, label, get, set, ...}`
descriptor array into stacked WTUI widgets, so screens can describe their rows as data
instead of hand-anchoring each one (see UI/Screens/SettingsScreen.lua, the proving
consumer). This is an ADAPTER over WTUI's OWN constructor shapes — `CreateToggle(parent,
onChange)` + `SetChecked(initial, true)`, `CreateSlider(parent, title, min, max, onChange,
initialValue)`, `CreateEditBox(parent, title, onChange)`, `CreateSectionHeader(parent,
title)`, `CreateButton(parent, title, OnClick)` — NOT a copy of any other addon's
`(parent, label, getValue, setValue)` signatures.
Each descriptor's `get()` is called exactly once, at build time, to seed the control's
initial value; `set` is wrapped as the control's own onChange/commit callback. This file
ships in the packaged release, so it must stay consumer-agnostic: no references to any
dev-only screen, its data, or the local deploy tooling.
]]

-- Sensible row-height defaults for types whose own constructor doesn't report a height
-- consumers can measure directly (Slider/EditBox render extra chrome — Low/High labels,
-- title — around their native control).
local DEFAULT_SLIDER_HEIGHT = 48 -- Title (13px) + track (20px) + Low/High labels (13px) + breathing room
local DEFAULT_EDITBOX_HEIGHT = Theme.H_INPUT

-- BUILDERS[type](parent, def) -> ctrl, height
-- ctrl is the frame BuildWidgets anchors/stacks; height is what BuildWidgets accumulates
-- into the total it returns. ctrl may optionally define a `ctrl:Refresh()` method, used by
-- BuildWidgets' own returned `refresh` function to re-sync from `def.get()` without ever
-- re-firing `def.set` (each Refresh below goes through the control's own suppress-callback
-- path where one exists).
local BUILDERS = {
    -- toggle -> namespace:CreateToggle (the WTUI pill Toggle widget, NOT CheckButton —
    -- standing project memory / Pitfall 5). Returns a row Frame containing the toggle +
    -- its label, matching SettingsScreen's prior hand-anchored row shape exactly.
    toggle = function(parent, def)
        local row = CreateFrame("Frame", nil, parent)
        row:SetHeight(Theme.H_ROW)

        local ctrl = namespace:CreateToggle(row, function(checked) def.set(checked) end)
        ctrl:SetPoint("LEFT", row, "LEFT", 0, 0)
        ctrl:SetChecked(def.get(), true) -- true = suppress the initial OnValueChanged fire

        local text = row:CreateFontString(nil, "ARTWORK")
        text:SetFontObject(WildThingsFont13)
        text:SetText(def.label)
        text:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)
        text:SetPoint("LEFT", ctrl, "RIGHT", Theme.GAP, 0)

        function row:Refresh()
            ctrl:SetChecked(def.get(), true) -- true = suppress OnValueChanged, never fires set
        end

        -- Forward Enable/Disable to the inner control so BuildWidgets' generic
        -- def.disabled dispatch (below) can reach the toggle through the wrapper row.
        function row:Enable() ctrl:Enable() end
        function row:Disable() ctrl:Disable() end

        return row, Theme.H_ROW
    end,

    -- slider -> namespace:CreateSlider, seeded with def.get() as the constructor's own
    -- initialValue slot; def.set wired as the mouse-up-commit onChange WTUI-03 already
    -- gives every Slider. Wrapped in a row Frame so BuildWidgets' full-width stretching
    -- only affects the wrapper — the slider keeps its native width and the valueBox
    -- stays visible to the right of the track.
    slider = function(parent, def)
        local row = CreateFrame("Frame", nil, parent)
        row:SetHeight(DEFAULT_SLIDER_HEIGHT)

        local ctrl = namespace:CreateSlider(row, def.label, def.min, def.max,
            function(v) def.set(v) end, def.get())
        ctrl:SetPoint("TOPLEFT", row, "TOPLEFT", 0, -8)

        function row:Refresh()
            ctrl:SetValueSilent(def.get())
        end

        function row:Enable() ctrl:Enable() end
        function row:Disable() ctrl:Disable() end

        return row, DEFAULT_SLIDER_HEIGHT
    end,

    -- editbox -> namespace:CreateEditBox, wrapped for commit-on-blur/Enter (Pitfall 4):
    -- EditBox's own OnValueChanged fires live on every keystroke, so def.set is NOT wired
    -- there (a no-op is passed instead) — it is wired to OnEditFocusLost, mirroring
    -- Slider.lua's own inline-EditBox commit pattern (Enter routes here too, since the
    -- widget's native OnEnterPressed script calls ClearFocus()).
    editbox = function(parent, def)
        local ctrl = namespace:CreateEditBox(parent, def.label, function() end)
        if def.get then
            ctrl:SetText(tostring(def.get()))
        end

        ctrl:HookScript("OnEditFocusLost", function()
            def.set(ctrl.currentValue)
        end)

        function ctrl:Refresh()
            if def.get then
                ctrl:SetText(tostring(def.get())) -- OnTextSet fires the no-op, never def.set
            end
        end

        return ctrl, DEFAULT_EDITBOX_HEIGHT
    end,

    -- button -> namespace:CreateButton, wrapped in a row Frame like toggle so the
    -- dispatch loop's two-sided anchoring (TOPLEFT + TOPRIGHT) stretches the wrapper,
    -- not the button: CreateButton derives its own width from its font string, and
    -- anchoring both sides would override that sizing. The inner control is anchored
    -- LEFT only. Consumes def.label (button title), def.onClick (action), and
    -- optional def.disabled. No Refresh: a button carries no state (like header).
    button = function(parent, def)
        local row = CreateFrame("Frame", nil, parent)
        row:SetHeight(Theme.H_ROW)

        local ctrl = namespace:CreateButton(row, def.label, function() def.onClick() end, {
            condition = def.condition,
            events = def.events,
        })
        ctrl:SetPoint("LEFT", row, "LEFT", 0, 0)

        -- Forward Enable/Disable to the inner control so BuildWidgets' generic
        -- def.disabled dispatch can reach the button through the wrapper row.
        function row:Enable() ctrl:Enable() end
        function row:Disable() ctrl:Disable() end

        return row, Theme.H_ROW
    end,

    -- header -> namespace:CreateSectionHeader (static display, no get/set).
    header = function(parent, def)
        local ctrl = namespace:CreateSectionHeader(parent, def.label)
        return ctrl, ctrl:GetHeight()
    end,

    -- text -> a wrapping paragraph (static display, no get/set, like header).
    -- CRITICAL (Pitfall 1): a wrapping FontString only reports a correct
    -- multi-line GetStringHeight() once its width is bounded -- the label's
    -- width must be set BEFORE SetText/GetStringHeight are called, which in
    -- turn depends on `parent` already having a real width (the caller must
    -- size its content frame before invoking BuildWidgets -- see
    -- UI/Screens/SettingsScreen.lua's scroll-frame content-width-first step).
    text = function(parent, def)
        local ctrl = CreateFrame("Frame", nil, parent)

        local label = ctrl:CreateFontString(nil, "ARTWORK")
        label:SetFontObject(WildThingsFont13)
        label:SetTextColor(Theme.TEXT_DIM.r, Theme.TEXT_DIM.g, Theme.TEXT_DIM.b, Theme.TEXT_DIM.a)
        label:SetJustifyH("LEFT")
        label:SetWordWrap(true)
        -- Clamp to >= 1: `or 1` doesn't guard a not-yet-resolved parent (GetWidth() returns 0,
        -- and `0 - PAD*2` is negative -- SetWidth throws a hard error on a negative value).
        label:SetWidth(math.max((parent:GetWidth() or 0) - Theme.PAD_WINDOW * 2, 1))
        label:SetPoint("TOPLEFT", ctrl, "TOPLEFT", 0, 0)
        label:SetText(def.label)

        local h = label:GetStringHeight()
        ctrl:SetHeight(h)

        return ctrl, h
    end,
}

-- namespace:BuildWidgets(parent, descriptors)
-- Builds `descriptors` IN ORDER into `parent`, stacking each row below the previous,
-- separated by one Theme.GAP between every transition (SPACE-02: even vertical rhythm
-- for all row-to-row transitions, not just after headers). Returns:
--   1. rows       - array of {ctrl, def} in build order
--   2. totalHeight - accumulated height of everything built (PAD_WINDOW insets are the
--                    caller's responsibility, matching where SettingsScreen's own
--                    root:SetHeight math already lived)
--   3. refresh    - re-reads every descriptor's get() and re-syncs its control
-- An empty `descriptors` array builds nothing and returns cleanly. An unknown/nil
-- descriptor type fails fast rather than silently skipping a row.
function namespace:BuildWidgets(parent, descriptors)
    local rows = {}
    local prevAnchor = parent
    local totalHeight = 0
    local isFirst = true

    for _, def in ipairs(descriptors) do
        local builder = BUILDERS[def.type]
        if not builder then
            error("BuildWidgets: unknown descriptor type '" .. tostring(def.type) .. "'")
        end

        local ctrl, height = builder(parent, def)

        if isFirst then
            -- Flush to the content frame (0,0): the caller's scroll frame already supplies the
            -- PAD_WINDOW margin from the screen edge, so adding another here double-margined
            -- Settings vs. Invite Tools/Roster (v2.3 UAT). Matches this file's own contract:
            -- "PAD_WINDOW insets are the caller's responsibility".
            ctrl:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
            ctrl:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0)
        else
            ctrl:SetPoint("TOPLEFT", prevAnchor, "BOTTOMLEFT", 0, -Theme.GAP)
            ctrl:SetPoint("TOPRIGHT", prevAnchor, "BOTTOMRIGHT", 0, -Theme.GAP)
            totalHeight = totalHeight + Theme.GAP
        end

        if def.disabled and ctrl.Disable and ctrl.Enable then
            if def.disabled() then ctrl:Disable() else ctrl:Enable() end
        end

        totalHeight = totalHeight + height

        rows[#rows + 1] = {ctrl = ctrl, def = def}
        prevAnchor = ctrl
        isFirst = false
    end

    local function refresh()
        for _, row in ipairs(rows) do
            if row.ctrl.Refresh then
                row.ctrl:Refresh()
            end
        end
    end

    return rows, totalHeight, refresh
end
