;;; calibre-edit.el --- Edit Book metadata  -*- lexical-binding: t; -*-

;; Copyright (C) 2023  Free Software Foundation, Inc.

;; This file is part of calibre.el.

;; Author: Kjartan Oli Agustsson <kjartanoli@disroot.org>

;; calibre.el 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.

;; calibre.el 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 calibre.el.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;; This file contains the code required to edit the metadata of books
;; in a calibre library.

;;; Code:
(require 'compat) ; for defvar-keymap
(require 'wid-edit)
(require 'calibre-widgets)
(require 'calibre-core)
(require 'calibre-util)
(require 'calibre-exec)

(defvar calibre-edit--edited-books nil
  "A list containing the original copies of edited books.")

;; Declare these variables to prevent free variable warnings
(defvar-local calibre-edit--book nil
  "The book being edited in the current buffer.")
(defvar-local calibre-edit--title nil
  "The title widget in the current buffer.")
(defvar-local calibre-edit--authors nil
  "The authors widget in the current buffer.")
(defvar-local calibre-edit--publisher nil
  "The publisher widget in the current buffer.")
(defvar-local calibre-edit--series nil
  "The series widget in the current buffer.")
(defvar-local calibre-edit--tags nil
  "The tags widget in the current buffer.")

(defun calibre-edit-apply (&rest _)
  "Apply any edits to the book in the current buffer."
  (interactive)
  (setf (calibre-book-title calibre-edit--book) (widget-value calibre-edit--title)
        (calibre-book-authors calibre-edit--book) (widget-value calibre-edit--authors)
        (calibre-book-publisher calibre-edit--book) (widget-value calibre-edit--publisher)
        (calibre-book-series calibre-edit--book) (if (consp (widget-value calibre-edit--series))
                                                     (car (widget-value calibre-edit--series))
                                                   nil)
        (calibre-book-series-index calibre-edit--book) (if (consp (widget-value calibre-edit--series))
                                                           (cdr (widget-value calibre-edit--series))
                                                         1)
        (calibre-book-tags calibre-edit--book) (widget-value calibre-edit--tags))
    (calibre-library--refresh)
    (let ((book calibre-edit--book))
      (with-current-buffer (get-buffer calibre-library-buffer)
        (calibre-library--find-book book)
        (tabulated-list-put-tag (char-to-string calibre-mod-marker)))))

(defun calibre-edit-abort (&rest _)
  "Abort any changes made in the current buffer."
  (interactive)
  (let ((book calibre-edit--book))
    (unless (calibre-edit--different-fields book (calibre-edit--find-original book))
      (setf calibre-edit--edited-books (seq-remove (lambda (b)
                                                     (= (calibre-book-id b)
                                                        (calibre-book-id book)))
                                                   calibre-edit--edited-books))))
  (quit-window t))

(defun calibre-edit-reset (&rest _)
  "Undo any changes made during this editing session."
  (interactive)
  (calibre-edit-book calibre-edit--book))

(defun calibre-edit-confirm (&rest _)
  "Apply any changes and exit."
  (interactive)
  (calibre-edit-apply)
  (quit-window t))

(defvar-keymap calibre-edit-mode-map
  :doc "Keymap for `calibre-edit-mode'."
  :parent widget-keymap
  "C-c C-c" #'calibre-edit-confirm
  "C-c C-a" #'calibre-edit-apply
  "C-c C-r" #'calibre-edit-reset
  "C-c C-k" #'calibre-edit-abort)

(defvar-keymap calibre-edit-field-keymap
  :doc "Keymap used inside editable fields in calibre edit buffers."
  :parent widget-field-keymap
  "C-c C-c" #'calibre-edit-confirm
  "C-c C-a" #'calibre-edit-apply
  "C-c C-r" #'calibre-edit-reset
  "C-c C-k" #'calibre-edit-abort)

(define-derived-mode calibre-edit-mode nil "Calibre Edit"
  :group 'calibre
  (widget-put (get 'editable-field 'widget-type) :keymap calibre-edit-field-keymap))

(defun calibre-edit-book (book)
  "Edit the metadata of BOOK."
  (interactive (list (tabulated-list-get-id)) calibre-library-mode)
  (unless (calibre-util-find-book book calibre-edit--edited-books)
    (push (copy-calibre-book book) calibre-edit--edited-books))
  (let ((buffer (calibre-edit--create-buffer book)))
    (pop-to-buffer buffer)))

(defun calibre-edit--create-buffer (book)
  "Create a buffer for editing BOOK.
Returns a buffer for editing BOOK, creating it if necessary."
  (let ((buffer (get-buffer-create (format "*%s - Metadata*" (calibre-book-title book)))))
    (with-current-buffer buffer
      (kill-all-local-variables)
      (let ((inhibit-read-only t))
        (erase-buffer))
      (remove-overlays)
      (calibre-edit-mode)
      (setf calibre-edit--book book)
      (setq-local
       header-line-format
       (substitute-command-keys
        "\\<calibre-edit-mode-map>Finish `\\[calibre-edit-confirm]', Apply `\\[calibre-edit-apply]', Reset `\\[calibre-edit-reset]', Cancel `\\[calibre-edit-abort]'"))
      (setf calibre-edit--title
            (widget-create 'string
                           :value (calibre-book-title book)
                           :tag "Title"
                           :doc "The title of the book."
                           :format "%t: %v%d"))
      (widget-insert "\n")
      (setf calibre-edit--authors
            (widget-create 'repeat
                           :value (calibre-book-authors book)
                           :tag "Authors"
                           :doc "The authors of the book."
                           :format "%t:\n%v%i\n%d"
                           'calibre-author))
      (widget-insert "\n")
      (setf calibre-edit--publisher
            (widget-create 'choice
                           :value (calibre-book-publisher book)
                           :tag "Publisher"
                           :doc "The publisher of the book."
                           :format "%[%t%]%v%d"
                           '(const :tag ": None" :menu-tag "None" nil)
                           '(calibre-publisher :tag "" :menu-tag "Publisher")))
      (widget-insert "\n")
      (setf calibre-edit--series
            (widget-create 'choice
                           :value (if (calibre-book-series book)
                                      (cons (calibre-book-series book)
                                            (calibre-book-series-index book))
                                    nil)
                           :tag "Series"
                           :format "%[%t%]%v%d"
                           '(const :tag ": None" :menu-tag "None" nil)
                           `(cons :tag ""
                                  :menu-tag "Series"
                                  (calibre-series :tag "Name"
                                                  :doc "The series the book is part of."
                                                  :format "%t:  %v%d")
                                  (number :tag "Index"
                                          :doc "The book's position within its series"
                                          :format "%t: %v%d"
                                          :value ,(calibre-book-series-index book)))))
      (widget-insert "\n")
      (setf calibre-edit--tags
            (widget-create 'repeat
                           :value (calibre-book-tags book)
                           :tag "Tags"
                           :doc "Tags associated with the book."
                           :format "%t:\n%v%i\n%d"
                           'calibre-tag))
      (widget-insert "\n")
      (widget-create 'push-button
                     :tag "Confirm"
                     :help-echo "Apply any changes and exit."
                     :notify (lambda (&rest _)
                               (calibre-edit-confirm)
                               (kill-buffer)))
      (widget-insert "   ")
      (widget-create 'push-button
                     :tag "Apply"
                     :help-echo "Apply any changes."
                     :notify #'calibre-edit-apply)
      (widget-insert "   ")
      (widget-create 'push-button
                     :tag "Reset"
                     :help-echo "Undo any changes."
                     :notify #'calibre-edit-reset)
      (widget-insert "   ")
      (widget-create 'push-button
                     :tag "Cancel"
                     :help-echo "Abort all changes."
                     :notify #'calibre-edit-abort)
      (widget-setup)
      (goto-char (point-min)))
    buffer))

(defun calibre-edit-revert (book)
  "Undo any edits performed to BOOK in this session."
  (let ((compare-ids (lambda (b)
                            (= (calibre-book-id book)
                               (calibre-book-id b)))))
    (setf calibre--books
        (cl-substitute-if (calibre-util-find-book book calibre-edit--edited-books)
                          compare-ids
                          calibre--books))
    (calibre-library--refresh)))

(defun calibre-edit--find-original (book)
  "Return BOOK absent any modifications performed in this session."
  (calibre-util-find-book book calibre-edit--edited-books))

(defun calibre-edit--different-fields (a b)
  "Return a list of slot names whose values differ in A and B."
  (let (diff)
    (dolist (descriptor (cdr (cl-struct-slot-info 'calibre-book)))
      (let* ((slot-name (car descriptor))
             (a (cl-struct-slot-value 'calibre-book slot-name a))
             (b (cl-struct-slot-value 'calibre-book slot-name b)))
        (unless (equal a b)
          (push slot-name diff))))
    diff))

(defun calibre-util-uglify-field-name (field)
  "Return FIELD replacing - with -."
  (string-join (split-string (symbol-name field) "-") "_"))

(defun calibre-edit--command (book)
  "Return the command to commit edits to BOOK to disk."
  `("set_metadata"
    ,@(flatten-list (mapcar (lambda (field)
                              `("-f" ,(format "%s:%s" (calibre-util-uglify-field-name field)
                                             (cl-case field
                                               (authors (string-join (calibre-book-authors book) " & "))
                                               (title (calibre-book-title book))
                                               (publisher (calibre-book-publisher book))
                                               (series (calibre-book-series book))
                                               (series-index (calibre-book-series-index book))
                                               (tags (string-join (calibre-book-tags book) ","))))))
                           (calibre-edit--different-fields book (calibre-edit--find-original book))))
    ,(int-to-string (calibre-book-id book))))

(defun calibre-edit-commit-edits (books)
  "Commit edits to BOOKS to disk."
  (calibre-exec--queue-commands (mapcar #'calibre-edit--command books)))

(provide 'calibre-edit)
;;; calibre-edit.el ends here
