;;; bibtex-completion.el --- A BibTeX backend for completion frameworks

;; Author: Titus von der Malsburg <malsburg@posteo.de>
;;         Justin Burkett <justin@burkett.cc>
;; Maintainer: Titus von der Malsburg <malsburg@posteo.de>
;; Version: 1.0.0
;; Package-Requires: ((parsebib "1.0") (s "1.9.0") (dash "2.6.0") (f "0.16.2") (cl-lib "0.5"))

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

;; A BibTeX backend for completion frameworks

;; There are currently two fronends: helm-bibtex and ivy-bibtex.
;;
;; See the github page for details:
;;
;;    https://github.com/tmalsburg/helm-bibtex

;;; Code:

(require 'browse-url)
(require 'parsebib)
(require 'cl-lib)
(require 'dash)
(require 's)
(require 'f)
(require 'biblio)
(require 'filenotify)

;; Silence byte-compiler
(declare-function reftex-what-macro "reftex-parse")
(declare-function reftex-get-bibfile-list "reftex-cite")
(declare-function outline-show-all "outline")
(declare-function org-narrow-to-subtree "org")
(declare-function org-cycle-hide-drawers "org")
(declare-function org-find-property "org")
(declare-function org-show-entry "org")
(declare-function org-entry-get "org")
(declare-function org-element-parse-buffer "org-element")
(declare-function org-element-map "org-element")
(declare-function org-element-property "org-element")

(defgroup bibtex-completion nil
  "Helm plugin for searching entries in a BibTeX bibliography."
  :group 'completion)

(defcustom bibtex-completion-bibliography nil
  "The BibTeX file or list of BibTeX files. Org-bibtex users can
also specify org-mode bibliography files, in which case it will
be assumed that a BibTeX file exists with the same name and
extension bib instead of org. If the bib file has a different
name, use a cons cell (\"orgfile.org\" . \"bibfile.bib\") instead."
  :group 'bibtex-completion
  :type '(choice file (repeat file)))

(defcustom bibtex-completion-library-path nil
  "A directory or list of directories in which PDFs are stored.
Bibtex-completion assumes that the names of these PDFs are
composed of the BibTeX-key plus a \".pdf\" suffix."
  :group 'bibtex-completion
  :type '(choice directory (repeat directory)))

(defcustom bibtex-completion-pdf-open-function 'find-file
  "The function used for opening PDF files.  This can be an
arbitrary function that takes one argument: the path to the PDF
file.  The default is `find-file' which opens the PDF in
Emacs (either with docview or, if installed, the much superior
pdf-tools.  When set to `helm-open-file-with-default-tool', the
systems default viewer for PDFs is used."
  :group 'bibtex-completion
  :type 'function)

(defcustom bibtex-completion-pdf-extension ".pdf"
  "The extension of a BibTeX entry's \"PDF\" file. This makes it
  possible to use another file type. It can also be a list of
  file types, which are then tried sequentially until a file is
  found. Beware that adding file types can reduce performance for
  large bibliographies.  This variable has no effect if PDFs are
  referenced via the file field."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-find-additional-pdfs nil
  "If non-nil, all files whose base name starts with the BibTeX
  key and ends with `bibtex-completion-pdf-extension' are
  considered as PDFs, not only \"<key>.<extension>\". Note that
  for performance reasons, an entry is only marked as having a
  PDF if \"<key>.<extension\" exists."
  :group 'bibtex-completion
  :type 'boolean)

(defcustom bibtex-completion-pdf-symbol "⌘"
  "Symbol used to indicate that a PDF file is available for a
publication.  This should be a single character."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-format-citation-functions
  '((org-mode      . bibtex-completion-format-citation-ebib)
    (latex-mode    . bibtex-completion-format-citation-cite)
    (markdown-mode . bibtex-completion-format-citation-pandoc-citeproc)
    (default       . bibtex-completion-format-citation-default))
  "The functions used for formatting citations.  The publication
can be cited, for example, as \cite{key} or ebib:key depending on
the major mode of the current buffer.  Note that the functions
should accept a list of keys as input.  With multiple marked
entries one can insert multiple keys at once,
e.g. \cite{key1,key2}. See the functions
`bibtex-completion-format-citation-ebib' and
`bibtex-completion-format-citation-cite' as examples."
  :group 'bibtex-completion
  :type '(alist :key-type symbol :value-type function))

(defcustom bibtex-completion-notes-path nil
  "The place where notes are stored.  This is either a file, in
which case all notes are stored in that file, or a directory, in
which case each publication gets its own notes file in that
directory.  In the latter case, bibtex-completion assumes that the
names of the note files are composed of the BibTeX-key plus a
suffix that is specified in `bibtex-completion-notes-extension'."
  :group 'bibtex-completion
  :type '(choice file directory (const nil)))

(defcustom bibtex-completion-notes-template-multiple-files
  "#+TITLE: Notes on: ${author-or-editor} (${year}): ${title}\n\n"
  "Template used to create a new note when each note is stored in a
separate file. '${field-name}' can be used to insert the value of a
BibTeX field into the template. Apart from the fields defined in the
entry, one can also use the virtual fields `author-or-editor` -- which
contains the author names if defined and otherwise the names of the
editors -- and `author-abbrev` -- which abbreviates to 'First author
et al.' when there are three or more authors."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-notes-template-one-file
  "\n* ${author-or-editor} (${year}): ${title}\n  :PROPERTIES:\n  :Custom_ID: ${=key=}\n  :END:\n\n"
  "Template used to create a new note when all notes are stored
in one file.  '${field-name}' can be used to insert the value of
a BibTeX field into the template.  Apart from the fields defined in
the entry, one can also use the virtual field `author-or-editor` -- which
contains the author names if defined and otherwise the names of the
editors -- and `author-abbrev` -- which abbreviates to 'First author
et al.' when there are three or more authors."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-notes-key-pattern
  ":Custom_ID: +%s\\( \\|$\\)"
  "The pattern used to find entries in the notes file.  Only
relevant when all notes are stored in one file.  The key can be
inserted into the pattern using the `format` function."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-notes-extension ".org"
  "The extension of the files containing notes.  This is only
used when `bibtex-completion-notes-path' is a directory (not a file)."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-notes-symbol "✎"
  "Symbol used to indicate that a publication has notes.  This
should be a single character."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-fallback-options
  '(("CrossRef                                  (biblio.el)"
     . (lambda (search-expression) (biblio-lookup #'biblio-crossref-backend search-expression)))
    ("arXiv                                     (biblio.el)"
     . (lambda (search-expression) (biblio-lookup #'biblio-arxiv-backend search-expression)))
    ("DBLP (computer science bibliography)      (biblio.el)"
     . (lambda (search-expression) (biblio--lookup-1 #'biblio-dblp-backend search-expression)))
    ("HAL (French open archive)                 (biblio.el)"
     . (lambda (search-expression) (biblio--lookup-1 #'biblio-hal-backend search-expression)))
    ("IEEE                                      (biblio.el)"
     . (lambda (search-expression) (biblio--lookup-1 #'biblio-ieee-backend search-expression)))
    ("Google Scholar                            (web)"
     . "https://scholar.google.co.uk/scholar?q=%s")
    ("Pubmed                                    (web)"
     . "https://www.ncbi.nlm.nih.gov/pubmed/?term=%s")
    ("Bodleian Library                          (web)"
     . "http://solo.bodleian.ox.ac.uk/primo_library/libweb/action/search.do?vl(freeText0)=%s&fn=search&tab=all")
    ("Library of Congress                       (web)"
     . "https://www.loc.gov/search/?q=%s&all=true&st=list")
    ("Deutsche Nationalbibliothek               (web)"
     . "https://portal.dnb.de/opac.htm?query=%s")
    ("British National Library                  (web)"
     . "http://explore.bl.uk/primo_library/libweb/action/search.do?&vl(freeText0)=%s&fn=search")
    ("Bibliothèque nationale de France          (web)"
     . "http://catalogue.bnf.fr/servlet/RechercheEquation?host=catalogue?historique1=Recherche+par+mots+de+la+notice&niveau1=1&url1=/jsp/recherchemots_simple.jsp?host=catalogue&maxNiveau=1&categorieRecherche=RechercheMotsSimple&NomPageJSP=/jsp/recherchemots_simple.jsp?host=catalogue&RechercheMotsSimpleAsauvegarder=0&ecranRechercheMot=/jsp/recherchemots_simple.jsp&resultatsParPage=20&x=40&y=22&nbElementsHDJ=6&nbElementsRDJ=7&nbElementsRCL=12&FondsNumerise=M&CollectionHautdejardin=TVXZROM&HDJ_DAV=R&HDJ_D2=V&HDJ_D1=T&HDJ_D3=X&HDJ_D4=Z&HDJ_SRB=O&CollectionRezdejardin=UWY1SPQM&RDJ_DAV=S&RDJ_D2=W&RDJ_D1=U&RDJ_D3=Y&RDJ_D4=1&RDJ_SRB=P&RDJ_RLR=Q&RICHELIEU_AUTRE=ABCDEEGIKLJ&RCL_D1=A&RCL_D2=K&RCL_D3=D&RCL_D4=E&RCL_D5=E&RCL_D6=C&RCL_D7=B&RCL_D8=J&RCL_D9=G&RCL_D10=I&RCL_D11=L&ARSENAL=H&LivrePeriodique=IP&partitions=C&images_fixes=F&son=S&images_animees=N&Disquette_cederoms=E&multimedia=M&cartes_plans=D&manuscrits=BT&monnaies_medailles_objets=JO&salle_spectacle=V&Monographie_TN=M&Periodique_TN=S&Recueil_TN=R&CollectionEditorial_TN=C&Ensemble_TN=E&Spectacle_TN=A&NoticeB=%s")
    ("Gallica Bibliothèque Numérique            (web)"
     . "http://gallica.bnf.fr/Search?q=%s"))
  "Alist of online sources that can be used to search for
publications.  The key of each entry is the name of the online
source.  The value is the URL used for retrieving results.  This
URL must contain a %s in the position where the search term
should be inserted.  Alternatively, the value can be a function
that will be called when the entry is selected."
  :group 'bibtex-completion
  :type '(alist :key-type string
                :value-type (choice (string :tag "URL")
                            (function :tag "Function"))))

(defcustom bibtex-completion-browser-function nil
  "The browser that is used to access online resources.  If
nil (default), the value of `browse-url-browser-function' is
used.  If that value is nil, Helm uses the first available
browser in `helm-browse-url-default-browser-alist'"
  :group 'bibtex-completion
  :type '(choice
          (const         :tag "Default" :value nil)
          (function-item :tag "Emacs interface to w3m" :value w3m-browse-url)
          (function-item :tag "Emacs W3" :value  browse-url-w3)
          (function-item :tag "W3 in another Emacs via `gnudoit'"
                         :value  browse-url-w3-gnudoit)
          (function-item :tag "Mozilla" :value  browse-url-mozilla)
          (function-item :tag "Firefox" :value browse-url-firefox)
          (function-item :tag "Chromium" :value browse-url-chromium)
          (function-item :tag "Galeon" :value  browse-url-galeon)
          (function-item :tag "Epiphany" :value  browse-url-epiphany)
          (function-item :tag "Netscape" :value  browse-url-netscape)
          (function-item :tag "eww" :value  eww-browse-url)
          (function-item :tag "Mosaic" :value  browse-url-mosaic)
          (function-item :tag "Mosaic using CCI" :value  browse-url-cci)
          (function-item :tag "Text browser in an xterm window"
                         :value browse-url-text-xterm)
          (function-item :tag "Text browser in an Emacs window"
                         :value browse-url-text-emacs)
          (function-item :tag "KDE" :value browse-url-kde)
          (function-item :tag "Elinks" :value browse-url-elinks)
          (function-item :tag "Specified by `Browse Url Generic Program'"
                         :value browse-url-generic)
          (function-item :tag "Default Windows browser"
                         :value browse-url-default-windows-browser)
          (function-item :tag "Default Mac OS X browser"
                         :value browse-url-default-macosx-browser)
          (function-item :tag "GNOME invoking Mozilla"
                         :value browse-url-gnome-moz)
          (function-item :tag "Default browser"
                         :value browse-url-default-browser)
          (function      :tag "Your own function")
          (alist         :tag "Regexp/function association list"
                         :key-type regexp :value-type function)))

(defcustom bibtex-completion-additional-search-fields nil
  "The fields that are used for searching in addition to author,
editor, title, year, BibTeX key, and entry type."
  :group 'bibtex-completion
  :type '(repeat string))

(defcustom bibtex-completion-no-export-fields nil
  "A list of fields that should be ignored when exporting BibTeX
entries."
  :group 'bibtex-completion
  :type '(repeat string))

(defcustom bibtex-completion-cite-commands '("cite" "Cite" "parencite"
"Parencite" "footcite" "footcitetext" "textcite" "Textcite"
"smartcite" "Smartcite" "cite*" "parencite*" "supercite" "autocite"
"Autocite" "autocite*" "Autocite*" "citeauthor" "Citeauthor"
"citeauthor*" "Citeauthor*" "citetitle" "citetitle*" "citeyear"
"citeyear*" "citedate" "citedate*" "citeurl" "nocite" "fullcite"
"footfullcite" "notecite" "Notecite" "pnotecite" "Pnotecite"
"fnotecite")
  "The list of LaTeX cite commands.  When creating LaTeX
citations, these can be accessed as future entries in the
minibuffer history, i.e. by pressing the arrow down key.  The
default entries are taken from biblatex.  There is currently no
special support for multicite commands and volcite et al.  These
commands can be used but bibtex-completion does not prompt for their
extra arguments."
  :group 'bibtex-completion
  :type '(choice string (repeat string)))

(defcustom bibtex-completion-cite-default-command "cite"
  "The LaTeX cite command that is used if the user doesn't enter
anything when prompted for such a command."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-cite-prompt-for-optional-arguments t
  "If t, bibtex-completion prompts for pre- and postnotes for
LaTeX cite commands.  Choose nil for no prompts."
  :group 'bibtex-completion
  :type 'boolean)

(defcustom bibtex-completion-cite-default-as-initial-input nil
  "This variable controls how the default command defined in
`bibtex-completion-cite-default-command' is used.  If t, it is inserted
into the minibuffer before reading input from the user.  If nil,
it is used as the default if the user doesn't enter anything."
  :group 'bibtex-completion
  :type 'boolean)

(defcustom bibtex-completion-pdf-field nil
  "The name of the BibTeX field in which the path to PDF files is
stored or nil if no such field should be used.  If an entry has
no value for this field, or if the specified file does not exist,
or if this variable is nil, bibtex-completion will look up the PDF in
the directories listed in `bibtex-completion-library-path'."
  :group 'bibtex-completion
  :type 'string)

(defcustom bibtex-completion-display-formats
  '((t . "${author:36} ${title:*} ${year:4} ${=has-pdf=:1}${=has-note=:1} ${=type=:7}"))
  "Alist of format strings for displaying entries in the results list.
The key of each element of this list is either a BibTeX entry
type (in which case the format string applies to entries of this
type only) or t (in which case the format string applies to all
other entry types). The value is the format string.

In the format string, expressions like \"${author:36}\",
\"${title:*}\", etc, are expanded to the value of the
corresponding field. An expression like \"${author:N}\" is
truncated to a width of N characters, whereas an expression like
\"${title:*}\" is truncated to the remaining width in the results
window. Three special fields are available: \"=type=\" holds the
BibTeX entry type, \"=has-pdf=\" holds
`bibtex-completion-pdf-symbol' if the entry has a PDF file, and
\"=has-notes=\" holds `bibtex-completion-notes-symbol' if the
entry has a notes file. The \"author\" field is expanded to
either the author names or, if the entry has no author field, the
editor names."
  :group 'bibtex-completion
  :type '(alist :key-type symbol :value-type string))

(defvar bibtex-completion-cross-referenced-entry-types
  '("proceedings" "mvproceedings" "book" "mvbook" "collection" "mvcollection")
  "The list of potentially cross-referenced entry types (in
  lowercase). Only entries of these types are checked in
  order to resolve cross-references. The default list is usually
  sufficient; adding more types can slow down resolution for
  large biblioraphies.")

(defvar bibtex-completion-display-formats-internal nil
  "Stores `bibtex-completion-display-formats' together with the
\"used width\" of each format string. This is set internally.")

(defvar bibtex-completion-cache nil
  "A cache storing the hash of the bibliography content and the
corresponding list of entries, for each bibliography file,
obtained when the bibliography was last parsed. When the
current bibliography hash is identical to the cached hash, the
cached list of candidates is reused, otherwise the bibliography
file is reparsed.")

(defvar bibtex-completion-string-cache nil
  "A cache storing bibtex strings, for each bibliography file, obtained when the bibliography was last parsed.")

(defvar bibtex-completion-string-hash-table nil
  "A hash table used for string replacements.")


(defun bibtex-completion-normalize-bibliography (&optional type)
  "Returns a list of bibliography file(s) in
`bibtex-completion-bibliography'. If there are org-mode
bibliography-files, their corresponding bibtex files are listed
as well, unless TYPE is 'main. If TYPE is 'bibtex, org-mode
bibliography-files are instead replaced with their associated
bibtex files."
  (delete-dups
   (cl-loop
    for bib-file in (-flatten (list bibtex-completion-bibliography))
    for main-file = (if (consp bib-file)
                        (car bib-file)
                      bib-file)
    for bibtex-file = (if (consp bib-file)
                          (cdr bib-file)
                        (concat (file-name-sans-extension main-file) ".bib"))
    unless (equal type 'bibtex)
    collect main-file
    unless (equal type 'main)
    collect bibtex-file)))

(defvar bibtex-completion-file-watch-descriptors nil
  "List of file watches monitoring bibliography files for changes.")

(defun bibtex-completion-init ()
  "Checks that the files and directories specified by the user
actually exist. Also sets `bibtex-completion-display-formats-internal'."

  ;; Remove current watch-descriptors for bibliography files:
  (mapc (lambda (watch-descriptor)
          (file-notify-rm-watch watch-descriptor))
        bibtex-completion-file-watch-descriptors)
  (setq bibtex-completion-file-watch-descriptors nil)

  ;; Check that all specified bibliography files exist and add file
  ;; watches for automatic reloading of the bibliography when a file
  ;; is changed:
  (mapc (lambda (file)
          (if (f-file? file)
              (let ((watch-descriptor
                     (file-notify-add-watch file
                                            '(change)
                                            (lambda (event) (bibtex-completion-candidates)))))
                (setq bibtex-completion-file-watch-descriptors
                      (cons watch-descriptor bibtex-completion-file-watch-descriptors)))
            (user-error "Bibliography file %s could not be found." file)))
            (bibtex-completion-normalize-bibliography))

  ;; Pre-calculate minimal widths needed by the format strings for
  ;; various entry types:
  (setq bibtex-completion-display-formats-internal
        (mapcar (lambda (format)
                  (let* ((format-string (cdr format))
                         (fields-width 0)
                         (string-width
                          (length
                           (s-format format-string
                                     (lambda (field)
                                       (setq fields-width
                                             (+ fields-width
                                                (string-to-number
                                                 (or (cadr (split-string field ":"))
                                                     ""))))
                                       "")))))
                    (-cons* (car format) format-string (+ fields-width string-width))))
                bibtex-completion-display-formats)))

(defun bibtex-completion-clear-cache (&optional files)
  "Clears FILES from cache. If FILES is omitted, all files in `bibtex-completion-biblography' are cleared."
  (setq bibtex-completion-cache
        (cl-remove-if
         (lambda (x)
           (member (car x)
                   (or files
                       (bibtex-completion-normalize-bibliography 'bibtex))))
         bibtex-completion-cache)))

(defun bibtex-completion-clear-string-cache (&optional files)
  "Clears FILES from cache. If FILES is omitted, all files in `bibtex-completion-bibliography' are cleared."
  (setq bibtex-completion-string-cache
        (cl-remove-if
         (lambda (x)
           (member (car x)
                   (or files
                       (-flatten (list bibtex-completion-bibliography)))))
         bibtex-completion-string-cache)))

(defun bibtex-completion-parse-strings (&optional ht-strings)
  "Parse the BibTeX strings listed in the current buffer and
return a list of entries in the order in which they appeared in
the BibTeX file.

If HT-STRINGS is provided it is assumed to be a hash table used
for string replacement."
  (goto-char (point-min))
  (let  ((strings (cl-loop
                   with ht = (if ht-strings ht-strings (make-hash-table :test #'equal))
                   for entry-type = (parsebib-find-next-item)
                   while entry-type
                   if (string= (downcase entry-type) "string")
                   collect (let ((entry (parsebib-read-string (point) ht)))
                             (puthash (car entry) (cdr entry) ht)
                             entry)
                   )))
    (-filter (lambda (x) x) strings)))

(defun bibtex-completion-update-strings-ht (ht strings)
  (cl-loop
   for entry in strings
   do (puthash (car entry) (cdr entry) ht)))

(defvar bibtex-completion-cached-notes-keys nil
  "A cache storing notes keys obtained when the bibliography was last parsed.")

(defun bibtex-completion-candidates ()
  "Reads the BibTeX files and returns a list of conses, one for
each entry.  The first element of these conses is a string
containing authors, editors, title, year, type, and key of the
entry.  This is string is used for matching.  The second element
is the entry (only the fields listed above) as an alist."
  (let ((files (nreverse (bibtex-completion-normalize-bibliography 'bibtex)))
        (ht-strings (make-hash-table :test #'equal))
        reparsed-files)

    ;; Open each bibliography file in a temporary buffer,
    ;; check hash of bibliography and mark for reparsing if necessary:

    (cl-loop
     for file in files
     do
     (with-temp-buffer
       (insert-file-contents file)
       (let ((bibliography-hash (secure-hash 'sha256 (current-buffer))))
         (unless (string= (cadr (assoc file bibtex-completion-cache))
                          bibliography-hash)
           ;; Mark file as reparsed.
           ;; This will be useful to resolve cross-references:
           (push file reparsed-files)))))

    (when (and bibtex-completion-notes-path
               (f-file? bibtex-completion-notes-path))
      (require 'org-element)
      (with-temp-buffer
	(org-mode)     ;;  need this to avoid error in emacs 25.3.1
        (insert-file-contents bibtex-completion-notes-path)
        (setq bibtex-completion-cached-notes-keys
              (let ((tree (org-element-parse-buffer 'headline)))
                (org-element-map tree 'headline
                  (lambda (key) (org-element-property :CUSTOM_ID key)))))))

    ;; reparse if necessary

    (when reparsed-files
      (cl-loop
       for file in files
       do
       (with-temp-buffer
         (insert-file-contents file)
         (let ((bibliography-hash (secure-hash 'sha256 (current-buffer))))
           (if (not (member file reparsed-files))
               (bibtex-completion-update-strings-ht ht-strings
                                                    (cddr (assoc file bibtex-completion-string-cache)))
             (progn
               (message "Parsing bibliography file %s ..." file)
               (bibtex-completion-clear-string-cache (list file))
               (push (-cons* file
                             bibliography-hash
                             (bibtex-completion-parse-strings ht-strings))
                     bibtex-completion-string-cache)

               (bibtex-completion-clear-cache (list file))
               (push (-cons* file
                             bibliography-hash
                             (bibtex-completion-parse-bibliography ht-strings))
                     bibtex-completion-cache))))))
      (setf bibtex-completion-string-hash-table ht-strings))

    ;; If some files were reparsed, resolve cross-references:
    (when reparsed-files
      (message "Resolving cross-references ...")
      (bibtex-completion-resolve-crossrefs files reparsed-files))

    ;; Finally return the list of candidates:
    (message "Done (re)loading bibliography.")
    (nreverse
     (cl-loop
      for file in files
      append (cddr (assoc file bibtex-completion-cache))))))

(defun bibtex-completion-resolve-crossrefs (files reparsed-files)
  "Expand all entries with fields from cross-referenced entries
in FILES, assuming that only those files in REPARSED-FILES were
reparsed whereas the other files in FILES were up-to-date."
  (cl-loop
   with entry-hash = (bibtex-completion-make-entry-hash files reparsed-files)
   for file in files
   for entries = (cddr (assoc file bibtex-completion-cache))
   if (member file reparsed-files)
   ;; The file was reparsed.
   ;; Resolve crossrefs then make candidates for all entries:
   do (setf
       (cddr (assoc file bibtex-completion-cache))
       (cl-loop
        for entry in entries
        ;; Entries are alists of \(FIELD . VALUE\) pairs.
        for crossref = (bibtex-completion-get-value "crossref" entry)
        collect (bibtex-completion-make-candidate
                 (if crossref
                     (bibtex-completion-remove-duplicated-fields
                      ;; Insert an empty field so we can discard the crossref info if needed:
                      (append entry
                              (cl-acons "" ""
                                     (gethash (downcase crossref) entry-hash))))
                   entry))))
   else
   ;; The file was not reparsed.
   ;; Resolve crossrefs then make candidates for the entries with a crossref field:
   do (setf
       (cddr (assoc file bibtex-completion-cache))
       (cl-loop
        for entry in entries
        ;; Entries are \(STRING . ALIST\) conses.
        for entry-alist = (cdr entry)
        for crossref = (bibtex-completion-get-value "crossref" entry-alist)
        collect (if crossref
                    (bibtex-completion-make-candidate
                     (bibtex-completion-remove-duplicated-fields
                      ;; Discard crossref info and resolve crossref again:
                      (append (--take-while (> (length (car it)) 0) entry-alist)
                              (cl-acons "" ""
                                     (gethash (downcase crossref) entry-hash)))))
                  entry)))))

(defun bibtex-completion-make-entry-hash (files reparsed-files)
  "Return a hash table of all potentially cross-referenced bibliography entries in FILES,
assuming that only those files in REPARSED-FILES were reparsed
whereas the other files in FILES were up-to-date. Only entries
whose type belongs to
`bibtex-completion-cross-referenced-entry-types' are included in
the hash table."
  (cl-loop
   with entries =
     (cl-loop
      for file in files
      for entries = (cddr (assoc file bibtex-completion-cache))
      if (member file reparsed-files)
      ;; Entries are alists of \(FIELD . VALUE\) pairs.
      append entries
      ;; Entries are \(STRING . ALIST\) conses.
      else
      append (mapcar 'cdr entries))
   with ht = (make-hash-table :test #'equal :size (length entries))
   for entry in entries
   for key = (bibtex-completion-get-value "=key=" entry)
   if (member (downcase (bibtex-completion-get-value "=type=" entry))
              bibtex-completion-cross-referenced-entry-types)
   do (puthash (downcase key) entry ht)
   finally return ht))

(defun bibtex-completion-make-candidate (entry)
  "Return a candidate for ENTRY."
  (cons (bibtex-completion-clean-string
         (s-join " " (-map #'cdr entry)))
        entry))

(defun bibtex-completion-parse-bibliography (&optional ht-strings)
  "Parse the BibTeX entries listed in the current buffer and
return a list of entries in the order in which they appeared in
the BibTeX file. Also do some preprocessing of the entries.

If HT-STRINGS is provided it is assumed to be a hash table."
  (goto-char (point-min))
  (cl-loop
   with fields = (append '("title" "crossref")
                         (-map (lambda (it) (if (symbolp it) (symbol-name it) it))
                               bibtex-completion-additional-search-fields))
   for entry-type = (parsebib-find-next-item)
   while entry-type
   unless (member-ignore-case entry-type '("preamble" "string" "comment"))
   collect (let* ((entry (parsebib-read-entry entry-type (point) ht-strings))
                  (fields (append
                           (list (if (assoc-string "author" entry 'case-fold)
                                     "author"
                                   "editor")
                                 (if (assoc-string "date" entry 'case-fold)
                                     "date"
                                   "year"))
                           fields)))
             (-map (lambda (it)
                     (cons (downcase (car it)) (cdr it)))
                   (bibtex-completion-prepare-entry entry fields)))))

(defun bibtex-completion-get-entry (entry-key)
  "Given a BibTeX key this function scans all bibliographies
listed in `bibtex-completion-bibliography' and returns an alist of the
record with that key.  Fields from crossreferenced entries are
appended to the requested entry."
  (let* ((entry (bibtex-completion-get-entry1 entry-key))
         (crossref (bibtex-completion-get-value "crossref" entry))
         (crossref (when crossref (bibtex-completion-get-entry1 crossref))))
    (bibtex-completion-remove-duplicated-fields (append entry crossref))))

(defun bibtex-completion-get-entry1 (entry-key &optional do-not-find-pdf)
  (let ((bib (bibtex-completion-normalize-bibliography 'bibtex)))
    (with-temp-buffer
      (mapc #'insert-file-contents bib)
      (goto-char (point-min))
      (if (re-search-forward (concat "^[ \t]*@\\(" parsebib--bibtex-identifier
                                     "\\)[[:space:]]*[\(\{][[:space:]]*"
                                     (regexp-quote entry-key) "[[:space:]]*,")
                             nil t)
          (let ((entry-type (match-string 1)))
            (reverse (bibtex-completion-prepare-entry
                      (parsebib-read-entry entry-type (point) bibtex-completion-string-hash-table) nil do-not-find-pdf)))
        (progn
          (display-warning :warning (concat "Bibtex-completion couldn't find entry with key \"" entry-key "\"."))
          nil)))))

(defun bibtex-completion-find-pdf-in-field (key-or-entry)
  "Returns the path of the PDF specified in the field
`bibtex-completion-pdf-field' if that file exists.  Returns nil if no
file is specified, or if the specified file does not exist, or if
`bibtex-completion-pdf-field' is nil."
  (when bibtex-completion-pdf-field
    (let* ((entry (if (stringp key-or-entry)
                      (bibtex-completion-get-entry1 key-or-entry t)
                    key-or-entry))
           (value (bibtex-completion-get-value bibtex-completion-pdf-field entry)))
      (cond
       ((not value) nil)         ; Field not defined.
       ((f-file? value) (list value))   ; A bare full path was found.
       ((-any 'f-file? (--map (f-join it (f-filename value)) (-flatten bibtex-completion-library-path))) (-filter 'f-file? (--map (f-join it (f-filename value)) (-flatten bibtex-completion-library-path))))
       (t                               ; Zotero/Mendeley/JabRef format:
        (let ((value (replace-regexp-in-string "\\([^\\]\\);" "\\1\^^" value)))
          (cl-loop  ; Looping over the files:
           for record in (s-split "\^^" value)
                                        ; Replace unescaped colons by field separator:
           for record = (replace-regexp-in-string "\\([^\\]\\|^\\):" "\\1\^_" record)
                                        ; Unescape stuff:
           for record = (replace-regexp-in-string "\\\\\\(.\\)" "\\1" record)
                                        ; Now we can safely split:
           for record = (s-split "\^_" record)
           for file-name = (nth 0 record)
           for path = (or (nth 1 record) "")
           for paths = (if (s-match "^[A-Z]:" path)
                           (list path)                 ; Absolute Windows path
                                        ; Something else:
                         (append
                          (list
                           path
                           file-name
                           (f-join (f-root) path) ; Mendeley #105
                           (f-join (f-root) path file-name)) ; Mendeley #105
                          (--map (f-join it path)
                                 (-flatten bibtex-completion-library-path)) ; Jabref #100
                          (--map (f-join it path file-name)
                                 (-flatten bibtex-completion-library-path)))) ; Jabref #100
           for result = (-first (lambda (path)
                                  (if (and (not (s-blank-str? path))
                                           (f-exists? path))
                                      path nil)) paths)
           if result collect result)))))))


(defun bibtex-completion-find-pdf-in-library (key-or-entry &optional find-additional)
  "Searches the directories in `bibtex-completion-library-path'
for a PDF whose name is composed of the BibTeX key plus
`bibtex-completion-pdf-extension'.  The path of the first matching
PDF is returned.

If FIND-ADDITIONAL is non-nil, the paths of all PDFs whose name
starts with the BibTeX key and ends with
`bibtex-completion-pdf-extension' are returned instead."
  (let* ((key (if (stringp key-or-entry)
                  key-or-entry
                (bibtex-completion-get-value "=key=" key-or-entry)))
         (main-pdf (cl-loop
                    for dir in (-flatten bibtex-completion-library-path)
                    append (cl-loop
                            for ext in (-flatten bibtex-completion-pdf-extension)
                            collect (f-join dir (s-concat key ext))))))
    (if find-additional
        (sort ; move main pdf on top of the list if needed
         (cl-loop
          for dir in (-flatten bibtex-completion-library-path)
          append (directory-files dir t
                                  (s-concat "^" (regexp-quote key)
                                            ".*\\("
                                            (mapconcat 'regexp-quote
                                                       (-flatten bibtex-completion-pdf-extension)
                                                       "\\|")
                                            "\\)$")))
         (lambda (x y)
           (and (member x main-pdf)
                (not (member y main-pdf)))))
      (-flatten (-first 'f-file? main-pdf)))))

(defun bibtex-completion-find-pdf (key-or-entry &optional find-additional)
  "Returns the path of the PDF associated with the specified
entry.  This is either the path(s) specified in the field
`bibtex-completion-pdf-field' or, if that does not exist, the
first PDF in any of the directories in
`bibtex-completion-library-path' whose name is composed of the
the BibTeX key plus `bibtex-completion-pdf-extension' (or if
FIND-ADDITIONAL is non-nil, all PDFs in
`bibtex-completion-library-path' whose name starts with the
BibTeX key and ends with `bibtex-completion-pdf-extension').
Returns nil if no PDF is found."
  (or (bibtex-completion-find-pdf-in-field key-or-entry)
      (bibtex-completion-find-pdf-in-library key-or-entry find-additional)))

(defun bibtex-completion-prepare-entry (entry &optional fields do-not-find-pdf)
  "Prepare ENTRY for display.
ENTRY is an alist representing an entry as returned by
parsebib-read-entry. All the fields not in FIELDS are removed
from ENTRY, with the exception of the \"=type=\" and \"=key=\"
fields. If FIELDS is empty, all fields are kept. Also add a
=has-pdf= and/or =has-note= field, if they exist for ENTRY.  If
DO-NOT-FIND-PDF is non-nil, this function does not attempt to
find a PDF file."
  (when entry ; entry may be nil, in which case just return nil
    (let* ((fields (when fields (append fields (list "=type=" "=key=" "=has-pdf=" "=has-note="))))
           ; Check for PDF:
           (entry (if (and (not do-not-find-pdf) (bibtex-completion-find-pdf entry))
                      (cons (cons "=has-pdf=" bibtex-completion-pdf-symbol) entry)
                    entry))
           (entry-key (cdr (assoc "=key=" entry)))
           ; Check for notes:
           (entry (if (or
                       ;; One note file per entry:
                       (and bibtex-completion-notes-path
                            (f-directory? bibtex-completion-notes-path)
                            (f-file? (f-join bibtex-completion-notes-path
                                             (s-concat entry-key
                                                       bibtex-completion-notes-extension))))
                       ;; All notes in one file:
                       (and bibtex-completion-notes-path
                            (f-file? bibtex-completion-notes-path)
			    (member entry-key bibtex-completion-cached-notes-keys)))
                      (cons (cons "=has-note=" bibtex-completion-notes-symbol) entry)
                    entry))
           ; Remove unwanted fields:
           (entry (if fields
                       (--filter (member-ignore-case (car it) fields) entry)
                     entry)))
      ;; Normalize case of entry type:
      (setcdr (assoc "=type=" entry) (downcase (cdr (assoc "=type=" entry))))
      ;; Remove duplicated fields:
      (bibtex-completion-remove-duplicated-fields entry))))

(defun bibtex-completion-remove-duplicated-fields (entry)
  "Remove duplicated fields from ENTRY."
  (cl-remove-duplicates entry
                        :test (lambda (x y) (string= (s-downcase x) (s-downcase y)))
                        :key 'car :from-end t))


(defun bibtex-completion-format-entry (entry width)
  "Formats a BibTeX ENTRY for display in results list. WIDTH is
the width of the results list. The display format is governed by
the variable `bibtex-completion-display-formats'."
  (let* ((format
          (or (assoc-string (bibtex-completion-get-value "=type=" entry)
                            bibtex-completion-display-formats-internal
                            'case-fold)
              (assoc t bibtex-completion-display-formats-internal)))
         (format-string (cadr format)))
    (s-format
     format-string
     (lambda (field)
       (let* ((field (split-string field ":"))
              (field-name (car field))
              (field-width (cadr field))
              (field-value (bibtex-completion-get-value field-name entry)))
         (when (and (string= field-name "author")
                    (not field-value))
           (setq field-value (bibtex-completion-get-value "editor" entry)))
         (when (and (string= field-name "year")
                    (not field-value))
           (setq field-value (car (split-string (bibtex-completion-get-value "date" entry "") "-"))))
         (setq field-value (bibtex-completion-clean-string (or field-value " ")))
         (when (member field-name '("author" "editor"))
           (setq field-value (bibtex-completion-shorten-authors field-value)))
         (if (not field-width)
             field-value
           (setq field-width (string-to-number field-width))
           (truncate-string-to-width
            field-value
            (if (> field-width 0)
                field-width
              (- width (cddr format)))
            0 ?\s)))))))


(defun bibtex-completion-clean-string (s)
  "Removes quoting and superfluous white space from BibTeX field
values."
  (if s (replace-regexp-in-string "[\n\t ]+" " "
         (replace-regexp-in-string "[\"{}]+" "" s))
    nil))

(defun bibtex-completion-shorten-authors (authors)
  "Returns a comma-separated list of the surnames in authors."
  (if authors
      (cl-loop for a in (s-split " and " authors)
               for p = (s-split "," a t)
               for sep = "" then ", "
               concat sep
               if (eq 1 (length p))
               concat (-last-item (s-split " +" (car p) t))
               else
               concat (car p))
    nil))


(defun bibtex-completion-open-pdf (keys &optional fallback-action)
  "Open the PDFs associated with the marked entries using the
function specified in `bibtex-completion-pdf-open-function'.
If multiple PDFs are found for an entry, ask for the one to
open using `completion-read'.  If FALLBACK-ACTION is non-nil, it is called in
case no PDF is found."
  (dolist (key keys)
    (let ((pdf (bibtex-completion-find-pdf key bibtex-completion-find-additional-pdfs)))
      (cond
       ((> (length pdf) 1)
        (let* ((pdf (f-uniquify-alist pdf))
               (choice (completing-read "File to open: " (mapcar 'cdr pdf) nil t))
               (file (car (rassoc choice pdf))))
          (funcall bibtex-completion-pdf-open-function file)))
       (pdf
        (funcall bibtex-completion-pdf-open-function (car pdf)))
       (fallback-action
        (funcall fallback-action (list key)))
       (t
        (message "No PDF(s) found for this entry: %s"
                 key))))))

(defun bibtex-completion-open-url-or-doi (keys)
  "Open the associated URL or DOI in a browser."
  (dolist (key keys)
    (let* ((entry (bibtex-completion-get-entry key))
           (url (bibtex-completion-get-value "url" entry))
           (doi (bibtex-completion-get-value "doi" entry))
           (browse-url-browser-function
            (or bibtex-completion-browser-function
                browse-url-browser-function)))
      (if url
          (browse-url url)
        (if doi (browse-url
                 (s-concat "http://dx.doi.org/" doi))
          (message "No URL or DOI found for this entry: %s"
                   key))))))

(defun bibtex-completion-open-any (keys)
  "Open the PDFs associated with the marked entries using the
function specified in `bibtex-completion-pdf-open-function'.
If multiple PDFs are found for an entry, ask for the one to
open using `completion-read'.  If no PDF is found, try to open a URL
or DOI in the browser instead."
  (bibtex-completion-open-pdf keys 'bibtex-completion-open-url-or-doi))

(defun bibtex-completion-format-citation-default (keys)
  "Default formatter for keys, separates multiple keys with commas."
  (s-join ", " keys))

(defvar bibtex-completion-cite-command-history nil
  "History list for LaTeX citation commands.")

(defun bibtex-completion-format-citation-cite (keys)
  "Formatter for LaTeX citation commands. Prompts for the command
and for arguments if the commands can take any. If point is
inside or just after a citation command, only adds KEYS to it."
  (let (macro)
    (cond
     ((and (require 'reftex-parse nil t)
           (setq macro (reftex-what-macro 1))
           (stringp (car macro))
           (string-match "\\`\\\\cite\\|cite\\'" (car macro)))
      ;; We are inside a cite macro. Insert key at point, with appropriate delimiters.
      (delete-horizontal-space)
      (concat (pcase (preceding-char)
                (?\{ "")
                (?, " ")
                (_ ", "))
              (s-join ", " keys)
              (if (member (following-char) '(?\} ?,))
		     ""
                ", ")))
     ((and (equal (preceding-char) ?\})
           (require 'reftex-parse nil t)
           (save-excursion
             (forward-char -1)
             (setq macro (reftex-what-macro 1)))
           (stringp (car macro))
           (string-match "\\`\\\\cite\\|cite\\'" (car macro)))
      ;; We are right after a cite macro. Append key and leave point at the end.
      (delete-char -1)
      (delete-horizontal-space t)
      (concat (pcase (preceding-char)
                (?\{ "")
                (?, " ")
                (_ ", "))
              (s-join ", " keys)
              "}"))
     (t
      ;; We are not inside or right after a cite macro. Insert a full citation.
      (let* ((initial (when bibtex-completion-cite-default-as-initial-input
                        bibtex-completion-cite-default-command))
             (default (unless bibtex-completion-cite-default-as-initial-input
                        bibtex-completion-cite-default-command))
             (default-info (if default (format " (default \"%s\")" default) ""))
             (cite-command (completing-read
                            (format "Cite command%s: " default-info)
                            bibtex-completion-cite-commands nil nil initial
                            'bibtex-completion-cite-command-history default nil)))
        (if (member cite-command '("nocite" "supercite"))  ; These don't want arguments.
            (format "\\%s{%s}" cite-command (s-join ", " keys))
          (let ((prenote (if bibtex-completion-cite-prompt-for-optional-arguments
                             (read-from-minibuffer "Prenote: ")
                           ""))
                (postnote (if bibtex-completion-cite-prompt-for-optional-arguments
                              (read-from-minibuffer "Postnote: ")
                            "")))
            (cond ((not (string= "" prenote))
                   (format "\\%s[%s][%s]{%s}" cite-command prenote postnote (s-join ", " keys)))
                  ((not (string= "" postnote))
                   (format "\\%s[%s]{%s}" cite-command postnote (s-join ", " keys)))
                  (t
                   (format "\\%s{%s}" cite-command (s-join ", " keys)))))))))))

(defun bibtex-completion-format-citation-pandoc-citeproc (keys)
  "Formatter for pandoc-citeproc citations."
  (let* ((prenote  (if bibtex-completion-cite-prompt-for-optional-arguments (read-from-minibuffer "Prenote: ") ""))
         (postnote (if bibtex-completion-cite-prompt-for-optional-arguments (read-from-minibuffer "Postnote: ") ""))
         (prenote  (if (string= "" prenote)  "" (concat prenote  " ")))
         (postnote (if (string= "" postnote) "" (concat ", " postnote))))
    (format "[%s%s%s]" prenote (s-join "; " (--map (concat "@" it) keys)) postnote)))

(defun bibtex-completion-format-citation-ebib (keys)
  "Formatter for ebib references."
  (s-join ", "
          (--map (format "ebib:%s" it) keys)))

(defun bibtex-completion-format-citation-org-link-to-PDF (keys)
  "Formatter for org-links to PDF.  Uses first matching PDF if
several are available.  Entries for which no PDF is available are
omitted."
  (s-join ", " (cl-loop
                for key in keys
                for pdfs = (bibtex-completion-find-pdf key bibtex-completion-find-additional-pdfs)
                append (--map (format "[[%s][%s]]" it key) pdfs))))

(defun bibtex-completion-format-citation-org-apa-link-to-PDF (keys)
  "Formatter for org-links to PDF.  Link text loosely follows APA
format.  Uses first matching PDF if several are available."
  (s-join ", " (cl-loop
                for key in keys
                for entry = (bibtex-completion-get-entry key)
                for author = (bibtex-completion-shorten-authors
                              (or (bibtex-completion-get-value "author" entry)
                                  (bibtex-completion-get-value "editor" entry)))
                for year = (or (bibtex-completion-get-value "year" entry)
                               (car (split-string (bibtex-completion-get-value "date" entry "") "-")))
                for pdf = (car (bibtex-completion-find-pdf key))
                if pdf
                  collect (format "[[file:%s][%s (%s)]]" pdf author year)
                else
                  collect (format "%s (%s)" author year))))

;; When you want to create a todo list about reading, I think using
;; PDF's title is more intuitive.
(defun bibtex-completion-format-citation-org-title-link-to-PDF (keys)
  "Formatter for org-links to PDF. Link text follows file title format.
Uses first matching PDF if several are available."
  (s-join ", " (cl-loop
                for key in keys
                for entry = (bibtex-completion-get-entry key)
                for title = (bibtex-completion-get-value "title" entry)
                for pdf = (or (car (bibtex-completion-find-pdf key))
                              (bibtex-completion-get-value "url" entry))
                if pdf
                  collect (format "[[file:%s][%s]]" pdf title)
                else
                  collect (format "%s" title))))

(defun bibtex-completion-insert-citation (keys)
  "Insert citation at point.  The format depends on
`bibtex-completion-format-citation-functions'."
  (let ((format-function
         (cdr (or (assoc major-mode bibtex-completion-format-citation-functions)
                  (assoc 'default   bibtex-completion-format-citation-functions)))))
    (insert
     (funcall format-function keys))))

(defun bibtex-completion-insert-reference (keys)
  "Insert a reference for each selected entry."
  (let* ((refs (--map
                (s-word-wrap fill-column
                             (concat "\n- " (bibtex-completion-apa-format-reference it)))
                keys)))
    (insert "\n" (s-join "\n" refs) "\n")))

(defun bibtex-completion-apa-format-reference (key)
  "Returns a plain text reference in APA format for the
publication specified by KEY."
  (let*
   ((entry (bibtex-completion-get-entry key))
    (ref (pcase (downcase (bibtex-completion-get-value "=type=" entry))
           ("article"
            (s-format
             "${author} (${year}). ${title}. ${journal}, ${volume}(${number}), ${pages}.${doi}"
             'bibtex-completion-apa-get-value entry))
           ("inproceedings"
            (s-format
             "${author} (${year}). ${title}. In ${editor}, ${booktitle} (pp. ${pages}). ${address}: ${publisher}."
             'bibtex-completion-apa-get-value entry))
           ("book"
            (s-format
             "${author} (${year}). ${title}. ${address}: ${publisher}."
             'bibtex-completion-apa-get-value entry))
           ("phdthesis"
            (s-format
             "${author} (${year}). ${title} (Doctoral dissertation). ${school}, ${address}."
             'bibtex-completion-apa-get-value entry))
           ("inbook"
            (s-format
             "${author} (${year}). ${title}. In ${editor} (Eds.), ${booktitle} (pp. ${pages}). ${address}: ${publisher}."
             'bibtex-completion-apa-get-value entry))
           ("incollection"
            (s-format
             "${author} (${year}). ${title}. In ${editor} (Eds.), ${booktitle} (pp. ${pages}). ${address}: ${publisher}."
             'bibtex-completion-apa-get-value entry))
           ("proceedings"
            (s-format
             "${editor} (Eds.). (${year}). ${booktitle}. ${address}: ${publisher}."
             'bibtex-completion-apa-get-value entry))
           ("unpublished"
            (s-format
             "${author} (${year}). ${title}. Unpublished manuscript."
             'bibtex-completion-apa-get-value entry))
           (_
            (s-format
             "${author} (${year}). ${title}."
             'bibtex-completion-apa-get-value entry)))))
   (replace-regexp-in-string "\\([.?!]\\)\\." "\\1" ref))) ; Avoid sequences of punctuation marks.

(defun bibtex-completion-apa-get-value (field entry &optional default)
  "Return FIELD or ENTRY formatted following the APA
guidelines.  Return DEFAULT if FIELD is not present in ENTRY."
  ;; Virtual fields:
  (cond
    ((string= field "author-or-editor")
     (let ((value (bibtex-completion-get-value "author" entry)))
       (if value
           (bibtex-completion-apa-format-authors value)
         (bibtex-completion-apa-format-editors
          (bibtex-completion-get-value "editor" entry)))))
    ((string= field "author-abbrev")
     (let ((value (bibtex-completion-get-value "author" entry)))
       (bibtex-completion-apa-format-authors-abbrev value)))
    (t
     ;; Real fields:
     (let ((value (bibtex-completion-get-value field entry)))
       (if value
           (pcase field
             ;; https://owl.english.purdue.edu/owl/resource/560/06/
             ("author" (bibtex-completion-apa-format-authors value))
             ("editor" (bibtex-completion-apa-format-editors value))
             ;; When referring to books, chapters, articles, or Web pages,
             ;; capitalize only the first letter of the first word of a
             ;; title and subtitle, the first word after a colon or a dash
             ;; in the title, and proper nouns. Do not capitalize the first
             ;; letter of the second word in a hyphenated compound word.
             ("title" (replace-regexp-in-string ; remove braces
                       "[{}]"
                       ""
                       (replace-regexp-in-string ; remove macros
                        "\\\\[[:alpha:]]+{"
                        ""
                        (replace-regexp-in-string ; upcase initial letter
                         "^[[:alpha:]]"
                         'upcase
                         (replace-regexp-in-string ; preserve stuff in braces from being downcased
                          "\\(^[^{]*{\\)\\|\\(}[^{]*{\\)\\|\\(}.*$\\)\\|\\(^[^{}]*$\\)"
                          (lambda (x) (downcase (s-replace "\\" "\\\\" x)))
                          value)))))
             ("booktitle" value)
             ;; Maintain the punctuation and capitalization that is used by
             ;; the journal in its title.
             ("pages" (s-join "–" (s-split "[^0-9]+" value t)))
             ("doi" (s-concat " http://dx.doi.org/" value))
             ("year" (or value
                         (car (split-string (bibtex-completion-get-value "date" entry "") "-"))))
             (_ value))
         "")))))

(defun bibtex-completion-apa-format-authors (value)
  (cl-loop for a in (s-split " and " value t)
           if (s-index-of "{" a)
             collect
             (replace-regexp-in-string "[{}]" "" a)
             into authors
           else if (s-index-of "," a)
             collect
             (let ((p (s-split " *, *" a t)))
               (concat
                (car p) ", "
                (s-join " " (-map (lambda (it) (concat (s-left 1 it) "."))
                                  (s-split " " (cadr p))))))
             into authors
           else
             collect
             (let ((p (s-split " " a t)))
               (concat
                (-last-item p) ", "
                (s-join " " (-map (lambda (it) (concat (s-left 1 it) "."))
                                  (-butlast p)))))
             into authors
           finally return
             (let ((l (length authors)))
               (cond
                 ((= l 1) (car authors))
                 ((< l 8) (concat (s-join ", " (-butlast authors))
                                  ", & " (-last-item authors)))
                 (t (concat (s-join ", " (-slice authors 0 7)) ", …"))))))

(defun bibtex-completion-apa-format-authors-abbrev (value)
  (cl-loop for a in (s-split " and " value t)
           if (s-index-of "{" a)
             collect
             (replace-regexp-in-string "[{}]" "" a)
             into authors
           else if (s-index-of "," a)
             collect
             (let ((p (s-split " *, *" a t)))
               (concat
                (car p) ", "
                (s-join " " (-map (lambda (it) (concat (s-left 1 it) "."))
                                  (s-split " " (cadr p))))))
             into authors
           else
             collect
             (let ((p (s-split " " a t)))
               (concat
                (-last-item p) ", "
                (s-join " " (-map (lambda (it) (concat (s-left 1 it) "."))
                                  (-butlast p)))))
             into authors
           finally return
             (let ((l (length authors)))
               (cond
                 ((= l 1) (car authors))
                 ((= l 2) (concat (s-join " & " authors)))
                 (t (format "%s et al." (car authors)))))))

(defun bibtex-completion-apa-format-editors (value)
  (cl-loop for a in (s-split " and " value t)
           if (s-index-of "," a)
             collect
             (let ((p (s-split " *, *" a t)))
               (concat
                (s-join " " (-map (lambda (it) (concat (s-left 1 it) "."))
                                  (s-split " " (cadr p))))
                " " (car p)))
             into authors
           else
             collect
             (let ((p (s-split " " a t)))
               (concat
                (s-join " " (-map (lambda (it) (concat (s-left 1 it) "."))
                                  (-butlast p)))
                " " (-last-item p)))
             into authors
           finally return
             (let ((l (length authors)))
               (cond
                 ((= l 1) (car authors))
                 ((< l 8) (concat (s-join ", " (-butlast authors))
                                  ", & " (-last-item authors)))
                 (t (concat (s-join ", " authors) ", ..."))))))

(defun bibtex-completion-get-value (field entry &optional default)
  "Return the requested value or `default' if the value is not
defined.  Surrounding curly braces are stripped."
  (let ((value (cdr (assoc-string field entry 'case-fold))))
    (if value
        (replace-regexp-in-string
         "\\(^[[:space:]]*[\"{][[:space:]]*\\)\\|\\([[:space:]]*[\"}][[:space:]]*$\\)"
         ""
         (s-collapse-whitespace value))
      default)))

(defun bibtex-completion-insert-key (keys)
  "Insert BibTeX key at point."
  (insert
   (funcall 'bibtex-completion-format-citation-default keys)))

(defun bibtex-completion-insert-bibtex (keys)
  "Insert BibTeX key at point."
  (insert (s-join "\n" (--map (bibtex-completion-make-bibtex it) keys))))

(defun bibtex-completion-make-bibtex (key)
  (let* ((entry (bibtex-completion-get-entry key))
         (entry-type (bibtex-completion-get-value "=type=" entry)))
    (format "@%s{%s,\n%s}\n"
            entry-type key
            (cl-loop
             for field in entry
             for name = (car field)
             for value = (cdr field)
             unless (member name
                            (append (-map (lambda (it) (if (symbolp it) (symbol-name it) it))
                                          bibtex-completion-no-export-fields)
                             '("=type=" "=key=" "=has-pdf=" "=has-note=" "crossref")))
             concat
             (format "  %s = {%s},\n" name value)))))

(defun bibtex-completion-add-PDF-attachment (keys)
  "Attach the PDFs of the selected entries where available."
  (dolist (key keys)
    (let ((pdf (bibtex-completion-find-pdf key bibtex-completion-find-additional-pdfs)))
      (if pdf
          (mapc 'mml-attach-file pdf)
        (message "No PDF(s) found for this entry: %s"
                 key)))))

(define-minor-mode bibtex-completion-notes-mode
  "Minor mode for managing notes."
  :keymap (let ((map (make-sparse-keymap)))
            (define-key map (kbd "C-c C-c") 'bibtex-completion-exit-notes-buffer)
            (define-key map (kbd "C-c C-w") 'org-refile)
            map)
  (setq-local
   header-line-format
   (substitute-command-keys
    " Finish \\[bibtex-completion-exit-notes-buffer], refile \\[org-refile]")))

;; Define global minor mode. This is needed to the toggle minor mode.
(define-globalized-minor-mode bibtex-completion-notes-global-mode bibtex-completion-notes-mode bibtex-completion-notes-mode)

(defun bibtex-completion-exit-notes-buffer ()
  "Exit notes buffer and delete its window.
This will also disable `bibtex-completion-notes-mode' and remove the header
line."
  (interactive)
  (widen)
  (bibtex-completion-notes-global-mode -1)
  (setq-local
   header-line-format nil)
  (save-buffer)
  (let ((window (get-buffer-window (get-file-buffer bibtex-completion-notes-path))))
    (if (and window (not (one-window-p window)))
        (delete-window window)
      (switch-to-buffer (other-buffer)))))

(defun bibtex-completion-edit-notes (keys)
  "Open the notes associated with the selected entries using `find-file'."
  (dolist (key keys)
    (let* ((entry (bibtex-completion-get-entry key))
           (year (or (bibtex-completion-get-value "year" entry)
                     (car (split-string (bibtex-completion-get-value "date" entry "") "-"))))
           (entry (push (cons "year" year) entry)))
      (if (and bibtex-completion-notes-path
               (f-directory? bibtex-completion-notes-path))
                                        ; One notes file per publication:
          (let* ((path (f-join bibtex-completion-notes-path
                               (s-concat key bibtex-completion-notes-extension))))
            (find-file path)
            (unless (f-exists? path)
              (insert (s-format bibtex-completion-notes-template-multiple-files
                                'bibtex-completion-apa-get-value
                                entry))))
                                        ; One file for all notes:
        (unless (and buffer-file-name
                     (f-same? bibtex-completion-notes-path buffer-file-name))
          (find-file-other-window bibtex-completion-notes-path))
        (widen)
        (outline-show-all)
        (goto-char (point-min))
        (if (re-search-forward (format bibtex-completion-notes-key-pattern (regexp-quote key)) nil t)
                                        ; Existing entry found:
            (when (eq major-mode 'org-mode)
              (org-narrow-to-subtree)
              (re-search-backward "^\*+ " nil t)
              (org-cycle-hide-drawers nil)
              (bibtex-completion-notes-mode 1))
                                        ; Create a new entry:
            (goto-char (point-max))
            (save-excursion (insert (s-format bibtex-completion-notes-template-one-file
                                              'bibtex-completion-apa-get-value
                                              entry)))
            (re-search-forward "^*+ " nil t))
        (when (eq major-mode 'org-mode)
          (org-narrow-to-subtree)
          (re-search-backward "^\*+ " nil t)
          (org-cycle-hide-drawers nil)
          (goto-char (point-max))
          (bibtex-completion-notes-mode 1))))))

(defun bibtex-completion-buffer-visiting (file)
  (or (get-file-buffer file)
      (find-buffer-visiting file)))

(defun bibtex-completion-show-entry (keys)
  "Show the first selected entry in the BibTeX file."
  (catch 'break
    (dolist (bib-file (bibtex-completion-normalize-bibliography 'main))
      (let ((key (car keys))
            (buf (bibtex-completion-buffer-visiting bib-file)))
        (find-file bib-file)
        (widen)
        (if (eq major-mode 'org-mode)
            (let* ((prop (if (boundp 'org-bibtex-key-property)
                             org-bibtex-key-property
                           "CUSTOM_ID"))
                   (match (org-find-property prop key)))
              (when match
                (goto-char match)
                (org-show-entry)
                (throw 'break t)))
          (goto-char (point-min))
          (when (re-search-forward
                 (concat "^@\\(" parsebib--bibtex-identifier
                         "\\)[[:space:]]*[\(\{][[:space:]]*"
                         (regexp-quote key) "[[:space:]]*,") nil t)
            (throw 'break t)))
        (unless buf
          (kill-buffer))))))

(defun bibtex-completion-add-pdf-to-library (keys)
  "Add a PDF to the library for the first selected entry. The PDF
can be added either from an open buffer, a file, or a URL."
  (let* ((key (car keys))
         (source (char-to-string
                  (read-char-choice "Add pdf from [b]uffer, [f]ile, or [u]rl? " '(?b ?f ?u))))
         (buffer (when (string= source "b")
                   (read-buffer-to-switch "Add pdf buffer: ")))
         (file (when (string= source "f")
                 (expand-file-name (read-file-name "Add pdf file: " nil nil t))))
         (url (when (string= source "u")
                (read-string "Add pdf URL: ")))
         (path (-flatten (list bibtex-completion-library-path)))
         (path (if (cdr path)
                   (completing-read "Add pdf to: " path nil t)
                 (car path)))
         (pdf (expand-file-name (completing-read "Rename pdf to: "
                                                 (--map (s-concat key it)
                                                        (-flatten bibtex-completion-pdf-extension))
                                                 nil nil key)
                                path)))
    (cond
     (buffer
      (with-current-buffer buffer
        (write-file pdf t)))
     (file
      (copy-file file pdf 1))
     (url
      (url-copy-file url pdf 1)))))

(defun bibtex-completion-fallback-action (url-or-function search-expression)
  (let ((browse-url-browser-function
          (or bibtex-completion-browser-function
              browse-url-browser-function)))
    (cond
      ((stringp url-or-function)
        (browse-url (format url-or-function (url-hexify-string search-expression))))
      ((functionp url-or-function)
        (funcall url-or-function search-expression))
      (t (error "Don't know how to interpret this: %s" url-or-function)))))

(defun bibtex-completion-fallback-candidates ()
  "Compile list of fallback options.  These consist of the online
resources defined in `bibtex-completion-fallback-options' plus
one entry for each bibliography file that will open that file for
editing."
  (let ((bib-files (bibtex-completion-normalize-bibliography 'main)))
    (-concat
      (--map (cons (s-concat "Create new entry in " (f-filename it))
                   `(lambda (_search-expression) (find-file ,it) (goto-char (point-max)) (newline)))
             bib-files)
      bibtex-completion-fallback-options)))

(defun bibtex-completion-find-local-bibliography ()
  "Return a list of BibTeX files associated with the current
file. If the current file is a BibTeX file, return this
file. Otherwise, try to use `reftex' to find the associated
BibTeX files. If this fails, return nil."
  (or (and (buffer-file-name)
           (string= (or (f-ext (buffer-file-name)) "") "bib")
           (list (buffer-file-name)))
      (and (buffer-file-name)
           (require 'reftex-cite nil t)
           (ignore-errors (reftex-get-bibfile-list)))))

(defun bibtex-completion-key-at-point ()
  "Return the key of the BibTeX entry at point. If the current
file is a BibTeX file, return the key of the entry at
point. Otherwise, try to use `reftex' to check whether point is
at a citation macro, and if so return the key at
point. Otherwise, if the current file is an org-mode file, return
the value of `org-bibtex-key-property' (or
default to \"CUSTOM_ID\"). Otherwise, return nil."
  (or (and (eq major-mode 'bibtex-mode)
           (save-excursion
             (bibtex-beginning-of-entry)
             (and (looking-at bibtex-entry-maybe-empty-head)
                  (bibtex-key-in-head))))
      (and (require 'reftex-parse nil t)
           (save-excursion
             (skip-chars-backward "[:space:],;}")
             (let ((macro (reftex-what-macro 1)))
               (and (stringp (car macro))
                    (string-match "\\`\\\\cite\\|cite\\'" (car macro))
                    ;; allow '_' in citekeys
                    (let ((temp-syn-table (make-syntax-table)))
                      (modify-syntax-entry ?_ "_" temp-syn-table)
                      (with-syntax-table temp-syn-table
                        (thing-at-point 'symbol)))))))
      (and (eq major-mode 'org-mode)
           (let (key)
             (and (setq key (org-entry-get nil
                                           (if (boundp 'org-bibtex-key-property)
                                               org-bibtex-key-property
                                             "CUSTOM_ID")
                                           t))
                  ;; KEY may be the empty string the the property is
                  ;; present but has no value
                  (> (length key) 0)
                  key)))))

(provide 'bibtex-completion)

;; Local Variables:
;; byte-compile-warnings: (not cl-functions obsolete)
;; coding: utf-8
;; indent-tabs-mode: nil
;; End:

;;; bibtex-completion.el ends here
