local MAJOR, MINOR = "LibMatic-RosterFrame-1.0", 11
assert(LibStub, MAJOR .. " requires LibStub")
local lib = LibStub:NewLibrary(MAJOR, MINOR)
if not lib then return end

local FitText = LibStub("LibMatic-FitText-1.0", true)

local DEFAULT_ROW_H   = 22
local HEADER_OFFSET   = 70
local FOOTER_PAD      = 6
local HSCROLL_HEIGHT  = 16
local HSCROLL_FOOTER  = 26
local HSCROLL_STEP    = 28
local EMBLEM_SIZE     = 72
local MEDALLION_SIZE  = 42
local PORTRAIT_SIZE   = 18
local SCROLL_INSET_X  = 36
local TITLE_FONT_SIZE = 22
local ROW_FONT_SIZE   = 14

local RING_TEXTURE   = "Interface\\COMMON\\BlueMenuRing"
local RING_PRESETS   = {
    silver = { 0.85, 0.88, 0.95 },
    gold   = { 1.00, 0.85, 0.30 },
    bronze = { 0.85, 0.55, 0.25 },
}
local PORTRAIT_MASK  = "Interface\\CharacterFrame\\TempPortraitAlphaMask"
local HR_TEXTURE     = "Interface\\COMMON\\UI-TooltipDivider"
local GRIP_TEXTURE   = "Interface\\Tooltips\\UI-Tooltip-Border"
local ZEBRA_TEXTURE  = "Interface\\Buttons\\WHITE8X8"
local CLASS_ICONS    = "Interface\\TARGETINGFRAME\\UI-CLASSES-CIRCLES"

local function classColorHex(class)
    if not class then return "ffffff" end
    local t = _G.RAID_CLASS_COLORS and _G.RAID_CLASS_COLORS[class]
    if not t then return "ffffff" end
    return string.format("%02x%02x%02x",
        math.floor((t.r or 1) * 255),
        math.floor((t.g or 1) * 255),
        math.floor((t.b or 1) * 255))
end

local function applyCircularMask(tex)
    if not tex then return end
    if type(tex.AddMaskTexture) == "function" and tex.GetParent then
        local parent = tex:GetParent()
        if parent and type(parent) == "table" and type(parent.CreateMaskTexture) == "function" then
            local ok, mask = pcall(function()
                local m = parent:CreateMaskTexture()
                m:SetTexture(PORTRAIT_MASK, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
                m:SetAllPoints(tex)
                return m
            end)
            if ok and mask then
                pcall(tex.AddMaskTexture, tex, mask)
                return
            end
        end
    end
    if type(tex.SetMask) == "function" then
        pcall(tex.SetMask, tex, PORTRAIT_MASK)
    end
end

local function zebraColor(index, zebraOn)
    if zebraOn and index % 2 == 0 then
        return 0.20, 0.20, 0.24, 0.55
    end
    return 0.12, 0.12, 0.14, 0.55
end

local function applyRowBg(row, index, zebraOn)
    local r, g, b, a = zebraColor(index, zebraOn)
    row.bgColor = { r, g, b, a }
    row.bg:SetTexture(ZEBRA_TEXTURE)
    row.bg:SetVertexColor(r, g, b, a)
end

local function paintPortrait(tex, unit, class)
    if not tex then return false end
    if unit and type(UnitExists) == "function" and UnitExists(unit) and type(SetPortraitTexture) == "function" then
        local ok = pcall(SetPortraitTexture, tex, unit)
        if ok then
            tex:SetTexCoord(0, 1, 0, 1)
            tex:Show()
            return true
        end
    end
    if class and _G.CLASS_ICON_TCOORDS and _G.CLASS_ICON_TCOORDS[class] then
        tex:SetTexture(CLASS_ICONS)
        tex:SetTexCoord(unpack(_G.CLASS_ICON_TCOORDS[class]))
        tex:Show()
        return true
    end
    tex:Hide()
    return false
end

local refresh, scheduleRefresh, onSizeChanged, applyHScroll, updateHBarRange, ensureRow, saveLayout

local function hScrollEnabled(win)
    return win._hScroll == true
end

function updateHBarRange(win)
    local hbar = win._hbar
    if not hbar then return end
    if not hScrollEnabled(win) then
        hbar:SetMinMaxValues(0, 0)
        hbar:SetValue(0)
        return
    end
    local contentW  = win._content and win._content:GetWidth() or 0
    local viewportW = win._scroll and win._scroll:GetWidth() or 0
    local range = math.max(0, math.floor(contentW - viewportW + 0.5))
    hbar:SetMinMaxValues(0, range)
    if hbar:GetValue() > range then hbar:SetValue(range) end
    local val = hbar:GetValue()
    if win._hbarLeft then
        if val > 0 then win._hbarLeft:Enable() else win._hbarLeft:Disable() end
    end
    if win._hbarRight then
        if val < range then win._hbarRight:Enable() else win._hbarRight:Disable() end
    end
end

function onSizeChanged(win, w, h)
    if win._content then
        local viewport = math.max(1, w - SCROLL_INSET_X)
        if hScrollEnabled(win) then
            win._content:SetWidth(math.max(viewport, win._minContentW))
        else
            win._content:SetWidth(viewport)
        end
    end
    if win._title and win._title.ScaleToFit then
        win._title.fitMaxWidth = w - 70
        win._title:ScaleToFit()
    end
    updateHBarRange(win)
    scheduleRefresh(win)
end

function applyHScroll(win)
    if not win._scroll then return end
    local on = hScrollEnabled(win)
    win._scroll:SetPoint("BOTTOMRIGHT", -28, on and HSCROLL_FOOTER or FOOTER_PAD)
    local function vis(widget)
        if widget then
            if on then widget:Show() else widget:Hide() end
        end
    end
    vis(win._hbar)
    vis(win._hbarLeft)
    vis(win._hbarRight)
    if not on and win._scroll.SetHorizontalScroll then
        win._scroll:SetHorizontalScroll(0)
    end
    local f = win._frame
    onSizeChanged(win, f and f:GetWidth() or 0, f and f:GetHeight() or 0)
end

function saveLayout(win)
    local f = win._frame
    if not f then return end
    local p, _, _, x, y = f:GetPoint()
    if not p then return end
    local layout = win._config.layout
    if type(layout) ~= "table" then return end
    layout.point = p
    layout.x = x
    layout.y = y
    layout.w = f:GetWidth()
    layout.h = f:GetHeight()
end

function ensureRow(win, index)
    local rows = win._rows
    if rows[index] then return rows[index] end
    local r = CreateFrame("Button", nil, win._content)
    r:SetHeight(win._rowHeight)
    r:SetPoint("LEFT", win._content, "LEFT", 0, 0)
    r:SetPoint("RIGHT", win._content, "RIGHT", 0, 0)
    if index == 1 then
        r:SetPoint("TOPLEFT", win._content, "TOPLEFT", 0, 0)
    else
        r:SetPoint("TOPLEFT", rows[index - 1], "BOTTOMLEFT", 0, -1)
    end

    r.bg = r:CreateTexture(nil, "BACKGROUND")
    r.bg:SetAllPoints()
    applyRowBg(r, index, win._zebra)

    r.portrait = r:CreateTexture(nil, "ARTWORK")
    r.portrait:SetSize(PORTRAIT_SIZE, PORTRAIT_SIZE)
    r.portrait:SetPoint("LEFT", r, "LEFT", 4, 0)
    applyCircularMask(r.portrait)

    r.name = r:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    r.name:SetPoint("LEFT", r.portrait, "RIGHT", 6, 0)
    r.name:SetPoint("RIGHT", r, "RIGHT", -6, 0)
    r.name:SetJustifyH("LEFT")
    r.name:SetWordWrap(false)
    if win._config.applyFont then win._config.applyFont(r.name, ROW_FONT_SIZE) end

    r:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    r:SetScript("OnClick", function(self, button)
        if win._config.onRowClick and self._item then
            win._config.onRowClick(self, self._item, button)
        end
    end)
    r:SetScript("OnEnter", function(self)
        local c = self.bgColor
        if c then self.bg:SetVertexColor(c[1] + 0.10, c[2] + 0.10, c[3] + 0.10, math.min(1, c[4] + 0.10)) end
        if win._config.onRowEnter and self._item then
            win._config.onRowEnter(self, self._item)
        end
    end)
    r:SetScript("OnLeave", function(self)
        local c = self.bgColor
        if c then self.bg:SetVertexColor(c[1], c[2], c[3], c[4]) end
        if win._config.onRowLeave and self._item then
            win._config.onRowLeave(self, self._item)
        end
    end)

    if win._config.onRowCreate then win._config.onRowCreate(r) end

    rows[index] = r
    return r
end

function refresh(win)
    local f = win._frame
    if not f or not f:IsShown() then return end
    if win._skipRows then
        if win._config.onRefresh then win._config.onRefresh(win) end
        return
    end
    local items = (win._config.getItems and win._config.getItems()) or {}
    if win._config.sort then table.sort(items, win._config.sort) end

    for i = #items + 1, #win._rows do
        if win._rows[i] then win._rows[i]:Hide() end
    end

    for i, item in ipairs(items) do
        local r = ensureRow(win, i)
        r:Show()
        applyRowBg(r, i, win._zebra)
        r._item = item
        local hex = classColorHex(item.classToken)
        r.name:SetText(("|cff%s%s|r"):format(hex, tostring(item.name or "")))
        paintPortrait(r.portrait, item.unit, item.classToken)
        if win._config.onRowUpdate then win._config.onRowUpdate(r, item, i) end
    end

    win._content:SetHeight(math.max(1, #items * (win._rowHeight + 1)))
end

function scheduleRefresh(win)
    if win._refreshPending then return end
    win._refreshPending = true
    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(0.15, function()
            win._refreshPending = false
            refresh(win)
        end)
    else
        win._refreshPending = false
        refresh(win)
    end
end

local function buildEmblem(parent, medallionTexture, ringOpts)
    local emblem = CreateFrame("Frame", nil, parent)
    emblem:SetSize(EMBLEM_SIZE, EMBLEM_SIZE)
    local emblemPoint = ringOpts and ringOpts.point
    if emblemPoint then
        emblem:SetPoint(emblemPoint.point or "CENTER", parent, emblemPoint.relPoint or emblemPoint.point or "CENTER", emblemPoint.x or 0, emblemPoint.y or 0)
    else
        emblem:SetPoint("CENTER", parent, "TOPLEFT", 20, -26)
    end
    emblem:SetFrameLevel((parent:GetFrameLevel() or 0) + 10)

    emblem.icon = emblem:CreateTexture(nil, "BACKGROUND")

    if ringOpts and ringOpts.raw then
        emblem.icon:SetSize(EMBLEM_SIZE, EMBLEM_SIZE)
        emblem.icon:SetPoint("CENTER", 0, 0)
        if medallionTexture then emblem.icon:SetTexture(medallionTexture) end
        return emblem
    end

    if ringOpts and ringOpts.native then
        local nativeRingSize = ringOpts.ringSize or EMBLEM_SIZE
        local nativeIconSize = ringOpts.iconSize or MEDALLION_SIZE
        if ringOpts.backing then
            emblem.backing = emblem:CreateTexture(nil, "OVERLAY")
            emblem.backing:SetDrawLayer("OVERLAY", 2)
            emblem.backing:SetSize(ringOpts.backingSize or nativeRingSize, ringOpts.backingSize or nativeRingSize)
            emblem.backing:SetPoint("CENTER", ringOpts.backingOffsetX or 0, ringOpts.backingOffsetY or 0)
            emblem.backing:SetTexture("Interface\\PVPFrame\\PVP-Separation-Circle-Cooldown-overlay")
        end
        emblem.icon:SetDrawLayer("OVERLAY", 3)
        emblem.icon:SetSize(nativeIconSize, ringOpts.iconHeight or nativeIconSize)
        emblem.icon:SetPoint("CENTER", ringOpts.iconOffsetX or 0, ringOpts.iconOffsetY or 0)
        if medallionTexture then emblem.icon:SetTexture(medallionTexture) end
        emblem.ring = emblem:CreateTexture(nil, "OVERLAY")
        emblem.ring:SetDrawLayer("OVERLAY", 4)
        emblem.ring:SetSize(nativeRingSize, nativeRingSize)
        emblem.ring:SetPoint("CENTER", 0, 0)
        emblem.ring:SetTexture((ringOpts and ringOpts.texture) or RING_TEXTURE)
        if ringOpts.style or ringOpts.color then
            if emblem.ring.SetDesaturated then emblem.ring:SetDesaturated(true) end
            local nativeRingColor = ringOpts.color or RING_PRESETS[ringOpts.style] or RING_PRESETS.silver
            emblem.ring:SetVertexColor(nativeRingColor[1], nativeRingColor[2], nativeRingColor[3])
        end
        return emblem
    end

    emblem.icon:SetSize(MEDALLION_SIZE, MEDALLION_SIZE)
    emblem.icon:SetPoint("CENTER", -7, 8)
    if medallionTexture then emblem.icon:SetTexture(medallionTexture) end
    applyCircularMask(emblem.icon)

    local ringTexture = (ringOpts and ringOpts.texture) or RING_TEXTURE
    local ringColor = ringOpts and ringOpts.color
    if not ringColor then
        local style = ringOpts and ringOpts.style or "silver"
        ringColor = RING_PRESETS[style] or RING_PRESETS.silver
    end

    emblem.ring = emblem:CreateTexture(nil, "OVERLAY")
    emblem.ring:SetSize(EMBLEM_SIZE, EMBLEM_SIZE)
    emblem.ring:SetPoint("CENTER", 0, 0)
    emblem.ring:SetTexture(ringTexture)
    emblem.ring:SetTexCoord(0, 1, 0, 1)
    if emblem.ring.SetDesaturated then emblem.ring:SetDesaturated(true) end
    emblem.ring:SetVertexColor(ringColor[1], ringColor[2], ringColor[3])
    return emblem
end

local function buildHR(parent, anchorBelow)
    local hr = parent:CreateTexture(nil, "ARTWORK")
    hr:SetPoint("TOP", anchorBelow, "BOTTOM", 0, -6)
    hr:SetPoint("LEFT", parent, "LEFT", 18, 0)
    hr:SetPoint("RIGHT", parent, "RIGHT", -8, 0)
    hr:SetHeight(4)
    hr:SetTexture(HR_TEXTURE)
    if hr:GetTexture() then
        hr:SetVertexColor(0.85, 0.86, 0.92)
    else
        hr:SetColorTexture(0.78, 0.80, 0.86, 0.9)
    end
    return hr
end

local windowMethods = {}

function windowMethods:Show()    if self._frame then self._frame:Show() end end
function windowMethods:Hide()    if self._frame then self._frame:Hide() end end
function windowMethods:IsShown() return self._frame and self._frame:IsShown() or false end
function windowMethods:Toggle()
    if not self._frame then return end
    if self._frame:IsShown() then self._frame:Hide() else self._frame:Show() end
end
function windowMethods:GetFrame() return self._frame end
function windowMethods:GetContent() return self._content end
function windowMethods:GetScroll() return self._scroll end
function windowMethods:SetLayoutTable(layout)
    self._config.layout = layout
    local f = self._frame
    if not f then return end
    if type(layout) == "table" and layout.point then
        f:ClearAllPoints()
        f:SetPoint(layout.point, UIParent, layout.point, layout.x or 0, layout.y or 0)
        if layout.w and layout.h then f:SetSize(layout.w, layout.h) end
    elseif type(self._config.defaultPoint) == "table" then
        local dp = self._config.defaultPoint
        f:ClearAllPoints()
        f:SetPoint(dp.point or "CENTER", UIParent, dp.point or "CENTER", dp.x or 0, dp.y or 0)
    end
end
function windowMethods:RefitTitle()
    if self._title and self._title.ScaleToFit then self._title:ScaleToFit() end
end
function windowMethods:GetTitleFontString() return self._title end
function windowMethods:GetValueFontString() return self._value end
function windowMethods:SetValue(text)
    if self._value then
        if self._value.SetTextToFit then self._value:SetTextToFit(text) else self._value:SetText(text) end
    end
end
function windowMethods:Refresh() refresh(self) end
function windowMethods:ScheduleRefresh() scheduleRefresh(self) end
function windowMethods:IsHScrollEnabled() return hScrollEnabled(self) end
function windowMethods:SetHScroll(enabled)
    self._hScroll = enabled and true or false
    applyHScroll(self)
    return self._hScroll
end
function windowMethods:SetZebra(enabled)
    self._zebra = enabled and true or false
    refresh(self)
    return self._zebra
end

function lib:NewReadout(config)
    config = config or {}
    local win = setmetatable({}, { __index = windowMethods })
    win._config = config

    local width  = config.width  or 200
    local height = config.height or 96

    local f = CreateFrame("Frame", config.name, UIParent, "BackdropTemplate")
    win._frame = f
    f:SetSize(width, height)
    f:SetFrameStrata(config.strata or "MEDIUM")
    f:SetMovable(true)
    f:EnableMouse(true)
    f:SetClampedToScreen(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        saveLayout(win)
    end)

    if config.backdrop ~= false then
        pcall(function()
            f:SetBackdrop({
                bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
                tile = true, tileSize = 16, edgeSize = 16,
                insets = { left = 4, right = 4, top = 4, bottom = 4 },
            })
        end)
    end

    win._emblem = buildEmblem(f, config.emblem, {
        style       = config.ringStyle,
        color       = config.ringColor,
        texture     = config.ringTexture,
        raw         = config.emblemRaw,
        native      = config.ringNative,
        ringSize    = config.ringSize,
        iconSize    = config.iconSize,
        iconHeight  = config.iconHeight,
        backing     = config.ringBacking,
        backingSize = config.ringBackingSize,
        backingOffsetX = config.ringBackingOffsetX,
        backingOffsetY = config.ringBackingOffsetY,
        point       = config.emblemPoint,
        iconOffsetX = config.iconOffsetX,
        iconOffsetY = config.iconOffsetY,
    })

    if not config.hideTitle then
        local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        title:SetPoint("TOP", f, "TOP", 1, -10)
        if config.applyFont then config.applyFont(title, config.titleFontSize or TITLE_FONT_SIZE) end
        local colorHex = config.titleColorHex or "|cffe8e8e8"
        local titleText = colorHex .. tostring(config.title or "") .. "|r"
        if FitText and FitText.MixinFontString then
            FitText:MixinFontString(title, width - 16)
            title:SetTextToFit(titleText)
        else
            title:SetText(titleText)
        end
        win._title = title
    end

    local value = f:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    local valuePoint = config.valuePoint
    if valuePoint then
        value:SetPoint(valuePoint.point or "CENTER", f, valuePoint.relPoint or valuePoint.point or "CENTER", valuePoint.x or 0, valuePoint.y or 0)
    else
        value:SetPoint("CENTER", f, "CENTER", 0, -8)
    end
    value:SetJustifyH("CENTER")
    value:SetWordWrap(false)
    if config.applyFont then config.applyFont(value, config.valueFontSize or 26) end
    if FitText and FitText.MixinFontString then
        FitText:MixinFontString(value, width - 16)
    end
    win._value = value

    local layout = config.layout
    if type(layout) == "table" and layout.point then
        f:ClearAllPoints()
        f:SetPoint(layout.point, UIParent, layout.point, layout.x or 0, layout.y or 0)
    elseif type(config.defaultPoint) == "table" then
        local dp = config.defaultPoint
        f:SetPoint(dp.point or "CENTER", UIParent, dp.point or "CENTER", dp.x or 0, dp.y or 0)
    else
        f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    end

    f:Hide()
    return win
end

function lib:New(config)
    config = config or {}
    local win = setmetatable({}, { __index = windowMethods })
    win._config = config
    win._rows = {}
    win._refreshPending = false
    win._rowHeight = config.rowHeight or DEFAULT_ROW_H
    win._zebra = config.zebra ~= false
    win._hScroll = config.hScroll and true or false
    win._skipRows = config.skipRows and true or false

    local width  = config.width  or 295
    local height = config.height or 420
    win._minContentW = width - SCROLL_INSET_X

    local f = CreateFrame("Frame", config.name, UIParent, "BackdropTemplate")
    win._frame = f
    f:SetSize(width, height)
    f:SetFrameStrata(config.strata or "MEDIUM")
    f:SetMovable(true)
    f:EnableMouse(true)
    f:SetClampedToScreen(true)
    f:SetResizable(true)
    if f.SetResizeBounds then
        f:SetResizeBounds(config.minWidth or 220, config.minHeight or 180,
                          config.maxWidth or 700, config.maxHeight or 900)
    else
        if f.SetMinResize then f:SetMinResize(config.minWidth or 220, config.minHeight or 180) end
        if f.SetMaxResize then f:SetMaxResize(config.maxWidth or 700, config.maxHeight or 900) end
    end
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        saveLayout(win)
    end)
    f:SetScript("OnSizeChanged", function(self, w, h) onSizeChanged(win, w, h) end)

    pcall(function()
        f:SetBackdrop({
            bgFile   = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 },
        })
    end)

    win._emblem = buildEmblem(f, config.emblem, {
        style   = config.ringStyle,
        color   = config.ringColor,
        texture = config.ringTexture,
    })

    local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", f, "TOP", 1, -14)
    if config.applyFont then config.applyFont(title, TITLE_FONT_SIZE) end
    local colorHex = config.titleColorHex or "|cffe8e8e8"
    local titleText = colorHex .. tostring(config.title or "") .. "|r"
    if FitText and FitText.MixinFontString then
        FitText:MixinFontString(title, width - 70)
        title:SetTextToFit(titleText)
    else
        title:SetText(titleText)
    end
    win._title = title

    win._hr = buildHR(f, title)

    local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", 0, 0)
    close:SetScript("OnClick", function() f:Hide() end)

    local grip = CreateFrame("Button", nil, f)
    grip:SetSize(25, 25)
    grip:SetPoint("BOTTOMRIGHT", 0, 0)
    grip:SetFrameLevel((f:GetFrameLevel() or 0) + 8)
    grip:EnableMouse(true)
    grip:SetScript("OnMouseDown", function() f:StartSizing("BOTTOMRIGHT") end)
    grip:SetScript("OnMouseUp", function()
        f:StopMovingOrSizing()
        saveLayout(win)
    end)
    local function gripLine(size, layer)
        local tex = grip:CreateTexture(nil, layer)
        tex:SetSize(size, size)
        tex:SetPoint("BOTTOMRIGHT", -8, 8)
        tex:SetTexture(GRIP_TEXTURE)
        local inset = 0.1 * size / 17
        tex:SetTexCoord(0.05 - inset, 0.5, 0.05, 0.5 + inset, 0.05, 0.5 - inset, 0.5 + inset, 0.5)
        return tex
    end
    grip.lineLong  = gripLine(14, "OVERLAY")
    grip.lineShort = gripLine(8, "OVERLAY")
    grip:SetScript("OnEnter", function()
        grip.lineLong:SetVertexColor(1, 0.82, 0)
        grip.lineShort:SetVertexColor(1, 0.82, 0)
    end)
    grip:SetScript("OnLeave", function()
        grip.lineLong:SetVertexColor(1, 1, 1)
        grip.lineShort:SetVertexColor(1, 1, 1)
    end)
    win._grip = grip

    local scroll = CreateFrame("ScrollFrame",
        config.name and (config.name .. "Scroll") or nil, f, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 8, -HEADER_OFFSET)
    scroll:SetPoint("BOTTOMRIGHT", -28, FOOTER_PAD)
    win._scroll = scroll
    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(width - SCROLL_INSET_X, 1)
    scroll:SetScrollChild(content)
    win._content = content

    local scrollBar = scroll.ScrollBar or (config.name and _G[config.name .. "ScrollScrollBar"])
    if type(scrollBar) == "table" then
        for i = 1, scrollBar:GetNumPoints() do
            local point, rel, relPoint, x, y = scrollBar:GetPoint(i)
            if point and point:find("BOTTOM") then
                scrollBar:SetPoint(point, rel, relPoint, x or 0, (y or 0) + 20)
            end
        end
    end

    local hbarLeft = CreateFrame("Button", nil, f)
    hbarLeft:SetSize(16, 16)
    hbarLeft:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 10, 7)
    hbarLeft:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up")
    hbarLeft:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down")
    hbarLeft:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Disabled")
    hbarLeft:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight")

    local hbarRight = CreateFrame("Button", nil, f)
    hbarRight:SetSize(16, 16)
    hbarRight:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -30, 7)
    hbarRight:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up")
    hbarRight:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down")
    hbarRight:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Disabled")
    hbarRight:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight")

    for _, getter in ipairs({ "GetNormalTexture", "GetPushedTexture", "GetDisabledTexture", "GetHighlightTexture" }) do
        local lt = hbarLeft[getter](hbarLeft)
        if lt then lt:SetTexCoord(1, 0, 0, 0, 1, 1, 0, 1) end
        local rt = hbarRight[getter](hbarRight)
        if rt then rt:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0) end
    end

    local hbar = CreateFrame("Slider", config.name and (config.name .. "HBar") or nil, f)
    hbar:SetOrientation("HORIZONTAL")
    hbar:SetHeight(HSCROLL_HEIGHT)
    hbar:SetPoint("LEFT", hbarLeft, "RIGHT", 2, 0)
    hbar:SetPoint("RIGHT", hbarRight, "LEFT", -2, 0)
    hbar:SetMinMaxValues(0, 0)
    hbar:SetValueStep(1)
    hbar:SetValue(0)
    local hbarTrack = hbar:CreateTexture(nil, "BACKGROUND")
    hbarTrack:SetTexture(ZEBRA_TEXTURE)
    hbarTrack:SetVertexColor(0.08, 0.08, 0.10, 0.9)
    hbarTrack:SetPoint("LEFT", hbar, "LEFT", 0, 0)
    hbarTrack:SetPoint("RIGHT", hbar, "RIGHT", 0, 0)
    hbarTrack:SetHeight(6)
    hbar:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
    local hbarThumb = hbar:GetThumbTexture()
    if hbarThumb then hbarThumb:SetSize(28, 22) end
    hbar:SetScript("OnValueChanged", function(self, value)
        if scroll.SetHorizontalScroll then scroll:SetHorizontalScroll(value) end
        local lo, hi = self:GetMinMaxValues()
        if value > lo then hbarLeft:Enable() else hbarLeft:Disable() end
        if value < hi then hbarRight:Enable() else hbarRight:Disable() end
    end)
    hbarLeft:SetScript("OnClick", function() hbar:SetValue(hbar:GetValue() - HSCROLL_STEP) end)
    hbarRight:SetScript("OnClick", function() hbar:SetValue(hbar:GetValue() + HSCROLL_STEP) end)
    hbar:Hide()
    hbarLeft:Hide()
    hbarRight:Hide()
    win._hbar = hbar
    win._hbarLeft = hbarLeft
    win._hbarRight = hbarRight

    local layout = config.layout
    if type(layout) == "table" and layout.point then
        f:ClearAllPoints()
        f:SetPoint(layout.point, UIParent, layout.point, layout.x or 0, layout.y or 0)
    elseif type(config.defaultPoint) == "table" then
        local dp = config.defaultPoint
        f:SetPoint(dp.point or "CENTER", UIParent, dp.point or "CENTER", dp.x or 0, dp.y or 0)
    else
        f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    end
    if type(layout) == "table" and layout.w and layout.h then
        f:SetSize(layout.w, layout.h)
    end

    f:Hide()
    f:SetScript("OnShow", function() scheduleRefresh(win) end)

    applyHScroll(win)
    return win
end
