;;; +elfeed.el -*- lexical-binding: t; -*-

;;; Code:

(require 'elfeed)

;; https://karthinks.com/software/lazy-elfeed/
(defun +elfeed-scroll-up-command (&optional arg)
  "Scroll up or go to next feed item in Elfeed"
  (interactive "^P")
  (let ((scroll-error-top-bottom nil))
    (condition-case-unless-debug nil
        (scroll-up-command arg)
      (error (elfeed-show-next)))))

(defun +elfeed-scroll-down-command (&optional arg)
  "Scroll up or go to next feed item in Elfeed"
  (interactive "^P")
  (let ((scroll-error-top-bottom nil))
    (condition-case-unless-debug nil
        (scroll-down-command arg)
      (error (elfeed-show-prev)))))

(defun +elfeed-search-browse-generic ()
  "Browse a url with `browse-url-generic-browser'."
  (interactive)
  (elfeed-search-browse-url t))

(defun +elfeed-show-browse-generic ()
  "Browse a url with `browse-url-generic-browser'."
  (interactive)
  (elfeed-show-visit t))

(defun +elfeed-show-mark-read-and-advance ()
  "Mark an item as read and advance to the next item.
If multiple items are selected, don't advance."
  (interactive)
  (call-interactively #'elfeed-search-untag-all-unread)
  (unless (region-active-p)
    (call-interactively #'next-line)))

;;; Fetch feeds async
;; https://github.com/skeeto/elfeed/issues/367

(defun +elfeed--update-message ()
  (message "[Elfeed] Update in progress")
  'ignore)

(defvar +elfeed--update-running nil "Whether an update is currently running.")
(defvar +elfeed--update-count 0 "How many times `+elfeed-update-command' has run.")
(defcustom +elfeed-update-niceness 15
  "How \"nice\" `+elfeed-update-command' should be."
  :type 'integer
  :group 'elfeed)

(defun +elfeed-update-command ()
  (interactive)
  (unless (or +elfeed--update-running
              (derived-mode-p 'elfeed-show-mode 'elfeed-search-mode))
    (let ((script (expand-file-name "/tmp/elfeed-update.el"))
          (update-message-format "[Elfeed] Background update: %s"))
      (setq +elfeed--update-running t)
      (elfeed-db-save)
      (advice-add 'elfeed :override #'+elfeed--update-message)
      (ignore-errors (kill-buffer "*elfeed-search*"))
      (ignore-errors (kill-buffer "*elfeed-log*"))
      (elfeed-db-unload)
      (make-directory (file-name-directory script) :parents)
      (with-temp-buffer
        (insert
         (let ((print-level nil)
               (print-length nil))
           (prin1-to-string ;; Print the following s-expression to a string
            `(progn
               ;; Set up the environment
               (setq lexical-binding t)
               (load (locate-user-emacs-file "early-init"))
               (dolist (pkg '(elfeed elfeed-org))
                 (straight-use-package pkg)
                 (require pkg))
               ;; Copy variables from current environment
               (progn
                 ,@(cl-loop for copy-var in '(rmh-elfeed-org-files
                                              elfeed-db-directory
                                              elfeed-curl-program-name
                                              elfeed-use-curl
                                              elfeed-curl-extra-arguments
                                              elfeed-enclosure-default-dir)
                            collect `(progn (message "%S = %S" ',copy-var ',(symbol-value copy-var))
                                            (setq ,copy-var ',(symbol-value copy-var)))))
               ;; Define new variables for this environment
               (progn
                 ,@(cl-loop for (new-var . new-val) in '((elfeed-curl-max-connections . 4))
                            collect `(progn (message "%S = %S" ',new-var ',new-val)
                                            (setq ,new-var ',new-val))))
               ;; Redefine `elfeed-log' to log everything
               (defun elfeed-log (level fmt &rest objects)
                 (princ (format "[%s] [%s]: %s\n"
                                (format-time-string "%F %T")
                                level
                                (apply #'format fmt objects))))
               ;; Run elfeed
               (elfeed-org)
               (elfeed)
               (elfeed-db-load)
               (elfeed-update)
               ;; Wait for `elfeed-update' to finish
               (while (> (elfeed-queue-count-total) 0)
                 (sleep-for 5)
                 (message "%s" (elfeed-queue-count-total))
                 (accept-process-output))
               ;; Garbage collect and save the database
               (elfeed-db-gc)
               (elfeed-db-save)
               (princ (format ,update-message-format "done."))))))
        (write-file script))
      (chmod script #o777)
      (message update-message-format "start")
      (set-process-sentinel (start-process-shell-command
                             "Elfeed" "*+elfeed-update-background*"
                             (format "nice -n %d %s %s"
                                     +elfeed-update-niceness
                                     "emacs -Q --script"
                                     script))
                            (lambda (proc stat)
                              (advice-remove 'elfeed #'+elfeed--update-message)
                              (setq +elfeed--update-running nil)
                              (unless (string= stat "killed")
                                (setq +elfeed--update-count (1+ +elfeed--update-count)))
                              (message update-message-format (string-trim stat)))))))

(defvar +elfeed--update-timer nil "Timer for `elfeed-update-command'.")
(defvar +elfeed--update-first-time 6 "How long to wait for the first time.")
(defvar +elfeed--update-repeat (* 60 15) "How long between updates.")

(defcustom +elfeed-update-proceed-hook nil
  "Predicates to query before running `+elfeed-update-command'.
Each hook is passed no arguments."
  :type 'hook)

(defun +elfeed-update-command-wrapper ()
  "Run `+elfeed-update-command', but only sometimes.
If any of the predicates in `+elfeed-update-proceed-hook' return
nil, don't run `+elfeed-update-command'.  If they all return
non-nil, proceed."
  (when (run-hook-with-args-until-failure '+elfeed-update-proceed-hook)
    (+elfeed-update-command)))

(defun +elfeed--cancel-update-timer ()
  "Cancel `+elfeed--update-timer'."
  (unless +elfeed--update-running
    (ignore-errors (cancel-timer +elfeed--update-timer))
    (setq +elfeed--update-timer nil)))

(defun +elfeed--reinstate-update-timer ()
  "Reinstate `+elfeed--update-timer'."
  ;; First, unload the db
  (setq +elfeed--update-timer
        (run-at-time +elfeed--update-first-time
                     +elfeed--update-repeat
                     #'+elfeed-update-command-wrapper)))

(define-minor-mode +elfeed-update-async-mode
  "Minor mode to update elfeed async-style."
  :global t
  (if +elfeed-update-async-mode
      (progn                            ; enable
        (+elfeed--reinstate-update-timer)
        (advice-add 'elfeed :before '+elfeed--cancel-update-timer)
        (advice-add 'elfeed-search-quit-window :after '+elfeed--reinstate-update-timer))
    (progn                              ; disable
      (advice-remove 'elfeed '+elfeed--cancel-update-timer)
      (advice-remove 'elfeed-search-quit-window '+elfeed--reinstate-update-timer)
      (+elfeed--cancel-update-timer))))

(provide '+elfeed)
;;; +elfeed.el ends here
