--# CubeLifeModel
-- Pure data model: 6 faces, each N x N. Each cell stores .alive and .neighbors (array of 8 cell refs).
-- No rendering, no images.

CubeLifeModel = class()

-- face order: 1:+X, 2:-X, 3:+Y, 4:-Y, 5:+Z, 6:-Z
function CubeLifeModel:init(N)
    self.N = N or 16
    self.faces = {}   -- faces[f][y][x] = cell
    for f=1,6 do
        local face = {}
        for y=1,self.N do
            local row = {}
            for x=1,self.N do
                row[x] = {face=f, x=x, y=y, alive=0, neighbors=nil}
            end
            face[y] = row
        end
        self.faces[f] = face
    end
    self:_linkNeighbors()
end

-- Public: set a cell alive (1) or dead (0)
function CubeLifeModel:setAlive(f, x, y, v)
    self.faces[f][y][x].alive = (v and 1 or 0)
end

function CubeLifeModel:getAlive(f, x, y)
    return self.faces[f][y][x].alive
end

-- Iterator over all cells: for f,y,x,cell in model:cells() do ... end
function CubeLifeModel:cells()
    local f, y, x = 1, 1, 0
    return function()
        x = x + 1
        if x > self.N then x = 1; y = y + 1 end
        if y > self.N then y = 1; f = f + 1 end
        if f > 6 then return nil end
        return f, y, x, self.faces[f][y][x]
    end
end

----------------------------------------------------------------------
-- Neighbor wiring (precomputed graph)
-- We define a right-handed frame for each face so a step (dx,dy) in face-local
-- grid space maps to the correct neighbor face + rotated offset when crossing edges.

-- Basis per face: normal n, +u (x→+), +v (y→+). Matches your StandardSphere mapping.
local BASIS = {
    [1] = {n={1,0,0},  u={0,0,-1}, v={0,-1,0}},  -- +X
    [2] = {n={-1,0,0}, u={0,0, 1}, v={0,-1,0}},  -- -X
    [3] = {n={0, 1,0}, u={1,0, 0}, v={0, 0,1}},  -- +Y
    [4] = {n={0,-1,0}, u={1,0, 0}, v={0, 0,-1}}, -- -Y
    [5] = {n={0,0, 1}, u={1,0, 0}, v={0,-1,0}},  -- +Z
    [6] = {n={0,0,-1}, u={-1,0,0}, v={0,-1,0}},  -- -Z
}

-- Given face f, and integer step (sx,sy) in face-local cell space, compute the neighbor cell.
-- We do this by moving the logical (u,v) one cell and re-projecting which face owns that (u,v).
-- Because we only need edge-crossing behavior, we can do it with a small geometric helper.
local function vec3_add(a,b) return {a[1]+b[1], a[2]+b[2], a[3]+b[3]} end
local function vec3_mul(a,s) return {a[1]*s, a[2]*s, a[3]*s} end
local function abs(x) return x<0 and -x or x end

-- Direction → (face, u, v) with u,v in [-1,1] using same conventions as BASIS
local function dirToFaceUV(d)
    local ax, ay, az = abs(d[1]), abs(d[2]), abs(d[3])
    if ax >= ay and ax >= az then
        local m = (ax==0 and 1 or ax)
        if d[1] > 0 then return 1, -d[3]/m, -d[2]/m end
        return 2,  d[3]/m, -d[2]/m
    elseif ay >= ax and ay >= az then
        local m = (ay==0 and 1 or ay)
        if d[2] > 0 then return 3,  d[1]/m,  d[3]/m end
        return 4,  d[1]/m, -d[3]/m
    else
        local m = (az==0 and 1 or az)
        if d[3] > 0 then return 5,  d[1]/m, -d[2]/m end
        return 6, -d[1]/m, -d[2]/m
    end
end

-- Face-local (u,v) → direction
local function faceUVToDir(f, u, v)
    local b = BASIS[f]
    local n,uvec,vvec = b.n,b.u,b.v
    local dir = vec3_add( vec3_add(n, vec3_mul(uvec, u)), vec3_mul(vvec, v) )
    -- normalization not necessary for our face selection math
    return dir
end

-- Map a step from (x,y) by (sx,sy). Returns (nf, nx, ny).
function CubeLifeModel:_stepCell(f, x, y, sx, sy)
    local N = self.N
    -- convert cell center to [-1,1] uv
    local u = ((x-0.5)/N)*2 - 1
    local v = ((y-0.5)/N)*2 - 1
    local du = (2/N) * sx
    local dv = (2/N) * sy
    local d = faceUVToDir(f, u+du, v+dv)
    local nf, nu, nv = dirToFaceUV(d)
    
    -- Nudge off exact edges to avoid rounding to wrong side
    local eps = 1e-7
    if nu <= -1 then nu = -1 + eps elseif nu >= 1 then nu = 1 - eps end
    if nv <= -1 then nv = -1 + eps elseif nv >= 1 then nv = 1 - eps end
    
    -- back to indices
    local nx = math.floor((nu + 1) * 0.5 * N) + 1
    local ny = math.floor((nv + 1) * 0.5 * N) + 1
    if nx < 1 then nx = 1 elseif nx > N then nx = N end
    if ny < 1 then ny = 1 elseif ny > N then ny = N end
    return nf, nx, ny
end

function CubeLifeModel:_linkNeighbors()
    -- 8-neighborhood steps
    local steps = {
        {-1,-1},{0,-1},{1,-1},
        {-1, 0},        {1, 0},
        {-1, 1},{0, 1},{1, 1}
    }
    for f,y,x,cell in self:cells() do
        local nb = {}
        for i=1,8 do
            local sx,sy = steps[i][1], steps[i][2]
            local nf, nx, ny = self:_stepCell(f, x, y, sx, sy)
            nb[i] = self.faces[nf][ny][nx]
        end
        cell.neighbors = nb
    end
end