;; SPDX-FileCopyrightText: 2026 Andrew Oberstar <andrew@ajoberstar.org>
;; SPDX-License-Identifier: AGPL-3.0-only

(in-package #:org.ajoberstar.quixote.stats)

(defun oldest (readings)
  (reduce (lambda (a b)
            (if (dates:date< a b) a b))
          (mapcar #'model:reading-at
                  readings)
          :initial-value dates:maximum-date))

(defun newest (readings)
  (reduce (lambda (a b)
            (if (dates:date> a b) a b))
          (mapcar #'model:reading-at
                  readings)
          :initial-value dates:minimum-date))

(defun periods (period-fun days readings start)
  (loop with today = (dates:today)
        with start = (or start (funcall period-fun (oldest readings)))
        for date = start then (funcall period-fun (dates:date+ date days))
        while (dates:date< date today)
        collect date))

(defun period-pages (period-fun days readings &optional start)
  (let* ((periods (periods period-fun days readings start))
         (period-data (loop for period in periods
                            for filtered = (remove-if-not (lambda (reading)
                                                            (dates:date= period
                                                                         (funcall period-fun (model:reading-at reading))))
                                                          readings)
                            collect (reduce #'+ (mapcar #'model:reading-pages
                                                        filtered)))))
    (list :labels (mapcar (lambda (date) (dates:write-iso-date nil date)) periods)
          :dataset period-data)))

(defun year-pages (readings)
  (period-pages #'dates:date-start-of-year
                366
                readings))


(defun quarter-pages (start readings)
  (period-pages #'dates:date-start-of-quarter
                93
                readings
                start))

(defun week-pages (start readings)
  (period-pages (lambda (date)
                  (dates:date-prev date :monday))
                7
                readings
                start))

(defun map-pace (days dates readings)
  (loop for start = (dates:date- (first dates) 1)
        for date in dates
        for base = (let ((ideal-base (dates:date- date days)))
                     (if (dates:date< start ideal-base)
                         ideal-base
                         start))
        for filtered = (remove-if-not (lambda (reading)
                                        (and (dates:date< base (model:reading-at reading))
                                             (dates:date<= (model:reading-at reading) date)))
                                      readings)
        for period = (- (dates:date-ordinal date)
                        (dates:date-ordinal base))
        collect (/ (reduce #'+ (mapcar #'model:reading-pages filtered))
                   period)))

(defun map-ema (periods list)
  (loop with alpha = (/ 2.0 (1+ periods))
        with prev = nil
        for current in list
        for ema = (if prev
                      (+ (* alpha current)
                         (* (- 1.0 alpha) prev))
                      current)
        do (setf prev ema)
        collect ema))

(defun pace (readings)
  (let* ((pace-dates (let ((oldest (oldest readings)))
                       (loop for date = (dates:date-prev oldest :monday) then (dates:date+ date 7)
                             until (dates:date> date (dates:today))
                             collect date)))
         (pace-labels (mapcar (lambda (date) (dates:write-iso-date nil date))
                              pace-dates))
         (month (map-ema 4 (map-pace (* 7 4) pace-dates readings)))
         (quarter (map-ema 13 (map-pace (* 7 13) pace-dates readings)))
         (year (map-ema 52 (map-pace (* 7 52) pace-dates readings))))
    (list :labels pace-labels :month-dataset month :quarter-dataset quarter :year-dataset year)))

(defun progress (readings)
  (let* ((progress-dates (let ((oldest (oldest readings))
                               (newest (newest readings)))
                           (loop for date = oldest then (dates:date-1+ date)
                                 until (dates:date> date newest)
                                 collect date)))
         (progress-labels (mapcar (lambda (date) (dates:write-iso-date nil date))
                                  progress-dates))
         (progress-data (loop with total = 0
                              for date in progress-dates
                              for date-readings = (remove-if-not (lambda (reading)
                                                                   (dates:date= date (model:reading-at reading)))
                                                                 readings)
                              do (incf total (reduce #'+ (mapcar #'model:reading-pages date-readings)))
                              collect total)))
    (list :labels progress-labels :dataset progress-data)))
