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

;; Copyright (C) 2021 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:

;; Everything related to file management.

;; NOTE: This module is lazy-loaded.


;;; Code:

(require 'dash)
(require 'treemacs-core-utils)
(require 'treemacs-visuals)
(require 'treemacs-filewatch-mode)
(require 'treemacs-logging)
(require 'treemacs-rendering)

(eval-when-compile
  (require 'inline)
  (require 'treemacs-macros))

(with-eval-after-load 'recentf

  (declare-function recentf-remove-if-non-kept "recentf")
  (declare-function treemacs--remove-from-recentf-after-move/rename "treemacs-file-management")

  (defun treemacs--remove-from-recentf-after-move/rename (path _)
    "Remove PATH from recentf after the file was moved or renamed."
    (recentf-remove-if-non-kept path))

  (add-hook 'treemacs-rename-file-functions #'treemacs--remove-from-recentf-after-move/rename)
  (add-hook 'treemacs-move-file-functions   #'treemacs--remove-from-recentf-after-move/rename)
  (add-hook 'treemacs-delete-file-functions #'recentf-remove-if-non-kept))

(defconst treemacs--file-node-states
  '(file-node-open file-node-closed dir-node-open dir-node-closed)
  "List of node states treemacs is able to rename/delete etc.")

(define-inline treemacs--is-node-file-manageable? (btn)
  "Determines whether BTN is a file node treemacs can rename/delete."
  (declare (side-effect-free t))
  (inline-letevals (btn)
    (inline-quote
     (memq (treemacs-button-get ,btn :state)
           treemacs--file-node-states))))

;;;###autoload
(defun treemacs-delete-file (&optional arg)
  "Delete node at point.
A delete action must always be confirmed.  Directories are deleted recursively.
By default files are deleted by moving them to the trash.  With a prefix ARG
they will instead be wiped irreversibly."
  (interactive "P")
  (treemacs-block
   (treemacs-unless-let (btn (treemacs-current-button))
       (treemacs-pulse-on-failure "Nothing to delete here.")
     (treemacs-error-return-if (not (memq (treemacs-button-get btn :state)
                                          '(file-node-open file-node-closed dir-node-open dir-node-closed)))
       "Only files and directories can be deleted.")
     (treemacs--without-filewatch
      (let* ((delete-by-moving-to-trash (not arg))
             (path (treemacs--select-file-from-btn btn "Delete: "))
             (file-name (propertize (treemacs--filename path) 'face 'font-lock-string-face)))
        (cond
         ((file-symlink-p path)
          (if (yes-or-no-p (format "Remove link '%s -> %s' ? "
                                   file-name
                                   (propertize (file-symlink-p path) 'face 'font-lock-face)))
              (delete-file path delete-by-moving-to-trash)
            (treemacs-return (treemacs-log "Cancelled."))))
         ((file-regular-p path)
          (if (yes-or-no-p (format "Delete '%s' ? " file-name))
              (delete-file path delete-by-moving-to-trash)
            (treemacs-return (treemacs-log "Cancelled."))))
         ((file-directory-p path)
          (if (yes-or-no-p (format "Recursively delete '%s' ? " file-name))
              (delete-directory path t delete-by-moving-to-trash)
            (treemacs-return (treemacs-log "Cancelled."))))
         (t
          (treemacs-error-return
              (treemacs-pulse-on-failure
                  "Item is neither a file, a link or a directory - treemacs does not know how to delete it. (Maybe it no longer exists?)"))))
        (treemacs--on-file-deletion path)
        (treemacs-without-messages
         (treemacs-run-in-every-buffer
          (treemacs-delete-single-node path)))
        (run-hook-with-args 'treemacs-delete-file-functions path)
        (treemacs-log "Deleted %s."
          (propertize path 'face 'font-lock-string-face))))
     (treemacs--evade-image))))
(defalias 'treemacs-delete #'treemacs-delete-file)
(make-obsolete #'treemacs-delete #'treemacs-delete-file "v2.9.3")

;;;###autoload
(defun treemacs-move-file ()
  "Move file (or directory) at point.
Destination may also be a filename, in which case the moved file will also
be renamed."
  (interactive)
  (treemacs--copy-or-move :move))

;;;###autoload
(defun treemacs-copy-file ()
  "Copy file (or directory) at point.
Destination may also be a filename, in which case the copied file will also
be renamed."
  (interactive)
  (treemacs--copy-or-move :copy))

(defun treemacs--copy-or-move (action)
  "Internal implementation for copying and moving files.
ACTION will be either `:copy' or `:move', depending on whether we are calling
from `treemacs-copy-file' or `treemacs-move-file'."
  (let ((no-node-msg)
        (wrong-type-msg)
        (prompt)
        (action-function)
        (finish-msg))
    (pcase action
      (:copy
       (setf no-node-msg     "There is nothing to copy here."
             wrong-type-msg  "Only files and directories can be copied."
             prompt          "Copy to: "
             action-function (lambda (from to)
                               (if (file-directory-p from)
                                   (copy-directory from to)
                                 (copy-file from to)))
             finish-msg      "Copied %s to %s"))
      (:move
       (setf no-node-msg     "There is nothing to move here."
             wrong-type-msg  "Only files and directories can be moved."
             prompt          "Move to: "
             action-function #'rename-file
             finish-msg      "Moved %s to %s")))
    (treemacs-block
     (treemacs-unless-let (node (treemacs-node-at-point))
         (treemacs-error-return no-node-msg)
       (treemacs-error-return-if (not (treemacs-is-node-file-or-dir? node))
         wrong-type-msg)
       (let* ((source (treemacs--select-file-from-btn
                       node (if (eq action :copy "File to copy: " "File to move: "))))
              (source-name (treemacs--filename source))
              (destination (treemacs--unslash (read-file-name prompt nil default-directory)))
              (target-is-dir? (file-directory-p destination))
              (target-name (if target-is-dir? (treemacs--filename source) (treemacs--filename destination)))
              (destination-dir (if target-is-dir? destination (treemacs--parent-dir destination)))
              (target (treemacs--find-repeated-file-name (treemacs-join-path destination-dir target-name))))
         (unless (file-exists-p destination-dir)
           (make-directory destination-dir :parents))
         (when (eq action :move)
           ;; do the deletion *before* moving the file, otherwise it will no longer exist and treemacs will
           ;; not recognize it as a file path
           (treemacs-do-delete-single-node source))
         (treemacs--without-filewatch
          (funcall action-function source target))
         ;; no waiting for filewatch, if we copied to an expanded directory refresh it immediately
         (-let [parent (treemacs--parent target)]
           (when (treemacs-is-path-visible? parent)
             (treemacs-do-update-node parent)))
         (treemacs-goto-file-node target)
         (run-hook-with-args
          (pcase action
            (:copy 'treemacs-copy-file-functions)
            (:move 'treemacs-move-file-functions))
          source target)
         (treemacs-pulse-on-success finish-msg
           (propertize source-name 'face 'font-lock-string-face)
           (propertize destination 'face 'font-lock-string-face)))))))

;;;###autoload
(cl-defun treemacs-rename-file ()
  "Rename the file/directory at point.

Buffers visiting the renamed file or visiting a file inside the renamed
directory and windows showing them will be reloaded.  The list of recent files
will likewise be updated."
  (interactive)
  (treemacs-block
   (treemacs-unless-let (btn (treemacs-current-button))
       (treemacs-pulse-on-failure "Nothing to rename here.")
     (-let [old-path (treemacs--select-file-from-btn btn "Rename: ")]
       (treemacs-error-return-if (null old-path)
         "Found nothing to rename here.")
       (treemacs-error-return-if (not (treemacs--is-node-file-manageable? btn))
         "Only files and directories can be deleted.")
       (treemacs-error-return-if (not (file-exists-p old-path))
         "The file to be renamed does not exist.")
       (let* ((old-name  (treemacs--filename old-path))
              (new-name  (treemacs--read-string
                          "New name: " (file-name-nondirectory old-path)))
              (dir       (treemacs--parent-dir old-path))
              (new-path  (treemacs-join-path dir new-name))
              (parent    (treemacs-button-get btn :parent)))
         (treemacs-error-return-if
             (and (file-exists-p new-path)
                  (or (not (eq 'darwin system-type))
                      (not (string= old-name new-name))))
           "A file named %s already exists."
           (propertize new-name 'face font-lock-string-face))
         (treemacs--without-filewatch
          (rename-file old-path new-path)
          (treemacs--replace-recentf-entry old-path new-path)
          (-let [treemacs-silent-refresh t]
            (treemacs-run-in-every-buffer
             (treemacs--on-rename old-path new-path treemacs-filewatch-mode)
             (treemacs-update-node (treemacs-button-get parent :path)))))
         (treemacs--reload-buffers-after-rename old-path new-path)
         (run-hook-with-args
          'treemacs-rename-file-functions
          old-path new-path)
         (treemacs-pulse-on-success "Renamed %s to %s."
           (propertize (treemacs--filename old-path) 'face font-lock-string-face)
           (propertize new-name 'face font-lock-string-face)))))))

(defalias 'treemacs-rename #'treemacs-rename-file)
(make-obsolete #'treemacs-rename #'treemacs-rename-file "v2.9.3")

;;;###autoload
(defun treemacs-create-file ()
  "Create a new file.
Enter first the directory to create the new file in, then the new file's name.
The pre-selection for what directory to create in is based on the \"nearest\"
path to point - the containing directory for tags and files or the directory
itself, using $HOME when there is no path at or near point to grab."
  (interactive)
  (treemacs--create-file/dir t))

;;;###autoload
(defun treemacs-create-dir ()
  "Create a new directory.
Enter first the directory to create the new dir in, then the new dir's name.
The pre-selection for what directory to create in is based on the \"nearest\"
path to point - the containing directory for tags and files or the directory
itself, using $HOME when there is no path at or near point to grab."
  (interactive)
  (treemacs--create-file/dir nil))

(defun treemacs--create-file/dir (is-file?)
  "Interactively create either a file or directory, depending on IS-FILE.

IS-FILE?: Bool"
  (interactive)
  (let* ((curr-path (--if-let (treemacs-current-button)
                        (treemacs--select-file-from-btn it "Create in: ")
                      (expand-file-name "~")))
         (path-to-create (treemacs-canonical-path
                          (read-file-name
                           (if is-file? "Create File: " "Create Directory: ")
                           (treemacs--add-trailing-slash
                            (if (file-directory-p curr-path)
                                curr-path
                              (treemacs--parent-dir curr-path)))))))
    (treemacs-block
     (treemacs-error-return-if (file-exists-p path-to-create)
       "%s already exists." (propertize path-to-create 'face 'font-lock-string-face))
     (treemacs--without-filewatch
      (if is-file?
          (-let [dir (treemacs--parent-dir path-to-create)]
            (unless (file-exists-p dir)
              (make-directory dir t))
            (write-region "" nil path-to-create nil 0))
        (make-directory path-to-create t))
      (run-hook-with-args 'treemacs-create-file-functions path-to-create))
     (-when-let (project (treemacs--find-project-for-path path-to-create))
       (-when-let* ((created-under (treemacs--parent path-to-create))
                    (created-under-btn (treemacs-find-visible-node created-under)))
         ;; update only the part that changed to keep things smooth
         ;; for files that's just their parent, for directories we have to take
         ;; flattening into account
         (if (treemacs-button-get created-under-btn :collapsed)
             (treemacs-update-node (treemacs-button-get (treemacs-button-get created-under-btn :parent) :path))
           (treemacs-update-node (treemacs-button-get created-under-btn :path))))
       (treemacs-goto-file-node path-to-create project)
       (recenter))
     (treemacs-pulse-on-success
         "Created %s." (propertize path-to-create 'face 'font-lock-string-face)))))

(defun treemacs--select-file-from-btn (btn prompt)
  "Select the file represented by BTN for file management.
Offer a specifying dialogue with PROMPT when BTN is flattened."
  (declare (side-effect-free t))
  (-if-let (collapse-info (treemacs-button-get btn :collapsed))
      (completing-read prompt collapse-info nil :require-match)
    (treemacs-button-get btn :key)))

(provide 'treemacs-file-management)

;;; treemacs-file-management.el ends here
