;;; org-roam-bibtex.el --- Org Roam meets BibTeX -*- lexical-binding: t -*-

;; Copyright © 2020-2022 Mykhailo Shevchuk
;; Copyright © 2020 Leo Vivier

;; Author: Mykhailo Shevchuk <mail@mshevchuk.com>
;;      Leo Vivier <leo.vivier+dev@gmail.com>
;; URL: https://github.com/org-roam/org-roam-bibtex
;; Keywords: bib hypermedia outlines wp
;; Version: 0.6.1
;; Package-Requires: ((emacs "27.1") (org-roam "2.2.0") (bibtex-completion "2.0.0"))

;; This file is NOT part of GNU Emacs.

;; 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, 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; see the file LICENSE.  If not, visit
;; <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Org-roam-bibtex, ORB for short, offers integration of Org-roam with BibTeX
;; Emacs software: Org-ref, Helm/Ivy-bibtex and Citar.  The main task of ORB is
;; to seamlessly expose Org-roam as a note management solution to these
;; packages, shadowing their native facilities for taking bibliographic
;; notes.  As its main feature, ORB enables expansion of BibTeX keywords in
;; Org-roam templates.
;;
;; Main usage:
;;
;; Call interactively `org-roam-bibtex-mode' or call (org-roam-bibtex-mode +1)
;; from Lisp.  Enabling `org-roam-bitex-mode' sets appropriate functions for
;; creating and retrieving Org-roam notes from Org-ref, Helm/Ivy-bibtex and
;; Citar.
;;
;; Other commands:
;;
;; - `orb-insert-link':  insert a link or citation to an Org-roam note that has
;;    an associated BibTeX entry
;; - `orb-note-actions': call a dispatcher of useful note actions
;;
;; Soft dependencies: Org-ref, Citar, Helm, Ivy, Hydra, Projectile, Persp-mode

;;; Code:

;; ============================================================================
;;;; Dependencies
;; ============================================================================

(require 'orb-core)

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

;; declare own functions and variables
(declare-function orb-helm-insert "orb-helm")
(declare-function orb-note-actions-helm "orb-helm")
(declare-function orb-ivy-insert "orb-ivy")
(declare-function orb-note-actions-ivy "orb-ivy")
(declare-function orb-pdf-scrapper-run "orb-pdf-scrapper" (key))

(declare-function orb-section-abstract "orb-section" (node))
(declare-function orb-section-file "orb-section" (node))
(declare-function orb-section-reference "orb-section" (node))
(autoload #'orb-section-abstract "orb-section")
(autoload #'orb-section-file "orb-section")
(autoload #'orb-section-reference "orb-section")

;; declare external functions and variables

;; Projectile, Perspective-mode
(declare-function projectile-relevant-open-projects "ext:projectile")
(declare-function persp-switch "ext:persp-mode")
(declare-function persp-names "ext:persp-mode")

;; Org-ref
(defvar org-ref-notes-function)
(declare-function org-ref-find-bibliography "ext:org-ref-core")
;;
;; Citar
(defvar citar-open-note-function)

;; Hydra
(declare-function defhydra "ext:hydra")

;; ============================================================================
;;;; Customize definitions
;; ============================================================================

(defcustom orb-preformat-templates t
  "Non-nil to enable template pre-expanding.
See `orb-edit-note' for details."
  :type '(choice
          (const :tag "Yes" t)
          (const :tag "No" nil))
  :group 'org-roam-bibtex)

(defcustom orb-preformat-keywords
  '("citekey" "entry-type" "date" "pdf?" "note?" "file"
    "author" "editor" "author-abbrev" "editor-abbrev"
    "author-or-editor-abbrev")
  "A list of template placeholders for pre-expanding.
Any BibTeX field can be set for pre-expanding including
Bibtex-completion virtual fields such as '=key=' and '=type='.
BibTeX fields can be referred to by means of their aliases
defined in `orb-bibtex-field-aliases'.

Usage example:

\(setq orb-preformat-keywords '(\"citekey\" \"author\" \"date\"))
\(setq orb-templates
      '((\"r\" \"reference\" plain
         \"#+ROAM_KEY: %^{citekey}%?
%^{author} published %^{entry-type} in %^{date}: fullcite:%\\1.\"
         :target
         (file+head \"references/${citekey.org}\" \"#+title: ${title}\n\")
         :unnarrowed t)))

Special cases:

The \"file\" keyword will be treated specially if the value of
`orb-process-file-keyword' is non-nil.  See its docstring for an
explanation.

This variable takes effect when `orb-preformat-templates' is set
to t (default). See also `orb-edit-note' for further details.

Consult Bibtex-completion documentation for additional
information on BibTeX field names."
  :type '(repeat :tag "BibTeX field names" string)
  :group 'org-roam-bibtex)

(defcustom orb-process-file-keyword t
  "Whether to treat the file keyword specially during template pre-expanding.
When this variable is non-nil, the \"%^{file}\" and \"${file}\"
wildcards will be processed by `org-process-file-field' rather
than simply replaced with the field value.  This may be useful in
situations when the file field contains several file names and
only one file name is desirable for retrieval.  The \"file\"
keyword must be set for pre-expanding in `orb-preformat-keywords'
as usual.

If this variable is `string', for example \"my-file\", use its
value as the wildcard keyword instead of the default \"file\"
keyword.  Thus, it will be possible to get both the raw file
field value by expanding the %^{file} and ${file} wildcards and a
single file name by expanding the %^{my-file} and ${my-file}
wildcards.  The keyword, e.g. \"my-file\", must be set for
pre-expanding in `orb-preformat-keywords' as usual.

The variable `orb-attached-file-extensions' controls filtering of
file names based on file extensions."
  ;; TODO: check if a custom string is really working as described
  :group 'org-roam-bibtex
  :type '(choice
          (const :tag "Yes" t)
          (const :tag "No" nil)
          (string :tag "Custom wildcard keyword")))

(defcustom orb-roam-ref-format 'org-ref-v2
  "Defines the format of citation key in the `ROAM_REFS' property.
Should be one of the following symbols:
- `org-ref-v2': Old Org-ref `cite:links'
- `org-ref-v3': New Org-ref `cite:&links'
- `org-cite'  : Org-cite `@elements'

This can also be a custom `format' string with a single `%s' specifier."
  :type '(radio
          (const :tag "Org-ref v2" org-ref-v2)
          (const :tag "Org-ref v3" org-ref-v3)
          (const :tag "Org-cite" org-cite)
          (string :tag "Custom format string"))
  :group 'org-roam-bibtex)

(defcustom orb-bibtex-entry-get-value-function #'bibtex-completion-apa-get-value
  "Function to be used by ORB for values from a BibTeX entry.

The default value of this variable is `bibtex-completion-apa-get-value',
which offers some post-formatting for author fields.

Another possible choice available out of the box is
`bibtex-completion-get-value', which returns a verbatim value.

Set this to a custom function if you need more flexibility.
This function should take two arguments FIELD-NAME and ENTRY.
FIELD-NAME is the name of the field whose value should be retrieved.
ENTRY is a BibTeX entry as returned by `bibtex-completion-get-entry'."
  :risky t
  :group 'org-roam-bibtex
  :type '(radio (function-item bibtex-completion-apa-get-value)
                (function-item bibtex-completion-get-value)
                (function :tag "Custom function")))

(defcustom orb-persp-project `("notes" . ,org-roam-directory)
  "Perspective name and path to the project with bibliography notes.
A cons cell (PERSP-NAME . PROJECT-PATH).  Only relevant when
`orb-switch-persp' is set to t.

PERSP-NAME should be a valid Perspective name, PROJECT-PATH should be
an open Projectile project.

See `orb-edit-note' for details"
  :type '(cons (string :tag "Perspective name")
               (directory :tag "Projectile directory"))
  :group 'org-roam-bibtex)

(defcustom orb-switch-persp nil
  "Non-nil to enable switching to the notes perspective.
Set the name of the perspective and the path to the notes project
in `orb-persp-project' for this to take effect.

Perspective switching works with Pers-mode and Projectile."
  :type '(choice
          (const :tag "Yes" t)
          (const :tag "No" nil))
  :group 'org-roam-bibtex)

(defcustom orb-ignore-bibtex-store-link-functions
  '(org-bibtex-store-link)
  "Functions to override with `ignore' during note creation process.

Org Ref defines function `org-ref-bibtex-store-link' to store
links to a BibTeX buffer, e.g. with `org-store-link'.  At the
same time, Org ref requires `ol-bibtex' library, which defines
`org-bibtex-store-link' to do the same.  When creating a note
with `orb-edit-note' from a BibTeX buffer, for example by calling
`org-ref-open-bibtex-notes', the initiated `org-capture' process
implicitly calls `org-store-link'.  The latter loops through all
the functions for storing links, and if more than one function
can store links to the location, the BibTeX buffer in this
particular case, the user will be prompted to choose one.  This
is definitely annoying, hence ORB will advise all functions in
this list to return nil to trick `org-capture' and get rid of the
prompt.

The default value is `(org-bibtex-store-link)', which means this
function will be ignored and `org-ref-bibtex-store-link' will be
used to store a link to the BibTeX buffer.  See
`org-capture-templates' on how to use the link in your templates."
  :type '(repeat (function))
  :risky t
  :group 'org-roam-bibtex)

(defcustom orb-insert-interface 'generic
  "Interface frontend to use with `orb-insert-link'.
Possible values are the symbols `helm-bibtex', `ivy-bibtex', or
`generic' (default).  In the first two cases the respective
commands will be used, while in the latter case the command
`orb-insert-generic' will be used.

When using `helm-bibtex' or `ivy-bibtex' as `orb-insert-interface',
choosing the action \"Edit note & insert a link\" will insert the
desired link.  For convenience, this action is made default for
the duration of an `orb-insert-link' session.  It will not
persist when `helm-bibtex' or `ivy-bibtex' proper are run.
Otherwise, the command is just the usual `helm-bibtex'/`ivy-bibtex'.
For example, it is possible to run other `helm-bibtex' or
`ivy-bibtex' actions.  When action other than \"Edit note &
insert a link\" is run, no link will be inserted, although the
session can be resumed later with `helm-resume' or `ivy-resume',
respectively, where it will be possible to select the \"Edit note
& insert a link\" action.

When using the `generic' interface, a simple list of available
citation keys is presented using `completion-read' and after
choosing a candidate the appropriate link will be inserted.

Please note that this variable should be set using the Customize
interface, `use-package''s `:custom' keyword, or Doom's `setq!'
macro.  Simple `setq' will not work."
  :group 'org-roam-bibtex
  :type '(radio
          (const helm-bibtex)
          (const ivy-bibtex)
          (const generic))
  :set (lambda (var value)
         (cond
          ((eq value 'ivy-bibtex)
           (require 'orb-ivy))
          ((eq value 'helm-bibtex)
           (require 'orb-helm)))
         (set-default var value)))

(defcustom orb-insert-link-description 'title
  "Link description format for links created with `orb-insert-link'.
The command `orb-insert-link' can be used to create Org links
to bibliographic notes of type [[id:note_id][Description]].
This variable determines the 'Description' part from the example
above.  It is an `s-format' string, where special placeholders of
form '${field}' will be expanded with data from the respective
BibTeX field of the associated BibTeX entry.  If the field's
value cannot be retrieved, the user will be prompted to input a
value interactively.  When retrieving BibTeX data, the user
options `orb-bibtex-field-aliases' and
`orb-bibtex-entry-get-value-function' are respected.

This variable can also be one of the following symbols:

`title'              - equivalent to \"${title}\"
`citekey'            - equivalent to \"${citekey}\"
`citation-org-ref-2' - create Org-ref v2 'cite:citekey' citation instead
`citation-org-ref-3' - create Org-ref v3 'cite:&citekey' citation instead
`citation-org-cite'  - create Org-cite '[cite:@citekey]' citation instead

The default value set by this variable can be overriden by
calling `orb-insert-link' with an appropriated numerical prefix
argument.  See its docstring for more information."
  :group 'org-roam-bibtex
  :type '(choice
          (string :tag "Format string")
          (const :tag "Title" title)
          (const :tag "Citation key" citekey)
          (const :tag "Citation link" citation-org-ref-2)
          (const :tag "Citation link" citation-org-ref-3)
          (const :tag "Citation link" citation-org-cite)))

(defcustom orb-insert-follow-link nil
  "Whether to follow a newly inserted link."
  :group 'orb-roam-bibtex
  :type '(choice
          (const :tag "Yes" t)
          (const :tag "No" nil)))

(defcustom orb-insert-generic-candidates-format 'key
  "Format of selection candidates for `orb-insert-generic' interface.
Possible values are `key' and `entry'."
  :group 'org-roam-bibtex
  :type '(choice
          (const key)
          (const entry)))

(defcustom orb-note-actions-interface 'default
  "Interface frontend for `orb-note-actions'.
Supported values (interfaces) are symbols `default', `ido',
`hydra', `ivy' and `helm'.

Alternatively, it can be set to a function, in which case the
function should expect one argument CITEKEY, which is a list
whose car is the citation key associated with the org-roam note
the current buffer is visiting.  Also, it should ideally make use
of `orb-note-actions-default', `orb-note-actions-extra' and
`orb-note-actions-user' for providing an interactive interface,
through which the combined set of note actions is presented as a
list of candidates and the function associated with the candidate
is executed upon selecting it.

This variable should be set using the Customize interface,
`use-package''s `:custom' keyword, or Doom's `setq!' macro.
Simple `setq' will not work."
  :risky t
  :type '(radio
          (const :tag "Default" default)
          (const :tag "Ido" ido)
          (const :tag "Hydra" hydra)
          (const :tag "Ivy" ivy)
          (const :tag "Helm" helm)
          (function :tag "Custom function"))
  :set (lambda (var value)
         (cond
          ((eq value 'ivy)
           (require 'orb-ivy))
          ((eq value 'helm)
           (require 'orb-helm))
          ((eq value 'hydra)
           (require 'hydra)))
         (set-default var value))
  :group 'orb-note-actions)

(defcustom orb-note-actions-default
  '(("Open PDF file(s)" . orb-open-attached-file)
    ("Add PDF to library" . bibtex-completion-add-pdf-to-library)
    ("Open URL or DOI in browser" . bibtex-completion-open-url-or-doi)
    ("Show record in the bibtex file" . bibtex-completion-show-entry))
  "Default actions for `orb-note-actions'.
Each action is a cons cell DESCRIPTION . FUNCTION."
  :risky t
  :type '(alist
          :tag "Default actions for `orb-note-actions'"
          :key-type (string :tag "Description")
          :value-type (function :tag "Function"))
  :group 'orb-note-actions)

(defcustom orb-note-actions-extra
  '(("Save citekey to kill-ring and clipboard" . orb-note-actions-copy-citekey)
    ("Run Orb PDF Scrapper" . orb-note-actions-scrap-pdf))
  "Extra actions for `orb-note-actions'.
Each action is a cons cell DESCRIPTION . FUNCTION."
  :risky t
  :type '(alist
          :tag "Extra actions for `orb-note-actions'"
          :key-type (string :tag "Description")
          :value-type (function :tag "Function"))
  :group 'orb-note-actions)

(defcustom orb-note-actions-user nil
  "User actions for `orb-note-actions'.
Each action is a cons cell DESCRIPTION . FUNCTION."
  :risky t
  :type '(alist
          :tag "User actions for `orb-note-actions'"
          :key-type (string :tag "Description")
          :value-type (function :tag "Function"))
  :group 'orb-note-actions)

;; ============================================================================
;;;; Orb edit notes
;; ============================================================================

(defun orb--switch-perspective ()
  "Helper function for `orb-edit-note'."
  (when (and (require 'projectile nil t)
             (require 'persp-mode nil t))
    (let ((notes-project (cdr orb-persp-project))
          (projects (projectile-relevant-open-projects))
          openp)
      (dolist (project projects openp)
        (setq openp (or (f-equal? project notes-project) openp)))
      (when openp
        (let ((p-names (cdr (persp-names))))
          (dolist (p-name p-names)
            (when (s-equals? p-name (car orb-persp-project))
              (persp-switch p-name))))))))

(defun orb--store-link-functions-advice (action)
  "Add or remove advice for each of `orb-ignore-bibtex-store-link-functions'.
ACTION should be a symbol `add' or `remove'.  A piece of advice
is the function `ignore', it is added as `:override'."
  (when orb-ignore-bibtex-store-link-functions
    (let ((advice-func (intern (format "advice-%s" action)))
          (advice (cl-case action
                    (add (list :override #'ignore))
                    (remove (list #'ignore))
                    (t (user-error "Action type not recognised: %s" action)))))
      (dolist (advisee orb-ignore-bibtex-store-link-functions)
        (apply advice-func (push advisee advice))))))

(defun orb--pre-expand-template (template entry)
  "Helper function for `orb--new-note'.
TEMPLATE is an element of `org-roam-capture-templates' and ENTRY
is a BibTeX entry as returned by `bibtex-completion-get-entry'."
  ;; Handle org-roam-capture part
  (letrec (;; Org-capture templates: handle different types of
           ;; org-capture-templates: string, file and function; this is
           ;; a stripped down version of `org-capture-get-template'
           (org-template
            (pcase (nth 3 template)       ; org-capture template is here
              (`nil 'nil)
              ((and (pred stringp) tmpl) tmpl)
              (`(file ,file)
               (let ((flnm (expand-file-name file org-directory)))
                 (if (file-exists-p flnm) (f-read-text flnm)
                   (format "Template file %S not found" file))))
              (`(function ,fun)
               (if (functionp fun) (funcall fun)
                 (format "Template function %S not found" fun)))
              (_ (user-error "ORB: Invalid capture template"))))
           ;;  org-roam capture properties are here
           (plst (cddddr template))
           ;; regexp for org-capture prompt wildcard
           (rx "\\(%\\^{[[:alnum:]-_]*}\\)")
           (file-keyword (when orb-process-file-keyword
                           (or (and (stringp orb-process-file-keyword)
                                    orb-process-file-keyword)
                               "file")))
           ;; inline function to handle :target list expansion
           (expand-roam-template
            (lambda (roam-template-list old new)
              (let (elements)
                (dolist (el roam-template-list)
                  (if (listp el)
                      (setq elements
                            (nreverse
                             (append elements
                                     (list (funcall expand-roam-template
                                                    el old new)))))
                    (push (s-replace old new el) elements)))
                (nreverse elements))))
           (lst nil))
    ;; First run:
    ;; 1) Make a list of (org-wildcard field-value match-position) for the
    ;; second run
    ;; 2) replace org-roam-capture wildcards
    (dolist (keyword orb-preformat-keywords)
      (let* (;; prompt wildcard keyword
             (keyword (cond
                       ;; for some backward compatibility with old
                       ;; `orb-preformat-keywords'
                       ((consp keyword) (car keyword))
                       ((stringp keyword) keyword)
                       (t (user-error "Error in `orb-preformat-keywords': \
Keyword \"%s\" has invalid type (string was expected)" keyword))))
             ;; bibtex field name
             (field-name (orb-resolve-field-alias keyword))
             ;; get the bibtex field value
             (field-value
              ;; maybe process file keyword
              (or (if (and file-keyword (string= field-name file-keyword))
                      (prog1
                          (orb-get-attached-file
                           (funcall orb-bibtex-entry-get-value-function "=key=" entry))
                        ;; we're done so don't even compare file-name with
                        ;; file-keyword in the successive cycles
                        (setq file-keyword nil))
                    ;; do the usual processing otherwise
                    ;; condition-case to temporary workaround an upstream bug
                    (condition-case nil
                        (funcall orb-bibtex-entry-get-value-function field-name entry)
                      (error "")))
                  ""))
             ;; org-capture prompt wildcard
             (org-wildcard (concat "%^{" (or keyword "citekey") "}"))
             ;; org-roam-capture prompt wildcard
             (roam-wildcard (concat "${" (or keyword "citekey") "}"))
             ;; org-roam-capture :target property
             (roam-template (or (plist-get plst :if-new) (plist-get plst :target)))
             (i 1)                      ; match counter
             pos)
        ;; Search for org-wildcard, set flag m if found
        (when org-template
          (while (string-match rx org-template pos)
            (if (string= (match-string 1 org-template) org-wildcard)
                (progn
                  (setq pos (length org-template))
                  (cl-pushnew (list org-wildcard field-value i) lst ))
              (setq pos (match-end 1)
                    i (1+ i)))))
        ;; Replace placeholders in org-roam-capture-templates :target property
        (when roam-template
          (setcdr roam-template
                  (funcall expand-roam-template
                           (cdr roam-template) roam-wildcard field-value)))))
    ;; Second run: replace prompts and prompt matches in org-capture
    ;; template string
    (dolist (l lst)
      (when (and org-template (nth 1 l))
        (let ((pos (concat "%\\" (number-to-string (nth 2 l)))))
          ;; replace prompt match wildcards with prompt wildcards
          ;; replace prompt wildcards with BibTeX field value
          (setq org-template (s-replace pos (car l) org-template)
                org-template (s-replace (car l) (nth 1 l) org-template))))
      (setf (nth 3 template) org-template))
    template))

(defun orb--new-note (citekey &optional props)
  "Process templates and run `org-roam-capture-'.
CITEKEY is the citation key of an entry for which the note is
created.  PROPS are additional properties for `org-roam-capture-'."
  ;; Check if the requested BibTeX entry actually exists and fail
  ;; gracefully otherwise
  (if-let* ((entry (or (bibtex-completion-get-entry citekey)
                       (orb-warning
                        "Could not find the BibTeX entry" citekey)))
            ;; Depending on the templates used: run
            ;; `org-roam-capture--capture' or call `org-roam-node-find'
            (org-capture-templates org-roam-capture-templates)
            ;; hijack org-capture-templates
            ;; entry is our bibtex entry, it just happens that
            ;; `org-capture' calls a single template entry "entry";
            (template (--> (if (null (cdr org-capture-templates))
                               ;; if only one template is defined, use it
                               (car org-capture-templates)
                             (org-capture-select-template))
                        (when (listp it)
                          (copy-tree it))
                        ;; optionally pre-expand templates
                        (if (and it orb-preformat-templates)
                            (orb--pre-expand-template it entry)
                          it)))
            ;; pretend we had only one template
            ;; `org-roam-capture--capture' behaves specially in this case
            ;; NOTE: this circumvents using functions other than
            ;; `org-capture', see `org-roam-capture-function'.
            ;; If the users start complaining, we may revert previous
            ;; implementation
            (org-roam-capture-templates (list template))
            ;; Org-roam coverts the templates to its own syntax;
            ;; since we are telling `org-capture' to use the template entry
            ;; (by setting `org-capture-entry'), and Org-roam converts the
            ;; whole template list, we must do the conversion of the entry
            ;; ourselves
            (props (--> (or props (list :finalize 'find-file))
                        (plist-put it :call-location (point-marker))))
            (org-capture-entry
             (org-roam-capture--convert-template template props))
            (citekey-ref (format
                          (pcase orb-roam-ref-format
                            ('org-ref-v2 "cite:%s")
                            ('org-ref-v3 "cite:&%s")
                            ('org-cite "@%s")
                            ((pred stringp) orb-roam-ref-format)
                            (_ (user-error "Invalid format `orb-roam-ref-format'")))
                          citekey))
            (title
             (or (funcall orb-bibtex-entry-get-value-function "title" entry)
                 (and
                  (orb-warning "Title not found for this entry")
                  ;; this is not critical, the user may input their own
                  ;; title
                  "No title")))
            (node (org-roam-node-create :title title)))
      (org-roam-capture-
       :node node
       :info (list :ref citekey-ref))
    (user-error "Abort")))

;;;###autoload
(defun orb-edit-note (citekey)
  "Open an Org-roam note associated with the CITEKEY or create a new one.

This function allows to use Org-roam as a backend for managing
bibliography notes.  It relies on `bibtex-completion' to get
retrieve bibliographic information from a BibTeX file.

Implementation details and features:

1. This function first tries to find the note file associated
with the citation key CITEKEY.  A citation key is an Org-roam
'ref' set with the '#+ROAM_KEY:' in-buffer keyword or
':ROAM_REFS:' headline property.  Three types of Org-roam 'ref's
are recognized by ORB: Org-ref v2 'cite:citekey' and Org-ref v3
'cite:&citekey' links, and Org-cite '[cite:@citekey]' citations.

2. If the Org-roam reference was found, the function calls
`org-roam-node-find' passing to it the title associated with the
CITEKEY as retrieved by `bibtex-completion-get-entry'.  The
prompt presented by `org-roam-node-find' will thus be
pre-populated with the record title.

3. Optionally, when `orb-preformat-templates' is non-nil, any
prompt wildcards in `orb-templates' or
`org-roam-capture-templates', associated with the bibtex record
fields as specified in `orb-preformat-templates', will be
preformatted.  Both `org-capture-templates' (%^{}) and
`org-roam-capture-templates' (`s-format', ${}) prompt syntaxes
are supported.

See `orb-preformat-keywords' for more details on how
to properly specify prompts for replacement.

Please pay attention when using this feature that by setting
title for preformatting, it will be impossible to change it in
the `org-roam-node-find' interactive prompt since all the
template expansions will have taken place by then.  All the title
wildcards will be replace with the BibTeX field value.

4. Optionally, if you are using Projectile and Persp-mode and
have a dedicated workspace to work with your Org-roam collection,
you may want to set the perspective name and project path in
`orb-persp-project' and `orb-switch-persp' to t.  In this case,
the perspective will be switched to the Org-roam notes project
before calling any Org-roam functions.

If optional argument ENTRY is non-nil, use it to fetch the
bibliographic information."
  ;; Optionally switch to the notes perspective
  (when orb-switch-persp
    (orb--switch-perspective))
  (orb-make-notes-cache)
  (if-let ((node (orb-note-exists-p citekey)))
      (ignore-errors (org-roam-node-visit node))
    ;; fix some Org-ref related stuff
    (orb--store-link-functions-advice 'add)
    ;; TODO: consider using unwind-protect and let the errors through
    (condition-case error-msg
        (orb--new-note citekey)
      ((debug error)
       (orb--store-link-functions-advice 'remove)
       (message "%s"
                (concat (and (eq (car error-msg) 'error)
                             "orb-edit-note caught an error during capture: ")
                        (error-message-string error-msg)))))))

;; FIXME: this does not work anymore
;; (defun orb--get-non-ref-path-completions ()
;;   "Return a list of cons for titles of non-ref notes to absolute path.
;; CANDIDATES is a an alist of candidates to consider.  Defaults to
;; `org-roam--get-title-path-completions' otherwise."
;;   (let* ((rows (org-roam-db-query
;;                 [:select [titles:file titles:title tags:tags]
;;                  :from titles
;;                  :left :join tags
;;                  :on (= titles:file tags:file)
;;                  :left :join refs :on (= titles:file refs:file)
;;                  :where refs:file :is :null]))
;;          completions)
;;     (dolist (row rows completions)
;;       (pcase-let ((`(,file-path ,title ,tags) row))
;;         (let ((title (or title
;;                          (list (org-roam--path-to-slug file-path)))))
;;           (let ((k (concat
;;                     (when tags
;;                       (format "(%s) " (s-join org-roam-tag-separator tags)))
;;                     title))
;;                 (v (list :path file-path :title title)))
;;             (push (cons k v) completions)))))))

;;;###autoload
(defun orb-edit-citation-note (&optional element)
  "Edit a note for current Org-cite citation or reference.
If the note does not exist, create a new one.

When used from LISP, if optional ELEMENT is non-nil, use it
instead of the element at point.  ELEMENT should be the Org-cite
citation or reference element.  Providing it allows for quicker
computation."
  (interactive)
  (cond
   ((derived-mode-p 'org-mode)
    (let* ((around-point (not element))
           (element (if around-point (org-element-context) element))
           (type (org-element-type element))
           (citekey
            (cond
             ((eq type 'citation)
              (org-element-property
               :key (car (org-cite-get-references element))))
             ((eq type 'citation-reference)
              (org-element-property :key element))
             (around-point
              (user-error "Cursor not in an Org-cite element"))
             (t
              (user-error "Invalid optional argument ELEMENT: %s.  Org-cite\
citation or reference expected" element)))))
      (orb-edit-note citekey)))
   (t
    (user-error "This function works only in Org mode"))))


;; ============================================================================
;;;; Orb insert
;; ============================================================================
(defvar orb-insert-lowercase nil
  "Internal.  Dynamic variable for `orb-insert-link' and `orb-insert--link'.")

(defun orb-insert--link (node info)
  "Insert a link to NODE.
INFO contains additional information."
  ;; citekey &optional description lowercase region-text beg end
  (-let (((&plist :region :orb-link-description :orb-citekey :orb-entry) info))
    (when region
      (org-roam-unshield-region (car region) (cdr region))
      (delete-region (car region) (cdr region))
      (set-marker (car region) nil)
      (set-marker (cdr region) nil))
    (pcase orb-link-description
      ((pred stringp)
       (let ((description
              (-->
               (s-format orb-link-description
                         (lambda (template entry)
                           (funcall orb-bibtex-entry-get-value-function
                                    (orb-resolve-field-alias template) entry))
                         orb-entry)
               (if (and it orb-insert-lowercase) (downcase it) it))))
         (insert (org-link-make-string
                  (concat "id:" (org-roam-node-id node)) description))))
      (`citation-org-cite
       (insert (format "[cite:@%s]" orb-citekey)))
      (ref-format
       (let ((cite-link (if (boundp 'org-ref-default-citation-link)
                            (concat org-ref-default-citation-link ":")
                          "cite:")))
         (insert (concat cite-link
                         (when (eq ref-format 'citation-org-ref-3) "&")
                         orb-citekey)))))))

(defun orb--finalize-insert-link ()
  "Insert a link to a just captured note.
This function is used by ORB calls to `org-roam-capture-' instead
of `org-roam-capture--finalize-insert-link'."
  (let* ((mkr (org-roam-capture--get :call-location))
         (buf (marker-buffer mkr))
         (region (org-roam-capture--get :region))
         (node (org-roam-populate (org-roam-node-create :id (org-roam-capture--get :id))))
         (citekey (org-capture-get :orb-citekey))
         (entry (bibtex-completion-get-entry citekey)))
    (with-current-buffer buf
      (org-with-point-at mkr
        (orb-insert--link node (list
                                :region region
                                :orb-citekey citekey
                                :orb-entry entry
                                :orb-link-description (org-capture-get :orb-link-description)))))))

(defun orb--insert-captured-ref-h ()
  "Insert value of `:ref' key from `org-roam-capture--info'.
Internal function.  To be installed in `org-roam-capture-new-node-hook'."
  (when-let ((ref (plist-get org-roam-capture--info :ref)))
    (org-roam-ref-add ref)))

(defun orb-insert-edit-note (citekey)
  "Insert a link to a note with citation key CITEKEY.
Capture a new note if it does not exist yet.

CITEKEY can be a list of citation keys (for compatibility with
Bibtex-completion), in which case only the first element of that
list is used."
  (unwind-protect
      ;; Group functions together to avoid inconsistent state on quit
      (atomic-change-group
        (let* ((citekey (cl-typecase citekey
                          (string citekey)
                          (list (car citekey))
                          (t (user-error "Invalid citation key data type: %s.  \
String or list of strings expected" citekey))))
               (entry (bibtex-completion-get-entry citekey))
               (title
                (funcall orb-bibtex-entry-get-value-function "title" entry ""))
               (node (or (orb-note-exists-p citekey)
                         (org-roam-node-create :title title)))
               region-text
               beg end
               (_ (when (region-active-p)
                    (setq beg (set-marker (make-marker) (region-beginning)))
                    (setq end (set-marker (make-marker) (region-end)))
                    (setq region-text
                          (buffer-substring-no-properties beg end))))
               (description (--> orb-insert-link-description
                                 (cl-case it
                                   (title "${title}")
                                   (citekey "${citekey}")
                                   (t it))
                                 (or region-text it)))
               (info (--> (list :orb-link-description description
                                :orb-citekey citekey
                                :orb-entry entry
                                :finalize #'orb--finalize-insert-link)
                          (if (and beg end)
                              (append it (list :region (cons beg end)))
                            it))))
          (if (org-roam-node-id node)
              (orb-insert--link node info)
            (orb--new-note citekey info)))
        (deactivate-mark)))
  (when (and orb-insert-follow-link
             (looking-at org-link-any-re))
    (org-open-at-point)))

(defun orb-insert-generic (&optional arg)
  "Present a list of BibTeX entries for completion.
This is a generic completion function for `orb-insert-link', which
runs `orb-insert-edit-note' on the selected entry.  The list is
made by `bibtex-completion-candidates'.

The appearance of selection candidates is determined by
`orb-insert-generic-candidates-format'.

This function is not interactive, set `orb-insert-interface' to
`generic' and call `orb-insert-link' interactively instead.

If ARG is non-nil, rebuild `bibtex-completion-cache'."
  (when arg
    (bibtex-completion-clear-cache))
  (let* ((candidates (bibtex-completion-candidates))
         (candidates2
          (if (eq orb-insert-generic-candidates-format 'key)
              (mapcar (lambda (item)
                        (alist-get "=key=" (cdr item) nil nil #'equal))
                      candidates)
            (mapcar #'car candidates)))
         (selection (completing-read "BibTeX entry:" candidates2 nil t))
         (citekey (if (eq orb-insert-generic-candidates-format 'key)
                      selection
                    (--> (alist-get selection candidates nil nil #'equal)
                         (cdr it)
                         (alist-get "=key=" it  nil nil #'equal)))))
    (orb-insert-edit-note citekey)))

;;;###autoload
(defun orb-insert-link (&optional arg)
  "Insert a link to an Org-roam bibliography note.
If the note does not exist yet, it will be created using
`orb-edit-note' function.

\\<universal-argument-map>\\<org-roam-bibtex-mode-map> The
customization option `orb-insert-link-description' determines
what will be used as the link's description.  It is possible to
override the default value of the variable with a numerical
prefix ARG:

`C-1' \\[orb-insert-link] will force `title'
`C-2' \\[orb-insert-link] will force `citekey'

`C-0' \\[orb-insert-link] will force `citation-org-ref-2'
`C-9' \\[orb-insert-link] will force `citation-org-ref-3'
`C-8' \\[orb-insert-link] will force `citation-org-cite'

If a region of text is active (selected) when calling `orb-insert-link',
the text in the region will be replaced with the link and the
text string will be used as the link's description — similar to
`org-roam-node-insert'.

Normally, the case of the link description will be preserved.  It
is possible to force lowercase by supplying either one or three
universal arguments `\\[universal-argument]'.

Finally, `bibtex-completion-cache' will be re-populated if either
two or three universal arguments `\\[universal-argument]' are supplied.

The customization option `orb-insert-interface' allows to set the
completion interface backend for the candidates list."
  (interactive "P")
  ;; parse arg
  ;; C-u or C-u C-u C-u => force lowercase
  ;; C-u C-u or C-u C-u C-u => force `bibtex-completion-clear-cache'
  ;; C-1 force title in description
  ;; C-2 force citekey in description
  ;; C-0,C-9,C-8 force inserting the link as Org-ref org Org-cite citation
  (let* ((lowercase (or (equal arg '(4))
                        (equal arg '(64))))
         (clear-cache (or (equal arg '(16))
                          (equal arg '(64))))
         (link-type (cl-case arg
                      (1 'title)
                      (2 'citekey)
                      (0 'citation-org-ref-2)
                      (9 'citation-org-ref-3)
                      (8 'citation-org-cite)
                      (t nil)))
         (orb-insert-link-description
          (or link-type orb-insert-link-description))
         (orb-insert-lowercase (or lowercase orb-insert-lowercase)))
    (orb-make-notes-cache)
    (cl-case orb-insert-interface
      (helm-bibtex
       (cond
        ((fboundp 'orb-helm-insert)
         (orb-helm-insert clear-cache))
        (t
         (orb-warning "helm-bibtex not available; using generic completion")
         (orb-insert-generic clear-cache))))
      (ivy-bibtex
       (cond
        ((fboundp 'orb-ivy-insert)
         (orb-ivy-insert clear-cache))
        (t
         (orb-warning "ivy-bibtex not available; using generic completion")
         (orb-insert-generic clear-cache))))
      (t
       (orb-insert-generic clear-cache)))))

;; ============================================================================
;;;; Non-ref functions
;; ============================================================================

;; ;;;###autoload
;; (defun orb-find-non-ref-file (&optional initial-prompt)
;;   "Find and open an Org-roam, non-ref file.
;; INITIAL-PROMPT is the initial title prompt.
;; See `org-roam-node-finds' and
;; `orb--get-non-ref-path-completions' for details."
;;   (interactive)
;;   (org-roam-node-find initial-prompt
;;                       (orb--get-non-ref-path-completions)))

;; ;;;###autoload
;; (defun orb-insert-non-ref ()
;;   "Find a non-ref Org-roam file, and insert a relative org link to it at point.
;; If PREFIX, downcase the title before insertion.  See
;; `org-roam-insert' and `orb--get-non-ref-path-completions' for
;; details."
;;   (interactive)
;;   ;; FIXME: this is not correct
;;   (org-roam-node-insert (orb--get-non-ref-path-completions)))

;; ============================================================================
;;;; Orb note actions
;; ============================================================================

(orb-note-actions-defun default
  (let ((f (cdr (assoc (completing-read name candidates) candidates))))
    (funcall f (list citekey))))

(orb-note-actions-defun ido
  (let* ((c (cl-map 'list 'car candidates))
         (f (cdr (assoc (ido-completing-read name c) candidates))))
    (funcall f (list citekey))))

(declare-function orb-note-actions-hydra/body "org-roam-bibtex" nil t)
(orb-note-actions-defun hydra
  ;; we don't use candidates here because for a nice hydra we need each
  ;; group of completions separately (default, extra, user), so just
  ;; silence the compiler
  (ignore candidates)
  (let ((k ?a)
        actions)
    (dolist (type (list "Default" "Extra" "User"))
      (let ((actions-var
             (intern (concat "orb-note-actions-" (downcase type)))))
        (dolist (action (symbol-value actions-var))
          ;; this makes defhydra HEADS list of the form:
          ;; ("a" (some-action citekey-value) "Some-action description"
          ;;     :column "Type")
          (cl-pushnew
           `(,(format "%c" k) (,(cdr action) (list ,citekey))
             ,(car action) :column ,(concat type " actions"))
           actions)
          ;; increment key a->b->c...
          (setq k (1+ k)))))        ; TODO: figure out a way to supply
                                        ; mnemonic keys
    (setq actions (nreverse actions))
    ;; yes, we redefine hydra on every call
    (eval
     `(defhydra orb-note-actions-hydra (:color blue :hint nil)
        ;; defhydra docstring
        ,(format  "^\n  %s \n\n^"
                  (s-word-wrap (- (window-body-width) 2) name))
        ;; defhydra HEADS
        ,@actions)))
  (orb-note-actions-hydra/body))

(defun orb-note-actions--run (interface citekey )
  "Run note actions on CITEKEY with INTERFACE."
  (when (and (memq interface '(ivy helm hydra))
             (not (featurep interface)))
    (orb-warning
     (format "Feature `%s' not available, using default interface" interface))
    (setq interface 'default))
  (funcall (intern (concat "orb-note-actions-" (symbol-name interface)))
           citekey))

;;;###autoload
(defun orb-note-actions ()
  "Run an interactive prompt to offer note-related actions.
The prompt interface can be set in `orb-note-actions-interface'.
In addition to default actions, which are not supposed to be
modified, there is a number of prefined extra actions
`orb-note-actions-extra' that can be customized.  Additionally,
user actions can be set in `orb-note-actions-user'."
  (interactive)
  (if-let ((non-default-interfaces (list 'hydra 'ido 'ivy 'helm))
           (citekey (orb-get-node-citekey nil 'assert)))
      (cond ((memq orb-note-actions-interface non-default-interfaces)
             (orb-note-actions--run orb-note-actions-interface citekey))
            ((functionp orb-note-actions-interface)
             (funcall orb-note-actions-interface citekey))
            (t
             (unless (eq orb-note-actions-interface 'default)
               (orb-warning
                (format "Feature `%s' not available, using default interface"
                        orb-note-actions-interface)))
             (orb-note-actions--run 'default citekey)))
    (user-error "Could not retrieve the citekey.  Check ROAM_REFS property \
of current node")))

;;;;;; Note actions

(defun orb-note-actions-copy-citekey (citekey)
  "Save note's citation key to `kill-ring' and copy it to clipboard.
CITEKEY is a list whose car is a citation key."
  (with-temp-buffer
    (insert (car citekey))
    (copy-region-as-kill (point-min) (point-max))))

(defun orb-note-actions-scrap-pdf (citekey)
  "Wrapper around `orb-pdf-scrapper-insert'.
CITEKEY is a list whose car is a citation key."
  (require 'orb-pdf-scrapper)
  (orb-pdf-scrapper-run (car citekey)))

;; ============================================================================
;;;; Org-roam-bibtex minor mode
;; ============================================================================
;;

(defvar orb--external-vars-original-values nil
  "Variable to hold original values of variables from external packages.
Internal use.")

;;;###autoload
(defun orb-org-ref-edit-note (citekey)
  "Open an Org-roam note associated with the CITEKEY or create a new one.
Set `org-ref-notes-function' to this function if your
bibliography notes are managed by Org-roam and you want some
extra integration between the two packages.

This is a wrapper function around `orb-edit-note' intended for
use with Org-ref.

NOTE: This function is no longer needed for Org-ref v3."
  (when (require 'org-ref nil t)
    (let ((bibtex-completion-bibliography (org-ref-find-bibliography)))
      (orb-edit-note citekey))))

;;;###autoload
(defun orb-citar-edit-note (citekey &optional _entry)
  "Open an Org-roam note associated with the CITEKEY or create a new one.
This is a wrapper function around `orb-edit-note' meant to be used with
`citar-file-open-note-function'.
Optional argument ENTRY is ignored."
  (orb-edit-note citekey))

;;;###autoload
(defun orb-bibtex-completion-edit-note (keys)
  "Open or create an Org-roam note.

This is a wrapper function around `orb-edit-note' meant to be
used with `bibtex-completion-edit-notes-function'.

Only the first KEY of the list KEYS will actually be used.  KEY
must be a string."
  (orb-edit-note (car keys)))

(defvar org-roam-bibtex-mode-map
  (make-sparse-keymap)
  "Keymap for `org-roam-bibtex-mode'.")

;;;###autoload
(define-minor-mode org-roam-bibtex-mode
  "Sets an appropriate function for editing bibliography notes.
Supports Org-ref, Helm-bibtex/Ivy-bibtex, and Citar.

When called interactively, toggle `org-roam-bibtex-mode'. with
prefix ARG, enable `org-roam-bibtex-mode' if ARG is positive,
otherwise disable it.

When called from Lisp, enable `org-roam-bibtex-mode' if ARG is
omitted, nil, or positive.  If ARG is `toggle', toggle
`org-roam-bibtex-mode'.  Otherwise, behave as if called
interactively."
  :lighter " orb"
  :keymap  org-roam-bibtex-mode-map
  :group 'org-roam-bibtex
  :require 'orb
  :global t
  ;; TODO: Revert external variables to their original values rather than to
  ;; their defaults
  (cond (org-roam-bibtex-mode
         (setq orb--external-vars-original-values nil)
         (let ((var-alist
                '((citar-open-note-function . citar)
                  (bibtex-completion-edit-notes-function . bibtex-completion)
                  ;; Only for Org-ref v2
                  (org-ref-notes-function . org-ref))))
           (dolist (el var-alist)
             (let* ((var (car el))
                    (pkg (cdr el))
                    (val (and (require pkg nil t)
                              (boundp var)
                              (symbol-value var))))
               (when val
                 (push (cons var val) orb--external-vars-original-values)
                 (set var (intern (format "orb-%s-edit-note" pkg)))))))
         (add-to-list 'bibtex-completion-find-note-functions #'orb-find-note-file)
         (add-to-list 'bibtex-completion-key-at-point-functions #'orb-get-node-citekey)
         (add-hook 'org-capture-after-finalize-hook #'orb-make-notes-cache)
         (add-hook 'org-roam-capture-new-node-hook #'orb--insert-captured-ref-h)
         ;; HACK: temporary to address #256; see also `orb-format-entry'
         (bibtex-completion-init)
         (orb-make-notes-cache))
        (t
         (dolist (var-alist orb--external-vars-original-values)
           (set (car var-alist) (cdr var-alist)))
         (setq bibtex-completion-find-note-functions
               (delq #'orb-find-note-file
                     bibtex-completion-find-note-functions))
         (setq bibtex-completion-key-at-point-functions
               (delq #'orb-get-node-citekey
                     bibtex-completion-key-at-point-functions))
         (remove-hook 'org-roam-capture-new-node-hook #'orb--insert-captured-ref-h)
         (remove-hook 'org-capture-after-finalize-hook #'orb-make-notes-cache))))

(provide 'org-roam-bibtex)

;;; org-roam-bibtex.el ends here
;; Local Variables:
;; coding: utf-8
;; fill-column: 79
;; package-lint-main-file: "org-roam-bibtex.el"
;; End:
