[download] []
 
(ns advent-of-code-2021.day11
  (:require  [clojure.java.io :as io]
             [clojure.string :as str]))


;; Lets think of the input as the rectangular grid of a cellular automaton.
;; The grid is stored as a one dimensional vector of cells together with the size of one side.
(def grid (let [cells  (->> "day11/input.txt"
                               io/resource
                               slurp
                               str/split-lines
                               (into [] (mapcat (fn [row]  (map  #(- (int %) 48) row)))))
                   size  (Math/round (Math/sqrt (count cells)))]
               {:cells cells
                :size size}))

;; Individual cells in the grid are adressed by their index. 
;; To calculate the neighbour positions we need the grid size in addition to the cell position.
(defn neighbour-indices
  [cell-index grid-size]                             
  (let [x (mod cell-index grid-size)
        y (quot cell-index grid-size)]
    (->> 
      (map (fn [x-offs y-offs]                                   
             [(+ x x-offs) (+ y y-offs)])
           [1 1 0 -1 -1 -1 0 1] 
           [0 1 1 1 0 -1 -1 -1])
      (filter  (fn [[x y]] (and  (>= x 0) (>= y 0) (< x grid-size) (< y grid-size))))
      (map (fn [[x y]] (+ (* y grid-size) x))))))
    


;; Take until first occurence of first repeating element (including this element).
(defn take-until-first-occurrence-of-first-repeating
  [s]
  (let [first-repeating (->> s
                            (reduce (fn [visited e]
                                      (if (visited e)
                                        (reduced e)
                                        (conj visited e)))
                                    #{}))]    
    (concat (take-while #(not= first-repeating %) s) 
            (list first-repeating))))


;; Given a initial grid and a grid transform (see below) calculate
;; the sequence of transformed grids until a grid repeats.
(defn repeatedly-until-first-cycle 
  [grid grid-transform]
  (take-until-first-occurrence-of-first-repeating (iterate grid-transform grid))) 


;; Grid transfroms
;;
;; A grid transform is a function that transforms a grid into another grid.

;; Grid transform: Apply a cell rule (rule-fn) to each cell.
;; 
;; A cell rule is a function that takes the current cell value and the 
;; current cell values of its neighbour cells and returns the new cell value.
(defn rule-application 
  [rule-fn]
  (fn [grid] 
    (let [grid-size (:size grid)
          cells (:cells grid)]
      (->> cells
           count
           range
           (mapv (fn [cell-index]
                   (let [cell (cells cell-index)
                         neighbour-cells (map cells (neighbour-indices cell-index grid-size))]
                     (rule-fn cell neighbour-cells))))
           (assoc grid :cells)))))

;; Grid transform: Apply another grid transform until a cycle occurs.
(defn repeated-until-first-cycle 
  [grid-transform]
  (fn [grid] 
    #_(last (take-until-first-occurrence-of-first-repeating (iterate grid-transform grid)))
    (last (repeatedly-until-first-cycle grid grid-transform))))

;; Specific grid transforms for our problem:

;; Grid transform: increase the energy level of each cell by 1
(def increase-energy-level 
  (rule-application (fn [cell _] (inc cell))))

;; Grid transform: mark all currently flashing calls (use -1 as marker).
(def mark-flashing 
  (rule-application (fn [cell _]  (if (> cell 9) -1 cell))))


;; Grid transform: increase the energy level by adding 1 for each flashing neighbour
(def increase-energy-level-from-flashing-neighbours 
  (rule-application 
    (fn [cell neighbour-cells]
      (if (<= cell 0)
        cell
        (let [flashing-neighbour-cells (filter #(= % -1) neighbour-cells)]
          (+ cell (count flashing-neighbour-cells)))))))

;; Grid transform: mark flashing cells as flashed (-1 -> 0)
(def mark-flashed 
  (rule-application (fn [cell _]  (if (= cell -1) 0 cell))))


;; Grid transform: repeatedly flash until no more changes occur
(def flashing-process 
  (repeated-until-first-cycle 
    (comp mark-flashed 
          increase-energy-level-from-flashing-neighbours
          mark-flashing)))                   

;; Grid transform: a single step consists of increasing the energy level and doing the flashing
(def step (comp flashing-process
                 increase-energy-level))

;; To solve part 2 apply the steps until a cycle is detected 
(def part-2 (->> (repeatedly-until-first-cycle grid step)
                 count
                 ;; the result includes the initial grid                
                 dec))