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


(let [edges (->> "day12/input.txt"
                 io/resource
                 slurp
                 str/split-lines
                 (mapcat (fn [line] ((juxt identity #(into [] (reverse %)))  
                                     (mapv keyword (str/split line #"-")))))
                 ((fn [edges] (reduce (fn [edge-map [from to]]
                                        (update edge-map from (fn [tos]
                                                                (if tos
                                                                  (conj tos to)
                                                                  #{to})))) 
                                      {}
                                      edges))))

      small-cave? (fn [node] (= (name node)  (clojure.string/lower-case (name node))))

      part-1-visit-state (fn visit-state 
                           [visited]
                           {:can-visit? (fn [node]
                                          (or (not (small-cave? node))
                                              (not (visited node))))
                            :with-visit (fn [node]
                                          (cond
                                            (not (small-cave? node)) (visit-state visited)
                                            (not (visited node)) (visit-state (conj visited node))))})

      part-1-start-path ['(:start) (part-1-visit-state #{:start})]

      part-2-visit-state (fn visit-state 
                           [visited visited-twice]
                           {:can-visit? (fn [node]
                                          (or (not (small-cave? node))
                                              (not (visited node))
                                              (and (not (#{:start} node)) 
                                                   (not visited-twice))))
                            :with-visit (fn [node]
                                          (cond
                                            (not (small-cave? node)) (visit-state visited visited-twice)
                                            (not (visited node)) (visit-state (conj visited node) visited-twice)
                                            (not visited-twice) (visit-state visited true)))})


      part-2-start-path ['(:start) (part-2-visit-state #{:start} false)]

      find-next-paths-for-one (fn [unfinished-path]
                                (let [[current-nodes visit-state] unfinished-path
                                      last-node (first current-nodes)
                                      next-nodes (edges last-node)
                                      visitable-nodes (filter (:can-visit? visit-state) next-nodes)]                        
                                  (mapv (fn [next-node]
                                               [(conj current-nodes next-node)                                               
                                                ((:with-visit visit-state) next-node)])
                                       visitable-nodes)))

      find-next-paths-for-multi (fn [unfinished-paths]
                                  (into []  (mapcat find-next-paths-for-one unfinished-paths)))     

      find-next-all-paths (fn [{finished-paths :finished unfinished-paths :unfinished}]
                            (let [next-paths (find-next-paths-for-multi unfinished-paths)
                                  next-finished-and-unfinished (group-by (fn [[current-nodes _]]
                                                                           (if (= :end (first current-nodes))
                                                                             :finished
                                                                             :unfinished))
                                                                         next-paths)]
                              (if finished-paths 
                                (update next-finished-and-unfinished 
                                        :finished                                      
                                        #(if % 
                                           (into finished-paths %)
                                           finished-paths))
                                next-finished-and-unfinished)))

      search-paths (fn [start-path]
                     (iterate find-next-all-paths {:unfinished  [start-path]}))]
  (map (fn [start-path]
         (->> (search-paths start-path)
              (drop-while #(seq (:unfinished %)))
              first
              :finished
              count))  [part-1-start-path part-2-start-path]))