;;; treemacs-bookmarks.el --- A tree style file viewer package -*- lexical-binding: t -*-

;; Copyright (C) 2024 Alexander Miller

;; 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:

;; Integrates treemacs with bookmark.el.

;; NOTE: This module is lazy-loaded.

;;; Code:

(require 'bookmark)
(require 'dash)
(require 'treemacs-follow-mode)
(require 'treemacs-interface)
(require 'treemacs-scope)
(require 'treemacs-logging)
(require 'treemacs-tags)
(require 'treemacs-workspaces)

(eval-when-compile
  (require 'cl-lib)
  (require 'treemacs-macros))

(treemacs-import-functions-from "treemacs"
  treemacs-select-window)

;;;###autoload
(defun treemacs-bookmark (&optional arg)
  "Find a bookmark in treemacs.
Only bookmarks marking either a file or a directory are offered for selection.
Treemacs will try to find and focus the given bookmark's location, in a similar
fashion to `treemacs-find-file'.

With a prefix argument ARG treemacs will also open the bookmarked location."
  (interactive "P")
  (treemacs-block
   (bookmark-maybe-load-default-file)
   (-let [bookmarks
          (cl-loop
           for b in bookmark-alist
           for name = (car b)
           for location = (treemacs-canonical-path (bookmark-location b))
           when (or (file-regular-p location) (file-directory-p location))
           collect (propertize name 'location location))]
     (treemacs-error-return-if (null bookmarks)
       "Didn't find any bookmarks pointing to files.")
     (let* ((bookmark (completing-read "Bookmark: " bookmarks))
            (location (treemacs-canonical-path (get-text-property 0 'location (--first (string= it bookmark) bookmarks))))
            (dir (if (file-directory-p location) location (treemacs--parent-dir location)))
            (project (treemacs--find-project-for-path dir)))
       (treemacs-error-return-if (null project)
         "Bookmark at %s does not fall under any project in the workspace."
         (propertize location 'face 'font-lock-string-face))
       (pcase (treemacs-current-visibility)
         ('visible (treemacs--select-visible-window))
         ('exists  (treemacs--select-not-visible-window))
         ('none    (treemacs--init)))
       (treemacs-goto-file-node location project)
       (treemacs-pulse-on-success)
       (when arg (treemacs-visit-node-no-split))))))

;;;###autoload
(defun treemacs--bookmark-handler (record)
  "Open Treemacs into a bookmark RECORD."
  (let ((path (bookmark-prop-get record 'treemacs-bookmark-path)))
    (unless path
      ;; Don't rely on treemacs-pulse-on-failure to display the error, since the
      ;; error must be handled in bookmark.el.
      (user-error "Treemacs--bookmark-handler invoked for a non-Treemacs bookmark"))
    (treemacs-select-window)
    ;; XXX temporary workaround for incorrect move to a saved tag node
    ;; must be fixed after tags were rewritten in new extension api
    (if (and (listp path)
             (stringp (car path))
             (file-regular-p (car path)))
        (treemacs-goto-node (car path))
      (treemacs-goto-node path))
    ;; If the user has bookmarked a directory, they probably want to operate on
    ;; its contents. Expand it, and select the first child.
    (treemacs-with-current-button
     "Could not select the current bookmark"
     (when (eq (treemacs-button-get current-btn :state) 'dir-node-closed)
       (treemacs-TAB-action))
     (when (eq (treemacs-button-get current-btn :state) 'dir-node-open)
       (let ((depth (treemacs-button-get current-btn :depth))
             (next-button (next-button current-btn)))
         (when (and next-button (> (treemacs-button-get next-button :depth) depth))
           (treemacs-next-line 1)))))))

(defun treemacs--format-bookmark-title (btn)
  "Format the bookmark title for BTN with `treemacs-bookmark-title-template'."
  (s-format
   treemacs-bookmark-title-template
   (lambda (pattern)
     (or
      (cond
       ;; ${label} - Label of the current button
       ((string= pattern "label")
        (treemacs--get-label-of btn))

       ;; ${label:1} - Label of Nth parent
       ((s-starts-with? "label:" pattern)
        (let ((depth (string-to-number (s-chop-prefix "label:" pattern)))
              (current-button btn))
          (dotimes (_ depth)
            (setq current-button (when current-button (treemacs-button-get current-button :parent))))
          (when current-button
            (treemacs--get-label-of current-button))))

       ;; ${label-path} and ${label-path:4} - Path of labels, optionally limited by a number.
       ((or (string= pattern "label-path") (s-starts-with? "label-path:" pattern))
        (let ((depth (when (s-starts-with? "label-path:" pattern)
                       (string-to-number (s-chop-prefix "label-path:" pattern))))
              (current-button btn)
              (path))
          (while (and current-button (not (eq 0 depth)))
            (push (treemacs--get-label-of current-button) path)
            (when depth (cl-decf depth))
            (setq current-button (treemacs-button-get current-button :parent)))
          (s-join "/" path)))

       ;; ${project} - Label of the project or top-level extension node.
       ((string= pattern "project")
        ;; Find the root button by iterating - don't use `treemacs-project-of-node`
        ;; to make this work for variadic top-level extensions.
        (let ((current-button btn))
          (while (> (treemacs-button-get current-button :depth) 0)
            (setq current-button (treemacs-button-get current-button :parent)))
          (treemacs--get-label-of current-button)))

       ;; ${file-path} - Filesystem path.
       ((string= pattern "file-path")
        (treemacs--nearest-path btn))

       ;; ${file-path:3} - N components of the file path
       ((s-starts-with? "file-path:" pattern)
        (let ((n (string-to-number (s-chop-prefix "file-path:" pattern))))
          (-when-let (path (treemacs--nearest-path btn))
            (let ((components (last (s-split "/" path) (1+ n))))
              ;; Add the leading slash for absolute paths
              (when (and (> (length components) n) (not (string= "" (car components))))
                (pop components))
              (s-join "/" components)))))

       (t
        ;; Don't rely on treemacs-pulse-on-failure to display the error, since the
        ;; error must be handled in bookmark.el.
        (treemacs-pulse-on-failure)
        (user-error "Bookmark template pattern %s was not recognized" pattern)))
      ""))))

(defun treemacs--make-bookmark-record ()
  "Make a bookmark record for the current Treemacs button.

This function is installed as the `bookmark-make-record-function'."
  (treemacs-unless-let (current-btn (treemacs-current-button))
      (progn
        ;; Don't rely on treemacs-pulse-on-failure to display the error, since the
        ;; error must be handled in bookmark.el.
        (treemacs-pulse-on-failure)
        (user-error "Nothing to bookmark here"))
    (let* ((path (treemacs-button-get current-btn :path)))
      (unless path
        (treemacs-pulse-on-failure)
        (user-error "Could not find the path of the current button"))

      `((defaults . (,(treemacs--format-bookmark-title current-btn)))
        (treemacs-bookmark-path . ,path)
        (handler . treemacs--bookmark-handler)
        ,@(when (stringp path) `((filename . ,path)))))))

;;;###autoload
(defun treemacs-add-bookmark ()
  "Add the current node to Emacs' list of bookmarks.
For file and directory nodes their absolute path is saved.  Tag nodes
additionally also save the tag's position.  A tag can only be bookmarked if the
treemacs node is pointing to a valid buffer position."
  (interactive)
  (treemacs-with-current-button
   "There is nothing to bookmark here."
   (pcase (treemacs-button-get current-btn :state)
     ((or 'file-node-open 'file-node-closed 'dir-node-open 'dir-node-closed)
      (-let [name (treemacs--read-string "Bookmark name: ")]
        (when name
          (bookmark-store name `((filename . ,(treemacs-button-get current-btn :path))) nil))))
     ('tag-node
      (-let [(tag-buffer . tag-pos)
             (treemacs--extract-position (treemacs-button-get current-btn :marker) nil)]
        (if (buffer-live-p tag-buffer)
            (-let [name (treemacs--read-string "Bookmark name: ")]
              (when name
                (bookmark-store
                 name
                 `((filename . ,(buffer-file-name tag-buffer))
                   (position . ,tag-pos))
                 nil)))
          (treemacs-log-failure "Tag info can not be saved because it is not pointing to a live buffer."))))
     ((or 'tag-node-open 'tag-node-closed)
      (treemacs-pulse-on-failure "There is nothing to bookmark here.")))))

(provide 'treemacs-bookmarks)

;;; treemacs-bookmarks.el ends here
