-- VDS.Memo
-- ============================================================================
-- Selector memoization (reselect-style). Spec: UI_WIDGET_TAXONOMY.md s11.
-- Ref: LUA_SAMPLES_UTILITIES.md s3c.
--
-- 1. Memo({ inputs={fn,...}, compute=fn }) -- 1-entry cache; compute runs on input change.
-- 2. MakeFactory({ inputs=..., compute=... }) -- factory(key) -> per-key selector.
--    Use when N callers each need a selector keyed by their own ID; a single shared
--    selector would thrash its 1-entry cache across N keys.
--
-- Reselect rules (spec s11):
--   1. Input selectors must return stable references -- no {} inside inputs.
--   2. Only memoize non-trivial compute; memoizing a direct field read adds overhead.
--   3. Per-instance selectors use MakeFactory.

VDS = VDS or {}
VDS.Memo = VDS.Memo or {}

local M = VDS.Memo

-- ===== Single-entry Memo (reselect style) =================================

-- Returns a memoized selector. Inputs called with selector args; results
-- compared by reference identity (Lua 5.1 == on tables; stable ref = unchanged).
function M.Memo(spec)
    if type(spec) ~= "table" then
        error("VDS.Memo.Memo: expected a spec table", 2)
    end
    if type(spec.compute) ~= "function" then
        error("VDS.Memo.Memo: spec.compute must be a function", 2)
    end
    local inputs = spec.inputs or {}
    if type(inputs) ~= "table" then
        error("VDS.Memo.Memo: spec.inputs must be a list of functions", 2)
    end
    for i, inputFn in ipairs(inputs) do
        if type(inputFn) ~= "function" then
            error(("VDS.Memo.Memo: spec.inputs[%d] must be a function"):format(i), 2)
        end
    end

    -- Cache state (closure-captured)
    local cachedInputs = nil        -- last inputs (list); nil = no cache yet
    local cachedResult = nil
    local n = #inputs

    local function call(...)
        -- Zero inputs = always recompute (no cache; allowed for API consistency).
        if n == 0 then
            return spec.compute(...)
        end

        local currentInputs = {}
        for i = 1, n do
            currentInputs[i] = inputs[i](...)
        end

        -- Reference equality: stable input reference = cache hit (contract).
        local hit = cachedInputs ~= nil
        if hit then
            for i = 1, n do
                if cachedInputs[i] ~= currentInputs[i] then
                    hit = false
                    break
                end
            end
        end

        if hit then
            return cachedResult
        end

        cachedInputs = currentInputs
        cachedResult = spec.compute(unpack(currentInputs, 1, n))
        return cachedResult
    end

    -- Callable table with :Clear() for path-based InvalidateMemos in Selectors.lua.
    -- Without :Clear() the reactive invalidation system can't evict stale values.
    return setmetatable({
        Clear = function() cachedInputs = nil; cachedResult = nil end,
    }, { __call = function(_self, ...) return call(...) end })
end

-- ===== Factory (per-instance memoization) =================================

-- Returns factory(key) -> selector. Each key gets its own 1-entry cache.
-- spec.inputs = list of (state, key, ...) -> value; key is captured in the closure.
function M.MakeFactory(spec)
    if type(spec) ~= "table" then
        error("VDS.Memo.MakeFactory: expected a spec table", 2)
    end
    if type(spec.compute) ~= "function" then
        error("VDS.Memo.MakeFactory: spec.compute must be a function", 2)
    end
    local inputs = spec.inputs or {}

    -- Strong cache (not weak): weak-value caches cause GC to evict selectors held only
    -- in local vars, rebuilding them with empty caches on next access. Evict explicitly.
    local cache = {}

    local function makeSelector(key)
        if cache[key] then return cache[key] end

        -- Build a Memo selector with `key` captured in input/compute closures.
        local boundInputs = {}
        for i, inputFn in ipairs(inputs) do
            boundInputs[i] = function(...) return inputFn(key, ...) end
        end

        local selector = M.Memo({
            inputs = boundInputs,
            compute = function(...) return spec.compute(key, ...) end,
        })

        cache[key] = selector
        return selector
    end

    -- Callable table: __call = factory(key), plus :Evict/:EvictAll (Lua can't attach methods to functions).
    local factory = setmetatable({
        Evict = function(_self, key) cache[key] = nil end,
        EvictAll = function() cache = {} end,
    }, {
        __call = function(_self, key) return makeSelector(key) end,
    })
    return factory
end

-- ===== Helpers ============================================================

-- Identity input selector: Memo({ inputs = { Memo.Identity }, compute = fn })
function M.Identity(...)
    return (...)
end

-- Dotted-path input selector: Memo.Path("account.sets") -> function(state) -> state.account.sets
function M.Path(path)
    if type(path) ~= "string" or path == "" then
        error("VDS.Memo.Path: expected non-empty dotted string", 2)
    end
    local segments = {}
    for seg in path:gmatch("[^%.]+") do segments[#segments + 1] = seg end
    return function(state)
        local cur = state
        for _, seg in ipairs(segments) do
            if type(cur) ~= "table" then return nil end
            cur = cur[seg]
        end
        return cur
    end
end
