-- VDS_Selectors_Browse
-- ============================================================================
-- Tab bar, sub-filter chip strip, and the main browse-list row arrays.

local Selectors = VDS.Selectors

-- Suppressed in live mode (no subfilter UX) and crafting/Stock (warehouse body already groups by color group).
Selectors:Register("subfilter.shown", {
    reads = { "session.ui.filterMode" },
    fn    = function(state)
        -- Suppressed in live + crafting; all other tabs show chips.
        local m = state.session.ui.filterMode
        return m ~= "live" and m ~= "crafting"
    end,
})

-- ADR-024: return structured data; tabBar widget handles label + active styling.

Selectors:Register("browse.tabs.chips", {
    reads = { "session.ui.filterMode", "session.ui.live.active" },
    fn    = function(state)
        local active   = state.session.ui.filterMode
        local liveOn   = state.session.ui.live.active
        -- Stock/Herbs/Canvas hidden in live mode (planning surfaces; donor parity).
        -- Custom shown in live: saved palettes are exactly what you reach for while editing.
        local allChips = {
            { id = "category", label = "Colors",                       color = { r = 1.00, g = 0.84, b = 0.00 } },  -- gold
            { id = "custom",   label = "Custom",                       color = { r = 0.20, g = 0.80, b = 0.20 } },  -- bright green
            { id = "theme",    label = "Theme",                        color = { r = 0.78, g = 0.45, b = 0.95 } },  -- purple
            { id = "crafting", label = "Stock",   liveHidden = true,   color = { r = 0.00, g = 0.75, b = 1.00 } },  -- sky blue
            { id = "herbs",    label = "Herbs",   liveHidden = true,   color = { r = 0.63, g = 0.85, b = 0.39 } },  -- moss green
            { id = "canvas",   label = "Canvas",  liveHidden = true,   color = { r = 0.95, g = 0.30, b = 0.40 } },  -- crimson
        }
        local chips = {}
        for _, c in ipairs(allChips) do
            if not (liveOn and c.liveHidden) then
                c.active = (c.id == active)
                chips[#chips + 1] = c
            end
        end
        return chips
    end,
})

-- Secondary filter chips for the active tab (color groups / palette names / pigment categories).

Selectors:Register("browse.subfilter.chips", {
    reads = {
        "session.ui.activeTab",
        "session.ui.filterMode",
        "session.ui.byView",
        "session.ui.live.active",
        "session.dyes.catalog",
        "account.customThemes",
    },
    fn    = function(state)
        local ui     = state.session.ui
        local bucket = ui.byView[ui.filterMode]

        -- Herbs: pigment-category chips; "All" clears to grouped headers (donor parity).
        if ui.filterMode == "herbs" then
            local pigments = VDS.StaticData.GetPigments()
            if not pigments then return {} end
            local chips = {
                { id = "__all__", label = "All", active = (bucket.activeHerbGroup == nil) },
            }
            local seen = {}
            for _, pig in pairs(pigments) do
                if pig.category and not seen[pig.category] then
                    seen[pig.category] = true
                    chips[#chips + 1] = {
                        id     = pig.category,
                        label  = pig.category,
                        rgb    = VDS.StaticData.COLOR_GROUP_RGB[pig.category],
                        active = (bucket.activeHerbGroup == pig.category),
                    }
                end
            end
            -- "All" first, then alphabetical.
            table.sort(chips, function(a, b)
                if a.id == "__all__" then return true end
                if b.id == "__all__" then return false end
                return a.label < b.label
            end)
            return chips
        end

        if ui.filterMode == "category" then
            local chips = {
                { id = "__all__", label = "All", active = (bucket.activeColorGroup == nil) },
            }
            local groups = VDS.StaticData.COLOR_GROUP_ORDER
            for _, group in ipairs(groups) do
                local rgb = VDS.StaticData.COLOR_GROUP_RGB[group]
                chips[#chips + 1] = {
                    id     = group,
                    label  = group,
                    rgb    = rgb,
                    active = (bucket.activeColorGroup == group),
                }
            end
            return chips
        end

        -- Theme: curated palette-name chips (themed + class).
        if ui.filterMode == "theme" then
            local chips = {
                { id = "__all__", label = "All", active = (bucket.activeThemePalette == nil) },
            }
            for _, p in ipairs(VDS.StaticData.GetThemedPalettes()) do
                chips[#chips + 1] = {
                    id     = p.name,
                    label  = p.name,
                    rgb    = p.rgb,
                    active = (bucket.activeThemePalette == p.name),
                }
            end
            for _, p in ipairs(VDS.StaticData.GetClassPalettes()) do
                chips[#chips + 1] = {
                    id     = p.name,
                    label  = p.name,
                    rgb    = p.rgb,
                    active = (bucket.activeThemePalette == p.name),
                }
            end
            return chips
        end

        -- Stock: pigment-category chips (same order as Herbs).
        if ui.filterMode == "crafting" then
            local chips = {
                { id = "__all__", label = "All", active = (bucket.activeCraftGroup == nil) },
            }
            local seen = {}
            for _, c in ipairs(VDS.StaticData.COLOR_GROUP_ORDER) do
                seen[c] = false  -- mark known
            end
            for _, pig in pairs(VDS.StaticData.GetPigments()) do
                if pig.category and seen[pig.category] == false then
                    seen[pig.category] = true
                    chips[#chips + 1] = {
                        id     = pig.category,
                        label  = pig.category,
                        rgb    = VDS.StaticData.COLOR_GROUP_RGB[pig.category],
                        active = (bucket.activeCraftGroup == pig.category),
                    }
                end
            end
            -- Preserve COLOR_GROUP_ORDER sort.
            table.sort(chips, function(a, b)
                if a.id == "__all__" then return true end
                if b.id == "__all__" then return false end
                local ao, bo
                for i, c in ipairs(VDS.StaticData.COLOR_GROUP_ORDER) do
                    if c == a.id then ao = i end
                    if c == b.id then bo = i end
                end
                return (ao or 999) < (bo or 999)
            end)
            return chips
        end

        -- Custom: live mode = palette-name chips (pick a saved combo); default mode = swatch chips for click-to-remove.
        if ui.filterMode == "custom" then
            if state.session.ui.live.active then
                local activeName = ui.byView[ui.filterMode].activeTheme
                local chips = {
                    { id = "__all__", label = "All", active = (activeName == nil) },
                }
                for _, t in ipairs(state.account.customThemes) do
                    chips[#chips + 1] = {
                        id     = t.name,
                        label  = t.name,
                        active = (t.name == activeName),
                    }
                end
                return chips
            end

            local activeName = ui.byView[ui.filterMode].editingThemeName or ui.byView[ui.filterMode].activeTheme
            if not activeName then return {} end
            local palette
            for _, t in ipairs(state.account.customThemes) do
                if t.name == activeName then palette = t; break end
            end
            if not palette or not palette.dyes then return {} end

            local catalog = state.session.dyes.catalog
            local infos   = catalog.infos  -- strict: catalog pre-init with empty infos
            local chips   = {}
            for _, dyeID in ipairs(palette.dyes) do
                local info = infos[dyeID]
                local rgb  = info and info.swatchRGB  -- exception(nullable): catalog info can miss pre-DYE_DATA_INIT
                chips[#chips + 1] = {
                    id      = dyeID,
                    label   = "",                 -- swatch-only chip
                    rgb     = rgb,
                    active  = false,
                    -- Routes to CUSTOM_THEME_TOGGLE_DYE, not SET_ACTIVE_SUBFILTER.
                    customDyeChip = true,
                }
            end
            return chips
        end

        return {}
    end,
})

-- Herbs-only rows (separate pool eliminates cross-kind hide pattern).
Selectors:Register("browse.herbs.rows", {
    reads = {
        "session.ui.searchText",
        "session.ui.byView",
        "session.items.names",
        "session.items.tick",
    },
    fn    = function(state)
        VDS.Log:Debug("items", string.format("[5] browse.herbs.rows re-derive tick=%d",
            state.session.items.tick or -1))
        local pigments = VDS.StaticData.GetPigments()
        if not pigments then return {} end
        local ui          = state.session.ui
        local bucket      = ui.byView[ui.filterMode] or {}
        local rows        = {}
        local searchRaw   = ui.searchText
        local search      = (searchRaw ~= "" and searchRaw) and searchRaw:lower() or nil
        local activeGroup = bucket.activeHerbGroup
        local SD          = VDS.StaticData

        -- Per-pigmentItem, not per category: one category maps to multiple pigments; each herb contributes only to its own.
        local dyesByPigItem = SD.GetDyesByPigmentItem()

        local function pigmentsByCategory()
            local byCat = {}
            for pigItemID, pig in pairs(pigments) do
                if pig.category then
                    byCat[pig.category] = { pig = pig, pigItemID = pigItemID }
                end
            end
            return byCat
        end
        local byCat = pigmentsByCategory()
        local catOrder
        if activeGroup then
            catOrder = { activeGroup }
        else
            catOrder = {}
            for _, c in ipairs(SD.COLOR_GROUP_ORDER) do
                if byCat[c] then catOrder[#catOrder + 1] = c end
            end
            for cat in pairs(byCat) do
                local found = false
                for _, c in ipairs(catOrder) do
                    if c == cat then found = true; break end
                end
                if not found then catOrder[#catOrder + 1] = cat end
            end
        end

        for _, cat in ipairs(catOrder) do
            local entry = byCat[cat]
            if entry then
                local pig        = entry.pig
                local pigItemID  = entry.pigItemID
                local nameCache  = state.session.items.names
                local groupRows  = {}
                local dyeItems   = dyesByPigItem[pigItemID] or {}
                for expIdx, herbID in ipairs(pig.herbs or {}) do
                    local name = nameCache[herbID]
                    if not search or (name and name:lower():find(search, 1, true)) then
                        groupRows[#groupRows + 1] = {
                            kind       = "herb",
                            herbID     = herbID,
                            name       = name,
                            pigment    = pig.category,
                            expIdx     = expIdx,
                            price      = SD.GetAuctionPrice(herbID),  -- copper or nil
                            dyeItemIDs = dyeItems,
                        }
                    end
                end
                if #groupRows > 0 then
                    local sortMode = bucket.herbSortMode
                    if sortMode == "Price Low" or sortMode == "Price High" then
                        local asc = (sortMode == "Price Low")
                        table.sort(groupRows, function(a, b)
                            local pa = a.price or math.huge
                            local pb = b.price or math.huge
                            if asc then return pa < pb end
                            return pa > pb
                        end)
                    end
                    local UP = cat:upper()
                    rows[#rows + 1] = {
                        kind  = "herbHeader",
                        label = "Salvage in " .. UP .. " pigment recipe for " .. UP .. " dye",
                        count = #groupRows,
                    }
                    for _, r in ipairs(groupRows) do rows[#rows + 1] = r end
                end
            end
        end
        return rows
    end,
})

-- Visibility gates so the two scrollBoxes swap on filterMode without sharing a pool.
Selectors:Register("browse.dyes.shown", {
    reads = { "session.ui.filterMode" },
    fn    = function(state) return state.session.ui.filterMode ~= "herbs" end,
})
Selectors:Register("browse.herbs.shown", {
    reads = { "session.ui.filterMode" },
    fn    = function(state) return state.session.ui.filterMode == "herbs" end,
})

-- All non-herbs rows (pool isolation: factory only binds dye/header rows here).
Selectors:Register("browse.dyes.rows", {
    reads = {
        "session.dyes.catalog",
        "session.dyes.ownership",
        "session.ui.searchText",
        "session.ui.filterMode",
        "session.ui.byView",
        "session.ui.live.active",
        "session.ui.selectedDyeID",  -- row factory paints selection at bind time
        "account.config.ownedOnly",
        "account.customThemes",
        "account.dyeableItems",
    },
    calls = { "canvas.scope.dyeIDs" },
    fn    = function(state)
        local catalog = state.session.dyes.catalog  -- exception(false-positive): per-view-mode row grammar; branches return early, helpers extracted
        if not catalog.dyes then return {} end
        if state.session.ui.filterMode == "herbs" then return {} end

        local ui           = state.session.ui
        local bucket       = ui.byView[ui.filterMode]
        local infos        = catalog.infos
        local perDye       = state.session.dyes.ownership.perDye
        local cfg          = state.account.config

        local ownedOnly = (ui.filterMode == "owned") or cfg.ownedOnly

        -- Custom: edit mode = all dyes (click toggles membership); view mode = restrict to palette; none = all dyes.
        local themeDyes
        if ui.filterMode == "custom" then
            local viewBucket = ui.byView[ui.filterMode]
            if viewBucket.editingThemeName then
                themeDyes = nil  -- show all; clicks toggle membership
            elseif viewBucket.activeTheme then
                for _, theme in ipairs(state.account.customThemes) do
                    if theme.name == viewBucket.activeTheme then
                        themeDyes = theme.dyes; break
                    end
                end
            else
                themeDyes = nil  -- show all (Colors-tab parity)
            end
        end

        local searchRaw = ui.searchText or ""
        local search    = (searchRaw ~= "") and searchRaw:lower() or nil
        local colorGroup = bucket.activeColorGroup       -- nil when "All"

        -- Canvas scope: { [dyeID] = true } set keyed by membership; nil when scope is
        -- "All Dyes" (no filter). Only applied in canvas view -- elsewhere the scope
        -- state is dormant in byView.canvas and the selector returns nil there too.
        local scopeDyes = (ui.filterMode == "canvas")
            and Selectors:Call("canvas.scope.dyeIDs", state, {}) or nil

        local function passes(dyeID, info, ownedCount)
            if ownedOnly and ownedCount == 0 then return false end
            if colorGroup and info and info.colorGroup ~= colorGroup then return false end  -- exception(nullable): catalog info can miss
            if search and info and info.nameLower  -- exception(nullable): catalog info can miss
               and not info.nameLower:find(search, 1, true) then return false end
            if themeDyes then
                local inTheme = false
                for _, id in ipairs(themeDyes) do
                    if id == dyeID then inTheme = true; break end
                end
                if not inTheme then return false end
            end
            if scopeDyes and not scopeDyes[dyeID] then return false end
            return true
        end

        local function makeRow(dyeID, info, ownedCount)
            return {
                kind       = "dye",
                dyeID      = dyeID,
                name       = info and info.name or nil,  -- exception(nullable): catalog info can miss
                owned      = ownedCount > 0,
                ownedCount = ownedCount,
                dyeColor   = info and info.swatchRGB or nil,  -- exception(nullable): catalog info can miss
            }
        end

        -- Custom + live + "All": section-grouped rows per palette (donor parity).
        -- Dye appearing in multiple palettes renders once per palette so counts are honest.
        if ui.filterMode == "custom"
           and state.session.ui.live.active
           and not ui.byView[ui.filterMode].activeTheme
           and not ui.byView[ui.filterMode].editingThemeName then
            local rows = {}
            for _, palette in ipairs(state.account.customThemes) do
                local dyes = palette.dyes or {}
                local bucket = {}
                for _, dyeID in ipairs(dyes) do
                    local info       = infos[dyeID]
                    local ownedCount = perDye[dyeID] or 0  -- exception(nullable): sparse ownership map; miss = own 0
                    if passes(dyeID, info, ownedCount) then
                        bucket[#bucket + 1] = makeRow(dyeID, info, ownedCount)
                    end
                end
                if #bucket > 0 then
                    rows[#rows + 1] = {
                        kind  = "header",
                        label = (palette.name or "PALETTE"):upper(),
                        count = #bucket,
                    }
                    for _, r in ipairs(bucket) do rows[#rows + 1] = r end
                end
            end
            return rows
        end

        -- Grouped: category tab OR custom with no palette selected, AND "All" chip active.
        local customNoPalette = (ui.filterMode == "custom"
                                 and not ui.byView[ui.filterMode].editingThemeName
                                 and not ui.byView[ui.filterMode].activeTheme)
        if (ui.filterMode == "category" or customNoPalette) and not colorGroup then
            local SD = VDS.StaticData
            local buckets = {}
            for _, dyeID in ipairs(catalog.dyes) do
                local info       = infos[dyeID]
                local ownedCount = perDye[dyeID] or 0  -- exception(nullable): sparse ownership map; miss = own 0
                if passes(dyeID, info, ownedCount) then
                    local g = (info and info.colorGroup) or "Other"  -- exception(nullable): catalog info can miss
                    buckets[g] = buckets[g] or {}
                    buckets[g][#buckets[g] + 1] = makeRow(dyeID, info, ownedCount)
                end
            end

            local rows = {}
            for _, group in ipairs(SD.COLOR_GROUP_ORDER) do
                local bucket = buckets[group]
                if bucket and #bucket > 0 then
                    rows[#rows + 1] = {
                        kind  = "header",
                        label = group:upper(),
                        count = #bucket,
                    }
                    for _, r in ipairs(bucket) do rows[#rows + 1] = r end
                    buckets[group] = nil  -- consumed
                end
            end
            -- Unrecognized groups ("Other") append at end.
            for group, bucket in pairs(buckets) do
                if #bucket > 0 then
                    rows[#rows + 1] = { kind = "header", label = group:upper(), count = #bucket }
                    for _, r in ipairs(bucket) do rows[#rows + 1] = r end
                end
            end
            return rows
        end

        -- Theme: grouped headers per palette; activeThemePalette narrows to one.
        if ui.filterMode == "theme" then
            local itemIDToDye = catalog.itemIDToDye
            local SD = VDS.StaticData
            local palettes = {}
            local activePalette = bucket.activeThemePalette
            if activePalette then
                local p = SD.GetPaletteByName(activePalette)
                if p then palettes[1] = p end
            else
                for _, p in ipairs(SD.GetThemedPalettes()) do palettes[#palettes + 1] = p end
                for _, p in ipairs(SD.GetClassPalettes())  do palettes[#palettes + 1] = p end
            end

            local rows = {}
            for _, palette in ipairs(palettes) do
                local groupRows = {}
                for _, itemID in ipairs(palette.items or {}) do
                    local dyeID = itemIDToDye[itemID]
                    if dyeID then
                        local info       = infos[dyeID]
                        local ownedCount = perDye[dyeID] or 0  -- exception(nullable): sparse ownership map; miss = own 0
                        if passes(dyeID, info, ownedCount) then
                            groupRows[#groupRows + 1] = makeRow(dyeID, info, ownedCount)
                        end
                    end
                end
                if #groupRows > 0 then
                    rows[#rows + 1] = { kind = "header", label = palette.name:upper(), count = #groupRows }
                    for _, r in ipairs(groupRows) do rows[#rows + 1] = r end
                end
            end
            return rows
        end

        -- Stock: headers per pigment category; warehouse view supersedes when currentView = "stock".
        if ui.filterMode == "crafting" then
            local itemIDToDye = catalog.itemIDToDye
            local SD = VDS.StaticData
            local byCat = SD.GetDyesByPigment()  -- [pigCategory] = { dyeItemID, ... }
            local active = bucket.activeCraftGroup   -- nil = all

            local catOrder
            if active then
                catOrder = { active }
            else
                catOrder = {}
                for _, c in ipairs(SD.COLOR_GROUP_ORDER) do
                    if byCat[c] then catOrder[#catOrder + 1] = c end
                end
                for cat in pairs(byCat) do
                    local found = false
                    for _, c in ipairs(catOrder) do if c == cat then found = true; break end end
                    if not found then catOrder[#catOrder + 1] = cat end
                end
            end

            local rows = {}
            for _, cat in ipairs(catOrder) do
                local dyeItemIDs = byCat[cat]
                if dyeItemIDs then
                    local groupRows = {}
                    for _, dyeItemID in ipairs(dyeItemIDs) do
                        local dyeID = itemIDToDye[dyeItemID]
                        if dyeID then
                            local info       = infos[dyeID]
                            local ownedCount = perDye[dyeID] or 0  -- exception(nullable): sparse ownership map; miss = own 0
                            if passes(dyeID, info, ownedCount) then
                                local row = makeRow(dyeID, info, ownedCount)
                                row.price = info and info.itemID  -- exception(nullable): info/itemID can miss; price via optional Auctionator
                                            and SD.GetAuctionPrice(info.itemID) or nil
                                groupRows[#groupRows + 1] = row
                            end
                        end
                    end
                    if #groupRows > 0 then
                        rows[#rows + 1] = {
                            kind  = "header",
                            label = cat:upper() .. " PIGMENT",
                            count = #groupRows,
                        }
                        for _, r in ipairs(groupRows) do rows[#rows + 1] = r end
                    end
                end
            end
            return rows
        end

        -- Flat list (single-group or remaining modes).
        local rows = {}
        for _, dyeID in ipairs(catalog.dyes) do
            local info       = infos[dyeID]
            local ownedCount = perDye[dyeID] or 0  -- exception(nullable): sparse ownership map; miss = own 0
            if passes(dyeID, info, ownedCount) then
                rows[#rows + 1] = makeRow(dyeID, info, ownedCount)
            end
        end
        return rows
    end,
})

-- Palette rows for the customPanel sidebar scrollbox. { kind="palette", name, dyes, rgb, dyeCount, selected }.

Selectors:Register("custom.palette.rows", {
    reads = {
        "account.customThemes",
        "session.ui.filterMode",
        "session.ui.byView",
    },
    fn    = function(state)
        local themes  = state.account.customThemes
        local bucket  = state.session.ui.byView[state.session.ui.filterMode]
        local active  = bucket.activeTheme
        local editing = bucket.editingThemeName
        local rows = {}
        for _, theme in ipairs(themes) do
            rows[#rows + 1] = {
                kind     = "palette",
                name     = theme.name,
                dyes     = theme.dyes,
                rgb      = theme.rgb,
                dyeCount = theme.dyes and #theme.dyes or 0,
                selected = (active == theme.name),
                editing  = (editing == theme.name),
            }
        end
        table.sort(rows, function(a, b) return (a.name or "") < (b.name or "") end)
        return rows
    end,
})
