-- VDS_Controller_Canvas
-- ============================================================================
-- OnClick handlers for canvas view widgets. Row/channel factories delegate
-- here (cookbook 08); free-standing buttons wired in :Wire().

VDS = VDS or {}
VDS.Controller_Canvas = VDS.Controller_Canvas or {}

local C = VDS.Controller_Canvas
local A = VDS.Constants.ACTIONS

-- Loadout chip click. Reads decorID + dyes from the projected ed (ADR-041);
-- selector stamps decorID into each row so we don't dive back into state.
function C:OnLoadoutChipClick(loadout)
    if not (loadout and loadout.dyes and loadout.decorID) then return end  -- exception(boundary): click can race pool reconfigure
    local SD = VDS.StaticData
    local channelSet  = SD.GetDecorChannelSet(loadout.decorID)   or { 0, 1, 2 }
    local numChannels = SD.GetDecorChannelCount(loadout.decorID) or 3  -- exception(nullable): channel count unknown pre-catalog; assume 3
    VDS.Store:Dispatch({
        type    = A.CANVAS_APPLY_LOADOUT,
        payload = {
            decorID     = loadout.decorID,
            numChannels = numChannels,
            channelSet  = channelSet,
            dyes        = loadout.dyes,
        },
    })
end

-- "+" save-tile click. Opens the existing save-loadout popup (same path as the
-- Save button in the action row).
function C:OnLoadoutSaveClick()
    local state = VDS.Store:GetState()  -- top-level only: snapshot decorID + current dyes
    local canvas = state.session.ui.canvas
    if not canvas.decorID then
        print(VDS.Constants.CHAT_PREFIX .. " Save Loadout: select a decor item first")
        return
    end
    local dyesSnap = { [0] = canvas.dyes[0], [1] = canvas.dyes[1], [2] = canvas.dyes[2] }
    local popup = _G.StaticPopup_Show("VDS_CANVAS_SAVE_LOADOUT")
    if popup then
        popup.data = { decorID = canvas.decorID, dyes = dyesSnap }
    end
end

-- Flat-list row click. Reads the row's projected ed envelope (decorID +
-- channels are in ed by the time the row was painted).  No expand-toggle
-- (loadouts moved to the col 3 chip wall in Phase 4).
function C:OnDecorListClick(ed)
    if not ed.decorID then return end
    local SD = VDS.StaticData
    local channelSet  = SD.GetDecorChannelSet(ed.decorID)   or { 0, 1, 2 }
    local numChannels = SD.GetDecorChannelCount(ed.decorID) or 3  -- exception(nullable): channel count unknown pre-catalog; assume 3
    VDS.Store:Dispatch({
        type    = A.CANVAS_SET_DECOR,
        payload = {
            decorID     = ed.decorID,
            numChannels = numChannels,
            channelSet  = channelSet,
        },
    })
end

-- Tree-era OnTreeNodeClick + OnLoadoutClick handlers retired in Phase 5.
-- Replacements:
--   OnDecorListClick (above)       -- decor row click in the flat list
--   OnLoadoutChipClick (above)     -- loadout chip click in col 3 chip wall

function C:OnLoadoutDelete(decorID, idx)
    if not decorID or not idx then return end
    VDS.Store:Dispatch({
        type    = A.CANVAS_DELETE_LOADOUT,
        payload = { decorID = decorID, idx = idx },
    })
end

function C:OnLoadoutRename(loadout)
    if not (loadout and loadout.decorID and loadout.idx) then return end  -- exception(boundary): click can race pool reconfigure
    local dlg = _G.StaticPopup_Show("VDS_CANVAS_RENAME_LOADOUT")
    if dlg then
        dlg.data = { decorID = loadout.decorID, idx = loadout.idx, oldName = loadout.name }
        if dlg.EditBox then  -- exception(boundary): StaticPopup EditBox exists only for hasEditBox dialogs
            dlg.EditBox:SetText(loadout.name or "")
            dlg.EditBox:HighlightText()
            dlg.EditBox:SetFocus()
        end
    end
end

function C:OnChannelClick(channelIdx)
    local slot = channelIdx - 1  -- state is 0-indexed (gradient-mask API)
    -- Sparse-aware: some decor uses {0,2} skipping ch1. EnableMouse(false) on
    -- unavailable channel rows is first line; this is defense-in-depth.
    local canvas = VDS.Store:GetState().session.ui.canvas  -- top-level only: channel click gate
    local set = canvas.channelSet
    if set then
        local found = false
        for _, idx in ipairs(set) do if idx == slot then found = true; break end end
        if not found then return end
    elseif channelIdx > canvas.numChannels then
        return
    end
    VDS.Store:Dispatch({
        type    = A.CANVAS_SET_ACTIVE_CHANNEL,
        payload = { ch = slot },
    })
end

local function dispatchRandom()
    local state = VDS.Store:GetState()  -- top-level only: needs channel set + scope dye-IDs
    local canvas = state.session.ui.canvas
    if not canvas.decorID then
        print(VDS.Constants.CHAT_PREFIX .. " Random: select a decor item first")
        return
    end
    -- Canvas is what-if preview -- no ownership filter. Pool = scope dyes when
    -- scope is set, else every dye in the catalog.
    local scopeDyes = VDS.Selectors:Call("canvas.scope.dyeIDs", state, {})
    local pool = {}
    if scopeDyes then
        for dyeID in pairs(scopeDyes) do pool[#pool + 1] = dyeID end
    else
        for dyeID in pairs(state.session.dyes.catalog.dyes or {}) do pool[#pool + 1] = dyeID end
    end
    if #pool == 0 then
        local scope = state.session.ui.byView.canvas.scope
        local label = scope and (" in '" .. scope.name .. "'") or ""
        print(VDS.Constants.CHAT_PREFIX .. " Random: no dyes available" .. label)
        return
    end
    local dyes = { [0] = nil, [1] = nil, [2] = nil }
    -- Sparse-aware: only fill channels in channelSet; dense fallback when DB lacks the entry.
    local set = canvas.channelSet
    if set then
        for _, idx in ipairs(set) do
            dyes[idx] = pool[math.random(1, #pool)]
        end
    else
        for ch = 0, canvas.numChannels - 1 do
            dyes[ch] = pool[math.random(1, #pool)]
        end
    end
    VDS.Store:Dispatch({
        type    = A.CANVAS_RANDOM,
        payload = { dyes = dyes },
    })
end

local function dispatchClear()
    -- Channels are 0-indexed; state has slots [0]/[1]/[2] regardless of decor
    -- channelSet. Clear all three unconditionally so sparse decor ({0,2})
    -- doesn't leave channel 2 stuck on a previous dye.
    for ch = 0, 2 do
        VDS.Store:Dispatch({ type = A.CANVAS_CLEAR_CHANNEL, payload = { ch = ch } })
    end
end

local function dispatchSaveLoadout()
    local state = VDS.Store:GetState()  -- top-level only: save reads current decorID + dyes
    local canvas = state.session.ui.canvas
    if not canvas.decorID then
        print(VDS.Constants.CHAT_PREFIX .. " Save Loadout: select a decor item first")
        return
    end
    -- Shallow-copy: popup's OnAccept fires later; snapshot prevents mutation.
    local dyesSnap = { [0] = canvas.dyes[0], [1] = canvas.dyes[1], [2] = canvas.dyes[2] }
    local popup = _G.StaticPopup_Show("VDS_CANVAS_SAVE_LOADOUT")
    if popup then
        popup.data = { decorID = canvas.decorID, dyes = dyesSnap }
    end
end

local function dispatchScopeReset()
    VDS.Store:Dispatch({ type = A.CANVAS_SET_SCOPE, payload = { value = nil } })
end

-- ============================================================================
-- Canvas Harmonise menu (MenuUtil)
-- ============================================================================
-- Replaces the broken bespoke popup (Iron Invariant 5: reuse Blizzard
-- primitives). MenuUtil owns positioning, dismissal, and click dispatch --
-- no custom frame, no click-catcher, no z-order juggling.
--
-- Entry label = strategy name + inline WHITE8X8 colour swatches (vertex-tint
-- texture escapes, same technique as buildProportionBar in TooltipRecipes).
-- Dim unowned swatches by halving rgb (owned=false -> rgb*0.5).
-- ============================================================================

-- Build the swatch-escape string for one strategy row.
-- Each swatch: 14x14 WHITE8X8 block, vertex-tinted to the dye's rgb.
local SWATCH_SIZE = 14
local function buildSwatchEscapes(swatches)
    local parts = {}
    for _, s in ipairs(swatches) do
        local rgb = s.rgb
        local r, g, b
        if s.owned then
            r = math.floor(rgb.r * 255)
            g = math.floor(rgb.g * 255)
            b = math.floor(rgb.b * 255)
        else
            -- Dim unowned swatches.
            r = math.floor(rgb.r * 255 * 0.5)
            g = math.floor(rgb.g * 255 * 0.5)
            b = math.floor(rgb.b * 255 * 0.5)
        end
        parts[#parts + 1] = string.format(
            "|TInterface\\Buttons\\WHITE8X8:%d:%d:0:0:8:8:0:8:0:8:%d:%d:%d|t",
            SWATCH_SIZE, SWATCH_SIZE, r, g, b)
    end
    return table.concat(parts, " ")
end

-- Apply a strategy key. MenuUtil dismisses itself after the callback fires.
local function dispatchHarmonise(strategyKey)
    local state  = VDS.Store:GetState()  -- top-level only: harmonise reads canvas + catalog
    local canvas = state.session.ui.canvas
    if not canvas.decorID then
        _G.print(VDS.Constants.CHAT_PREFIX .. " Harmonise: select a decor item first")
        return
    end
    local anchorID = canvas.dyes[canvas.activeChannel]
    if not anchorID then
        _G.print(VDS.Constants.CHAT_PREFIX .. " Harmonise: active channel has no dye to anchor on")
        return
    end

    -- Collect other channels (those in channelSet, excluding activeChannel).
    local otherChannels = {}
    if canvas.channelSet then
        for _, idx in ipairs(canvas.channelSet) do
            if idx ~= canvas.activeChannel then
                otherChannels[#otherChannels + 1] = idx
            end
        end
    else
        for ch = 0, (canvas.numChannels or 0) - 1 do
            if ch ~= canvas.activeChannel then
                otherChannels[#otherChannels + 1] = ch
            end
        end
    end

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

    local picks = VDS.Harmony.autoFill(res, strategyKey, #otherChannels)

    -- Build the full dyes map: active channel keeps current dye; others get picks.
    -- Canvas is a what-if preview -- no ownership filter (mirrors dispatchRandom comment).
    local dyes = { [0] = canvas.dyes[0], [1] = canvas.dyes[1], [2] = canvas.dyes[2] }
    for i, ch in ipairs(otherChannels) do
        local pick = picks[i]
        if pick then
            dyes[ch] = pick.dyeID
        end
        -- If autoFill returned fewer picks than channels (tiny family), leave that
        -- channel's current dye unchanged rather than clearing it.
    end

    VDS.Store:Dispatch({
        type    = A.CANVAS_HARMONISE,
        payload = { dyes = dyes },
    })
end

-- Open the MenuUtil context menu anchored to the harmoniseBtn.
local function openHarmoniseMenu(owner)
    if not VDS.Selectors:Call("canvas.harmonise.enabled", VDS.Store:GetState(), {}) then  -- top-level only: gate re-checked on click
        return
    end
    local data = VDS.Selectors:Call("canvas.harmonise.popup", VDS.Store:GetState(), {})  -- top-level only: reads current canvas + catalog state
    if not data or #data.rows == 0 then return end

    MenuUtil.CreateContextMenu(owner, function(_owner, rootDescription)
        rootDescription:CreateTitle("Harmonise")
        for _, row in ipairs(data.rows) do
            local swatchStr = buildSwatchEscapes(row.swatches)
            local label = row.label
            if swatchStr ~= "" then
                label = label .. "  " .. swatchStr
            end
            local capturedKey = row.key
            local capturedRow = row
            local btn = rootDescription:CreateButton(label, function()
                dispatchHarmonise(capturedKey)
            end)
            -- Hover tooltip: the strategy's effect + the same proportion "area
            -- guide" bar as the Colors detail tooltip. Canvas is where players
            -- actually trial harmonies, so the richer guidance lives here too.
            btn:SetTooltip(function(tooltip)
                _G.GameTooltip_SetTitle(tooltip, capturedRow.label)
                _G.GameTooltip_AddNormalLine(tooltip, capturedRow.effect)
                if capturedRow.barPicks then
                    _G.GameTooltip_AddNormalLine(tooltip,
                        VDS.TooltipRecipes.BuildProportionBar(capturedRow.barPicks, capturedRow.ratios), false)
                    _G.GameTooltip_AddNormalLine(tooltip, "Area guide for the decor")
                end
            end)
        end
    end)
end

local CLICK_HANDLERS = {
    ["canvasPreview.randomBtn"]      = dispatchRandom,
    ["canvasPreview.clearBtn"]       = dispatchClear,
    ["canvasPreview.saveLoadoutBtn"] = dispatchSaveLoadout,
    ["canvasScope.resetBtn"]         = dispatchScopeReset,
}

function C:Wire(rootFrame)
    if not (rootFrame and rootFrame.widgets) then return end  -- exception(false-positive): Wire-on-empty-rootFrame is a supported no-op (wire smoke contract)
    for id, handler in pairs(CLICK_HANDLERS) do
        local widget = rootFrame.widgets[id]
        if widget and widget.SetScript then  -- exception(false-positive): Wire tolerates absent widgets (empty-root contract)
            widget:SetScript("OnClick", handler)
        end
    end

    for ch = 1, 3 do
        local widget = rootFrame.widgets["canvasPreview.ch" .. ch]
        if widget and widget.SetScript then  -- exception(false-positive): Wire tolerates absent widgets (empty-root contract)
            widget:SetScript("OnClick", function(_self) C:OnChannelClick(ch) end)
        end
    end

    -- Harmonise button: open MenuUtil menu on click; enabled gate mirrors
    -- Random's decorID check but also requires the active channel to have an
    -- anchor dye and at least one other channel to fill.
    local harmoniseBtn = rootFrame.widgets["canvasPreview.harmoniseBtn"]
    if harmoniseBtn and harmoniseBtn.SetScript then  -- exception(false-positive): Wire tolerates absent widgets (empty-root contract)
        harmoniseBtn:SetScript("OnClick", function(self)
            openHarmoniseMenu(self)
        end)

        -- Enabled gate: subscribe to relevant invalidation actions so the button
        -- stays in sync as the user picks decor + channels.
        local function refreshHarmoniseEnabled()
            local enabled = VDS.Selectors:Call("canvas.harmonise.enabled", VDS.Store:GetState(), {})  -- top-level only: Store subscriber callback
            harmoniseBtn:SetEnabled(enabled)
        end
        refreshHarmoniseEnabled()
        if C._harmoniseEnabledToken then VDS.Store:Unsubscribe(C._harmoniseEnabledToken) end
        C._harmoniseEnabledToken = VDS.Store:Subscribe(function(actionType)
            if actionType == A.CANVAS_SET_DECOR
                or actionType == A.CANVAS_SET_CHANNEL_DYE
                or actionType == A.CANVAS_CLEAR_CHANNEL
                or actionType == A.CANVAS_RANDOM
                or actionType == A.CANVAS_HARMONISE
                or actionType == A.CANVAS_APPLY_LOADOUT
                or actionType == A.CANVAS_SET_ACTIVE_CHANNEL
                or actionType == A.CANVAS_CYCLE_ACTIVE_CHANNEL then
                refreshHarmoniseEnabled()
            end
        end)
    end
end
