Advent of Code 2021 solutions
(ql:quickload 'split-sequence)

(defun read-data (path)
  (with-open-file (stream path)
    (loop for line = (read-line stream nil)
          while line collect line)))

(defparameter *input-source*
  (cadr *posix-argv*))

;; Useful in testing
;; (defparameter *input-source* "example_input")
;; (defparameter *input-source* "input")

(defparameter *data* (read-data *input-source*))

(defstruct board
  board
  x-marked-count
  y-marked-count
  value-index
  unmarked-sum)

(defun build-value-index (arr)
  (destructuring-bind (x y) (array-dimensions arr)
    (loop for i from 0 below x
          with table = (make-hash-table)
          do (loop for j from 0 below y
                   do (setf (gethash (aref arr i j) table) (list i j)))
          finally (return table))))

(defun sum-values (arr)
  (destructuring-bind (x y) (array-dimensions arr)
    (loop for i from 0 below x
          sum (loop for j from 0 below y
                    sum (aref arr i j)))))

(defun new-board (numbers)
  (let* ((dimension-x (length numbers))
         (dimension-y (length (car numbers)))
         (arr (make-array (list dimension-x dimension-y)
                          :initial-contents numbers)))
    (make-board :board arr
                :x-marked-count (make-array dimension-x)
                :y-marked-count (make-array dimension-y)
                :value-index (build-value-index arr)
                :unmarked-sum (sum-values arr))))

(defun mark-drawn-number (number board)
  (when (gethash number (board-value-index board))
    (destructuring-bind (x y) (gethash number (board-value-index board))
      (setf (board-unmarked-sum board) (- (board-unmarked-sum board) number))
      (setf (aref (board-x-marked-count board) x) (1+ (aref (board-x-marked-count board) x)))
      (setf (aref (board-y-marked-count board) y) (1+ (aref (board-y-marked-count board) y))))))

(defun board-win-p (board)
  (destructuring-bind (x y) (array-dimensions (board-board board))
    (or (some (lambda (v) (= v x)) (board-x-marked-count board))
        (some (lambda (v) (= v y)) (board-y-marked-count board)))))

(defun parse-draws (draws)
  (map 'list #'parse-integer
       (split-sequence:split-sequence #\, draws)))

(defun parse-board-row (row)
  (map 'list #'parse-integer
       (split-sequence:split-sequence #\Space row :remove-empty-subseqs T)))

(defun parse-board (board)
  (new-board (map 'list #'parse-board-row board)))


(defun parse-boards (boards)
  (map 'list #'parse-board boards))

(defun parse-input (lines)
  (let ((groups (split-sequence:split-sequence "" lines :test #'string=)))
    (values (parse-draws (car (car groups)))
            (parse-boards (cdr groups)))))

(defun play-bingo (draws boards)
  (loop for number in draws
        with winning-boards = '()
        with playing-boards = boards
        do (dolist (board playing-boards)
             (mark-drawn-number number board))
        do (setf winning-boards (remove-if-not #'board-win-p playing-boards))
        do (setf playing-boards (remove-if #'board-win-p playing-boards))
        when winning-boards
          collect (list number winning-boards)
        while playing-boards))

(defun part1 (scores)
  (destructuring-bind (number boards) (car scores)
    (* number
       (board-unmarked-sum (car boards)))))

(defun part2 (scores)
  (destructuring-bind (number boards) (car (last scores))
    (* number
       (board-unmarked-sum (car (last boards))))))

(time
 (multiple-value-bind (draws boards) (parse-input *data*)
   (let ((scores (play-bingo draws boards)))
     (format t "Part 1: ~A~%" (part1 scores))
     (format t "Part 2: ~A~%" (part2 scores)))))