-- VDS_Selectors_CanvasDecor
-- ============================================================================
-- Canvas decor picker selectors -- category filter chain.
-- Will be extended in Phase 3 with the row-list selector replacing the tree.
--
-- Category state lives at session.ui.byView.canvas.selectedCategory:
--   nil    = "All" (show every dyeable decor)
--   <catID> = filter to that Housing catalog category (1 = Furnishings, etc.)

local Selectors = VDS.Selectors

-- Raw category value.
Selectors:Register("canvas.category.current", {
    reads = { "session.ui.byView.canvas.selectedCategory" },
    fn    = function(state) return state.session.ui.byView.canvas.selectedCategory end,
})

-- Dropdown trigger label. Style2 auto-renders from the selected radio at menu
-- open time, so this is the FALLBACK for SetDefaultText when nothing selected.
-- session.canvas.dyeableDecor is read so the label re-fires when the catalog
-- finishes priming (pre-CATALOG_READY we render "Loading..." rather than a
-- misleading "All Categories (0)").
Selectors:Register("canvas.category.label", {
    reads = {
        "session.ui.byView.canvas.selectedCategory",
        "session.canvas.decorCategoryNames",
        "session.canvas.dyeableDecor",
        "session.canvas.catalog",
    },
    fn = function(state)
        if #state.session.canvas.dyeableDecor == 0 then
            -- Reflect the resolver's status so the dropdown agrees with the
            -- emptyHint message (loading vs retrying vs failed).
            local cat = state.session.canvas.catalog
            if cat.status == "retrying" then return ("Retrying %d/%d"):format(cat.attempt or 0, cat.max or 3) end  -- exception(nullable): attempt/max set only while retrying
            if cat.status == "failed"   then return "Unavailable" end
            return "Loading..."
        end
        local cid = state.session.ui.byView.canvas.selectedCategory
        if not cid then return "All Categories" end
        return state.session.canvas.decorCategoryNames[cid] or ("Category " .. cid)
    end,
})

-- Loadouts list for the currently-selected decor. Drives the col 3 chip wall.
-- Returns an array sized to 20 (the max), with filled entries first and
-- placeholder nils after. The row factory paints each cell as a "loadout chip"
-- (3 striped colors) or as an "empty slot" / "+ New" tile based on index.
--
-- Row shape: { idx, name, dyes = {[0]=dyeID,[1]=dyeID,[2]=dyeID}, ts } or nil
Selectors:Register("canvas.loadouts.forSelected", {
    reads = {
        "session.ui.canvas.decorID",
        "account.canvas.loadouts",
        -- session.dyes.catalog: chip wall's row factory resolves dyeID -> RGB
        -- via DyeOwnershipObserver:ResolveInfo at paint time. Pre-DYE_DATA_INIT
        -- the infos cache is empty -> chips paint grey. Reading the catalog
        -- here re-fires the selector when DYE_DATA_INIT dispatches.
        "session.dyes.catalog",
    },
    fn = function(state)
        local decorID = state.session.ui.canvas.decorID
        if not decorID then return {} end
        local bucket = state.account.canvas.loadouts[decorID]
        if not bucket then return {} end
        -- Shallow project; row factory does dyeID -> RGB at paint time
        -- (ADR-001: API resolution is the boundary, not the selector).
        -- Project decorID into each row so OnLoadoutChipClick reads it from
        -- row._ed.decorID instead of diving back into state (ADR-041).
        local rows = {}
        for i, lo in ipairs(bucket) do
            rows[i] = { idx = i, name = lo.name, dyes = lo.dyes, ts = lo.ts, decorID = decorID }
        end
        return rows
    end,
})

-- True when the player has dye on at least one channel for the selected decor
-- AND that exact {d0,d1,d2} tuple isn't already saved as a loadout (and we're
-- below the per-decor cap). Drives the "+" save-tile pulse on the chip wall:
-- it nudges the player to save unsaved work, then quiets once the combo is
-- saved (or matches an existing one, since SAVE_LOADOUT promotes duplicates).
Selectors:Register("canvas.hasUnsavedCombo", {
    reads = { "session.ui.canvas.decorID", "session.ui.canvas.dyes", "account.canvas.loadouts" },
    fn = function(state)
        local decorID = state.session.ui.canvas.decorID
        if not decorID then return false end
        local dyes = state.session.ui.canvas.dyes
        if not dyes or (dyes[0] == nil and dyes[1] == nil and dyes[2] == nil) then return false end
        local saved = state.account.canvas.loadouts[decorID]
        if saved then
            local MAX = VDS.Constants.FLOATING_WIDGET.CANVAS_MAX_LOADOUTS_PER_DECOR
            if #saved >= MAX then return false end
            for _, entry in ipairs(saved) do
                local ed = entry.dyes or {}
                if ed[0] == dyes[0] and ed[1] == dyes[1] and ed[2] == dyes[2] then
                    return false
                end
            end
        end
        return true
    end,
})

-- Header text: "Loadouts (N/MAX)" for the selected decor; reads the cap from
-- Constants so a future cap change updates the header automatically.
Selectors:Register("canvas.loadouts.headerText", {
    reads = { "session.ui.canvas.decorID", "account.canvas.loadouts" },
    fn = function(state)
        local decorID = state.session.ui.canvas.decorID
        if not decorID then return "Loadouts" end
        local bucket = state.account.canvas.loadouts[decorID]
        local n = bucket and #bucket or 0
        local cap = VDS.Constants.FLOATING_WIDGET.CANVAS_MAX_LOADOUTS_PER_DECOR
        return ("Loadouts (%d/%d)"):format(n, cap)
    end,
})

-- Flat decor-row list for the new (post-tree) picker. Replaces canvas.tree.nodes
-- for the new scrollBoxList. Filtered by search + selected category. Each row
-- carries loadoutCount so the row factory can paint the right-side cluster.
--
-- Row shape: { kind = "decor", decorID, name, isOwned, icon, channels,
--              selected, loadoutCount }
Selectors:Register("canvas.decor.rows", {
    reads = {
        "session.canvas.dyeableDecor",
        "session.ui.canvas.pickerSearch",
        "session.ui.canvas.decorID",
        "session.ui.byView.canvas.selectedCategory",
        "account.canvas.loadouts",
    },
    fn = function(state)
        local list = state.session.canvas.dyeableDecor
        VDS.Log:Debug("catalog", string.format(
            "[C6] canvas.decor.rows re-derive dyeableDecor=%d",
            (type(list) == "table") and #list or -1))
        if type(list) ~= "table" or #list == 0 then return {} end

        local search     = state.session.ui.canvas.pickerSearch
        if search ~= "" then search = search:lower() end
        local selectedID = state.session.ui.canvas.decorID
        local catFilter  = state.session.ui.byView.canvas.selectedCategory
        local loadouts   = state.account.canvas.loadouts
        local SD = VDS.StaticData  -- shipped DB read, not runtime catalog

        local rows = {}
        for _, item in ipairs(list) do
            local name = item.name or ""
            local matchSearch = search == "" or name:lower():find(search, 1, true)
            local matchCat    = catFilter == nil or item.categoryID == catFilter
            if matchSearch and matchCat then
                local lds = loadouts[item.decorID]
                rows[#rows + 1] = {
                    kind         = "decor",
                    decorID      = item.decorID,
                    name         = item.name,
                    isOwned      = item.isOwned,
                    icon         = item.icon,
                    -- Channels from VDS_DecorDyeSlotsDB (covers ALL decor; the
                    -- runtime catalog's item.channels only populates for owned
                    -- variants -- ~30% of items).
                    channels     = SD.GetDecorChannelCount(item.decorID),
                    selected     = selectedID == item.decorID,
                    loadoutCount = lds and #lds or 0,
                }
            end
        end
        -- Sort: rows with saved loadouts first (tree-era affordance), then
        -- alphabetical. The catalog arrives alphabetical so secondary sort by
        -- name is stable but explicit for clarity.
        table.sort(rows, function(a, b)
            local hasA = (a.loadoutCount or 0) > 0
            local hasB = (b.loadoutCount or 0) > 0
            if hasA ~= hasB then return hasA end
            return (a.name or "") < (b.name or "")
        end)
        return rows
    end,
})

-- Menu items for the category dropdown. Sorted by count desc so the dominant
-- category (Furnishings) sits at top. "All" is a sentinel row at index 1.
--
-- Shape: { { id = nil|catID, name, count }, ... }
Selectors:Register("canvas.categories.menuItems", {
    reads = {
        "session.canvas.dyeableDecor",
        "session.canvas.decorCategoryNames",
    },
    fn = function(state)
        local decor = state.session.canvas.dyeableDecor
        -- Pre-catalog-ready: surface a single "Loading..." row instead of
        -- "All Categories (0)" which reads as broken.
        if #decor == 0 then
            return { { id = nil, name = "Loading...", count = 0, loading = true } }
        end
        local names = state.session.canvas.decorCategoryNames
        local counts = {}
        local total = 0
        for _, item in ipairs(decor) do
            total = total + 1
            local cid = item.categoryID
            if cid then counts[cid] = (counts[cid] or 0) + 1 end
        end
        local rows = { { id = nil, name = "All Categories", count = total } }
        for cid, n in pairs(counts) do
            rows[#rows + 1] = { id = cid, name = names[cid] or ("Category " .. cid), count = n }
        end
        -- "All" stays at index 1; categories sort by count desc.
        table.sort(rows, function(a, b)
            if a.id == nil then return true  end
            if b.id == nil then return false end
            return a.count > b.count
        end)
        return rows
    end,
})
