local _, namespace = ...

local WTUI = namespace.WildThingsUI

local SPACING = 6 -- Spacing between scroll frame and scroll bar
local PADDING_RIGHT = 8
local SCROLLBAR_WIDTH = 8 -- MinimalScrollBar's own track width
local VIEWPORT_INSET = 1 -- per-edge inset of the inner WowScrollBox viewport (see below)

-- Published geometry (WR-03, 38-REVIEW.md). Callers that must size content
-- against this widget's internals used to hand-copy these numbers as file-local
-- literals -- RaidTimelineScreen.lua carried both a SCROLLFRAME_VERTICAL_INSET
-- and a SCROLLBAR_RESERVE derived by reading this file. Nothing linked the two,
-- so changing an inset here silently reintroduced the residual-scroll bug
-- G-38-17 was opened for, in a codebase with no automated UI tests. Both values
-- are now attached to every scroll frame this factory builds (below), so a
-- change here propagates instead of drifting.
local VERTICAL_INSET = VIEWPORT_INSET * 2 -- 1px top + 1px bottom
-- The SHOWN-state horizontal reserve. It is what every caller reads today and stays the
-- constant published on parentFrame.scrollBarReserve for callers that do not opt in. A
-- caller passing opts.reclaimHiddenScrollBarWidth (below) instead sees that published
-- value drop to 0 whenever the managed visibility behavior hides the bar, because the
-- viewport is re-anchored to the widget's own right edge in that state.
local SCROLLBAR_RESERVE = PADDING_RIGHT + SPACING + 2 + SCROLLBAR_WIDTH

-- Pixels covered by one scroll (one wheel tick, one scrollbar step); about two
-- rows at the 26px timeline-roster and 32px summon-list row heights this
-- widget's consumers use.
local DEFAULT_PAN_EXTENT = 64

-- opts is optional and every behavior below is on the nil-opts path, so the five existing
-- single-argument call sites (Bank/Invite/Roster/Settings x2) are untouched.
--   opts.reclaimHiddenScrollBarWidth -- when the managed visibility behavior hides the
--     scrollbar, move the viewport's BOTTOMRIGHT out to this widget's own right edge and
--     publish scrollBarReserve = 0, so a caller sizing content against the reserve can let
--     its rows reach the full inner width. Opt-in because it changes the viewport rect,
--     which every other screen currently relies on being constant.
function namespace:CreateScrollFrame(parent, opts)
    opts = opts or {}

    local parentFrame = CreateFrame("Frame", nil, parent)

    -- Scroll bar
    parentFrame.scrollBar = CreateFrame("EventFrame", nil, parentFrame, "MinimalScrollBar")
    parentFrame.scrollBar:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -PADDING_RIGHT, 0)
    parentFrame.scrollBar:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -PADDING_RIGHT, 0)

    -- Scroll frame
    parentFrame.scrollFrame = CreateFrame("Frame", nil, parentFrame, "WowScrollBox")
    parentFrame.scrollFrame:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", VIEWPORT_INSET, -VIEWPORT_INSET)
    parentFrame.scrollFrame:SetPoint("BOTTOMRIGHT", parentFrame.scrollBar, "BOTTOMLEFT", -SPACING - 2, VIEWPORT_INSET)

    -- Geometry contract for callers (WR-07): the true scrollable viewport is
    -- verticalInset shorter than this widget, and a caller sizing content to
    -- avoid the scrollbar must reserve scrollBarReserve horizontally. Read these
    -- instead of re-deriving the numbers from this file's internals.
    parentFrame.verticalInset = VERTICAL_INSET
    parentFrame.scrollBarReserve = SCROLLBAR_RESERVE

    parentFrame.scrollFrame.wheelPanScalar = 1 -- By default this is 2, making scroll wheel scroll twice as fast as clicking the arrows

    -- No enclosing border: scroll regions render borderless to match the Roster screen's clean
    -- look (v2.3 UAT — the 4-edge border read as an unwanted box around the Settings/Invite content).

    -- Content frame
    -- The width of this frame is always kept the same as that of the scroll frame
    -- Whenever the height of this frame changes (through SetHeight()), remember to call FullUpdate()
    parentFrame.contentFrame = CreateFrame("Frame", nil, parentFrame.scrollFrame)
    parentFrame.contentFrame.scrollable = true

    -- View
    local view = CreateScrollBoxLinearView()

    -- Without an explicit extent, ScrollBoxLinearViewMixin:GetPanExtent() derives one
    -- from its first scrollable child -- here that is the single full-height
    -- contentFrame, saturating the wheel step to the entire scroll range.
    view:SetPanExtent(DEFAULT_PAN_EXTENT)

    -- Init
    ScrollUtil.InitScrollBoxWithScrollBar(parentFrame.scrollFrame, parentFrame.scrollBar, view)

    -- Auto-hide the scrollbar track when the content fits (G-38-17). Without this the
    -- MinimalScrollBar track is rendered unconditionally, so a scroll region whose content
    -- exactly fits its viewport still shows a dead scrollbar. AddManagedScrollBarVisibilityBehavior
    -- is the standard Blizzard idiom (it wires SetHideIfUnscrollable plus the layout
    -- re-evaluation that makes it react to later FullUpdate calls).
    -- Behavior-only and shared by every caller (Roster/Settings/Invite/Bank): a region
    -- whose content genuinely overflows still gets its scrollbar, unchanged.
    --
    -- WR-13: called unguarded, matching InitScrollBoxWithScrollBar above. The former
    -- `elseif SetHideIfUnscrollable` fallback could not execute -- both supported client
    -- builds ship this API alongside the WowScrollBox/MinimalScrollBar templates this
    -- widget already depends on unconditionally, and if it were absent
    -- InitScrollBoxWithScrollBar would have errored two lines earlier.
    ScrollUtil.AddManagedScrollBarVisibilityBehavior(parentFrame.scrollFrame, parentFrame.scrollBar)

    -- Opt-in only (see opts above). This has to live in the widget rather than screen-side:
    -- the viewport's BOTTOMRIGHT is anchored to the scrollBar whether or not the bar is
    -- shown, and the viewport CLIPS its children -- so a caller widening its own content
    -- frame while the bar is hidden would just have the surplus clipped. The viewport
    -- anchor itself is what must move.
    --
    -- Idempotent: re-issuing the same named point ("BOTTOMRIGHT") replaces it, and the
    -- TOPLEFT point set above is never touched, so calling this any number of times in
    -- either state converges on the same geometry.
    if opts.reclaimHiddenScrollBarWidth then
        local function ApplyScrollBarReserve()
            local shown = parentFrame.scrollBar:IsShown()

            if shown then
                parentFrame.scrollFrame:SetPoint(
                    "BOTTOMRIGHT", parentFrame.scrollBar, "BOTTOMLEFT", -SPACING - 2, VIEWPORT_INSET)
                parentFrame.scrollBarReserve = SCROLLBAR_RESERVE
            else
                parentFrame.scrollFrame:SetPoint(
                    "BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -VIEWPORT_INSET, VIEWPORT_INSET)
                parentFrame.scrollBarReserve = 0
            end

            -- The WR-07 geometry contract stays the single source of truth callers read --
            -- it just becomes live instead of constant. This callback is how a caller that
            -- sized content against it learns to re-apply that arithmetic.
            if parentFrame.onScrollBarVisibilityChanged then
                parentFrame.onScrollBarVisibilityChanged(parentFrame, shown)
            end
        end

        -- Exposed as well as hooked: an already-shown (or already-hidden) bar fires no
        -- OnShow/OnHide, so a caller's very first paint needs a way to force one evaluation.
        parentFrame.ApplyScrollBarReserve = ApplyScrollBarReserve
        parentFrame.scrollBar:HookScript("OnShow", ApplyScrollBarReserve)
        parentFrame.scrollBar:HookScript("OnHide", ApplyScrollBarReserve)
    end

    function parentFrame:FullUpdate()
        parentFrame.scrollFrame:FullUpdate()
    end

    return parentFrame
end
