-- HDG_HT_Patterns.lua
-- Reusable internal layout patterns for dashboard widgets. Each pattern is a
-- factory that returns a frame plus :Update(data), :Resize(w, h), :GetMinSize().
-- Widgets compose a pattern + provide data; widgets contain no anchor-chain code.

local _, privateDB = ...
privateDB.HDG_HT = privateDB.HDG_HT or {}
local Patterns = {}
privateDB.HDG_HT.Patterns = Patterns

local SegmentedBar = privateDB.HDG_HT.SegmentedBar

-- Theme helpers: schemes provide hue (r/g/b); callers pick alpha. ApplyFont
-- honors user font choice + scheme size + font scale slider.
local function S()
    local HDG = _G.HousingDecorGuide
    return (HDG and HDG.Theme and HDG.Theme.GetCurrentScheme()) or {
        text     = { r = 1, g = 1, b = 1 },
        text_dim = { r = 0.7, g = 0.7, b = 0.7 },
        accent   = { r = 1, g = 0.83, b = 0.30 },
        panel    = { r = 0.06, g = 0.06, b = 0.08 },
        border   = { r = 0.30, g = 0.35, b = 0.40 },
    }
end
local function F(fs, scheme, ftype, sizeOverride)
    -- Cockpit-local typography: smaller fixed sizes, no global font-scale.
    -- Falls back to the canonical addon ApplyFont if the sub-theme isn't
    -- loaded yet (defensive — TOC ordering guarantees it is).
    local Theme = privateDB.HDG_HT and privateDB.HDG_HT.Theme
    if Theme and Theme.ApplyFont then
        Theme.ApplyFont(fs, scheme, ftype, sizeOverride)
        return
    end
    local HDG = _G.HousingDecorGuide
    if HDG and HDG.Theme and HDG.Theme.ApplyFont then
        HDG.Theme.ApplyFont(fs, scheme, ftype, sizeOverride)
    end
end

-- ============================================================================
-- GaugeBar — title + filled progress + sub-lines
-- ============================================================================
-- opts: {
--   barSegments    = 10,
--   barStyle       = "solid"|"blizzPip",
--   segmentHeight  = 12,
--   gap            = 2,
--   subLineCount   = 2,         -- max sub-lines below bar
--   labelFont      = "GameFontNormalSmall",
--   valueFont      = "GameFontHighlightSmall",
-- }
-- :Update(data) where data = {
--   value          = number,    -- current value (e.g. 1247)
--   max            = number,    -- max value (e.g. 2500)
--   overlayText    = string,    -- e.g. "1,247 / 2,500"
--   tier           = "healthy"|"warn"|"full"|nil,
--   subLines       = { string, string, ... },
-- }
function Patterns.GaugeBar(parent, dims, opts)
    opts = opts or {}
    local self = {}
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    -- Bar
    self.bar = SegmentedBar.Create(self.frame, {
        segments      = opts.barSegments  or dims.barSegments  or 10,
        segmentHeight = opts.segmentHeight or dims.fillbarHeight or 12,
        gap           = opts.gap          or 2,
        width         = 100,    -- placeholder; resized in :Resize
        style         = opts.barStyle or dims.barStyle or "solid",
        gradient      = opts.barGradient or dims.barGradient or false,
    })
    self.bar:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 0, -2)

    -- Overlay label (right of bar; numeric "1,247 / 2,500")
    self.overlay = self.frame:CreateFontString(nil, "OVERLAY", opts.valueFont or "GameFontHighlightSmall")
    self.overlay:SetPoint("LEFT", self.bar, "RIGHT", 6, 0)
    self.overlay:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
    F(self.overlay, scheme, "small")

    -- Sub-line text strings (created lazily up to subLineCount)
    self.subLines = {}
    local maxSubs = opts.subLineCount or dims.subLineCount or 2
    for i = 1, maxSubs do
        local fs = self.frame:CreateFontString(nil, "OVERLAY", opts.labelFont or "GameFontNormalSmall")
        fs:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 2, -((opts.segmentHeight or 12) + 8 + (i - 1) * 14))
        fs:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
        F(fs, scheme, "small")
        fs:Hide()
        self.subLines[i] = fs
    end

    function self:Update(data)
        if not data then self.frame:Hide(); return end
        self.frame:Show()
        local pct = (data.max and data.max > 0) and (data.value / data.max) or 0
        self.bar:Update(pct)
        self.overlay:SetText(data.overlayText or "")
        -- tier color shift on the bar's segments (only "solid" style)
        if data.tier and self.bar._style == "solid" then
            local sch = S()
            local c
            if     data.tier == "warn" then c = sch.warning or sch.accent
            elseif data.tier == "full" then c = sch.error   or sch.accent
            else                            c = sch.success or sch.accent end
            for _, seg in ipairs(self.bar.segments) do
                seg._litColor = { c.r, c.g, c.b, 1 }
            end
            self.bar:Update(pct)  -- re-apply with new colors
        end
        -- Sub-lines: show present, hide rest
        for i, fs in ipairs(self.subLines) do
            local txt = data.subLines and data.subLines[i]
            if txt then fs:SetText(txt); fs:Show() else fs:Hide() end
        end
    end

    function self:Resize(w, _)
        -- Bar takes ~55% of available width to leave room for overlay.
        -- Delegate segment-width math to SegmentedBar:Resize so it can
        -- enforce square segments for "circles" style.
        local targetWidth = math.max(60, math.floor((w or 200) * 0.55))
        if self.bar.Resize then
            self.bar:Resize(targetWidth)
        end
    end

    function self:GetMinSize() return 120, 40 end

    return self
end

-- ============================================================================
-- BigNumber — single hero metric (center)
-- ============================================================================
-- opts: { numberFont, labelFont, subFont, color = {r,g,b} }
-- :Update(data) where data = {
--   number    = string|number,
--   label     = string,        -- below the number
--   sub       = string|nil,    -- subtle line below label (optional)
--   color     = {r,g,b}|nil,   -- overrides default number color
-- }
function Patterns.BigNumber(parent, _, opts)
    opts = opts or {}
    local self = {}
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    self.number = self.frame:CreateFontString(nil, "OVERLAY", opts.numberFont or "GameFontNormalHuge")
    self.number:SetPoint("CENTER", self.frame, "CENTER", 0, 12)
    F(self.number, scheme, "header", 22)

    self.label = self.frame:CreateFontString(nil, "OVERLAY", opts.labelFont or "GameFontHighlight")
    self.label:SetPoint("TOP", self.number, "BOTTOM", 0, -2)
    self.label:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
    F(self.label, scheme, "body")

    self.sub = self.frame:CreateFontString(nil, "OVERLAY", opts.subFont or "GameFontHighlightSmall")
    self.sub:SetPoint("TOP", self.label, "BOTTOM", 0, -4)
    self.sub:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
    F(self.sub, scheme, "small")

    function self:Update(data)
        if not data then self.frame:Hide(); return end
        self.frame:Show()
        self.number:SetText(tostring(data.number or "-"))
        if data.color then self.number:SetTextColor(data.color[1], data.color[2], data.color[3])
        else self.number:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b) end
        self.label:SetText(data.label or "")
        if data.sub then self.sub:SetText(data.sub); self.sub:Show()
        else self.sub:Hide() end
    end

    function self:Resize(_, _)
        -- Center repositions automatically; nothing to compute.
    end

    function self:GetMinSize() return 80, 50 end

    return self
end

-- ============================================================================
-- IconRowList — N rows of [icon][lineA \n lineB][optional right chip]
-- ============================================================================
-- opts: { rowHeight = 26, iconSize = 22, chipFont, lineAFont, lineBFont, maxRowsCap }
-- :Update(data) where data = { rows = {{icon, lineA, lineB, chip?}, ...}, headerChip? }
-- Visible row count = min(maxRowsCap, floor(availableHeight / rowHeight)).
-- More rows than visible → only top-N render; rest stored.

function Patterns.RowsForHeight(height, rowHeight)
    if height <= 0 or rowHeight <= 0 then return 0 end
    return math.floor(height / rowHeight)
end

function Patterns.IconRowList(parent, dims, opts)
    opts = opts or {}
    local rowHeight   = opts.rowHeight or dims.rowHeight or 26
    local iconSize    = opts.iconSize  or dims.iconSize  or 22
    local maxRowsCap  = opts.maxRowsCap or 99
    local lineAFont   = opts.lineAFont or "GameFontHighlightSmall"
    local lineBFont   = opts.lineBFont or "GameFontDisableSmall"
    local chipFont    = opts.chipFont  or "GameFontHighlightSmall"

    local self = { rowFrames = {}, latestData = nil }
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    -- Header chip slot (top-right of the body region)
    self.headerChip = self.frame:CreateFontString(nil, "OVERLAY", chipFont)
    self.headerChip:SetPoint("TOPRIGHT", self.frame, "TOPRIGHT", 0, 0)
    self.headerChip:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b)
    F(self.headerChip, scheme, "small")

    local function ensureRow(i)
        if self.rowFrames[i] then return self.rowFrames[i] end
        local row = CreateFrame("Button", nil, self.frame)
        row:SetSize(100, rowHeight)
        row:SetPoint("TOPLEFT",  self.frame, "TOPLEFT",  0, -((i - 1) * rowHeight))
        row:SetPoint("TOPRIGHT", self.frame, "TOPRIGHT", 0, -((i - 1) * rowHeight))
        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(iconSize, iconSize)
        row.icon:SetPoint("LEFT", row, "LEFT", 0, 0)
        row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        row.chip = row:CreateFontString(nil, "OVERLAY", chipFont)
        -- Y offset so chip baseline aligns with lineB (the second/bottom
        -- text row), not vertically centred against the whole row.
        row.chip:SetPoint("RIGHT", row, "RIGHT", 0, -6)
        row.chip:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b)
        F(row.chip, scheme, "small")
        row.lineA = row:CreateFontString(nil, "OVERLAY", lineAFont)
        row.lineA:SetPoint("TOPLEFT", row.icon, "TOPRIGHT", 6, -2)
        row.lineA:SetPoint("RIGHT", row.chip, "LEFT", -10, 0)
        row.lineA:SetJustifyH("LEFT")
        row.lineA:SetWordWrap(false)
        row.lineA:SetNonSpaceWrap(false)
        row.lineA:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
        F(row.lineA, scheme, "small")
        row.lineB = row:CreateFontString(nil, "OVERLAY", lineBFont)
        row.lineB:SetPoint("TOPLEFT", row.lineA, "BOTTOMLEFT", 0, -2)
        row.lineB:SetPoint("RIGHT", row.chip, "LEFT", -10, 0)
        row.lineB:SetJustifyH("LEFT")
        row.lineB:SetWordWrap(false)
        row.lineB:SetNonSpaceWrap(false)
        row.lineB:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
        F(row.lineB, scheme, "small")
        row:Hide()
        self.rowFrames[i] = row
        return row
    end

    function self:Update(data)
        self.latestData = data
        if not data or not data.rows then
            for _, r in ipairs(self.rowFrames) do r:Hide() end
            self.headerChip:SetText("")
            return
        end
        self.headerChip:SetText(data.headerChip or "")
        local n = math.min(#data.rows, self._visible or maxRowsCap)
        for i = 1, n do
            local d = data.rows[i]
            local row = ensureRow(i)
            -- icon: false → hide column, lineA shifts left; nil → questionmark fallback.
            -- iconAtlas (string) → use SetAtlas instead of SetTexture (Blizzard reward atlases).
            if d.icon == false then
                row.icon:Hide()
                row.lineA:ClearAllPoints()
                row.lineA:SetPoint("TOPLEFT", row, "LEFT", 0, 8)
            else
                row.icon:Show()
                if d.iconAtlas then
                    row.icon:SetAtlas(d.iconAtlas)
                else
                    row.icon:SetTexture(d.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
                end
                row.lineA:ClearAllPoints()
                row.lineA:SetPoint("TOPLEFT", row.icon, "TOPRIGHT", 6, -2)
            end
            row.lineA:SetText(d.lineA or "")
            row.lineB:SetText(d.lineB or "")
            row.chip:SetText(d.chip or "")
            if d.onClick then
                row:SetScript("OnClick", d.onClick)
            else
                row:SetScript("OnClick", nil)
            end
            row:Show()
        end
        for i = n + 1, #self.rowFrames do self.rowFrames[i]:Hide() end
    end

    function self:Resize(_, h)
        self._visible = math.min(maxRowsCap, Patterns.RowsForHeight(h - 4, rowHeight))
        if self.latestData then self:Update(self.latestData) end
    end

    function self:GetMinSize() return 100, rowHeight end

    return self
end

-- ============================================================================
-- MiniCardStrip — N small cards in a horizontal row
-- ============================================================================
-- opts: { cardCount = 3, cardGap = 6, miniCardRenderer = function(miniFrame, data) ... end }
-- :Update(data) where data = { cards = {{title, value, total, pct, badge?}, ...} }

function Patterns.MiniCardStrip(parent, dims, opts)
    opts = opts or {}
    local cardCount = opts.cardCount or dims.cardCount or 3
    local cardGap   = opts.cardGap   or dims.cardGap   or 6
    local self = { miniFrames = {}, latestData = nil }
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    local function ensureMini(i)
        if self.miniFrames[i] then return self.miniFrames[i] end
        local mini = CreateFrame("Button", nil, self.frame, "BackdropTemplate")
        mini:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8x8",
            edgeFile = "Interface\\Buttons\\WHITE8x8",
            edgeSize = 1,
        })
        mini:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.6)
        mini:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.3)
        mini.title = mini:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        mini.title:SetPoint("TOP", mini, "TOP", 0, -4)
        mini.title:SetTextColor(scheme.accent.r, scheme.accent.g, scheme.accent.b)
        F(mini.title, scheme, "small")
        mini.value = mini:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        mini.value:SetPoint("BOTTOM", mini, "BOTTOM", 0, 6)
        mini.value:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
        F(mini.value, scheme, "body")
        mini.bar = privateDB.HDG_HT.SegmentedBar.Create(mini, {
            segments = opts.cardBarSegments or dims.cardBarSegments or 5,
            segmentHeight  = opts.cardBarHeight or dims.cardBarHeight or 6,
            gap            = 1, width = 60,
            style          = opts.cardBarStyle    or dims.cardBarStyle    or "solid",
            gradient       = opts.cardBarGradient or dims.cardBarGradient or false,
            gradientReverse= opts.cardBarGradientReverse or dims.cardBarGradientReverse or false,
        })
        mini.bar:SetPoint("CENTER", mini, "CENTER", 0, -2)
        mini:Hide()
        self.miniFrames[i] = mini
        return mini
    end

    function self:Update(data)
        self.latestData = data
        if not data or not data.cards then
            for _, m in ipairs(self.miniFrames) do m:Hide() end
            return
        end
        local n = math.min(#data.cards, cardCount)
        for i = 1, n do
            local d = data.cards[i]
            local mini = ensureMini(i)
            mini.title:SetText(d.title or "")
            mini.value:SetText(string.format("%d/%d (%d%%)",
                d.value or 0, d.total or 0,
                math.floor(((d.pct or 0) * 100) + 0.5)))
            mini.bar:Update(d.pct or 0)
            mini:Show()
        end
        for i = n + 1, #self.miniFrames do self.miniFrames[i]:Hide() end
    end

    function self:Resize(w, h)
        local n = cardCount
        local cardW = math.max(60, math.floor((w - cardGap * (n - 1)) / n))
        for i = 1, n do
            local mini = ensureMini(i)
            mini:SetSize(cardW, h)
            mini:ClearAllPoints()
            mini:SetPoint("TOPLEFT", self.frame, "TOPLEFT", (i - 1) * (cardW + cardGap), 0)
            -- Re-resize the segmented bar so circle-style segments stay
            -- square. Target a fraction of card width; circles mode ignores
            -- this and uses N × segmentHeight automatically.
            if mini.bar and mini.bar.Resize then
                mini.bar:Resize(math.max(40, cardW - 12))
            end
        end
        if self.latestData then self:Update(self.latestData) end
    end

    function self:GetMinSize() return cardCount * 60 + cardGap * (cardCount - 1), 60 end

    return self
end

-- ============================================================================
-- Donut+Legend — circular chart left + vertical legend right
-- ============================================================================
-- opts: { donutSize = 120, holeSize = 60, centerFont = "GameFontNormalLarge",
--         legendFont = "GameFontHighlightSmall" }
-- :Update(data) where data = {
--   segments     = { { label, value, color = {r,g,b} }, ... },  -- color may be nil
--   centerText   = string,
--   centerSubText = string,
-- }
-- Real donut implementation (verbatim from HDG_HouseTab.lua:100-172).
-- 90 triangle wedges via Texture:SetVertexOffset (collapse a quad to a triangle
-- by making 2 corners coincident at the centre). Inner disc with CircleMaskScalable
-- atlas gives the donut hole.
local DONUT_WEDGES = 90  -- 4° per wedge — smooth enough at this size

local function CreateDonut(parent, donutSize, holeSize)
    local frame = CreateFrame("Frame", nil, parent)
    frame:SetSize(donutSize, donutSize)
    local scheme = S()
    frame.wedges = {}
    -- Pre-allocate wedges (reused across redraws)
    for i = 1, DONUT_WEDGES do
        local tex = frame:CreateTexture(nil, "ARTWORK")
        tex:SetTexture("Interface\\Buttons\\WHITE8X8")
        tex:SetSize(0.001, 0.001) -- effectively zero; offsets place corners
        tex:SetPoint("CENTER", frame, "CENTER")
        frame.wedges[i] = tex
    end
    -- Inner hole (donut effect) — masked into a circle; colored to match panel.
    frame.hole = frame:CreateTexture(nil, "OVERLAY")
    frame.hole:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.hole:SetSize(holeSize, holeSize)
    frame.hole:SetPoint("CENTER", frame, "CENTER")
    frame.hole:SetVertexColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 1)
    local mask = frame:CreateMaskTexture()
    mask:SetAllPoints(frame.hole)
    mask:SetAtlas("CircleMaskScalable")
    frame.hole:AddMaskTexture(mask)
    -- Centre labels
    frame.label = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    frame.label:SetPoint("CENTER", frame, "CENTER", 0, 4)
    frame.label:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
    F(frame.label, scheme, "header")
    frame.sublabel = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.sublabel:SetPoint("CENTER", frame, "CENTER", 0, -10)
    frame.sublabel:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
    F(frame.sublabel, scheme, "small")

    -- UpdateWedges: paint wedges from segment data.
    -- segments = { { value, color = {r,g,b} }, ... }
    function frame:UpdateWedges(segments, centreText, centreSubText)
        local total = 0
        for _, s in ipairs(segments or {}) do total = total + (s.value or 0) end
        if total <= 0 then
            for _, w in ipairs(self.wedges) do w:Hide() end
            self.label:SetText("")
            self.sublabel:SetText("")
            return
        end
        local radius = self:GetWidth() / 2
        local wedgeAng = 2 * math.pi / DONUT_WEDGES
        local accum, segIdx = 0, 1
        for i = 1, DONUT_WEDGES do
            local mid = (i - 0.5) / DONUT_WEDGES
            while segIdx <= #segments and (accum + segments[segIdx].value) / total < mid do
                accum = accum + segments[segIdx].value
                segIdx = segIdx + 1
            end
            local seg = segments[segIdx]
            local w = self.wedges[i]
            if seg and seg.color then
                w:SetVertexColor(seg.color[1], seg.color[2], seg.color[3], 0.95)
                local a1 = (i - 1) * wedgeAng - math.pi / 2  -- start at 12 o'clock
                local a2 = a1 + wedgeAng
                local p1x, p1y =  math.cos(a1) * radius, -math.sin(a1) * radius
                local p2x, p2y =  math.cos(a2) * radius, -math.sin(a2) * radius
                -- Texture corner enums: UL=1, LL=2, UR=3, LR=4
                w:SetVertexOffset(1, p1x, p1y) -- UL → outer p1
                w:SetVertexOffset(3, p2x, p2y) -- UR → outer p2
                w:SetVertexOffset(2, 0, 0)     -- LL → centre
                w:SetVertexOffset(4, 0, 0)     -- LR → centre (collapses quad → triangle)
                w:Show()
            else
                w:Hide()
            end
        end
        if centreText    then self.label:SetText(centreText)       end
        if centreSubText then self.sublabel:SetText(centreSubText) end
    end

    return frame
end

function Patterns.DonutLegend(parent, dims, opts)
    opts = opts or {}
    local donutSize = opts.donutSize or dims.donutSize or 120
    local holeSize  = opts.holeSize  or dims.holeSize  or 60
    local self = {}
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    self.donut = CreateDonut(self.frame, donutSize, holeSize)
    self.donut:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 4, -4)
    -- Centre text lives on donut.label / donut.sublabel (set via UpdateWedges).

    self.legendFrame = CreateFrame("Frame", nil, self.frame)
    self.legendFrame:SetPoint("LEFT", self.donut, "RIGHT", 8, 0)
    self.legendFrame:SetPoint("RIGHT", self.frame, "RIGHT", -4, 0)
    self.legendFrame:SetPoint("TOP", self.frame, "TOP", 0, -2)
    self.legendFrame:SetPoint("BOTTOM", self.frame, "BOTTOM", 0, 2)

    self.legendRows = {}
    local function ensureLegendRow(i)
        if self.legendRows[i] then return self.legendRows[i] end
        local row = CreateFrame("Frame", nil, self.legendFrame)
        row:SetHeight(14)
        row:SetPoint("TOPLEFT",  self.legendFrame, "TOPLEFT",  0, -((i - 1) * 14))
        row:SetPoint("TOPRIGHT", self.legendFrame, "TOPRIGHT", 0, -((i - 1) * 14))
        row.swatch = row:CreateTexture(nil, "ARTWORK")
        row.swatch:SetTexture("Interface\\Buttons\\WHITE8x8")
        row.swatch:SetSize(8, 8)
        row.swatch:SetPoint("LEFT", row, "LEFT", 0, 0)
        row.label = row:CreateFontString(nil, "OVERLAY", opts.legendFont or "GameFontHighlightSmall")
        row.label:SetPoint("LEFT", row.swatch, "RIGHT", 4, 0)
        row.label:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
        F(row.label, scheme, "small")
        row.value = row:CreateFontString(nil, "OVERLAY", opts.legendFont or "GameFontHighlightSmall")
        row.value:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        row.value:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
        F(row.value, scheme, "small")
        row:Hide()
        self.legendRows[i] = row
        return row
    end

    function self:Update(data)
        if not data then self.frame:Hide(); return end
        self.frame:Show()
        local segs = data.segments or {}
        for i, s in ipairs(segs) do
            local row = ensureLegendRow(i)
            local c = s.color or { 0.5, 0.5, 0.5 }
            row.swatch:SetVertexColor(c[1], c[2], c[3], 1)
            row.label:SetText(s.label or "")
            row.value:SetText(tostring(s.value or 0))
            row:Show()
        end
        for i = #segs + 1, #self.legendRows do self.legendRows[i]:Hide() end
        -- Donut wedges (real impl, T18)
        if self.donut.UpdateWedges then
            self.donut:UpdateWedges(segs, data.centerText, data.centerSubText)
        end
    end

    function self:Resize(_, _)
        -- Donut stays fixed-size; legend area auto-grows from anchors.
    end

    function self:GetMinSize() return donutSize + 80, donutSize + 8 end

    return self
end

-- ============================================================================
-- BarRows — ranked list of horizontal bars
-- ============================================================================
-- opts: { rowHeight = 18, labelWidth = 80, valueWidth = 60, barSegments = 10 }
-- :Update(data) where data = { rows = {{label, value, total, pct, color?}, ...} }

function Patterns.BarRows(parent, dims, opts)
    opts = opts or {}
    local rowHeight  = opts.rowHeight  or dims.barHeight or 18
    local labelWidth = opts.labelWidth or 80
    local valueWidth = opts.valueWidth or 60
    local barSegs    = opts.barSegments or dims.barSegments or 10
    local self = { rowFrames = {}, latestData = nil }
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    local function ensureRow(i)
        if self.rowFrames[i] then return self.rowFrames[i] end
        local row = CreateFrame("Frame", nil, self.frame)
        row:SetHeight(rowHeight)
        row:SetPoint("TOPLEFT",  self.frame, "TOPLEFT",  0, -((i - 1) * (rowHeight + 2)))
        row:SetPoint("TOPRIGHT", self.frame, "TOPRIGHT", 0, -((i - 1) * (rowHeight + 2)))
        row.label = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.label:SetPoint("LEFT", row, "LEFT", 0, 0)
        row.label:SetWidth(labelWidth)
        row.label:SetJustifyH("LEFT")
        row.label:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
        F(row.label, scheme, "small")
        row.value = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.value:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        row.value:SetWidth(valueWidth)
        row.value:SetJustifyH("RIGHT")
        row.value:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
        F(row.value, scheme, "small")
        row.bar = privateDB.HDG_HT.SegmentedBar.Create(row, {
            segments = barSegs, segmentHeight = math.max(8, rowHeight - 4),
            gap = 2, width = 100, style = "solid",
        })
        row.bar:SetPoint("LEFT", row.label, "RIGHT", 4, 0)
        row.bar:SetPoint("RIGHT", row.value, "LEFT", -4, 0)
        row:Hide()
        self.rowFrames[i] = row
        return row
    end

    function self:Update(data)
        self.latestData = data
        if not data or not data.rows then
            for _, r in ipairs(self.rowFrames) do r:Hide() end
            return
        end
        local n = math.min(#data.rows, self._visible or 99)
        for i = 1, n do
            local d = data.rows[i]
            local row = ensureRow(i)
            row.label:SetText(d.label or "")
            row.value:SetText(string.format("%d/%d (%d%%)",
                d.value or 0, d.total or 0,
                math.floor(((d.pct or 0) * 100) + 0.5)))
            if d.color then
                for _, seg in ipairs(row.bar.segments) do
                    seg._litColor = { d.color[1], d.color[2], d.color[3], 1 }
                end
            end
            row.bar:Update(d.pct or 0)
            row:Show()
        end
        for i = n + 1, #self.rowFrames do self.rowFrames[i]:Hide() end
    end

    function self:Resize(w, h)
        self._visible = Patterns.RowsForHeight(h, rowHeight + 2)
        -- Recompute SegmentedBar internals on resize — bars are anchored
        -- LEFT/RIGHT so the frame stretches, but segment textures sit at
        -- positions calculated against the original 100px width. Without
        -- this, segments cluster at the left edge after resize.
        local barWidth = math.max(60, (w or 100) - labelWidth - valueWidth - 8)
        local gap = 2
        local segW = (barWidth - gap * (barSegs - 1)) / barSegs
        for _, row in pairs(self.rowFrames) do
            if row.bar and row.bar.segments then
                for i, seg in ipairs(row.bar.segments) do
                    seg:SetWidth(segW)
                    seg:ClearAllPoints()
                    seg:SetPoint("LEFT", row.bar, "LEFT", (i - 1) * (segW + gap), 0)
                end
            end
        end
        if self.latestData then self:Update(self.latestData) end
    end

    function self:GetMinSize() return labelWidth + valueWidth + 60, rowHeight end

    return self
end

-- ============================================================================
-- Specials
-- ============================================================================

-- ChipStrip — pills wrapping across rows. data = { chips = {{label, count, color?}, ...} }
function Patterns.ChipStrip(parent, _, opts)
    opts = opts or {}
    local chipHeight = opts.chipHeight or 18
    local chipGap    = opts.chipGap    or 4
    local self = { chipFrames = {} }
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    local function ensureChip(i)
        if self.chipFrames[i] then return self.chipFrames[i] end
        local chip = CreateFrame("Frame", nil, self.frame, "BackdropTemplate")
        chip:SetHeight(chipHeight)
        chip:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
        chip:SetBackdropColor(scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.6)
        chip:SetBackdropBorderColor(scheme.border.r, scheme.border.g, scheme.border.b, 0.3)
        chip.text = chip:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        chip.text:SetPoint("LEFT",  chip, "LEFT",  6, 0)
        chip.text:SetPoint("RIGHT", chip, "RIGHT", -6, 0)
        chip.text:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
        F(chip.text, scheme, "small")
        chip:Hide()
        self.chipFrames[i] = chip
        return chip
    end

    function self:Update(data)
        if not data or not data.chips then
            for _, c in ipairs(self.chipFrames) do c:Hide() end
            return
        end
        for i, c in ipairs(data.chips) do
            local chip = ensureChip(i)
            -- Per-chip label color (e.g. expansion tint). Count stays text color.
            local labelColored
            if c.color then
                labelColored = string.format("|cFF%02x%02x%02x%s|r",
                    math.floor(math.min(1, c.color[1] or 1) * 255 + 0.5),
                    math.floor(math.min(1, c.color[2] or 1) * 255 + 0.5),
                    math.floor(math.min(1, c.color[3] or 1) * 255 + 0.5),
                    tostring(c.label or ""))
            else
                labelColored = tostring(c.label or "")
            end
            chip.text:SetText(string.format("%s: %s", labelColored, tostring(c.count or "")))
            chip:SetWidth(chip.text:GetStringWidth() + 16)
            chip:Show()
        end
        for i = #data.chips + 1, #self.chipFrames do self.chipFrames[i]:Hide() end
        self:_layout()
    end

    function self:_layout()
        local x, y = 0, 0
        local availW = self.frame:GetWidth() or 100
        local availH = self.frame:GetHeight() or 100
        -- Clip children so chips that exceed the widget's height don't bleed
        -- into the card below (visible when a player has many currencies).
        if self.frame.SetClipsChildren then self.frame:SetClipsChildren(true) end
        for _, chip in ipairs(self.chipFrames) do
            if chip:IsShown() then
                local w = chip:GetWidth()
                if x + w > availW and x > 0 then x = 0; y = y - chipHeight - chipGap end
                -- Hide chips whose row would render below the widget's
                -- visible area — better than overflowing onto neighbours.
                if -y + chipHeight > availH then
                    chip:Hide()
                else
                    chip:ClearAllPoints()
                    chip:SetPoint("TOPLEFT", self.frame, "TOPLEFT", x, y)
                    x = x + w + chipGap
                end
            end
        end
    end

    function self:Resize(_, _) self:_layout() end
    function self:GetMinSize() return 80, chipHeight end
    return self
end

-- IconShelf — flowing icons that wrap. data = { items = {{itemID, iconID, name, owned}, ...} }
function Patterns.IconShelf(parent, _, opts)
    opts = opts or {}
    local iconSize = opts.iconSize or 28
    local iconGap  = opts.iconGap  or 4
    local self = { iconBtns = {} }
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)

    local function ensureIcon(i)
        if self.iconBtns[i] then return self.iconBtns[i] end
        local b = CreateFrame("Button", nil, self.frame)
        b:SetSize(iconSize, iconSize)
        b.tex = b:CreateTexture(nil, "ARTWORK")
        b.tex:SetAllPoints()
        b.tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        b:SetScript("OnEnter", function(self_)
            if self_._itemID then
                GameTooltip:SetOwner(self_, "ANCHOR_RIGHT")
                GameTooltip:SetItemByID(self_._itemID); GameTooltip:Show()
            end
        end)
        b:SetScript("OnLeave", function() GameTooltip:Hide() end)
        b:Hide()
        self.iconBtns[i] = b
        return b
    end

    function self:Update(data)
        if not data or not data.items then
            for _, b in ipairs(self.iconBtns) do b:Hide() end
            return
        end
        for i, it in ipairs(data.items) do
            local b = ensureIcon(i)
            b.tex:SetTexture(it.iconID or "Interface\\Icons\\INV_Misc_QuestionMark")
            b._itemID = it.itemID
            if it.owned == false then b.tex:SetVertexColor(0.3, 0.3, 0.3, 0.7)
            else b.tex:SetVertexColor(1, 1, 1, 1) end
            b:Show()
        end
        for i = #data.items + 1, #self.iconBtns do self.iconBtns[i]:Hide() end
        self:_layout()
    end

    function self:_layout()
        local x, y = 0, 0
        local availW = self.frame:GetWidth() or 100
        for _, b in ipairs(self.iconBtns) do
            if b:IsShown() then
                if x + iconSize > availW and x > 0 then x = 0; y = y - iconSize - iconGap end
                b:ClearAllPoints()
                b:SetPoint("TOPLEFT", self.frame, "TOPLEFT", x, y)
                x = x + iconSize + iconGap
            end
        end
    end

    function self:Resize(_, _) self:_layout() end
    function self:GetMinSize() return iconSize * 4 + iconGap * 3, iconSize end
    return self
end

-- StatLines — N rows of [label] [value]. data = { lines = {{label, value}, ...} }
function Patterns.StatLines(parent, _, opts)
    opts = opts or {}
    local rowHeight = opts.rowHeight or 16
    local self = { rowFrames = {} }
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    local function ensureRow(i)
        if self.rowFrames[i] then return self.rowFrames[i] end
        local row = CreateFrame("Frame", nil, self.frame)
        row:SetHeight(rowHeight)
        row:SetPoint("TOPLEFT",  self.frame, "TOPLEFT",  0, -((i - 1) * rowHeight))
        row:SetPoint("TOPRIGHT", self.frame, "TOPRIGHT", 0, -((i - 1) * rowHeight))
        row.label = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row.label:SetPoint("LEFT",  row, "LEFT",  0, 0)
        row.label:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
        F(row.label, scheme, "small")
        row.value = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.value:SetPoint("RIGHT", row, "RIGHT", 0, 0)
        row.value:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
        F(row.value, scheme, "small")
        row:Hide()
        self.rowFrames[i] = row
        return row
    end

    function self:Update(data)
        if not data or not data.lines then
            for _, r in ipairs(self.rowFrames) do r:Hide() end
            return
        end
        for i, l in ipairs(data.lines) do
            local row = ensureRow(i)
            row.label:SetText(l.label or "")
            row.value:SetText(tostring(l.value or ""))
            row:Show()
        end
        for i = #data.lines + 1, #self.rowFrames do self.rowFrames[i]:Hide() end
    end
    function self:Resize(_, _) end
    function self:GetMinSize() return 100, rowHeight end
    return self
end

-- HeroIcon — large icon left + 1-3 caption lines right. data = { icon, lines = {string,...} }
function Patterns.HeroIcon(parent, _, opts)
    opts = opts or {}
    local iconSize = opts.iconSize or 48
    local self = { lineFrames = {} }
    self.frame = CreateFrame("Frame", nil, parent)
    self.frame:SetAllPoints(parent)
    local scheme = S()

    self.icon = self.frame:CreateTexture(nil, "ARTWORK")
    self.icon:SetSize(iconSize, iconSize)
    self.icon:SetPoint("LEFT", self.frame, "LEFT", 4, 0)
    self.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    local function ensureLine(i, font)
        if self.lineFrames[i] then return self.lineFrames[i] end
        local fs = self.frame:CreateFontString(nil, "OVERLAY", font or "GameFontHighlightSmall")
        if i == 1 then fs:SetPoint("TOPLEFT", self.icon, "TOPRIGHT", 8, 0)
        else fs:SetPoint("TOPLEFT", self.lineFrames[i - 1], "BOTTOMLEFT", 0, -2) end
        if i == 1 then
            fs:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b)
            F(fs, scheme, "body")
        else
            fs:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b)
            F(fs, scheme, "small")
        end
        fs:Hide()
        self.lineFrames[i] = fs
        return fs
    end

    function self:Update(data)
        if not data then self.frame:Hide(); return end
        self.frame:Show()
        self.icon:SetTexture(data.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
        for i, txt in ipairs(data.lines or {}) do
            local fs = ensureLine(i, i == 1 and "GameFontHighlight" or "GameFontHighlightSmall")
            fs:SetText(txt); fs:Show()
        end
        for i = #(data.lines or {}) + 1, #self.lineFrames do self.lineFrames[i]:Hide() end
    end
    function self:Resize(_, _) end
    function self:GetMinSize() return iconSize + 80, iconSize end
    return self
end
