;;; syd-use-package.el -*- lexical-binding: t; -*-

(defvar syd-incremental-idle-timer 0.75
  "How long (in idle seconds) in between incrementally loading packages.")

(defvar syd-incremental-first-idle-timer (if (daemonp) 0 2.0)
  "How long (in idle seconds) until incremental loading starts.

Set this to nil to disable incremental loading at startup.
Set this to 0 to load all incrementally deferred packages immediately at
`after-init-hook'.")

(defvar syd-incremental-packages '(t)
  "A list of packages to load incrementally after startup. Any large packages
here may cause noticeable pauses, so it's recommended you break them up into
sub-packages. For example, `org' is comprised of many packages, and might be
broken up into:

  (syd-load-packages-incrementally
   '(calendar find-func format-spec org-macs org-compat
     org-faces org-entities org-list org-pcomplete org-src
     org-footnote org-macro ob org org-clock org-agenda
     org-capture))

This is already done by the lang/org module, however.

If you want to disable incremental loading altogether, either remove
`syd-load-packages-incrementally-h' from `after-init-hook' or set
`syd-incremental-first-idle-timer' to nil. Incremental loading does not occur
in daemon sessions (they are loaded immediately at startup).")

(defun syd-load-packages-incrementally (packages &optional now)
  "Registers PACKAGES to be loaded incrementally.

If NOW is non-nil, PACKAGES will be marked for incremental loading next time
Emacs is idle for `syd-incremental-first-idle-timer' seconds (falls back to
`syd-incremental-idle-timer'), then in `syd-incremental-idle-timer' intervals
afterwards."
  (let* ((gc-cons-threshold most-positive-fixnum)
         (first-idle-timer (or syd-incremental-first-idle-timer
                               syd-incremental-idle-timer)))
    (if (not now)
        (cl-callf append syd-incremental-packages packages)
      (while packages
        (let ((req (pop packages))
              idle-time)
          (unless (featurep req)
            (condition-case-unless-debug e
                (and
                 (or (null (setq idle-time (current-idle-time)))
                     (< (float-time idle-time) first-idle-timer)
                     (not
                      (while-no-input
                        ;; If `default-directory' doesn't exist or is
                        ;; unreadable, Emacs throws file errors.
                        (let ((default-directory user-emacs-directory)
                              (inhibit-message t)
                              (file-name-handler-alist
                               (list (rassq 'jka-compr-handler file-name-handler-alist))))
                          (message "loading %s" req)
                          (require req nil t)
                          t))))
                 (push req packages))
              (error
               (message "Error: failed to incrementally load %S because: %s" req e)
               (setq packages nil)))
            (unless (null packages)
              (run-at-time (if idle-time
                               syd-incremental-idle-timer
                             first-idle-timer)
                           nil #'syd-load-packages-incrementally
                           packages t)
              (setq packages nil))))))))

(defun syd-load-packages-incrementally-h ()
  "Begin incrementally loading packages in `syd-incremental-packages'.

If this is a daemon session, load them all immediately instead."
  (when (numberp syd-incremental-first-idle-timer)
    (if (zerop syd-incremental-first-idle-timer)
        (mapc #'require (cdr syd-incremental-packages))
      (run-with-idle-timer syd-incremental-first-idle-timer
                           nil #'syd-load-packages-incrementally
                           (cdr syd-incremental-packages) t))))

(unless noninteractive
  (add-hook 'after-init-hook #'syd-load-packages-incrementally-h 100))

(with-eval-after-load 'use-package-core
  (push :defer-incrementally use-package-deferring-keywords)
  (setq use-package-keywords (use-package-list-insert
                              :defer-incrementally use-package-keywords :after))
  (defalias 'use-package-normalize/:defer-incrementally
    #'use-package-normalize-symlist)
  (defun use-package-handler/:defer-incrementally
      (name _keyword targets rest state)
    (use-package-concat
     `((syd-load-packages-incrementally
        ',(if (equal targets '(t))
              (list name)
            (append targets (list name)))))
     (use-package-process-keywords name rest state))))

(provide 'syd-use-package)
;;; syd-use-package.el ends here
