-- VDS_Selectors_Harmony
-- ============================================================================
-- Selectors for the Colors-tab harmony band (Surface 1, Stage A).
-- The pure engine lives in Core/VDS_Harmony.lua; these are thin selector
-- wrappers that feed the BindingEngine. Iron Invariants:
--   §1  pure -- no Blizzard API calls
--   §3  accurate reads/calls declared
--   §4  no defensive guards at internal boundaries

local Selectors = VDS.Selectors
local HW = VDS.HarmonyWheel

-- harmony.dye
-- --------------------------------------------------------------------------
-- Runs the harmony engine for the selected dye. Returns nil when no dye is
-- selected, pre-catalog, or the catalog entry has no swatchRGB.
-- exception(nullable): engine returns nil pre-catalog or on bad id
Selectors:Register("harmony.dye", {
    reads = {
        "session.ui.selectedDyeID",
        "session.dyes.catalog",
        "session.dyes.ownership",
    },
    fn = function(state)
        local id = state.session.ui.selectedDyeID
        if not id then return nil end
        local infos     = state.session.dyes.catalog.infos
        local ownership = state.session.dyes.ownership.perDye
        return VDS.Harmony.forDye(id, infos, ownership)
    end,
})

-- harmony.visible
-- --------------------------------------------------------------------------
-- True when the harmony band should render: a dye is selected AND the active
-- view is the Colors tab (filterMode == "category"). Hidden in Custom, Theme,
-- Herbs, Canvas, Stock.
Selectors:Register("harmony.visible", {
    reads = {
        "session.ui.selectedDyeID",
        "session.ui.filterMode",
    },
    fn = function(state)
        local ui = state.session.ui
        return ui.selectedDyeID ~= nil and ui.filterMode == "category"
    end,
})

-- detail.showMeta
-- --------------------------------------------------------------------------
-- The name / Color / Type / Owned cluster shows only when a dye detail is up but
-- the harmony band is NOT (Theme / Stock views). In the Colors view the band
-- replaces the meta -- the slim. (detail.has.dye is already false in herbs /
-- canvas / custom, so those views are unaffected.)
Selectors:Register("detail.showMeta", {
    reads = { "session.ui.selectedDyeID", "session.ui.filterMode" },
    calls = { "detail.has.dye", "harmony.visible" },
    fn = function(state)
        return Selectors:Call("detail.has.dye", state, {})
            and not Selectors:Call("harmony.visible", state, {})
    end,
})

-- live.harmonyShowChromatic / live.harmonyShowNeutral
-- --------------------------------------------------------------------------
-- Gate the Live strip's two button sets. Chromatic shows when live is active
-- and the focused slot holds a chromatic (or no) dye; neutral shows only when
-- the focused slot's dye is explicitly neutral (Brown/Grey/Black/White).
-- Both selectors read the same state paths as dispatchHarmonise in the
-- controller; kept pure (no Blizzard API calls).
--
-- Classification: neutral = HW.NEUTRALS[colorGroup]; chromatic = HW.ANGLE[colorGroup].
-- When no dye is focused or catalog not ready, colorGroup is nil, which falls
-- through to chromatic=true so the strip shows its default (chromatic) state.
--
-- exception(nullable): catalog.infos lookup can miss pre-CATALOG_READY (focusedDyeID
--   present but no entry yet); treat as chromatic (colorGroup stays nil).

local function focusedDyeColorGroup(state)
    local live = state.session.ui.live
    if not (live.active and live.slots and live.focusedSlot) then return nil end
    for _, slot in ipairs(live.slots) do
        if slot.ID == live.focusedSlot then
            local id = slot.dyeColorID
            if not (id and id ~= 0) then return nil end  -- exception(boundary): Blizzard sentinel 0 = no dye
            local info = state.session.dyes.catalog.infos[id]
            if not info then return nil end  -- exception(nullable): catalog miss pre-CATALOG_READY; no group = treat as chromatic
            return info.colorGroup
        end
    end
    return nil
end

Selectors:Register("live.harmonyShowChromatic", {
    reads = {
        "session.ui.live.active",
        "session.ui.live.slots",
        "session.ui.live.focusedSlot",
        "session.dyes.catalog",
    },
    fn = function(state)
        if not state.session.ui.live.active then return false end
        local group = focusedDyeColorGroup(state)
        -- Chromatic set is the default: show when focused dye is chromatic OR
        -- no dye is focused yet (so the strip isn't blank on live-mode enter).
        return group == nil or HW.ANGLE[group] ~= nil
    end,
})

Selectors:Register("live.harmonyShowNeutral", {
    reads = {
        "session.ui.live.active",
        "session.ui.live.slots",
        "session.ui.live.focusedSlot",
        "session.dyes.catalog",
    },
    fn = function(state)
        if not state.session.ui.live.active then return false end
        local group = focusedDyeColorGroup(state)
        return group ~= nil and HW.NEUTRALS[group] == true
    end,
})

-- harmony.band.data
-- --------------------------------------------------------------------------
-- Projects harmony.dye output into the flat data shape the harmonyBand widget
-- kind consumes. Returns a table with:
--   { neutral, analogous, complement, clash, triad }
-- where each value is an array (possibly empty) of picks:
--   { dyeID, swatchRGB, swatchStart, swatchEnd, owned, dL }
-- swatchStart/swatchEnd resolved at paint time via DyeOwnershipObserver.
-- Returns nil when harmony.dye returns nil.
Selectors:Register("harmony.band.data", {
    reads = {
        "session.ui.selectedDyeID",
        "session.ui.filterMode",
        "session.dyes.catalog",
        "session.dyes.ownership",
    },
    calls = { "harmony.dye", "harmony.visible" },
    fn = function(state)
        -- Gate to the Colors view: the wash now lives on the detail panel (not the
        -- band frame), so the band data must go nil when the band isn't shown
        -- (Theme / Stock / Custom) or the wash would linger there.
        if not Selectors:Call("harmony.visible", state, {}) then return nil end
        local result = Selectors:Call("harmony.dye", state, {})
        if not result then return nil end
        -- Same set as Live/Canvas: Neutral, Mono, Analogous, Complement, Split,
        -- Triad, Clash (chromatic) or Tones, Contrast, Accent (neutral anchor).
        -- Every key is an array (possibly empty); the band hides empty groups.
        local by = result.byRelationship
        return {
            anchorID   = result.anchor,
            neutral    = result.neutral    or {},
            mono       = result.mono       or {},
            analogous  = by.Analogous      or {},
            complement = by.Complement     or {},
            split      = by.Split          or {},
            triad      = by.Triad          or {},
            clash      = by.Clash          or {},
            -- neutral-anchor (value/temperature) lens; empty for chromatic anchors
            tones      = result.tones      or {},
            contrast   = result.contrast   or {},
            accent     = result.accent     or {},
        }
    end,
})
