;;; org-ref-pdf.el --- Drag-n-drop PDF onto bibtex files  -*- lexical-binding: t; -*-

;; Copyright (C) 2015  John Kitchin

;; Author: John Kitchin <jkitchin@andrew.cmu.edu>
;; Keywords:

;; 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 <http://www.gnu.org/licenses/>.

;;; Commentary:

;; This library provides functions to enable drag-n-drop of pdfs onto a bibtex
;; buffer to add bibtex entries to it.

;; TODO: If no DOI is found, figure out a way to do a crossref/google query to
;; get a doi. This needs a reliable title/citation.

;;; Code:

(require 'f)

;; [2019-10-13 Sun] I am commenting this out for now. I added it for some
;; reason, but I cannot figure out why. It is pretty slow to load, so since I
;; don't know why it is here, I am commenting it out until it is obvious again.
;; (require 'pdf-tools)

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

(declare-function org-ref-bibtex-key-from-doi "org-ref-bibtex.el")

(defgroup org-ref-pdf nil
  "Customization group for org-ref-pdf"
  :tag "Org Ref PDF"
  :group 'org-ref-pdf)

(defcustom pdftotext-executable
  "pdftotext"
  "Executable for pdftotext. Set if the executable is not on your
path, or you want to use another version."
  :type 'file
  :group 'org-ref-pdf)

(defcustom org-ref-pdf-doi-regex
  "10\\.[0-9]\\{4,9\\}/[-+._;()/:A-Z0-9]+"
  "Regular expression to match DOIs in a pdf converted to text."
  :type 'regexp
  :group 'org-ref-pdf)

(defcustom org-ref-pdf-to-bibtex-function
  'copy-file
  "Function for getting  a pdf to the `org-ref-pdf-directory'.
Defaults to `copy-file', but could also be `rename-file'."
  :type 'File :group 'org-ref-pdf)

(defun org-ref-extract-doi-from-pdf (pdf)
  "Try to extract a doi from a PDF file.
There may be more than one doi in the file. This function returns
all the ones it finds based on two patterns: doi: up to a quote,
bracket, space or end of line. dx.doi.org/up to a quote, bracket,
space or end of line.

If there is a trailing . we chomp it off. Returns a list of doi
strings, or nil.

"
  (with-temp-buffer
    (insert (shell-command-to-string (format "%s %s -"
					     pdftotext-executable
					     (shell-quote-argument (dnd-unescape-uri pdf)))))
    (goto-char (point-min))
    (let ((matches '()))
      (while (re-search-forward org-ref-pdf-doi-regex nil t)
	;; I don't know how to avoid a trailing . on some dois with the
	;; expression above, so if it is there, I chomp it off here.
	(let ((doi (match-string 0)))
	  (when (s-ends-with? "." doi)
	    (setq doi (substring doi 0 (- (length doi) 1))))
	  (cl-pushnew doi matches :test #'equal)))
      matches)))


(defun org-ref-pdf-doi-candidates (dois)
  "Generate candidate list for helm source.
Used when multiple dois are found in a pdf file."
  (cl-loop for doi in dois
	collect
	(condition-case nil
	    (cons
	     (plist-get (doi-utils-get-json-metadata doi) :title)
	     doi)
	  (error (cons (format "%s read error" doi) doi)))))


(defun org-ref-pdf-add-dois (_)
  "Add all entries for CANDIDATE in `helm-marked-candidates'."
  (cl-loop for doi in (helm-marked-candidates)
	   do
	   (doi-utils-add-bibtex-entry-from-doi
	    doi
	    (buffer-file-name))))

;;;###autoload
(defun org-ref-pdf-to-bibtex ()
  "Add pdf of current buffer to bib file and save pdf to
`org-ref-default-bibliography'. The pdf should be open in Emacs
using the `pdf-tools' package."
  (interactive)
  (when (not (f-ext? (downcase (buffer-file-name)) "pdf"))
    (error "Buffer is not a pdf file"))
  ;; Get doi from pdf of current buffer
  (let* ((dois (org-ref-extract-doi-from-pdf (buffer-file-name)))
         (doi-utils-download-pdf nil)
         (doi (if (= 1 (length dois))
                  (car dois)
                (completing-read "Select DOI: " dois))))
    ;; Add bib entry from doi:
    (doi-utils-add-bibtex-entry-from-doi doi)
    ;; Copy pdf to `org-ref-pdf-directory':
    (let ((key (org-ref-bibtex-key-from-doi doi)))
      (funcall org-ref-pdf-to-bibtex-function
	       (buffer-file-name)
               (expand-file-name (format "%s.pdf" key)
                                 org-ref-pdf-directory)))))

;;;###autoload
;; (defun org-ref-pdf-dnd-func (event)
;;   "Drag-n-drop support to add a bibtex entry from a pdf file."
;;   (interactive "e")
;;   (goto-char (nth 1 (event-start event)))
;;   (x-focus-frame nil)
;;   (let* ((payload (car (last event)))
;;          (pdf (cadr payload))
;; 	 (dois (org-ref-extract-doi-from-pdf pdf)))
;;     (cond
;;      ((null dois)
;;       (message "No doi found in %s" pdf))
;;      ((= 1 (length dois))
;;       (doi-utils-add-bibtex-entry-from-doi
;;        (car dois)
;;        (buffer-file-name)))
;;      ;; Multiple DOIs found
;;      (t
;;       (helm :sources `((name . "Select a DOI")
;; 		       (candidates . ,(org-ref-pdf-doi-candidates dois))
;; 		       (action . org-ref-pdf-add-dois)))))))

;; This isn't very flexible, as it hijacks all drag-n-drop events. I switched to
;; using `dnd-protocol-alist'.
;; (define-key bibtex-mode-map (kbd "<drag-n-drop>") 'org-ref-pdf-dnd-func)

;; This is what the original dnd function was.
;; (define-key bibtex-mode-map (kbd "<drag-n-drop>") 'ns-drag-n-drop)


;; I replaced the functionality above with this new approach that leverages
;; ns-drag-n-drop. An alternative approach would be to adapt the function above
;; so that if the item dragged on wasn't a pdf, it would use another function.
;; that is essentially what ns-drag-n-drop enables, multiple handlers for
;; different uris that get dropped on the windwo.

(defun org-ref-pdf-dnd-protocol (uri action)
  "Drag-n-drop protocol.
PDF will be a string like file:path.
ACTION is what to do. It is required for `dnd-protocol-alist'.
This function should only apply when in a bibtex file."
  (if (and (buffer-file-name)
	   (f-ext? (buffer-file-name) "bib"))
      (let* ((path (substring uri 5))
	     dois)
	(cond
	 ((f-ext? path "pdf")
	  (setq dois (org-ref-extract-doi-from-pdf
		      path))
	  (cond
	   ((null dois)
	    (message "No doi found in %s" path)
	    nil)
	   ((= 1 (length dois))
	    ;; we do not need to get the pdf, since we have one.
	    (let ((doi-utils-download-pdf nil))
	      (doi-utils-add-bibtex-entry-from-doi
	       (car dois)
	       (buffer-file-name))
	      ;; we should copy the pdf to the pdf directory though
	      (let ((key (cdr (assoc "=key=" (bibtex-parse-entry)))))
	      	(copy-file (dnd-unescape-uri path) (expand-file-name (format "%s.pdf" key) org-ref-pdf-directory))))
	    action)
	   ;; Multiple DOIs found
	   (t
	    (helm :sources `((name . "Select a DOI")
			     (candidates . ,(org-ref-pdf-doi-candidates dois))
			     (action . org-ref-pdf-add-dois)))
	    action)))
	 ;; drag a bib file on and add contents to the end of the file.
	 ((f-ext? path "bib")
	  (goto-char (point-max))
	  (insert "\n")
	  (insert-file-contents path))))
    ;; ignoring. pass back to dnd. Copied from `org-download-dnd'. Apparently
    ;; returning nil does not do this.
    (let ((dnd-protocol-alist
           (rassq-delete-all
            'org-ref-pdf-dnd-protocol
            (copy-alist dnd-protocol-alist))))
      (dnd-handle-one-url nil action uri))))


(add-to-list 'dnd-protocol-alist '("^file:" . org-ref-pdf-dnd-protocol))


;;;###autoload
(defun org-ref-pdf-dir-to-bibtex (bibfile directory)
  "Create BIBFILE from pdf files in DIRECTORY."
  (interactive (list
		(read-file-name "Bibtex file: ")
		(read-directory-name "Directory: ")))
  (find-file bibfile)
  (goto-char (point-max))

  (cl-loop for pdf in (f-entries directory (lambda (f) (f-ext? f "pdf")))
	   do
	   (goto-char (point-max))
	   (let ((dois (org-ref-extract-doi-from-pdf pdf)))
	     (cond
	      ((null dois)
	       (insert (format "%% No doi found to create entry in %s.\n" pdf)))
	      ((= 1 (length dois))
	       (doi-utils-add-bibtex-entry-from-doi
		(car dois)
		(buffer-file-name))
	       (bibtex-beginning-of-entry)
	       (insert (format "%% [[file:%s]]\n" pdf)))
	      ;; Multiple DOIs found
	      (t
	       (insert (format "%% Multiple dois found in %s\n" pdf))
	       (helm :sources `((name . "Select a DOI")
				(candidates . ,(org-ref-pdf-doi-candidates dois))
				(action . org-ref-pdf-add-dois))))))))


;;;###autoload
(defun org-ref-pdf-debug-pdf (pdf-file)
  "Try to debug getting a doi from a pdf.
Opens a buffer with the pdf converted to text, and `occur' on the
variable `org-ref-pdf-doi-regex'."
  (interactive "fPDF: ")
  (switch-to-buffer (get-buffer-create "*org-ref-pdf debug*"))
  (erase-buffer)
  (insert (shell-command-to-string (format "%s %s -"
					   pdftotext-executable
					   (shell-quote-argument pdf-file))))
  (goto-char (point-min))
  (highlight-regexp org-ref-pdf-doi-regex)
  (occur org-ref-pdf-doi-regex)
  (switch-to-buffer-other-window "*Occur*"))


;;;###autoload
(defun org-ref-pdf-crossref-lookup ()
  "Lookup highlighted text in PDFView in CrossRef."
  (interactive)
  (require 'pdf-view)
  (pdf-view-assert-active-region)
  (let* ((txt (pdf-view-active-region-text)))
    (pdf-view-deactivate-region)
    (crossref-lookup (mapconcat 'identity txt "	 \n"))))


(provide 'org-ref-pdf)
;;; org-ref-pdf.el ends here
