--# ConwaysCubeLifeModel
-- Pure logic runner for Conway’s Life on a CubeLifeModel.
-- Tracks dirty cells & per-face dirty rects. No rendering, no images.

ConwaysCubeLifeModel = class()

function ConwaysCubeLifeModel:init(numOrCube)
    if type(numOrCube) == "table" and numOrCube.cells then
        self.model = numOrCube
    else
        local N = tonumber(numOrCube) or 16
        self.model = CubeLifeModel(N)
    end
    self.N = self.model.N
    self.decayTurns = -1  -- -1 = never decay; slider writes here
    -- at init
    self.birth   = { [3]=true }              -- Conway B3
    self.survive = { [2]=true, [3]=true }   -- Conway S23
    -- staging for next state (bool) – we don’t mutate .alive until commit
    self.nextAlive = {} -- nextAlive[f][y][x] = 0/1
    for f=1,6 do
        local face = {}
        for y=1,self.N do
            local row = {}
            for x=1,self.N do row[x] = 0 end
            face[y] = row
        end
        self.nextAlive[f] = face
    end
    -- dirty tracking
    self.dirtyCells = { [1]={},[2]={},[3]={},[4]={},[5]={},[6]={} }
    self.dirtyRect  = { [1]=false,[2]=false,[3]=false,[4]=false,[5]=false,[6]=false }
    -- randomization threshhold
    self.likelihood = 0.08
end

-- Clear dirty info after a renderer has consumed it
function ConwaysCubeLifeModel:clearDirty()
    for f=1,6 do
        self.dirtyCells[f] = {}
        self.dirtyRect[f]  = false
    end
end

-- Helpers for marking dirty
local function _markRect(rect, x, y)
    if not rect then return {minx=x, maxx=x, miny=y, maxy=y} end
    if x < rect.minx then rect.minx = x end
    if x > rect.maxx then rect.maxx = x end
    if y < rect.miny then rect.miny = y end
    if y > rect.maxy then rect.maxy = y end
    return rect
end

-- One Life step
function ConwaysCubeLifeModel:step()
    local faces = self.model.faces
    local N = self.N
    -- evaluate
    for f=1,6 do
        local F = faces[f]
        local NF = self.nextAlive[f]
        for y=1,N do
            local row = F[y]
            local nrow = NF[y]
            for x=1,N do
                local c = row[x]
                local count = 0
                local nb = c.neighbors
                -- sum neighbors (nb is array of 8 cell refs)
                for i=1,8 do
                    count = count + (nb[i].alive or 0)
                end
                local alive = (c.alive == 1)
                local nextAlive
                if alive then
                    nextAlive = self.survive[count] or false
                else
                    nextAlive = self.birth[count] or false
                end
                nrow[x] = nextAlive and 1 or 0
            end
        end
    end
    
    -- commit + dirty mark (replace your current commit loop with this)
    -- commit + dirty mark + static-neighborhood tracking
    for f=1,6 do
        local F  = faces[f]
        local NF = self.nextAlive[f]
        local dc = self.dirtyCells[f]
        local dr = self.dirtyRect[f]
        
        for y=1,N do
            local row  = F[y]
            local nrow = NF[y]
            for x=1,N do
                local c        = row[x]
                local oldAlive = c.alive or 0
                local newAlive = nrow[x] or 0
                
                -- Build the *next* 3×3 signature (center + 8 neighbors) using self.nextAlive.
                -- Encode as base-2 integer to avoid bit ops.
                -- Order: center, then neighbors[1..8]
                local sigNext = (newAlive == 1) and 1 or 0
                local nb = c.neighbors
                for i=1,8 do
                    local nc = nb[i]
                    local na = self.nextAlive[nc.face][nc.y][nc.x] or 0
                    sigNext = sigNext * 2 + (na == 1 and 1 or 0)
                end
                
                -- Compare to previous signature to see if the whole 3×3 is static
                local sigPrev     = c.staticSig or -1
                local staticTicks = c.staticTicks or 0
                if sigPrev == sigNext then
                    staticTicks = staticTicks + 1
                else
                    staticTicks = 0
                end
                
                -- Optional: carry a simple age if you like (not used for decay now)
                local oldAge = c.age or 0
                local newAge = (newAlive == 1) and ((oldAlive == 1) and (oldAge + 1) or 1) or 0
                
                -- Apply decay ONLY if the neighborhood is static long enough
                local T = self.decayTurns or -1
                if T >= 0 and newAlive == 1 and staticTicks > T then
                    newAlive   = 0           -- decay this alive cell
                    newAge     = 0
                    -- Note: neighborhood signature will change next frame naturally,
                    -- so we reset ticks now.
                    staticTicks = 0
                    -- Also flip the center bit of sigNext for consistency (not strictly required)
                    -- since we’re committing a death here; next frame’s recompute will match anyway.
                end
                
                -- Commit current state to the cell
                c.alive     = newAlive
                c.age       = newAge
                c.staticSig = sigNext
                c.staticTicks = staticTicks
                
                -- Dirty if anything visible changed
                if (oldAlive ~= newAlive) or (oldAge ~= newAge) then
                    dc[#dc+1] = {x=x, y=y}
                    dr = _markRect(dr, x, y)
                end
            end
        end
        self.dirtyRect[f] = dr
    end
end

function ConwaysCubeLifeModel:setRules(ruleString)
    self.birth, self.survive = {}, {}
    local bpart, spart = string.match(ruleString, "B(%d+)/S(%d+)")
    for n in bpart:gmatch("%d") do self.birth[tonumber(n)] = true end
    for n in spart:gmatch("%d") do self.survive[tonumber(n)] = true end
end

function ConwaysCubeLifeModel:seedRandomCells(likelihood)
    self.likelihood = likelihood or self.likelihood or 0.08
    local N = self.N
    for f=1,6 do
        for y=1,N do
            for x=1,N do
                if math.random() < self.likelihood then self:setAlive(f,x,y,1) end
            end
        end
    end
end

local function applyRules(s)
    -- minimal validation & fallback
    local b, sPart = string.match(s or "", "^%s*B(%d+)%s*/%s*S(%d+)%s*$")
    if b and sPart then
        life:clearAll()
        life:setRules(string.format("B%s/S%s", b, sPart))
        currentRule = string.format("B%s/S%s", b, sPart)
        print("Rules set to:", currentRule)
    else
        print("Invalid rule string. Expected like: B3/S23, B36/S23, B2/S, ...")
    end
end

-- Handy presets
function setConway()    applyRules("B3/S23")        end
function setHighLife()  applyRules("B36/S23")       end
function setSeeds()     applyRules("B2/S")          end
function setDayNight()  applyRules("B3678/S34678")  end
function setMorley()    applyRules("B368/S245")     end

-- Fun: random Life-like rule (B*/S*)
function setRandomRule()
    local function pick(density)
        -- density ~ probability each digit 0..8 is included
        local out = {}
        for n=0,8 do
            if math.random() < density then out[#out+1] = tostring(n) end
        end
        if #out == 0 then out[1] = tostring(math.random(0,8)) end
        return table.concat(out)
    end
    local b = pick(0.30)   -- births sparser
    local s = pick(0.45)   -- survival a bit denser
    applyRules("B"..b.."/S"..s)
end

function CubeLifeModel:getAge(f, x, y)
    return self.faces[f][y][x].age or 0
end

-- Convenience seeding (pure model writes; also tracks dirty)
function ConwaysCubeLifeModel:setAlive(f, x, y, v)
    local cell = self.model.faces[f][y][x]
    local v01 = (v and 1 or 0)
    local oldAlive, oldAge = cell.alive, cell.age or 0
    local newAlive = v01
    local newAge   = (v01 == 1) and (oldAlive == 1 and math.max(1, oldAge) or 1) or 0
    
    if oldAlive ~= newAlive or oldAge ~= newAge then
        cell.alive = newAlive
        cell.age   = newAge
        local dr = self.dirtyRect[f]
        self.dirtyRect[f] = _markRect(dr, x, y)
        local dc = self.dirtyCells[f]
        dc[#dc+1] = {x=x,y=y}
    end
end

function ConwaysCubeLifeModel:clearAll()
    for f=1,6 do
        for y=1,self.N do
            for x=1,self.N do
                self.model.faces[f][y][x].alive = 0
            end
        end
        self.dirtyCells[f] = {}
        self.dirtyRect[f]  = {minx=1, miny=1, maxx=self.N, maxy=self.N}
    end
end

-- Simple glider stamp on a single face (no wrapping at stamp time)
-- rot = 0..3 quarter turns
local function _rot(dx,dy,r)
    r = r % 4
    if r==0 then return dx,dy
    elseif r==1 then return -dy, dx
    elseif r==2 then return -dx,-dy
    else return dy,-dx end
end

function ConwaysCubeLifeModel:stampGlider(face, x, y, rot)
    local pat = {
        {0,1,0},
        {0,0,1},
        {1,1,1}
    }
    rot = rot or 0
    for j=1,3 do
        for i=1,3 do
            if pat[j][i]==1 then
                local dx,dy = _rot(i-1, j-1, rot)
                local xx,yy = x+dx, y+dy
                if xx>=1 and xx<=self.N and yy>=1 and yy<=self.N then
                    self:setAlive(face, xx, yy, 1)
                end
            end
        end
    end
end

-- Read-only accessors for renderers
function ConwaysCubeLifeModel:getDirtyRect(face)  -- may be false/nil if nothing changed
    return self.dirtyRect[face]
end

function ConwaysCubeLifeModel:iterDirtyCells(face)
    local list = self.dirtyCells[face]
    local i = 0
    return function()
        i = i + 1
        local v = list[i]
        if v then return v.x, v.y end
    end
end

-- inside ConwaysCubeLifeModel
function ConwaysCubeLifeModel:setStepInterval(sec)
    self.stepInterval = sec or 0.08
    self._acc = 0
end

function ConwaysCubeLifeModel:setPlaying(p)
    self.playing = p and true or false
end

function ConwaysCubeLifeModel:update(dt)
    if not self.playing then return false end
    self._acc = (self._acc or 0) + dt
    if self._acc >= (self.stepInterval or 0.08) then
        self._acc = self._acc - self.stepInterval
        self:step()
        return true
    end
    return false
end