-- VDS_Selectors_Progress
-- ============================================================================
-- Top-level mode + progress selectors. The smallest unit: leaf-state reads
-- that drive header progress bars, title-row checkboxes, and the
-- view-selection seam (currentView).
--
-- ADR-001 amendment: catalog.ready reads session.catalog.tick so any bound
-- widget that depends on resolved housing-catalog data invalidates the
-- moment DecorInfoResolver dispatches CATALOG_READY.

local Selectors = VDS.Selectors

-- cells sorted by COLOR_GROUP_ORDER so bar slices match the Colors tab.
-- Counts from catalog (not ownership.totalCount): observer scan can run before C_DyeColor is cached.
Selectors:Register("progress.collection", {
    reads = { "session.dyes.ownership", "session.dyes.catalog" },
    fn    = function(state)
        local o = state.session.dyes.ownership
        local catalog = state.session.dyes.catalog
        local cells = {}
        local owned, total = 0, 0
        if catalog and catalog.dyes then  -- exception(nullable): catalog absent pre-DYE_DATA_INIT
            local perDye = o.perDye or {}
            local infos  = catalog.infos or {}
            for _, dyeID in ipairs(catalog.dyes) do
                local isOwned = (perDye[dyeID] or 0) > 0
                cells[#cells + 1] = { dyeID = dyeID, owned = isOwned }
                total = total + 1
                if isOwned then owned = owned + 1 end
            end
            -- COLOR_GROUP_ORDER, then name; unclassified sinks to end.
            local order = VDS.StaticData.COLOR_GROUP_ORDER
            local groupIdx = {}
            for i, g in ipairs(order) do groupIdx[g] = i end
            local SINK = #order + 1
            table.sort(cells, function(a, b)
                local ia = infos[a.dyeID]
                local ib = infos[b.dyeID]
                local ga = (ia and ia.colorGroup and groupIdx[ia.colorGroup]) or SINK  -- exception(nullable): catalog info can miss; sinks to end
                local gb = (ib and ib.colorGroup and groupIdx[ib.colorGroup]) or SINK  -- exception(nullable): catalog info can miss; sinks to end
                if ga ~= gb then return ga < gb end
                local na = (ia and ia.name) or ""  -- exception(nullable): catalog info can miss
                local nb = (ib and ib.name) or ""  -- exception(nullable): catalog info can miss
                return na < nb
            end)
        end
        return { owned = owned, total = total, cells = cells }
    end,
})

-- "Colors: N/M:" for the search row, left of the progress bar.
Selectors:Register("progress.collection.label", {
    reads = { "session.dyes.ownership", "session.dyes.catalog" },
    fn    = function(state)
        local o = state.session.dyes.ownership
        local catalog = state.session.dyes.catalog
        if not (catalog and catalog.dyes) then return "Loading..." end  -- exception(nullable): catalog absent pre-DYE_DATA_INIT
        local owned, total = 0, 0
        local perDye = o.perDye or {}
        for _, dyeID in ipairs(catalog.dyes) do
            total = total + 1
            if (perDye[dyeID] or 0) > 0 then owned = owned + 1 end
        end
        if total == 0 then return "Loading..." end
        return string.format("Colors: %d/%d:", owned, total)
    end,
})

-- Title-row toggles (persisted in account.config).

Selectors:Register("filter.owned", {
    reads = { "account.config.ownedOnly" },
    fn    = function(state) return state.account.config.ownedOnly end,
})

Selectors:Register("config.replaceBlizzard", {
    reads = { "account.config.replaceBlizzard" },
    fn    = function(state) return state.account.config.replaceBlizzard end,
})

-- Toggles VDS.Log:Info verbosity.
Selectors:Register("config.debug", {
    reads = { "account.config.debug" },
    fn    = function(state) return state.account.config.debug end,
})

-- Flips account.minimap.hide (LibDBIcon negative form) to the positive "Show" value.
Selectors:Register("config.minimapShown", {
    reads = { "account.minimap.hide" },
    fn    = function(state) return not state.account.minimap.hide end,
})

-- Mode flags: drive visibleWhen on per-view widgets; mode.* for single-mode gates.

Selectors:Register("mode.default", {
    reads = { "session.ui.filterMode", "session.ui.live.active" },
    fn    = function(state)
        return (not state.session.ui.live.active)
           and state.session.ui.filterMode ~= "canvas"
    end,
})

Selectors:Register("mode.canvas", {
    reads = { "session.ui.filterMode", "session.ui.live.active" },
    fn    = function(state)
        return (not state.session.ui.live.active)
           and state.session.ui.filterMode == "canvas"
    end,
})

Selectors:Register("mode.live", {
    reads = { "session.ui.live.active" },
    fn    = function(state) return state.session.ui.live.active end,
})

Selectors:Register("mode.herbs", {
    reads = { "session.ui.filterMode" },
    fn    = function(state) return state.session.ui.filterMode == "herbs" end,
})

-- ADR-001: invalidation seam for housing-catalog reads; widgets declare `calls = { "catalog.ready" }`.

Selectors:Register("catalog.ready", {
    reads = { "session.catalog.tick" },
    fn    = function(state) return state.session.catalog.tick end,
})

-- Thin wrappers so the BindingEngine has a selector name for raw-state bindings.

Selectors:Register("ui.searchText", {
    reads = { "session.ui.searchText" },
    fn    = function(state) return state.session.ui.searchText end,
})
