;;; syd-buffers.el -*- lexical-binding: t; -*-

(require 'syd-prelude)

(syd-define-stub
 syd/kill-all-buffers
 :desc "Kill all buffers.  See `doom/kill-all-buffers'."
 :interactive t)

(syd-define-stub
 syd/kill-other-buffers
 :desc "Kill other buffers.  See `doom/kill-other-buffers'."
 :interactive t)

(syd-define-stub
 syd/kill-burried-buffers
 :desc "Kill burried buffers.  See `doom/kill-burried-buffers'."
 :interactive t)

(syd-define-stub
 syd/save-buffer-as-root
 :desc "Sudo save buffer as root.  See `doom/sudo-save-buffer'"
 :interactive t)

(defvar syd-fallback-buffer-name "*scratch*"
  "The name of the buffer to fall back to if no other buffers exist (will create
it if it doesn't exist).")

(defun syd-fallback-buffer ()
  "Returns the fallback buffer, creating it if necessary. By default this is the
scratch buffer. See `syd-fallback-buffer-name' to change this."
  (let (buffer-list-update-hook)
    (get-buffer-create syd-fallback-buffer-name)))

(defvar-local syd-real-buffer-p nil
  "If non-nil, this buffer should be considered real no matter what. See
`syd-real-buffer-p' for more information.")

(defun syd-special-buffer-p (buf)
  "Returns non-nil if BUF's name starts and ends with an *."
  (char-equal ?* (aref (buffer-name buf) 0)))

(defun syd-non-file-visiting-buffer-p (buf)
  (with-current-buffer buf
    (if buffer-file-name t nil)))

(defvar syd-unreal-buffer-functions
  '(minibufferp syd-special-buffer-p syd-non-file-visiting-buffer-p)
  "A list of predicate functions run to determine if a buffer is *not* real,
unlike `syd-real-buffer-functions'. They are passed one argument: the buffer to
be tested.

Should any of these functions return non-nil, the rest of the functions are
ignored and the buffer is considered unreal.

See `syd-real-buffer-p' for more information.")

(defun syd-dired-buffer-p (buf)
  "Returns non-nil if BUF is a dired buffer."
  (provided-mode-derived-p (buffer-local-value 'major-mode buf)
                           'dired-mode))

(defvar syd-real-buffer-functions
  '(syd-dired-buffer-p)
  "A list of predicate functions run to determine if a buffer is real, unlike
`syd-unreal-buffer-functions'. They are passed one argument: the buffer to be
tested.

Should any of its function returns non-nil, the rest of the functions are
ignored and the buffer is considered real.

See `syd-real-buffer-p' for more information.")

(defun syd-temp-buffer-p (buf)
  "Returns non-nil if BUF is temporary."
  (char-equal ?\s (aref (buffer-name buf) 0)))

(defun syd-real-buffer-p (buffer-or-name)
  "Returns t if BUFFER-OR-NAME is a 'real' buffer.

A real buffer is a useful buffer; a first class citizen. Real ones should get
special treatment, because we will be spending most of our time in them. Unreal
ones should be low-profile and easy to cast aside, so we can focus on real ones.

The exact criteria for a real buffer is:

  1. A non-nil value for the buffer-local value of the `syd-real-buffer-p'
     variable OR
  2. Any function in `syd-real-buffer-functions' returns non-nil OR
  3. None of the functions in `syd-unreal-buffer-functions' must return
     non-nil.

If BUFFER-OR-NAME is omitted or nil, the current buffer is tested."
  (or (bufferp buffer-or-name)
      (stringp buffer-or-name)
      (signal 'wrong-type-argument (list '(bufferp stringp) buffer-or-name)))
  (when-let (buf (get-buffer buffer-or-name))
    (when-let (basebuf (buffer-base-buffer buf))
      (setq buf basebuf))
    (and (buffer-live-p buf)
         (not (syd-temp-buffer-p buf))
         (or (buffer-local-value 'syd-real-buffer-p buf)
             (run-hook-with-args-until-success 'syd-real-buffer-functions buf)
             (not (run-hook-with-args-until-success 'syd-unreal-buffer-functions buf))))))

(defun syd-unreal-buffer-p (buffer-or-name)
  "Return t if BUFFER-OR-NAME is an 'unreal' buffer.

See `syd-real-buffer-p' for details on what that means."
  (not (syd-real-buffer-p buffer-or-name)))

(defun syd-fixup-windows (windows)
  "Ensure that each of WINDOWS is showing a real buffer or the fallback buffer."
  (dolist (window windows)
    (with-selected-window window
      (when (syd-unreal-buffer-p (window-buffer))
        (previous-buffer)
        (when (syd-unreal-buffer-p (window-buffer))
          (switch-to-buffer (syd-fallback-buffer)))))))

;;;###autoload
(defun syd-set-buffer-realness (buffer realness)
  (with-current-buffer buffer
    (setq syd-real-buffer-p realness)))

;;;###autoload
(defun syd-mark-buffer-as-real ()
  (syd-set-buffer-realness (current-buffer) t))

(defun syd-kill-buffer-fixup-windows (buffer)
  "Kill the BUFFER and ensure all the windows it was displayed in have switched
to a real buffer or the fallback buffer."
  (let ((windows (get-buffer-window-list buffer)))
    (kill-buffer buffer)
    (syd-fixup-windows (cl-remove-if-not #'window-live-p windows))))

(defun syd/kill-this-buffer-in-all-windows (buffer &optional dont-save)
  "Kill BUFFER globally and ensure all windows previously showing this buffer
have switched to a real buffer or the fallback buffer.

If DONT-SAVE, don't prompt to save modified buffers (discarding their changes)."
  (interactive
   (list (current-buffer) current-prefix-arg))
  (cl-assert (bufferp buffer) t)
  (when (and (buffer-modified-p buffer) dont-save)
    (with-current-buffer buffer
      (set-buffer-modified-p nil)))
  (syd-kill-buffer-fixup-windows buffer))

(provide 'syd-buffers)
