;; Advent of Code 2020 - Day 4

(require 'cl-lib)
(require 's)
(require 'dash)
(require 'f)

(cl-defstruct day4/passport
  byr ;; Birth Year
  iyr ;; Issue Year
  eyr ;; Expiration Year
  hgt ;; Height
  hcl ;; Hair Color
  ecl ;; Eye Color
  pid ;; Passport ID
  cid ;; Country ID
  )

(defun day4/parse-passport (input)
  (let* ((pairs (s-split " " (s-replace "\n" " " input) t))
         (slots
          (-map
           (lambda (pair)
             (pcase-let ((`(,key ,value) (s-split ":" (s-trim pair))))
               (list (intern (format ":%s" key)) value)))
           pairs)))
    (apply #'make-day4/passport (-flatten slots))))

(defun day4/parse-passports (input)
  (-map #'day4/parse-passport (s-split "\n\n" input t)))

(setq day4/input (day4/parse-passports (f-read "/tmp/aoc/day4.txt")))

;; Puzzle 1

(defun day4/validate (passport)
  "Check that all fields except CID are present."
  (cl-check-type passport day4/passport)
  (and (day4/passport-byr passport)
       (day4/passport-iyr passport)
       (day4/passport-eyr passport)
       (day4/passport-hgt passport)
       (day4/passport-hcl passport)
       (day4/passport-ecl passport)
       (day4/passport-pid passport)))

(message "Solution to day4/1: %s" (cl-loop for passport being the elements of day4/input
                                           count (day4/validate passport)))

;; Puzzle 2

(defun day4/year-bound (min max value)
  (and
   (s-matches? (rx (= 4 digit)) value)
   (<= min (string-to-number value) max)))

(defun day4/check-unit (unit min max value)
  (and
   (string-match (rx (group (+? digit)) (literal unit)) value)
   (<= min (string-to-number (match-string 1 value)) max)))

(defun day4/properly-validate (passport)
  "Opting for readable rather than clever here."
  (and
   (day4/validate passport)

   ;; byr (Birth Year) - four digits; at least 1920 and at most 2002.
   (day4/year-bound 1920 2002 (day4/passport-byr passport))

   ;; iyr (Issue Year) - four digits; at least 2010 and at most 2020.
   (day4/year-bound 2010 2020 (day4/passport-iyr passport))

   ;; eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
   (day4/year-bound 2020 2030 (day4/passport-eyr passport))

   ;; hgt (Height) - a number followed by either cm or in:
   ;; If cm, the number must be at least 150 and at most 193.
   ;; If in, the number must be at least 59 and at most 76.
   (or (day4/check-unit "cm" 150 193 (day4/passport-hgt passport))
       (day4/check-unit "in" 59 76 (day4/passport-hgt passport)))

   ;; hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
   (s-matches? (rx ?# (= 6 hex)) (day4/passport-hcl passport))

   ;; ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
   (-contains? '("amb" "blu" "brn" "gry" "grn" "hzl" "oth")
               (day4/passport-ecl passport))

   ;; pid (Passport ID) - a nine-digit number, including leading zeroes.
   (s-matches? (rx line-start (= 9 digit) line-end)
               (day4/passport-pid passport))

   ;; cid (Country ID) - ignored, missing or not.
   ))

(message "Solution to day4/2: %s"
         (cl-loop for passport being the elements of day4/input
                  count (day4/properly-validate passport)))