;;; mu4e-window.el --- Window management -*- lexical-binding: t; -*-

;; Copyright (C) 2022  Mickey Petersen
;; Copyright (C) 2023-2024 Dirk-Jan C. Binnema

;; Author: Mickey Petersen <mickey@masteringemacs.org>
;; Keywords: mail

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;;; Code:

;;; Buffer names for internal use

(defconst mu4e--sexp-buffer-name "*mu4e-sexp-at-point*"
  "Buffer name for sexp buffers.")

(defvar mu4e-main-buffer-name "*mu4e-main*"
  "Name of the mu4e main buffer.")

(defvar mu4e-embedded-buffer-name " *mu4e-embedded*"
  "Name for the embedded message view buffer.")

;; Buffer names for public use

(defvar mu4e-headers-buffer-name "*mu4e-headers*"
  "Name of the buffer for message headers.")

(defvar mu4e-view-buffer-name "*mu4e-article*"
  "Name of the view buffer.")

(defvar mu4e-headers-buffer-name-func nil
  "Function used to name the headers buffers.")

(defvar mu4e-view-buffer-name-func nil
  "Function used to name the view buffers.

The function is given one argument, the headers buffer it is
linked to.")

(defvar-local mu4e-linked-headers-buffer nil
  "Holds the headers buffer object that ties it to a view.")

(defcustom mu4e-split-view 'horizontal
  "How to show messages / headers.
A symbol which is either:
 * `horizontal':    split horizontally (headers on top)
 * `vertical':      split vertically (headers on the left).
 * `single-window': view and headers in one window (mu4e will try not to
        touch your window layout), main view in minibuffer
 * anything else:   don't split (show either headers or messages,
        not both).

Also see `mu4e-headers-visible-lines' and
`mu4e-headers-visible-columns'.

Note that in older mu4e version, the value could also be
function; this is no longer supported; instead you can use
`display-buffer-alist'."
  :type '(choice (const :tag "Split horizontally" horizontal)
                 (const :tag "Split vertically" vertical)
                 (const :tag "Single window" single-window)
                 (const :tag "Don't split" nil))
  :group 'mu4e-headers)

(defcustom mu4e-headers-visible-lines 10
  "Number of lines to display in the header view when using the
horizontal split-view. This includes the header-line at the top,
and the mode-line."
  :type 'integer
  :group 'mu4e-headers)

(defcustom mu4e-headers-visible-columns 30
  "Number of columns to display for the header view when using the
vertical split-view."
  :type 'integer
  :group 'mu4e-headers)

(defcustom mu4e-compose-switch nil
  "Where to display the new message?
A symbol:
- nil           : default (new buffer)
- window        : compose in new window
- frame or t    : compose in new frame
- display-buffer: use `display-buffer' / `display-buffer-alist'
  (for fine-tuning).

For backward compatibility with `mu4e-compose-in-new-frame', t is
treated as =\\'frame."
  :type 'symbol
  :group 'mu4e-compose)

(declare-function mu4e-view-mode "mu4e-view")
(declare-function mu4e-error     "mu4e-helpers")
(declare-function mu4e-warn      "mu4e-helpers")
(declare-function mu4e-message   "mu4e-helpers")

(defun mu4e-get-headers-buffer (&optional buffer-name create)
  "Return a related headers buffer optionally named BUFFER-NAME.

If CREATE is non-nil, the headers buffer is created if the
generated name does not already exist."
  (let* ((buffer-name
          (or
           ;; buffer name generator func. If a user wants
           ;; to supply its own naming scheme, we use that
           ;; in lieu of our own heuristic.
           (and mu4e-headers-buffer-name-func
                (funcall mu4e-headers-buffer-name-func))
           ;; if we're supplied a buffer name for a
           ;; headers buffer then try to use that one.
           buffer-name
           ;; if we're asking for a headers buffer from a
           ;; view, then we get our linked buffer. If
           ;; there is no such linked buffer -- it is
           ;; detached -- raise an error.
           (and (mu4e-current-buffer-type-p 'view)
                mu4e-linked-headers-buffer)
           ;; if we're already in a headers buffer then
           ;; that is the one we use.
           (and (mu4e-current-buffer-type-p 'headers)
                (current-buffer))
           ;; default name to use if all other checks fail.
           mu4e-headers-buffer-name))
         (buffer (get-buffer buffer-name)))
    (when (and (not (buffer-live-p buffer)) create)
      (setq buffer (get-buffer-create buffer-name)))
    ;; This may conceivably return a non-existent buffer if `create'
    ;; and `buffer-live-p' are nil.
    ;;
    ;; This is seemingly "OK" as various parts of the code check for
    ;; buffer liveness themselves.
    buffer))

(defun mu4e-get-view-buffers (pred)
  "Filter all known view buffers and keep those where PRED return non-nil.

The PRED function is called from inside the buffer that is being
tested."
  (seq-filter
   (lambda (buf)
     (with-current-buffer buf
       (and (mu4e-current-buffer-type-p 'view)
            (and pred (funcall pred buf)))))
   (buffer-list)))

(defun mu4e--view-detached-p (buffer)
  "Return non-nil if BUFFER is a detached view buffer."
  (with-current-buffer buffer
    (unless (mu4e-current-buffer-type-p 'view)
      (mu4e-error "Buffer `%s' is not a valid mu4e view buffer" buffer))
    (null mu4e-linked-headers-buffer)))

(defun mu4e--get-current-buffer-type ()
  "Return an internal symbol that corresponds to each mu4e major mode."
  (cond ((or (derived-mode-p 'mu4e-view-mode)
             (derived-mode-p 'mu4e-raw-view-mode)) 'view)
        ((derived-mode-p 'mu4e-headers-mode) 'headers)
        ((derived-mode-p 'mu4e-compose-mode) 'compose)
        ((derived-mode-p 'mu4e-main-mode) 'main)
        (t 'unknown)))

(defun mu4e-current-buffer-type-p (type)
  "Return non-nil if the current buffer is a mu4e buffer of TYPE.

Where TYPE is `view', `headers', `compose', `main' or `unknown'.

Checks are performed using `derived-mode-p' and the current
buffer's major mode."
  (eq (mu4e--get-current-buffer-type) type))


;; backward-compat; buffer-local-boundp was introduced in emacs 28.
(defun mu4e--buffer-local-boundp (symbol buffer)
  "Return non-nil if SYMBOL is bound in BUFFER.
Also see `local-variable-p'."
  (condition-case nil
      (buffer-local-value symbol buffer)
    (:success t)
    (void-variable nil)))


(defun mu4e-get-view-buffer (&optional headers-buffer create)
  "Return a view buffer belonging optionally to HEADERS-BUFFER.

If HEADERS-BUFFER is nil, the most likely (and available) headers
buffer is used.

Detached view buffers are ignored; that may result in a new view buffer
being created if CREATE is non-nil."
  ;; If `headers-buffer' is nil, then the caller does not have a
  ;; headers buffer preference.
  ;;
  ;; In that case, we request the most plausible headers buffer from
  ;; `mu4e-get-headers-buffer'.
  (when (setq headers-buffer (or headers-buffer (mu4e-get-headers-buffer)))
    (let ((buffer)
          ;; If `mu4e-view-buffer-name-func' is non-nil, then use that
          ;; to source the name of the view buffer to create or re-use.
          (buffer-name
           (or (and mu4e-view-buffer-name-func
                    (funcall mu4e-view-buffer-name-func headers-buffer))
               ;; If the variable is nil, use the default
               ;; name
               mu4e-view-buffer-name))
          ;; Search all view buffers and return those that are linked to
          ;; `headers-buffer'.
          (linked-buffer
           (mu4e-get-view-buffers
            (lambda (buf)
              (and (mu4e--buffer-local-boundp 'mu4e-linked-headers-buffer buf)
                   (eq mu4e-linked-headers-buffer headers-buffer))))))
      ;; If such a linked buffer exists and its buffer is live, we use that
      ;; buffer.
      (if (and linked-buffer (buffer-live-p (car linked-buffer)))
          ;; NOTE: It's possible for there to be more than one linked view
          ;; buffer.
          ;;
          ;; What, if anything, should the heuristic be to pick the
          ;; one to use? Presently `car' is used, but there are better
          ;; ways, no doubt. Perhaps preferring those with live windows?
          (setq buffer (car linked-buffer))
        (setq buffer (get-buffer buffer-name))
        ;; check if `buffer' is already live *and* detached. If it is,
        ;; we'll generate a new, unique name.
        (when (and (buffer-live-p buffer) (mu4e--view-detached-p buffer))
          (setq buffer (generate-new-buffer-name buffer-name)))
        (when (and (not (buffer-live-p buffer)) create)
          (setq buffer (get-buffer-create (or buffer buffer-name)))
          (with-current-buffer buffer
            (mu4e-view-mode))))
      (when (and buffer (buffer-live-p buffer))
        ;; Required. Callers expect the view buffer to be set.
        (set-buffer buffer)
        ;; Required. The call chain of `mu4e-view-mode' ends up
        ;; calling `kill-all-local-variables', which destroys the
        ;; local binding.
        (set (make-local-variable 'mu4e-linked-headers-buffer) headers-buffer))
      buffer)))

;; backward compat: `display-buffer-full-frame' only appears in emacs 29.
(unless (fboundp 'display-buffer-full-frame)
  (defun display-buffer-full-frame (buffer alist)
    "Display BUFFER in the current frame, taking the entire frame.
ALIST is an association list of action symbols and values.  See
Info node `(elisp) Buffer Display Action Alists' for details of
such alists.

This is an action function for buffer display, see Info
node `(elisp) Buffer Display Action Functions'.  It should be
called only by `display-buffer' or a function directly or
indirectly called by the latter."
    (when-let ((window (or (display-buffer-reuse-window buffer alist)
                           (display-buffer-same-window buffer alist)
                           (display-buffer-pop-up-window buffer alist)
                           (display-buffer-use-some-window buffer alist))))
      (delete-other-windows window)
      window)))


(defun mu4e-display-buffer (buffer-or-name &optional select)
  "Display BUFFER-OR-NAME as per `mu4e-split-view'.

If SELECT is non-nil, the final window (and thus BUFFER-OR-NAME)
is selected.

This function internally uses `display-buffer' (or
`pop-to-buffer' if SELECT is non-nil).

It is therefore possible to change the display behavior by
modifying `display-buffer-alist'.

If `mu4e-split-view' is a function, then it must return a live window
for BUFFER-OR-NAME to be displayed in."
  ;; For now, using a function for mu4e-split-view is not behaving well
  ;; Turn off.
  (when (functionp mu4e-split-view)
    (mu4e-message "Function for `mu4e-split-view' not supported; fallback")
    (setq mu4e-split-view 'horizontal))

  (let* ((buffer-name (or (get-buffer buffer-or-name)
                          (mu4e-error "Buffer `%s' does not exist"
                                      buffer-or-name)))
         (buffer-type
          (with-current-buffer buffer-name (mu4e--get-current-buffer-type)))
         (direction (cons 'direction
                          (pcase (cons buffer-type mu4e-split-view)
                            ;; views or headers can display
                            ;; horz/vert depending on the value of
                            ;; `mu4e-split-view'
                            (`(,(or 'view 'headers) . horizontal) 'below)
                            (`(,(or 'view 'headers) . vertical) 'right)
                            (`(,_ . t) nil))))
         (window-size
          (pcase (cons buffer-type mu4e-split-view)
            ;; views or headers can display
            ;; horz/vert depending on the value of
            ;; `mu4e-split-view'
            ('(view . horizontal)
             '((window-height . shrink-window-if-larger-than-buffer)))
            ('(view . vertical)
             '((window-min-width . fit-window-to-buffer)))
            (`(,_ . t) nil)))
         (window-action (cond
                         ;; main-buffer
                         ((eq buffer-type 'main)
                          '(display-buffer-reuse-window
                            display-buffer-reuse-mode-window
                            display-buffer-full-frame))
                         ;; compose-buffer
                         ((eq buffer-type 'compose)
                          (pcase mu4e-compose-switch
                            ('window          #'display-buffer-pop-up-window)
                            ((or 'frame 't)   #'display-buffer-pop-up-frame)
                            (_                '(display-buffer-reuse-window
                                                display-buffer-reuse-mode-window
                                                display-buffer-same-window))))
                         ;; headers buffer
                         ((memq buffer-type '(headers))
                          '(display-buffer-reuse-window
                            display-buffer-reuse-mode-window
                            display-buffer-same-window))

                         ((memq mu4e-split-view '(horizontal vertical))
                          '(display-buffer-in-direction))

                         ((memq mu4e-split-view '(single-window))
                          '(display-buffer-reuse-window
                            display-buffer-reuse-mode-window
                            display-buffer-same-window))
                         ;; I cannot discern a difference between
                         ;; `single-window' and "anything else" in
                         ;; `mu4e-split-view'.
                         (t '(display-buffer-reuse-window
                              display-buffer-reuse-mode-window
                              display-buffer-same-window))))
         (arg `((,@window-action)
                ,@window-size
                ,direction)))
    (funcall (if select #'pop-to-buffer #'display-buffer)
             buffer-name
             arg)))

(defun mu4e-resize-linked-headers-window ()
  "Resizes the linked headers window belonging to a view.

Resizes the current headers view according to `mu4e-split-view'
and `mu4e-headers-visible-lines' or
`mu4e-headers-visible-columns'.

This function is best called from the hook
`mu4e-view-rendered-hook'."
  (unless (mu4e-current-buffer-type-p 'view)
    (mu4e-error "Cannot resize as this is not a valid view buffer."))
  (when-let (win (and mu4e-linked-headers-buffer
                      (get-buffer-window mu4e-linked-headers-buffer)))
    ;; This can fail for any number of reasons. If it does, we do
    ;; nothing. If the user has customized the window display we may
    ;; find it impossible to resize the window, and that should not be
    ;; cause for error.
    (ignore-errors
      (cond ((eq mu4e-split-view 'vertical)
             (window-resize win (- mu4e-headers-visible-columns
                                   (window-width win nil))
                            t t nil))
            ((eq mu4e-split-view 'horizontal)
             (set-window-text-height win mu4e-headers-visible-lines))))))

(provide 'mu4e-window)
;;; mu4e-window.el ends here
