(in-package :ca.mhcat.advent2022)

;; --- Day 7: No Space Left On Device ---

;; You can hear birds chirping and raindrops hitting leaves
;; as the expedition proceeds. Occasionally, you can even
;; hear much louder sounds in the distance; how big do the
;; animals get out here, anyway?

;; The device the Elves gave you has problems with more than
;; just its communication system. You try to run a system
;; update:

;; $ system-update --please --pretty-please-with-sugar-on-top
;; Error: No space left on device

;; Perhaps you can delete some files to make space for the
;; update?

;; You browse around the filesystem to assess the situation
;; and save the resulting terminal output (your puzzle
;; input). For example:

;; $ cd /
;; $ ls
;; dir a
;; 14848514 b.txt
;; 8504156 c.dat
;; dir d
;; $ cd a
;; $ ls
;; dir e
;; 29116 f
;; 2557 g
;; 62596 h.lst
;; $ cd e
;; $ ls
;; 584 i
;; $ cd ..
;; $ cd ..
;; $ cd d
;; $ ls
;; 4060174 j
;; 8033020 d.log
;; 5626152 d.ext
;; 7214296 k

;; The filesystem consists of a tree of files (plain data)
;; and directories (which can contain other directories or
;; files). The outermost directory is called /. You can
;; navigate around the filesystem, moving into or out of
;; directories and listing the contents of the directory
;; you're currently in.

;; Within the terminal output, lines that begin with $ are
;; commands you executed, very much like some modern
;; computers:

;;     cd means change directory. This changes which
;;     directory is the current directory, but the specific
;;     result depends on the argument:

;;         cd x moves in one level: it looks in the current
;;         directory for the directory named x and makes it
;;         the current directory.

;;         cd .. moves out one level: it finds the directory
;;         that contains the current directory, then makes
;;         that directory the current directory.

;;         cd / switches the current directory to the
;;         outermost directory, /.

;;     ls means list. It prints out all of the files and
;;     directories immediately contained by the current
;;     directory:

;;         123 abc means that the current directory contains
;;         a file named abc with size 123.

;;         dir xyz means that the current directory contains
;;         a directory named xyz.

;; Given the commands and output in the example above, you
;; can determine that the filesystem looks visually like
;; this:

;; - / (dir)
;;   - a (dir)
;;     - e (dir)
;;       - i (file, size=584)
;;     - f (file, size=29116)
;;     - g (file, size=2557)
;;     - h.lst (file, size=62596)
;;   - b.txt (file, size=14848514)
;;   - c.dat (file, size=8504156)
;;   - d (dir)
;;     - j (file, size=4060174)
;;     - d.log (file, size=8033020)
;;     - d.ext (file, size=5626152)
;;     - k (file, size=7214296)

;; Here, there are four directories: / (the outermost
;; directory), a and d (which are in /), and e (which is in
;; a). These directories also contain files of various
;; sizes.

;; Since the disk is full, your first step should probably
;; be to find directories that are good candidates for
;; deletion. To do this, you need to determine the total
;; size of each directory. The total size of a directory is
;; the sum of the sizes of the files it contains, directly
;; or indirectly. (Directories themselves do not count as
;; having any intrinsic size.)

;; The total sizes of the directories above can be found as
;; follows:

;;     The total size of directory e is 584 because it
;;     contains a single file i of size 584 and no other
;;     directories.

;;     The directory a has total size 94853 because it
;;     contains files f (size 29116), g (size 2557), and
;;     h.lst (size 62596), plus file i indirectly (a
;;     contains e which contains i).

;;     Directory d has total size 24933642.

;;     As the outermost directory, / contains every file.
;;     Its total size is 48381165, the sum of the size of
;;     every file.

;; To begin, find all of the directories with a total size
;; of at most 100000, then calculate the sum of their total
;; sizes. In the example above, these directories are a and
;; e; the sum of their total sizes is 95437 (94853 + 584).
;; (As in this example, this process can count files more
;; than once!)

;; Find all of the directories with a total size of at most
;; 100000. What is the sum of the total sizes of those
;; directories?

(defparameter day7/test-data
  '("$ cd /"
    "$ ls"
    "dir a"
    "14848514 b.txt"
    "8504156 c.dat"
    "dir d"
    "$ cd a"
    "$ ls"
    "dir e"
    "29116 f"
    "2557 g"
    "62596 h.lst"
    "$ cd e"
    "$ ls"
    "584 i"
    "$ cd .."
    "$ cd .."
    "$ cd d"
    "$ ls"
    "4060174 j"
    "8033020 d.log"
    "5626152 d.ext"
    "7214296 k"))

(labels ((commandp (line) (string-equal "$" (car line)))
         (dirp (line) (string-equal "dir" (car line)))
         (file-size (line) (parse-integer (car line) :junk-allowed t))
         (filep (line) (integerp (file-size line)))
         (command (line) (second line))
         (cdp (line) (string-equal "cd" (command line)))
         (operand (line) (third line))
         (node-name (line) (second line))

         (change-dir (target cwd)
           (if (string-equal ".." target)
               (cdr cwd)
               (cons target cwd)))

         (directories (acc cwd lines)
           (let ((line (car lines)))
             (cond
               ((null line) acc)
               ((commandp line)
                (if (cdp line)
                    ;; change directory without modifying structure
                    (directories acc
                                 (change-dir (operand line) cwd)
                                 (cdr lines))
                    ;; otherwise it's a ls command, so throw it away
                    (directories acc cwd (cdr lines))))
               ((or (dirp line) (filep line))
                (let* ((n (or (position "$" lines :key #'car :test #'string-equal)
                              (length lines)))
                       (rest (nthcdr n lines)))
                  (if (assoc cwd acc :test #'equal)
                      (directories acc cwd rest)
                      (directories (acons cwd (subseq lines 0 n) acc) cwd rest))))
               (t (error "poop"))))))

  (defun day7/directories (lst)
    (directories nil nil lst)))

(defun day7/parse-line (line)
  (labels ((parse-line (acc line)
             (let ((sp (position #\space line :test #'char=)))
               (if (null sp)
                   (mapcar (lambda (l) (coerce l 'string))
                           (cons line acc))
                   (parse-line (cons (subseq line 0 sp) acc)
                               (subseq line (1+ sp)))))))
    (nreverse
     (parse-line nil (coerce line 'list)))))

(defun day7/load-dataset (lst)
  (mapcar #'day7/parse-line lst))

(defun day7/sum-contents (alst)
  (labels ((sum-files (lst)
             (loop for (x y) in (cdr lst)
                   if (string-equal "dir" x)
                     sum (sum-files (assoc (cons y (car lst)) alst
                                           :test #'equal))
                   else
                     sum (parse-integer x))))
    (mapcar (lambda (lst)
              (list (car lst) (sum-files lst)))
            alst)))

(defun day7/compute-part1 (lst)
  (let* ((parsed-data (day7/load-dataset lst))
         (dirs (day7/directories parsed-data))
         (dir-sizes (day7/sum-contents dirs))
         (sums (mapcar #'second dir-sizes))
         (smol-sums (remove-if-not #'(lambda (v) (<= v 100000)) sums)))
    (apply #'+ smol-sums)))

(defun day7/part1 ()
  (day7/compute-part1
   (load-lines "day7.txt")))

;; --- Part Two ---

;; Now, you're ready to choose a directory to delete.

;; The total disk space available to the filesystem is
;; 70000000. To run the update, you need unused space of at
;; least 30000000. You need to find a directory you can
;; delete that will free up enough space to run the update.

;; In the example above, the total size of the outermost
;; directory (and thus the total amount of used space) is
;; 48381165; this means that the size of the unused space
;; must currently be 21618835, which isn't quite the
;; 30000000 required by the update. Therefore, the update
;; still requires a directory with total size of at least
;; 8381165 to be deleted before it can run.

;; To achieve this, you have the following options:

;;     Delete directory e, which would increase unused space by 584.
;;     Delete directory a, which would increase unused space by 94853.
;;     Delete directory d, which would increase unused space by 24933642.
;;     Delete directory /, which would increase unused space by 48381165.

;; Directories e and a are both too small; deleting them
;; would not free up enough space. However, directories d
;; and / are both big enough! Between these, choose the
;; smallest: d, increasing unused space by 24933642.

;; Find the smallest directory that, if deleted, would free
;; up enough space on the filesystem to run the update. What
;; is the total size of that directory?

(defun day7/compute-part2 (lst)
  (let* ((parsed-data (day7/load-dataset lst))
         (dirs (day7/directories parsed-data))
         (dir-sizes (day7/sum-contents dirs))
         (total-used (second (assoc '("/") dir-sizes :test #'equal)))
         (available (- 70000000 total-used))
         (required (- 30000000 available))
         (candidates (remove-if (lambda (cons) (< (second cons) required)) dir-sizes)))
    (car (sort candidates #'< :key #'second))))

(defun day7/part2 ()
  (day7/compute-part2
   (load-lines "day7.txt")))