using Base.Iterators

abstract type Part1 end
abstract type Part2 end

function update!(::Type{Part1}, grid::AbstractMatrix{Char})
    previous = copy(grid)
    updated = false
    for j in 1:size(grid, 2), i in 1:size(grid, 1)        
        middle = previous[i, j]
        switch = true
        numoccupied = 0
        for (δj, δi) in product([-1, 0, 1], [-1, 0, 1])
            δi == δj == 0 && continue
            try
                neighbor = previous[i + δi, j + δj]
                if neighbor == '#'
                    middle == 'L' && (switch = false)
                    middle == '#' && (numoccupied += 1)
                end
            catch
            end
        end
        middle == 'L' && switch && (grid[i, j] = '#'; updated = true)
        middle == '#' && numoccupied ≥ 4 && (grid[i, j] = 'L'; updated = true)
    end
    updated
end

const Site = Tuple{Int, Int}

function visibility(init::AbstractMatrix{Char})
    N, M = size(init)

    visible = Dict{Site, Vector{Site}}()

    for j in 1:M, i in 1:N
        if init[i, j] != '.'
            for (δj, δi) in product([-1, 0, 1], [-1, 0, 1])
                δi == δj == 0 && continue
                u, v = i + δi, j + δj
                try
                    while init[u, v] == '.'
                        u += δi
                        v += δj
                    end
                    if haskey(visible, (i, j))
                        push!(visible[(i, j)], (u, v))
                    else
                        visible[(i, j)] = [(u, v)]
                    end
                catch
                end
            end
        end
    end
    visible
end

function update!(::Type{Part2}, grid::AbstractMatrix{Char}, visible)
    previous = copy(grid)
    updated = false
    for (i, j) in keys(visible)
        if previous[i, j] == 'L'
            switch = true
            for (u, v) in visible[(i, j)]
                if previous[u, v] == '#'
                    switch = false
                end
            end
            switch && (grid[i, j] = '#'; updated = true)
        elseif previous[i, j] == '#'
            numoccupied = 0
            for (u, v) in visible[(i, j)]
                if previous[u, v] == '#'
                    numoccupied += 1
                end
            end
            numoccupied ≥ 5 && (grid[i, j] = 'L'; updated = true)
        end
    end
    updated
end

function part1(grid)
    while update!(Part1, grid) end
    sum(grid .== '#')
end

function part2(grid)
    visible = visibility(grid)
    while update!(Part2, grid, visible) end
    sum(grid .== '#')
end

let init = hcat(collect.(readlines("input.txt"))...)
    @info "Solution" part1(init) part2(init)
end
