-- DopingControl core/config.lua
-- SavedVariables defaults + fill-in: single DEFAULTS table, per-field fill-in
-- with nil-checks (NEVER 'or' for booleans -- would flip saved false),
-- copy-on-assign for table values so editing a live value can never mutate
-- the defaults.
-- Pure Lua 5.0, no WoW API -- dofile-loadable offline.

DopingControl = DopingControl or {}
local DC = DopingControl

-- SavedVariables root (the client replaces this global at ADDON_LOADED;
-- core/init.lua then calls DC.EnsureDefaults(DopingControlDB)).
DopingControlDB = DopingControlDB or {}

-- ------------------------------------------------------------------
-- Defaults
--
-- Two keys intentionally have NO entry here (a nil value cannot live in a
-- Lua table):
--   framePos     -- default nil = "no saved position, use the anchor
--                   default"; written by the UI on drag as
--                   { point=..., relPoint=..., x=..., y=... }.
--   expectations -- default nil = "not initialized yet"; EnsureDefaults
--                   special-cases it below: the saved shape
--                   is PER CLASS (db.expectations[role][class][slotId]),
--                   materialized/migrated from the DC.DEFAULT_EXPECT seed
--                   by DC.EnsureExpectations.
-- ------------------------------------------------------------------
DC.DEFAULTS = {
    testMode          = false,  -- /dc test: simulated raid
    showMinimapButton = true,
    minimapAngle      = 215,    -- degrees around the minimap rim
    readyCheckScan    = true,   -- auto-scan on READY_CHECK
    readyCheckOpen    = true,   -- auto-open window on READY_CHECK
    collapse          = {},     -- per-role collapsed flags, e.g. collapse.RANGED = true
    filterGapsOnly    = false,  -- toolbar filter All | Gaps only
    roles             = {},     -- confirmed roles keyed by player NAME
    -- Weapon skill books: db.skillBooks[playerName][weaponSkillName] =
    -- true|false. A quest weapon-skill book (+3 skill) is INVISIBLE on
    -- other players -- foreign weapon skill cannot be read at all (see
    -- data/hit.lua) -- so it is an opt-in the user ticks per player and
    -- per weapon skill; it only shifts that player's melee/ranged hit CAP.
    -- Unchecking stores an explicit false (never nil), exactly like the
    -- expectation grid, so "deliberately off" survives any later fill-in.
    -- The own character ignores these flags: its weapon skill is measured
    -- and a measured value already contains the book.
    skillBooks        = {},
}

-- ------------------------------------------------------------------
-- DC.SimOwnsStore(db) -> bool
--
-- The ONE owner predicate for DC.store, and the question every scan gate
-- asks -- never db.testMode alone. Both simulated modes replace the store
-- with the simulated raid, and while either is on "real data never beats
-- the simulation":
--   * db.testMode  -- /dc test, persisted (DEFAULTS above)
--   * DC.demoMode  -- /dc demo, transient (core/demo.lua)
--
-- Why it exists: the gates named db.testMode only, so a finished scan
-- overwrote the sim store while demo mode was still armed. The demo
-- expectation layer then graded the REAL raid against all six school
-- protection columns (five of which ship with no seed entry at all): every
-- raider grew fabricated MISSING cells, the banner vanished with the "sim"
-- source that had been the only marker that this is a mode and not a
-- measurement, and each row's whisper button went live against a real
-- player.
--
-- Why HERE and not in core/demo.lua, where the mode lives: config.lua loads
-- second, ahead of every gate that calls this, so the gates need no
-- load-order guard -- and a demo module that fails to load can never take
-- test mode's protection down with it (DC.demoMode is then simply nil).
--
-- db is optional; the fallback is for callers that do not hold one.
-- Always returns a real boolean -- the gates invert it.
-- ------------------------------------------------------------------
function DC.SimOwnsStore(db)
    db = db or DC.db or DopingControlDB
    if db and db.testMode then
        return true
    end
    if DC.demoMode then
        return true
    end
    return false
end

-- Generic deep copy (tables only; keys are copied by reference, values
-- recursively). No cycle handling -- config/expectation shapes are trees.
function DC.DeepCopy(src)
    if type(src) ~= "table" then
        return src
    end
    local out = {}
    for k, v in pairs(src) do
        if type(v) == "table" then
            out[k] = DC.DeepCopy(v)
        else
            out[k] = v
        end
    end
    return out
end

-- ------------------------------------------------------------------
-- Expectations: the saved shape is
-- PER CLASS --
--     db.expectations[role][class][slotId] = true|nil
-- with one class line for every class that can perform the role
-- (DC_Roles.ROLE_CLASSES, the ordered inversion of CLASS_ROLES).
-- DC.DEFAULT_EXPECT (data/expectations.lua) STAYS the role x slot SEED.
--
-- EnsureExpectations guarantees the full structure:
--   * first load (expectations == nil): every (role, performing class)
--     gets a deep copy of the seed's role line.
--   * legacy saved tables (one line per role) -- detected by BOOLEANS directly under
--     [role] (new-shape values are class tables) -- are MIGRATED: the
--     old role line is copied to every performing class, then the
--     structure is replaced. User edits survive on every class where
--     they are representable.
--   * new-shape tables: existing class lines are NEVER touched; missing
--     (role,class) combos are filled from the seed. The fill is
--     behavior-neutral -- the model resolves a missing combo to the
--     DC.DEFAULT_EXPECT role line anyway (runtime fallback) -- it only
--     hands the options grid a complete structure.
-- Known ambiguity (accepted): an old-shape role line with EVERY slot
-- unchecked is {} and indistinguishable from an untouched new-shape
-- role table (the boolean marker is gone); it is re-seeded per class.
--
-- Resolved at CALL time, not file-load time: data/expectations.lua and
-- core/roles.lua load after this file (TOC order const -> config ->
-- data/* -> model -> roles), but EnsureDefaults only runs at
-- ADDON_LOADED when everything is in. Offline callers without DC_Roles
-- loaded simply skip the expectations block.
-- ------------------------------------------------------------------
local function isOldShapeLine(line)
    for _, v in pairs(line) do
        if type(v) == "boolean" then
            return true
        end
    end
    return false
end

-- Top up ONE class line in place: slots the seed marks true and the line
-- has NEVER SEEN (nil) become true. Deliberate unchecks survive (the
-- options grid stores those as explicit false, never nil). Shared by both
-- EnsureExpectations branches below so a freshly migrated line is already
-- complete -- not just on the SECOND call (idempotency: a migration must
-- converge in one pass, not two).
local function topUpLine(cl, seedLine)
    for slotId, v in pairs(seedLine) do
        if v == true and cl[slotId] == nil then
            cl[slotId] = true
        end
    end
end

function DC.EnsureExpectations(db)
    if DC.DEFAULT_EXPECT == nil or DC_Roles == nil
       or DC_Roles.ROLE_CLASSES == nil then
        return
    end
    if db.expectations == nil then
        db.expectations = {}
    end
    local exp = db.expectations
    local nRoles = table.getn(DC.ROLES)
    for r = 1, nRoles do
        local role = DC.ROLES[r]
        local classes = DC_Roles.ROLE_CLASSES[role] or {}
        local nClasses = table.getn(classes)
        local line = exp[role]
        local seedLine = DC.DEFAULT_EXPECT[role] or {}
        if line ~= nil and isOldShapeLine(line) then
            -- migration: copy the old role line to every performing
            -- class, then top up each new class line from the CURRENT
            -- seed (a slot added since the old line was saved must reach
            -- it immediately, not only on a later call -- see topUpLine),
            -- then replace the structure (never lose user edits where
            -- representable)
            local byClass = {}
            for c = 1, nClasses do
                local cl = DC.DeepCopy(line)
                topUpLine(cl, seedLine)
                byClass[classes[c]] = cl
            end
            exp[role] = byClass
        else
            if line == nil then
                line = {}
                exp[role] = line
            end
            -- fill missing (role,class) combos from the seed; existing
            -- class lines keep their values, but slots the seed knows
            -- and the line has NEVER SEEN (nil) are topped up -- without
            -- this, a slot added in a later version never reaches saved
            -- lines (field report: neck/ring columns arrived as "not
            -- expected" for everyone). Deliberate unchecks survive: the
            -- options grid stores them as explicit false, never nil.
            for c = 1, nClasses do
                local cls = classes[c]
                if line[cls] == nil then
                    line[cls] = DC.DeepCopy(seedLine)
                else
                    topUpLine(line[cls], seedLine)
                end
            end
        end
    end
end

-- Fill missing fields in db from DC.DEFAULTS. Existing values -- including
-- saved booleans that are false -- are never touched. Table defaults are
-- deep-copied on assign.
function DC.EnsureDefaults(db)
    for k, v in pairs(DC.DEFAULTS) do
        if db[k] == nil then
            if type(v) == "table" then
                db[k] = DC.DeepCopy(v)
            else
                db[k] = v
            end
        end
    end
    -- expectations: per-(role,class) shape, materialized/migrated from
    -- the DC.DEFAULT_EXPECT seed (see EnsureExpectations above)
    DC.EnsureExpectations(db)
    return db
end

-- Reset the whole config IN PLACE (keeps the table reference -- the client
-- saves the global DopingControlDB, so the reference must survive).
function DC.ResetConfig(db)
    for k in pairs(db) do
        db[k] = nil
    end
    return DC.EnsureDefaults(db)
end

-- Reset only the expectation matrix to the DC.DEFAULT_EXPECT seed,
-- materialized per (role, performing class) -- options button
-- "Reset expectations" (reseeds ALL classes).
function DC.ResetExpectations(db)
    db.expectations = nil
    DC.EnsureDefaults(db)
    return db.expectations
end
