-- VDS_Selectors_Canvas
-- ============================================================================
-- Canvas (virtual dye trial) selectors. The Canvas tab opens 3 popout panels
-- (picker tree + preview ModelScene + dye gallery) plus a 3-channel state
-- machine for the active item.
--
-- ADR-001: catalog reads route through DecorInfoResolver, NOT here. The
-- canvas.preview.title selector emits the decorID; the modelPreview widget
-- calls Resolver:Resolve(decorID) at paint time and reads .name from the
-- resolved info (per ADR-001 amended).

local Selectors = VDS.Selectors

-- Distinct from ui.searchText so the two search boxes don't fight each other.
Selectors:Register("canvas.pickerSearch", {
    reads = { "session.ui.canvas.pickerSearch" },
    fn    = function(state) return state.session.ui.canvas.pickerSearch end,
})

-- canvas.tree.nodes selector retired in Phase 5 (replaced by canvas.decor.rows
-- in VDS_Selectors_CanvasDecor.lua; flat list + col 3 chip wall took over from
-- the tree's nested decor + loadout layout).

-- Per-channel state: index + assigned dye + available flag for styling.

local function makeChannelSelector(channelIdx)
    return {
        reads = {
            "session.ui.canvas.dyes",
            "session.ui.canvas.activeChannel",
            "session.ui.canvas.numChannels",
            "session.ui.canvas.channelSet",
        },
        fn = function(state)
            local c = state.session.ui.canvas
            local slot = channelIdx - 1  -- 0-indexed to match Blizzard's dyeColorID3 / setGradientMaskWithDyes
            -- ~30% of decor uses sparse channel sets (e.g. {0,2}); use channelSet when present.
            local available = false
            if c.channelSet then
                for _, idx in ipairs(c.channelSet) do
                    if idx == slot then available = true; break end
                end
            else
                available = (channelIdx <= (c.numChannels or 0))
            end
            return {
                index     = channelIdx,
                dyeID     = c.dyes[slot],
                active    = (c.activeChannel == slot),
                available = available,
            }
        end,
    }
end

Selectors:Register("canvas.channel.1", makeChannelSelector(1))
Selectors:Register("canvas.channel.2", makeChannelSelector(2))
Selectors:Register("canvas.channel.3", makeChannelSelector(3))

-- Thin reads for binding the modelPreview widget (ADR-001 multi-field binding shape).

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

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

-- "Preview" / "Preview: <name>" / "Preview: #<id>" while catalog primes.

Selectors:Register("canvas.preview.title", {
    reads = { "session.ui.canvas.decorID" },
    calls = { "catalog.ready" },
    fn    = function(state)
        local decorID = state.session.ui.canvas.decorID
        if not decorID then return "Preview" end
        local info = VDS.DecorInfoResolver:Resolve(decorID)
        return info and info.name and ("Preview: " .. info.name)  -- exception(nullable): catalog lookup can miss pre-CATALOG_READY
            or ("Preview: #" .. decorID)
    end,
})

-- canvas.harmonise.enabled
-- --------------------------------------------------------------------------
-- True when the Harmonise button should be interactive:
--   - a decor is selected (decorID non-nil)
--   - the active channel has a dye to anchor on (dyes[activeChannel] non-nil)
--   - there is at least one OTHER channel in channelSet to fill (#channelSet > 1)
-- Mirrors how dispatchRandom gates on canvas.decorID. Gating is computed purely
-- from state so the BindingEngine drives Enable/Disable reactively.
Selectors:Register("canvas.harmonise.enabled", {
    reads = {
        "session.ui.canvas.decorID",
        "session.ui.canvas.dyes",
        "session.ui.canvas.activeChannel",
        "session.ui.canvas.channelSet",
        "session.ui.canvas.numChannels",
    },
    fn = function(state)
        local c = state.session.ui.canvas
        if not c.decorID then return false end
        if c.dyes[c.activeChannel] == nil then return false end
        -- Need at least 2 channels to have something to fill besides the anchor.
        if c.channelSet then
            return #c.channelSet > 1
        end
        return (c.numChannels or 0) > 1
    end,
})

-- canvas.harmonise.popup
-- --------------------------------------------------------------------------
-- Projects all available strategies + each strategy's autoFill preview picks
-- + owned flag for the popup list. Pure: resolves swatch RGB from catalog
-- infos, never calls Blizzard API.
-- Returns nil when: no decor selected, active channel has no dye, catalog
-- not ready, or only 1 channel (nothing to fill).
-- Chromatic anchor -> 7 strategy rows; neutral anchor -> 3 strategy rows.
local CHROMATIC_STRATEGIES = {
    { key = "Neutral",       label = "Neutral",            effect = "Grounds the colour. Safe with anything." },
    { key = "Monochromatic", label = "Monochromatic",      effect = "Lighter and darker of the same hue. Calm and cohesive." },
    { key = "Analogous",     label = "Analogous",          effect = "Wheel neighbours. Natural, blended." },
    { key = "Complement",    label = "Complementary",      effect = "The opposite hue. Bold pop of contrast." },
    { key = "Split",         label = "Split-complementary", effect = "Two flanking the opposite. Bold but softer." },
    { key = "Triad",         label = "Triadic",            effect = "Two evenly-spaced hues. Most energy." },
    { key = "Clash",         label = "Clash",              effect = "Deliberately discordant. Creates visual vibration." },
}
local NEUTRAL_STRATEGIES = {
    { key = "Tones",    label = "Tones",    effect = "Same undertone -- calm, cohesive neutral palette." },
    { key = "Contrast", label = "Contrast", effect = "Warm meets cool -- subtle contrast within neutrals." },
    { key = "Accent",   label = "Accent",   effect = "A vivid pop against the neutral." },
}

Selectors:Register("canvas.harmonise.popup", {
    reads = {
        "session.ui.canvas.decorID",
        "session.ui.canvas.dyes",
        "session.ui.canvas.activeChannel",
        "session.ui.canvas.channelSet",
        "session.ui.canvas.numChannels",
        "session.dyes.catalog",
        "session.dyes.ownership",
    },
    fn = function(state)
        local c       = state.session.ui.canvas
        if not c.decorID then return nil end
        local anchorID = c.dyes[c.activeChannel]
        if anchorID == nil then return nil end
        -- Need at least one other channel to fill.
        local otherCount
        if c.channelSet then
            local n = 0
            for _, idx in ipairs(c.channelSet) do
                if idx ~= c.activeChannel then n = n + 1 end
            end
            otherCount = n
        else
            otherCount = math.max(0, (c.numChannels or 0) - 1)
        end
        if otherCount == 0 then return nil end

        local infos     = state.session.dyes.catalog.infos
        local ownership = state.session.dyes.ownership.perDye
        local res = VDS.Harmony.forDye(anchorID, infos, ownership)
        if not res then return nil end  -- exception(nullable): pre-catalog or unknown anchor

        -- Choose strategy set based on whether anchor is chromatic or neutral.
        local anchorInfo  = infos[anchorID]  -- exception(nullable): catalog lookup can miss pre-CATALOG_READY
        local colorGroup  = anchorInfo and anchorInfo.colorGroup  -- exception(nullable): anchorInfo absent pre-catalog; no group = use chromatic strategies
        local isNeutral   = colorGroup ~= nil and VDS.HarmonyWheel.NEUTRALS[colorGroup] == true
        local strategies  = isNeutral and NEUTRAL_STRATEGIES or CHROMATIC_STRATEGIES

        local rows = {}
        for _, s in ipairs(strategies) do
            local picks = VDS.Harmony.autoFill(res, s.key, otherCount)
            -- Build per-pick swatch data (RGB from catalog infos, owned flag).
            local swatches = {}
            for _, p in ipairs(picks) do
                swatches[#swatches + 1] = {
                    dyeID    = p.dyeID,
                    rgb      = p.swatchRGB,
                    owned    = p.owned,
                }
            end
            -- Proportion-bar data (anchor swatch + suggestions, capped to the
            -- bar's ratio count) so the Canvas menu tooltip can render the same
            -- "area guide" bar as the Colors detail tooltip.
            local ratios   = VDS.HarmonyWheel.BAR[s.key] or {}
            local barPicks = {}
            if anchorInfo and anchorInfo.swatchRGB then  -- exception(nullable): anchorInfo absent pre-catalog
                barPicks[1] = { swatchRGB = anchorInfo.swatchRGB }
            end
            for _, p in ipairs(picks) do
                if #barPicks >= #ratios then break end
                barPicks[#barPicks + 1] = { swatchRGB = p.swatchRGB }
            end
            rows[#rows + 1] = {
                key      = s.key,
                label    = s.label,
                effect   = s.effect,
                swatches = swatches,
                ratios   = ratios,
                barPicks = (#barPicks >= 2) and barPicks or nil,  -- nil = too few for a bar
            }
        end
        return { anchorID = anchorID, rows = rows }
    end,
})

-- Empty-state hint: differentiates "still loading" from "no housing access".

Selectors:Register("canvas.picker.isEmpty", {
    reads = { "session.canvas.dyeableDecor" },
    fn    = function(state)
        local list = state.session.canvas.dyeableDecor
        return type(list) ~= "table" or #list == 0
    end,
})

Selectors:Register("canvas.picker.emptyHint", {
    reads = { "session.canvas.dyeableDecor", "session.canvas.catalog", "session.catalog.tick" },
    fn    = function(state)
        local list = state.session.canvas.dyeableDecor
        if type(list) == "table" and #list > 0 then return "" end
        -- Status-driven cold-start messaging. DecorInfoResolver dispatches
        -- CATALOG_STATUS_SET as it transitions loading -> retrying -> failed.
        local cat = state.session.canvas.catalog
        local C = VDS.Constants.COLOR_CODE
        if cat.status == "retrying" then
            return C.warn .. ("Catalog stuck, retrying... (%d/%d)"):format(cat.attempt or 0, cat.max or 3) .. C.reset  -- exception(nullable): attempt/max set only while retrying
        elseif cat.status == "failed" then
            return C.warn .. "Catalog unavailable." .. C.reset .. "\n"
                .. C.placeholder .. "Switch tabs and back, or /reload, to retry." .. C.reset
        end
        -- catalog.tick bumps once CATALOG_READY fires. Zero -> still loading.
        if state.session.catalog.tick == 0 then
            return C.placeholder .. "Loading dyeable decor..." .. C.reset
        end
        return C.warn .. "No dyeable decor available." .. C.reset .. "\n"
            .. C.placeholder .. "Housing access is required to populate the catalog." .. C.reset
    end,
})
