

-- Settings container (same structure, but gives consistent padding and bottom row)
function LifeUI.drawSettings()
    if not LifeUI.state.showSettings then return end
    local margin = LifeUI.ui.panelMarginPercent
    local w = WIDTH - (WIDTH * margin)
    local h = HEIGHT*0.7
    local cx, cy = WIDTH/2, HEIGHT/2
    
    pushStyle()
    stroke(255, 207)
    strokeWidth(0)
    fill(255, 39)
    
    uiPanel(cx, cy, w, h, function()
        local PAD = 20
        
        local segW   = w - 40
        local segH   = 42
        local segY   = h - 28                      -- near the top edge
        
        -- 1) Segmented control (centered)
        do
            pushStyle()
            fill(LifeUI.prefs.colors.settingsFill)
            stroke(LifeUI.prefs.colors.settingsStroke)
            rectMode(CENTER)                            -- <<< ensure CENTER semantics
            segmentedControl(w/2, segY, segW, segH,     -- <<< centered x = w/2
            "Rules",  function() LifeUI.state.settingsTab = 1 end,
            "Colors", function() LifeUI.state.settingsTab = 2 end
            )
            popStyle()
        end
        
        -- 2) Center a two-square row ("B" and "S") under the control
        do
            -- available content area under the segmented control
            local topMargin = (segH + 2 * (h - (segY + segH*0.5))) * 1.05 --plus a modifier making it 0.05 larger
            local bottomMargin = 36
            
            -- Tunables (keep your exact look, just control spacing)
            LifeUI.ui.sliderMetric = {
                belowBoxesPx   = h * 0.058,   -- distance from bottom edge of the boxes → label
                labelSliderGap = h * 0.025,   -- vertical gap: label above, slider below
                rowGapPx       = h * 0.016    -- gap between the 1st and 2nd slider bundles
            }
            
            drawBSChooser(w, h, topMargin)
            drawDecaySlider(w, h, topMargin)
            drawSeedPctSlider(w, h, topMargin)   -- will auto-stack under the first
            drawCommonRules(w, h,  topMargin)
            drawSaveLoadRow(w)
            drawCreditsLink(w)
        end
    end)
    popStyle()
end

-- one elongated box with 3×3 pad + bottom label (panel-local center coords)
function drawRuleBox(cx, cy, boxW, boxH, label, setTbl, keyPrefix)
    -- frame
    pushStyle()
    fill(255, 19); stroke(255, 70); strokeWidth(1)
    roundedRectangle{
        x = cx - boxW*0.5, y = cy - boxH*0.5, w = boxW, h = boxH,
        corners = 15, radius = 14
    }
    popStyle()
    
    -- layout
    local sidePad   = boxW * 0.06          -- left/right inset
    local padTop    = sidePad              -- top inset
    local labelMinH = math.max(22, boxW*0.14)
    
    local gridW     = boxW - sidePad*2
    local cols, rows = 3, 3
    
    -- spacing between keys
    local gap = LifeUI.ui.numPadSpacing
    if not gap then gap = math.floor(boxW * 0.04) end
    
    -- cell size that fits width and height
    local cellFromW  = (gridW - (cols-1)*gap) / cols
    local availH     = boxH - padTop - labelMinH
    local cellFromH  = (availH - (rows-1)*gap) / rows
    local cell       = math.min(cellFromW, cellFromH) * LifeUI.ui.numPadScale
    
    -- realized keypad size (NO override!)
    local padW = cols*cell + (cols-1)*gap
    local padH = rows*cell + (rows-1)*gap
    
    -- origin for keys: centered horizontally, fixed distance from top
    local gx0 = cx - padW*0.5 + cell*0.5
    local gy0 = cy + boxH*0.5 - padTop - cell*0.5
    
    -- draw keys 0..8
    pushStyle()
    fill(LifeUI.prefs.colors.settingsFill)
    stroke(LifeUI.prefs.colors.settingsStroke)
    strokeWidth(0)

    -- draw keys 0..8 (inside drawRuleBox)
    local t = (keyPrefix == "B") and LifeUI.model.birth or LifeUI.model.survive
    local n = 0
    for r=0,rows-1 do
        for c=0,cols-1 do
            local gx = gx0 + c*(cell + gap)
            local gy = gy0 - r*(cell + gap)
            
            -- capture-by-value locals for the closure
            local idx = n
            t[idx] = (t[idx] == true)  -- normalize: nil -> false
            local isOn = t[idx]
            local key  = (keyPrefix or "?") .. ":" .. idx
            
            statefulButton(
            tostring(idx), isOn, gx, gy, cell, cell,
            function()
                t[idx] = not t[idx]       -- flip the one we meant
                LifeUI.state.presetName = "Custom"
                LifeUI._changed = true
                LifeUI.trySelectPresetFromCurrent()
            end,
            { key = key, radius = cell * 0.22 }
            )
            
            n = n + 1
        end
    end
    popStyle()
    
    -- bottom label centered in remaining space
    local bottomH = boxH - padTop - padH
    local labelY  = cy + boxH*0.5 - padTop - padH - bottomH*0.5
    
    pushStyle()
    textAlign(CENTER); textMode(CENTER)
    fill(236,240,241,230)
    font("HelveticaNeue")
    --fontSize(bottomH*0.39)
    text(label, cx, labelY)
    popStyle()
end

-- panel-local placement (uses your correct spacing & topMargin)
function drawBSChooser(panelW, panelH, topMargin)
    local ruleBoxSpacing = 8
    local boxW = (panelW - ruleBoxSpacing*3) / 2
    local boxH = boxW * 1.28
    
    local yTop    = panelH - topMargin       -- top edge below segmented control
    local cyBoxes = yTop - boxH * 0.5
    local cxLeft  = ruleBoxSpacing + boxW * 0.5
    local cxRight = panelW - ruleBoxSpacing - boxW * 0.5
    
    drawRuleBox(cxLeft, cyBoxes, boxW, boxH, "B (# neighboring\ncells for Birth)",    LifeUI.prefs.B, "B")
    drawRuleBox(cxRight, cyBoxes, boxW, boxH, "S (# neighboring\ncells for Survival)", LifeUI.prefs.S, "S")
    
end

-- ──────────────────────────────────────────────────────────────
-- Common placer: computes Y from the boxes, then draws label + slider.
local function drawSettingsSlider(opts)
    local panelW, panelH, topMargin = opts.panelW, opts.panelH, opts.topMargin
    local xFrac, wFrac = opts.xFrac, opts.wFrac
    local trackPx, fontPx = opts.trackPx, opts.fontPx
    local name, minV, maxV, seed = opts.name, opts.minV, opts.maxV, opts.seed
    local labelText = opts.labelText   -- function(currentValue) -> string
    local metric = LifeUI.ui.sliderMetric
    
    -- Recompute box geometry to find the bottom edge
    local ruleBoxSpacing = 8
    local boxW = (panelW - ruleBoxSpacing*3) / 2
    local boxH = boxW * 1.28
    local yTop    = panelH - topMargin
    local cyBoxes = yTop - boxH * 0.5
    local yBottom = cyBoxes - boxH * 0.5
    
    -- If a previous bundle was drawn, start from that bundle’s slider Y minus rowGap
    local baseBottom = LifeUI._lastSliderBottom or yBottom
    local labelY  = baseBottom - metric.belowBoxesPx
    local sliderY = labelY   - metric.labelSliderGap
    
    -- Store for the next bundle to stack under this one
    LifeUI._lastSliderBottom = sliderY - metric.rowGapPx
    
    local x = panelW * xFrac
    local w = panelW * wFrac
    
    -- ensure slider state
    local invisible = color(255,255,255,0)
    local initial = (getSlider(name) ~= nil) and getSlider(name) or seed
    
    pushStyle()
    strokeWidth(trackPx)
    
    slider(name, x, sliderY, w, minV, maxV, initial, function(v)
        local nv = v
        if name == "decayTurns" then
            nv = (v < -0.5) and -1 or math.floor(v + 0.5)
            setSlider(name, nv)
            if LifeUI.model then LifeUI.model.decayTurns = nv end
            LifeUI._changed = true
            LifeUI.trySelectPresetFromCurrent()
        elseif name == "seedPct" then
            setSlider(name, nv)
            LifeUI.prefs.seedPct = nv
            LifeUI._changed = true
            if LifeUI.model then LifeUI.model.likelihood = nv 
            end
            LifeUI.trySelectPresetFromCurrent()
        else
            setSlider(name, nv)
            LifeUI._changed = true
        end
    end, invisible)
    
    -- Label (your font, your size)
    textMode(CORNER) textAlign(LEFT)
    fill(stroke())
    font("HelveticaNeue")
    fontSize(fontPx)
    local v = getSlider(name)
    text(labelText(v), panelW * 0.045, labelY)
    popStyle()
    
    return LifeUI._lastSliderBottom   -- <<<< gives next bundle a starting point
end

-- ──────────────────────────────────────────────────────────────
-- 1) Decay slider (−1..10, “never” for −1)
function drawDecaySlider(panelW, panelH, topMargin)
    -- reset stack start for the first bundle each frame
    LifeUI._lastSliderBottom = nil
    
    local seed = (LifeUI.prefs.decayTurns ~= nil) and LifeUI.prefs.decayTurns or -1
    return drawSettingsSlider{
        panelW=panelW, panelH=panelH, topMargin=topMargin,
        xFrac=LifeUI.ui.decay.xFrac, wFrac=LifeUI.ui.decay.wFrac,
        trackPx=LifeUI.ui.decay.trackPx, fontPx=LifeUI.ui.decay.fontPx,
        name="decayTurns", minV=-1, maxV=100, seed=seed,
        labelText=function(v) return "steps until static cells decay: " .. ((v == -1) and "never" or tostring(v)) end
    }
end

-- 2) Seeding percentage slider (0..1 smooth, shown as %)
function drawSeedPctSlider(panelW, panelH, topMargin)
    local seed = (LifeUI.prefs.seedPct ~= nil) and LifeUI.prefs.seedPct or 0.08
    return drawSettingsSlider{
        panelW=panelW, panelH=panelH, topMargin=topMargin,
        xFrac=LifeUI.ui.seed.xFrac, wFrac=LifeUI.ui.seed.wFrac,
        trackPx=LifeUI.ui.seed.trackPx, fontPx=LifeUI.ui.seed.fontPx,
        name="seedPct", minV=0, maxV=1, seed=seed,
        labelText = function(v)
            local pct = math.max(0, math.min(1, v)) * 100
            if pct > 100 then pct = 100 end
            local s = string.format("%.3f", pct)
            s = s:gsub("(%..-)0+$", "%1")  -- strip trailing zeros
            s = s:gsub("%.$", "")          -- strip dangling decimal point
            return "% living cells when reset: " .. s .. "%"
        end
    }
end

-- centered text-only button under the panel
function drawCreditsLink(panelW)
    local cx   = panelW * 0.5
    local y    = - 20  -- distance up from panel bottom (magic number; tweak)
    pushStyle()
    -- text-only look: no stroke/fill on the hit target
    fill(236, 240, 241, 161) ; font("HelveticaNeue") ; fontSize(16)
    textMode(CENTER) ; textAlign(CENTER)
    text("credits", cx, y)
    
    -- invisible hit target
    fill(0,0) ; strokeWidth(0)
    local padW, padH = 60, 24  -- clickable area
    button("", cx, y, padW, padH, function()
        LifeUI.showCredits = true   -- or call your credits screen
    end, { radius = 10, corners = 15 })
    popStyle()
end

-- Save/load the current S/B arrays as "Custom"
CUSTOM_RULES_KEY = "LifeSphere:CustomRuleSB"

function saveCustomRules()
    -- flatten 0..8 to arrays of true/false
    local out = { S={}, B={}, decay = LifeUI.model.decayTurns or -1, seed = LifeUI.model.likelihood or (LifeUI.prefs.seedPct or 0.08) }
    for i=0,8 do out.S[tostring(i)] = LifeUI.model.survive[i] == true end
    for i=0,8 do out.B[tostring(i)] = LifeUI.model.birth[i]   == true end
    saveLocalData(CUSTOM_RULES_KEY, json.encode(out))
end

function loadCustomRules()
    local data = readLocalData(CUSTOM_RULES_KEY)
    if not data then return end
    local ok, t = pcall(json.decode, data)
    if not ok or type(t) ~= "table" then return end
    
    -- apply B/S to live model
    LifeUI.ensureModelTables()
    for n=0,8 do
        LifeUI.model.survive[n] = (t.S and (t.S[tostring(n)] or t.S[n])) and true or false
        LifeUI.model.birth[n]   = (t.B and (t.B[tostring(n)] or t.B[n])) and true or false
    end
    
    -- decay + seed/likelihood
    local decay = (t.decay ~= nil) and t.decay or -1
    local seed  = (t.seed  ~= nil) and t.seed  or (LifeUI.prefs.seedPct or 0.08)
    
    LifeUI.model.decayTurns = decay
    LifeUI.model.likelihood = seed
    
    -- sync sliders + prefs + preset highlight
    setSlider("decayTurns", decay)
    setSlider("seedPct",    seed)
    LifeUI.prefs.decayTurns = decay
    LifeUI.prefs.seedPct    = seed
    
    LifeUI.trySelectPresetFromCurrent()  -- will clear highlight if not a preset
end

PRESET_ORDER = {
    "Conway", "Replicator",
    "HighLife","Gnarl",
    "Morley", "Maze"
}


-- ──────────────────────────────────────────────────────────────
-- Preset defs: per-preset B/S plus decay + seed
-- NOTE: Only list the counts that are true.
PRESET_DEFS = {
    Conway    = { B = {3},          S = {2,3},          decay = -1,  seed = 0.08 },
    Replicator= { B = {1,3,5,7},    S = {1,3,5,7},      decay = -1,  seed = 0.001 },
    HighLife  = { B = {3,6},        S = {2,3},          decay = 4,  seed = 0.08 },
    Gnarl     = { B = {1},          S = {1},            decay = -1,  seed = 0.08 },
    Morley    = { B = {3,6,8},      S = {2,4,5},        decay = -1,  seed = 0.78 }, -- AKA "Move"
    Maze      = { B = {3},          S = {1,2,3,4,5},    decay = 7,  seed = 0.11 },
}

-- The visual order you asked for:
local PRESET_ORDER = { "Conway","Replicator","HighLife","Gnarl","Morley","Maze" }

-- Helper: write B/S lists into the model tables (0..8 booleans)
function applyBS(model, Blist, Slist)
    for n=0,8 do
        model.birth[n]   = false
        model.survive[n] = false
    end
    for _,n in ipairs(Blist or {}) do model.birth[n]   = true end
    for _,n in ipairs(Slist or {}) do model.survive[n] = true end
end

function drawCommonRules(panelW, panelH, topMargin)
    local PADX   = panelW * 0.045
    local ROW_GAP= 8
    local COLS   = 3                 -- three per row (space saver)
    local GUT    = 7
    local BTN_H  = 34                -- a bit shorter to fit
    local RADIUS = 12
    
    -- start below last slider bundle
    local startTopY = (LifeUI._lastSliderBottom or (panelH - topMargin)) - 29
    
    -- label
    local labelY = startTopY
    pushStyle()
    textMode(CORNER) ; textAlign(LEFT)
    fill(240, 238, 238)
    font("HelveticaNeue") ; fontSize(18)
    text("common rules", PADX, labelY)

    
    local gridTop = labelY - 2
    local BTN_W   = (panelW - PADX*2 - GUT*(COLS-1)) / COLS
    
    -- unselected palette (IMUI styles already set outside)
    fill(LifeUI.prefs.colors.settingsFill)
    stroke(LifeUI.prefs.colors.settingsStroke)
    strokeWidth(0)
    local rows = math.ceil(#PRESET_ORDER / COLS)
    for r=1,rows do
        local cy = gridTop - (r-0.5)*(BTN_H + ROW_GAP)
        for c=1,COLS do
            local idx = (r-1)*COLS + c
            local name = PRESET_ORDER[idx]
            if name then
                local cx = PADX + (c-1)*(BTN_W + GUT) + BTN_W*0.5
                local isActive = (LifeUI.state.presetName == name)
                statefulButton(
                name, LifeUI.state.presetName == name, cx, cy, BTN_W, BTN_H,
                function()
                    LifeUI.applyPreset(name)          -- your existing writer to model + prefs
                    LifeUI.state.presetName = name    -- lock selection to this preset
                    LifeUI._changed = true
                end,
                { radius = RADIUS, corners = 15 }
                )
            end
        end
    end
    popStyle()
    -- leave stacking cursor just under the grid
    LifeUI._lastSliderBottom = gridTop - rows*(BTN_H + ROW_GAP) + ROW_GAP
end

function _rulesTableEquals(a, b)
    for n=0,8 do
        if (a[n] == true) ~= (b[n] == true) then return false end
    end
    return true
end

function _denseFromModelSets(src)
    local t = {}
    for n=0,8 do t[n] = (src and src[n] == true) or false end
    return t
end

function _denseFromList(list)
    local t = {}
    for n=0,8 do t[n]=false end
    for _,n in ipairs(list or {}) do if n>=0 and n<=8 then t[n]=true end end
    return t
end

function LifeUI.trySelectPresetFromCurrent()
    -- current, from model & prefs
    local curB = _denseFromModelSets(LifeUI.model.birth)
    local curS = _denseFromModelSets(LifeUI.model.survive)
    local curDecay = LifeUI.prefs.decayTurns or -1
    local curSeed  = LifeUI.prefs.seedPct    or 0.08
    local EPS = 1e-4
    
    -- try to match any preset
    for _,name in ipairs(PRESET_ORDER) do
        local spec = PRESETS[name]
        if spec then
            local pb = _denseFromList(spec.B)
            local ps = _denseFromList(spec.S)
            local pDecay = spec.decay or -1
            local pSeed  = spec.seed  or 0.08
            if _rulesTableEquals(curB,pb)
            and _rulesTableEquals(curS,ps)
            and curDecay == pDecay
            and math.abs((curSeed or 0) - (pSeed or 0)) <= EPS
            then
                LifeUI.state.presetName = name
                return name
            end
        end
    end
    
    -- no match → clear selection (no preset highlighted)
    LifeUI.state.presetName = nil
    return nil
end

-- Make sure this exists once (and only once)
PRESET_ORDER = { "Conway","Replicator","HighLife","Gnarl","Morley","Maze" }

-- Ensure the model tables exist before we write into them
function LifeUI.ensureModelTables()
    LifeUI.model = LifeUI.model or {}
    LifeUI.model.birth   = LifeUI.model.birth   or {}
    LifeUI.model.survive = LifeUI.model.survive or {}
end

-- Write B/S lists into the model tables (0..8 booleans)
function applyBS(model, Blist, Slist)
    for n=0,8 do
        model.birth[n]   = false
        model.survive[n] = false
    end
    for _,n in ipairs(Blist or {}) do model.birth[n]   = true end
    for _,n in ipairs(Slist or {}) do model.survive[n] = true end
end

-- ⭐ The missing piece: actually apply a preset to the live model + sliders
function LifeUI.applyPreset(name)
    local spec = PRESET_DEFS and PRESET_DEFS[name]
    if not spec then return end
    
    LifeUI.ensureModelTables()
    applyBS(LifeUI.model, spec.B, spec.S)
    
    local decay = (spec.decay ~= nil) and spec.decay or -1
    local seed  = (spec.seed  ~= nil) and spec.seed  or 0.08
    
    -- sliders for UI reflection
    setSlider("decayTurns", decay)
    setSlider("seedPct",    seed)
    
    -- single-source writes
    if LifeUI.model then
        LifeUI.model.decayTurns = decay
        LifeUI.model.likelihood = seed
    end
    
    LifeUI.state.presetName = name
    LifeUI._changed = true
end

function LifeUI.trySelectPresetFromCurrent()
    LifeUI.ensureModelTables()
    
    local curB = _denseFromModelSets(LifeUI.model.birth)
    local curS = _denseFromModelSets(LifeUI.model.survive)
    local curDecay = (LifeUI.model and LifeUI.model.decayTurns) or -1   -- <<< here
    local curSeed  = LifeUI.prefs.seedPct or 0.08
    local EPS = 1e-4
    
    for _,name in ipairs(PRESET_ORDER) do
        local spec = PRESET_DEFS[name]
        if spec then
            local pb = _denseFromList(spec.B)
            local ps = _denseFromList(spec.S)
            local pDecay = spec.decay or -1
            local pSeed  = spec.seed  or 0.08
            if _rulesTableEquals(curB,pb)
            and _rulesTableEquals(curS,ps)
            and curDecay == pDecay
            and math.abs((curSeed or 0) - (pSeed or 0)) <= EPS
            then
                LifeUI.state.presetName = name
                return name
            end
        end
    end
    
    LifeUI.state.presetName = nil
    return nil
end

-- two wide buttons under the presets grid, uses the stacking cursor
function drawSaveLoadRow(panelW)
    local PADX   = panelW * 0.045
    local GAP    = 10
    local BTN_H  = 42
    local RADIUS = 12
    
    -- push this row farther below the common rules
    local EXTRA_BELOW_PRESETS = 50   -- was 18; bump this to move it lower
    local rowCy = (LifeUI._lastSliderBottom or 0) - EXTRA_BELOW_PRESETS
    
    -- side-by-side widths
    local halfW  = (panelW - PADX*2 - GAP) * 0.5
    local leftX  = PADX + halfW*0.5
    local rightX = PADX + halfW + GAP + halfW*0.5
    
    pushStyle()
    fill(LifeUI.prefs.colors.settingsFill)
    stroke(LifeUI.prefs.colors.settingsStroke)
    strokeWidth(0)
    
    button("Save Current", leftX,  rowCy, halfW, BTN_H, function()
        saveCustomRules()
        LifeUI.state.presetName = nil
        LifeUI._changed = true
    end, { radius = RADIUS, corners = 15 })
    
    button("Load Saved",  rightX, rowCy, halfW, BTN_H, function()
        loadCustomRules()
        LifeUI.state.presetName = nil
        LifeUI._changed = true
    end, { radius = RADIUS, corners = 15 })
    
    popStyle()
    
    -- leave a bigger cushion before "credits"
    LifeUI._lastSliderBottom = rowCy - BTN_H*0.5 - 28   -- was 14
end


