mutable struct GameOfLife
    board::Array{Bool, 2}
    step::Integer
    maxSteps::Integer

    GameOfLife() = new(fill(false, (16, 16)), 0, 50)
    GameOfLife(b::Array{Bool, 2}; step = 0, maxSteps = 50) = new(b, step, maxSteps)
end

function Base.show(io::IO, b::GameOfLife)
    res = String[]

    rows, cols = size(b.board)

    for i in 1:rows
        push!(res, join((v -> v? '#' : ' ').(b.board[i, :])))
    end

    print(io, "\u2502", join(res, "\u2502\n\u2502"), "\u2502")
end

Base.start(iter::GameOfLife) = iter.step
Base.done(iter::GameOfLife, state) = iter.maxSteps <= iter.step

behaviours = (
    (x) -> false,
    (x) -> x,
    (x) -> true,
    (x) -> false
)

function Base.next(iter::GameOfLife, state)
    iter.step += 1

    if state == 0 return iter, iter.step end

    rows, cols = size(iter.board)

    newboard = fill(false, (rows, cols))

    for x in 1:rows, y in 1:cols
        neighbors = sum(iter.board[
            [x, x == 1 ? rows : x - 1, x == rows ? 1 : x + 1],
            [y, y == 1 ? cols : y - 1, y == cols ? 1 : y + 1]
        ]) - iter.board[x,y]
        newboard[x,y] = behaviours[clamp(neighbors, 1, 4)](iter.board[x,y])
    end

    iter.board = newboard

    return iter, iter.step
end

#board = reshape([rand(false:true) for i in 1:16], (4, 4))
board = fill(false, (8, 16))

# Glider
#   #
#    #
#  ###
board[2, 3] = true
board[3, 4] = true
board[4, 2:4] = true

g = GameOfLife(board, maxSteps = 100)

printborder(g, i) = println(done(g, i) ? "\u2514" : (i.step), "\u2500"^(done(g, i) ? size(i.board, 2) : size(i.board, 2) + 1 - length("$(i.step)")), done(g, i) ? "\u2518" : "\u2524")

println("Game of Life: $(g.maxSteps) cycles!")
println("\u250c", ^("\u2500", size(g.board, 2)) ,"\u2510")
for i in g
    println(i)
    printborder(g, i)
end