;;; ol-bible.el --- Org Link support for Bible Passages  -*- lexical-binding: t; -*-

;; Copyright (C) 2025  Samuel W. Flint <me@samuelwflint.com>

;; Author: Samuel W. Flint <me@samuelwflint.com>
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Homepage: https://git.sr.ht/~swflint/ol-bible
;; Package-Requires: ((emacs "27.1"))
;; Package-Version: 1.3.3
;; Package-Revision: 711f9a984c23
;; Keywords: convenience, outlines

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This package provides yet another implementation of linking to the
;; Bible from Org.  In particular, it is designed around multiple
;; export options (including LaTeX's bibleref
;; (https://ctan.org/pkg/bibleref)), and can generate links to several
;; major Bible websites.  Additionally, it can integrate with several
;; in-Emacs Bible applications to open passages within Emacs.
;;
;; Specific features include:
;;  - Smart LaTeX export using bibleref
;;  - Formatted link preview when no description is provided
;;  - Customizable link generation
;;  - Integration with `bible-mode', and customizable follow behavior
;;
;;; Link Syntax
;;
;; The following are examples of valid links:
;;
;; - Genesis/1:1-3:16 (A reference to a range of verses across
;;   multiple chapters)
;; - Genesis/1:1-3:16/NRSV (A reference to a range of verses across
;;   multiple chapters, with a reference to a specific translation)
;; - Genesis/1:1 (A reference to a single verse)
;; - Genesis/1:1/NRSV (A reference to a single verse, with
;;   translation)
;; - Genesis/1-3 (A reference to a range of chapters)
;; - Genesis/1-3/NRSV (A reference to a range of chapters, with
;;   translation)
;;
;;; Customization
;;
;;  - `ol-bible-http-provider', the http provider to use to generate a
;;    link.  Options include:
;;    - `biblegateway'
;;    - `bible-dot-com'
;;    - `bible-hub'
;;    - `oremus'
;;    - A function which takes an `ol-bible-reference' structure
;;
;; - `ol-bible-latex-export-type', what sort of export format should
;;   be used for LaTeX.  Options include:
;;   - `bibleref' generates a `\bibleref' command.  Make sure to
;;     include `\usepackage{bibleref}' somewhere in your LaTeX header.
;;   - `link' will create an HTTP link using `ol-bible-http-provider'.
;;     If no description is provided for the link, one will be
;;     automatically generated (see below).
;;   - `bibleref-in-link' combines the two above formats, generating
;;     the link description using `\bibleref'.
;;   - `text' will use the provided description or an autogenerated
;;     textual reference.
;;
;; - `ol-bible-viewer', the method that should be used to activate
;;   links.  There are four possibilities:
;;   - `bible-mode' will use `bible-mode' to open the passage.
;;   - `oremus' will open an oremus link with `eww'.
;;   - `link' will use a link from `ol-bible-http-provider' and
;;     `browse-url'
;;   - A function which takes an `ol-bible-reference' structure.
;;
;; - `ol-bible-description-format' provides the verse and range
;;   separators used for automatically generated references.
;;
;; - `ol-bible-display-formatted-references' can be used to disable
;;   generation of preview overlays.
;;
;;; The `ol-bible-reference' Structure
;;
;; The `ol-bible-reference' structure is used to describe a reference.
;; It contains several fields, and has a number of patterns of
;; acceptable nullity.  For more information, refer to the docstring.
;;
;;;; Errors and Patches
;;
;; If you find an error, or have a patch to improve this package,
;; please send an email to ~swflint/emacs-utilities@lists.sr.ht.

;;; Code:

(require 'ol)


;;; Customization

(defgroup ol-bible (list)
  "Customization for Bible links."
  :group 'org
  :group 'org-link)

(defcustom ol-bible-description-format (cons ":" "--")
  "Tokens for reference generation.

Cons cell of verse and range separators.  Car should be the verse
separator (default \":\"), cdr should be the range separator (default
\"--\")."
  :group 'ol-bible
  :type '(cons (string :tag "Verse Separator")
               (string :tag "Range Separator")))

(defcustom ol-bible-latex-export-type 'bibleref-in-link
  "Format to export links to in LaTeX.

Available options are:

 - `bibleref' which will generate a bibleref-compatible reference.  You
   must configure bibleref in your preamble, at minimum using:
    \\usepackage{bibleref}
 - `link' which will generate a link using `\\href' based on your
   `ol-bible-http-provider' setting, which see.
 - `bibleref-in-link' will use `bibleref' for a link description, and
   generate a link based on your `ol-bible-http-provider' setting.
 - `text' export only the description."
  :group 'ol-bible
  :type '(choice (const :tag "Use Bibleref Package" bibleref)
                 (const :tag "Link" link)
                 (const :tag "Bibleref in Link" bibleref-in-link)
                 (const :tag "Textual Format" text)))

(defcustom ol-bible-http-provider 'biblegateway
  "Which online Bible provider to use for link generation.

Available options are:

 - `biblegateway'
 - `bible-dot-com'
 - `bible-hub'
 - `oremus'
 - A custom function which takes an `ol-bible-reference' struct."
  :group 'ol-bible
  :type '(choice (const :tag "BibleGateway" biblegateway)
                 (const :tag "Bible.Com" bible-dot-com)
                 (const :tag "Bible Hub" bible-hub)
                 (const :tag "oremus" oremus)
                 (function :tag "Custom URL Function")))

(defcustom ol-bible-viewer (cond
                            ((featurep 'bible-mode) 'bible-mode)
                            (t 'link))
  "What method should be used to view linked scripture.

Available options are:

 - `bible-mode' which uses the `bible-mode' package.
 - `oremus' will use `eww' and a URL generated for `oremus'.
 - `link' which will use `browse-url' and your preferred
   `ol-bible-http-provider' (which see).
 - A custom function which takes an `ol-bible-reference' struct."
  :group 'ol-bible
  :type '(choice (const :tag "Bible Mode" bible-mode) ;bible-open
                 (const :tag "oremus" oremus)
                 (const :tag "Link" link)
                 (function :tag "Custom Function")))

(defcustom ol-bible-display-formatted-references t
  "Should overlays be generated for Bible links?

If non-nil (default) pretty references will be generated for Bible
links."
  :group 'ol-bible-open
  :type 'boolean)


;;; Data

(defconst ol-bible-books-chapters-verses
  '(("Genesis" 31 25 24 26 32 22 24 22 29 32 32 20 18 24 21 16 27 33 38 18 34 24 20 67 34 35 46 22 35 43 55 32 20 31 29 43 36 30 23 23 57 38 34 34 28 34 31 22 33 26)
    ("Exodus" 22 25 22 31 23 30 25 32 35 29 10 51 22 31 27 36 16 27 25 26 36 31 33 18 40 37 21 43 46 38 18 35 23 35 35 38 29 31 43 38)
    ("Leviticus" 17 16 17 35 19 30 38 36 24 20 47 8 59 57 33 34 16 30 37 27 24 33 44 23 55 46 34)
    ("Numbers" 54 34 51 49 31 27 89 26 23 36 35 16 33 45 41 50 13 32 22 29 35 41 30 25 18 65 23 31 40 16 54 42 56 29 34 13)
    ("Deuteronomy" 46 37 29 49 33 25 26 20 29 22 32 32 18 29 23 22 20 22 21 20 23 30 25 22 19 19 26 68 29 20 30 52 29 12)
    ("Joshua" 18 24 17 24 15 27 26 35 27 43 23 24 33 15 63 10 18 28 51 9 45 34 16 33)
    ("Judges" 36 23 31 24 31 40 25 35 57 18 40 15 25 20 20 31 13 31 30 48 25)
    ("Ruth" 22 23 18 22)
    ("1 Samuel" 28 36 21 22 12 21 17 22 27 27 15 25 23 52 35 23 58 30 24 42 15 23 29 22 44 25 12 25 11 31 13)
    ("2 Samuel" 27 32 39 12 25 23 29 18 13 19 27 31 39 33 37 23 29 33 43 26 22 51 39 25)
    ("1 Kings" 53 46 28 34 18 38 51 66 28 29 43 33 34 31 34 34 24 46 21 43 29 53)
    ("2 Kings" 18 25 27 44 27 33 20 29 37 36 21 21 25 29 38 20 41 37 37 21 26 20 37 20 30)
    ("1 Chronicles" 54 55 24 43 26 81 40 40 44 14 47 40 14 17 29 43 27 17 19 8 30 19 32 31 31 32 34 21 30)
    ("2 Chronicles" 17 18 17 22 14 42 22 18 31 19 23 16 22 15 19 14 19 34 11 37 20 12 21 27 28 23 9 27 36 27 21 33 25 33 27 23)
    ("Ezra" 11 70 13 24 17 22 28 36 15 44)
    ("Nehemiah" 11 20 32 23 19 19 73 18 38 39 36 47 31)
    ("Esther" 22 23 15 17 14 14 10 17 32 3)
    ("Job" 22 13 26 21 27 30 21 22 35 22 20 25 28 22 35 22 16 21 29 29 34 30 17 25 6 14 23 28 25 31 40 22 33 37 16 33 24 41 30 24 34 17)
    ("Psalms" 6 12 8 8 12 10 17 9 20 18 7 8 6 7 5 11 15 50 14 9 13 31 6 10 22 12 14 9 11 12 24 11 22 22 28 12 40 22 13 17 13 11 5 26 17 11 9 14 20 23 19 9 6 7 23 13 11 11 17 12 8 12 11 10 13 20 7 35 36 5 24 20 28 23 10 12 20 72 13 19 16 8 18 12 13 17 7 18 52 17 16 15 5 23 11 13 12 9 9 5 8 28 22 35 45 48 43 13 31 7 10 10 9 8 18 19 2 29 176 7 8 9 4 8 5 6 5 6 8 8 3 18 3 3 21 26 9 8 24 13 10 7 12 15 21 10 20 14 9 6)
    ("Proverbs" 33 22 35 27 23 35 27 36 18 32 31 28 25 35 33 33 28 24 29 30 31 29 35 34 28 28 27 28 27 33 31)
    ("Ecclesiastes" 18 26 22 16 20 12 29 17 18 20 10 14)
    ("Song of Solomon" 17 17 11 16 16 13 13 14)
    ("Isaiah" 31 22 26 6 30 13 25 22 21 34 16 6 22 32 9 14 14 7 25 6 17 25 18 23 12 21 13 29 24 33 9 20 24 17 10 22 38 22 8 31 29 25 28 28 25 13 15 22 26 11 23 15 12 17 13 12 21 14 21 22 11 12 19 12 25 24)
    ("Jeremiah" 19 37 25 31 31 30 34 22 26 25 23 17 27 22 21 21 27 23 15 18 14 30 40 10 38 24 22 17 32 24 40 44 26 22 19 32 21 28 18 16 18 22 13 30 5 28 7 47 39 46 64 34)
    ("Lamentations" 22 22 66 22 22)
    ("Ezekiel" 28 10 27 17 17 14 27 18 11 22 25 28 23 23 8 63 24 32 14 49 32 31 49 27 17 21 36 26 21 26 18 32 33 31 15 38 28 23 29 49 26 20 27 31 25 24 23 35)
    ("Daniel" 21 49 30 37 31 28 28 27 27 21 45 13)
    ("Hosea" 11 23 5 19 15 11 16 14 17 15 12 14 16 9)
    ("Joel" 20 32 21)
    ("Amos" 15 16 15 13 27 14 17 14 15)
    ("Obadiah" 21)
    ("Jonah" 17 10 10 11)
    ("Micah" 16 13 12 13 15 16 20)
    ("Nahum" 15 13 19)
    ("Habakkuk" 17 20 19)
    ("Zephaniah" 18 15 20)
    ("Haggai" 15 23)
    ("Zechariah" 21 13 10 14 11 15 14 23 17 12 17 14 9 21)
    ("Malachi" 14 17 18 6)
    ("Matthew" 25 23 17 25 48 34 29 34 38 42 30 49 58 36 39 28 26 34 30 34 46 46 38 51 46 75 66 20)
    ("Mark" 45 28 35 41 43 56 36 38 48 52 32 44 37 72 46 20)
    ("Luke" 80 52 38 44 39 49 50 56 62 42 54 59 35 35 32 31 36 43 48 47 38 71 55 53)
    ("John" 51 25 36 54 46 71 52 48 41 42 57 50 38 31 27 33 26 40 42 31 25)
    ("Acts" 26 47 26 37 42 15 60 39 43 48 30 25 52 28 40 40 34 28 41 38 40 30 35 26 27 32 44 30)
    ("Romans" 32 29 31 25 21 23 25 39 33 21 36 21 14 23 33 26)
    ("1 Corinthians" 31 16 23 21 13 20 40 13 27 33 34 31 13 40 58 24)
    ("2 Corinthians" 24 17 18 18 21 18 16 24 15 18 33 21 14)
    ("Galatians" 24 21 29 31 26 18)
    ("Ephesians" 23 22 21 32 33 24)
    ("Philippians" 30 30 21 23)
    ("Colossians" 29 23 25 18)
    ("1 Thessalonians" 10 20 13 18 28)
    ("2 Thessalonians" 12 17 18)
    ("1 Timothy" 20 15 16 16 25 21)
    ("2 Timothy" 18 26 17 22)
    ("Titus" 16 15 15)
    ("Philemon" 25)
    ("Hebrews" 14 18 19 16 14 20 28 13 28 39 40 29 25)
    ("James" 27 26 18 17 20)
    ("1 Peter" 25 25 22 19 14)
    ("2 Peter" 21 22 18)
    ("1 John" 10 29 24 21 21)
    ("2 John" 13)
    ("3 John" 15)
    ("Jude" 25)
    ("Revelation" 20 29 22 11 14 17 17 13 21 11 19 17 18 20 8 21 18 24 21 15 27 21)
    ("Tobit" 22 14 17 21 22 18 17 21 6 14 18 22 18 15)
    ("Judith" 16 28 10 15 24 21 32 36 14 23 23 20 20 19 14 25)
    ("Wisdom" 16 24 19 20 23 25 30 21 18 21 26 27 19 31 19 29 21 25 22)
    ("Ecclesiasticus" 29 18 30 31 17 37 36 19 18 30 34 18 25 27 20 28 27 33 26 30 28 27 27 31 25 20 30 26 28 25 31 24 33 26 24 27 30 34 35 30 24 25 35 23 26 20 25 25 16 29 30)
    ("Baruch" 22 35 38 37 9 72)
    ("Azariah" 68)
    ("Susanna" 64)
    ("Bel and the Dragon" 42)
    ("Manasseh")
    ("1 Maccabees" 63 70 59 61 68 63 50 32 73 89 74 53 53 49 41 24)
    ("2 Maccabees" 36 32 40 50 27 31 42 36 29 38 38 46 26 46 39)
    ("1 Esdras" 58 30 24 63 73 34 15 96 55)
    ("2 Esdras" 40 48 36 52 56 59 70 63 47 59 46 51 58 48 63 78))
  "Number of verses per chapter of each book of the Bible.

This data includes the Deuterocanon and Apocrypha, and is based on ESV-
& KJV-derived data.")

(defconst ol-bible-abbreviations
  '(("Gn" . "Genesis") ("Gen" . "Genesis")
    ("Exod" . "Exodus") ("Ex" . "Exodus") ("Exo" . "Exodus")
    ("Lv" . "Leviticus") ("Lev" . "Leviticus")
    ("Nu" . "Numbers") ("Num" . "Numbers")
    ("Dt" . "Deuteronomy") ("Deut" . "Deuteronomy")
    ("Josh" . "Joshua")
    ("Judg" . "Judges")
    ("1 Sm" . "1 Samuel") ("1 Sam" . "1 Samuel") ("1st Sam" . "1 Samuel") ("1st Samuel" . "1 Samuel") ("First Samuel" . "1 Samuel")
    ("2 Sm" . "2 Samuel") ("2 Sam" . "2 Samuel") ("2nd Sam" . "2 Samuel") ("2nd Samuel" . "2 Samuel") ("Second Samuel" . "2 Samuel")
    ("1 Ki" . "1 Kings") ("1 Kgs" . "1 Kings") ("1st Kgs" . "1 Kings") ("1st Kings" . "1 Kings") ("First Kings" . "1 Kings")
    ("2 Ki" . "2 Kings") ("2 Kgs" . "2 Kings") ("2nd Kgs" . "2 Kings") ("2nd Kings" . "2 Kings") ("Second Kings" . "2 Kings")
    ("1 Ch" . "1 Chronicles") ("1 Chr" . "1 Chronicles") ("1st Chr" . "1 Chronicles") ("1st Chronicles" . "1 Chronicles") ("First Chronicles" . "1 Chronicles")
    ("2 Ch" . "2 Chronicles") ("2 Chr" . "2 Chronicles") ("2nd Chr" . "2 Chronicles") ("2nd Chronicles" . "2 Chronicles") ("Second Chronicles" . "2 Chronicles")
    ("Ezr" . "Ezra")
    ("Ne" . "Nehemiah") ("Neh" . "Nehemiah")
    ("Esth" . "Esther") ("Est" . "Esther")
    ("Ps" . "Psalms") ("Pss" . "Psalms") ("Psalm" . "Psalms")
    ("Pr" . "Proverbs") ("Prov" . "Proverbs") ("Pro" . "Proverbs")
    ("Ec" . "Ecclesiastes") ("Eccl" . "Ecclesiastes") ("Ecc" . "Ecclesiastes")
    ("Song" . "Song of Solomon") ("Song of Songs" . "Song of Solomon") ("Cant" . "Song of Solomon") ("Canticle of Canticles" . "Song of Solomon")
    ("Is" . "Isaiah") ("Isa" . "Isaiah")
    ("Jer" . "Jeremiah")
    ("Lam" . "Lamentations")
    ("Ezk" . "Ezekiel") ("Ezek" . "Ezekiel")
    ("Dn" . "Daniel") ("Dan" . "Daniel")
    ("Ho" . "Hosea") ("Hos" . "Hosea")
    ("Jl" . "Joel")
    ("Am" . "Amos")
    ("Obad" . "Obadiah") ("Ob" . "Obadiah") ("Oba" . "Obadiah")
    ("Jon" . "Jonah")
    ("Mc" . "Micah") ("Mi" . "Micah") ("Mic" . "Micah")
    ("Nah" . "Nahum")
    ("Hab" . "Habakkuk")
    ("Zp" . "Zephaniah") ("Zeph" . "Zephaniah")
    ("Hg" . "Haggai") ("Hag" . "Haggai")
    ("Zc" . "Zechariah") ("Zech" . "Zechariah")
    ("Mal" . "Malachi")
    ("Mt" . "Matthew") ("Matt" . "Matthew")
    ("Mk" . "Mark")
    ("Lk" . "Luke")
    ("Jn" . "John")
    ("Ac" . "Acts")
    ("Ro" . "Romans") ("Rm" . "Romans")
    ("Rom" . "Romans")
    ("1 Cor" . "1 Corinthians") ("1st Cor" . "1 Corinthians") ("1st Corinthians" . "1 Corinthians") ("First Corinthians" . "1 Corinthians")
    ("2 Cor" . "2 Corinthians") ("2nd Cor" . "2 Corinthians") ("2nd Corinthians" . "2 Corinthians") ("Second Corinthians" . "2 Corinthians")
    ("Gal" . "Galatians")
    ("Eph" . "Ephesians")
    ("Ph" . "Philippians") ("Phil" . "Philippians")
    ("Col" . "Colossians")
    ("1 Th" . "1 Thessalonians") ("1 Thess" . "1 Thessalonians") ("1st Thess" . "1 Thessalonians") ("1st Thessalonians" . "1 Thessalonians") ("First Thessalonians" . "1 Thessalonians")
    ("2 Th" . "2 Thessalonians") ("2 Thess" . "2 Thessalonians") ("2nd Thess" . "2 Thessalonians") ("2nd Thessalonians" . "2 Thessalonians") ("Second Thessalonians" . "2 Thessalonians")
    ("1 Tim" . "1 Timothy") ("1st Tim" . "1 Timothy") ("1st Timothy" . "1 Timothy") ("First Timothy" . "1 Timothy")
    ("2 Tim" . "2 Timothy") ("2nd Tim" . "2 Timothy") ("2nd Timothy" . "2 Timothy") ("Second Timothy" . "2 Timothy")
    ("Tit" . "Titus")
    ("Phlm" . "Philemon")
    ("Heb" . "Hebrews")
    ("Ja" . "James") ("Jas" . "James")
    ("1 Pt" . "1 Peter") ("1 Pet" . "1 Peter") ("1st Pet" . "1 Peter") ("1st Peter" . "1 Peter") ("First Peter" . "1 Peter")
    ("2 Pt" . "2 Peter") ("2 Pet" . "2 Peter") ("2nd Pet" . "2 Peter") ("2nd Peter" . "2 Peter") ("Second Peter" . "2 Peter")
    ("1 Jn" . "1 John") ("1st John" . "1 John") ("First John" . "1 John")
    ("2 Jn" . "2 John") ("2nd John" . "2 John") ("Second John" . "2 John")
    ("3 Jn" . "3 John") ("3rd John" . "3 John") ("Third John" . "3 John")
    ("Rv" . "Revelation")
    ("Rev" . "Revelation"))
  "A-list of abbreviations for books of the Bible.")


;;; Parse/Format References

(cl-defstruct ol-bible-reference
  "This structure keeps a Bible reference.

It includes the following fields:

 - `book', a string name in full expansion, naming the book referenced.
   This should be a value from the car of
   `ol-bible-books-chapters-verses'.

 - `start-chapter', `end-chapter' the start and end chapter,
   respectively.

 - `start-verse', `end-verse' the start and end verses, respectively.

 - `version' the version referenced, this may be nil at any time.

Only `book' is required to be non-nil.  The following patterns of
nullity are considered acceptable:

 - `start-chapter', `start-verse', `end-chapter', `end-verse' (a
   reference only to the book)

 - `start-verse',`end-verse' (a reference from `start-chapter' to
   `end-chapter')

 - `end-chapter', `end-verse' (a reference to
   `start-chapter':`start-verse')"
  book
  start-chapter
  start-verse
  end-chapter
  end-verse
  version)

(defconst ol-bible-reference-regexp
  (rx (group-n 1 (and (? (+ num) space) (+ alpha)))
      (? (and ?/ (or (and (group-n 2 (+ digit))
                          ?:
                          (group-n 3 (+ digit))
                          (? ?-
                             (group-n 4 (+ digit))
                             ?:
                             (group-n 5 (+ digit))))
                     (and (group-n 2 (+ digit))
                          (? ?-
                             (group-n 4 (+ digit)))))))
      (? (and ?/ (group-n 6 (+ (or alpha digit))))))
  "Regular expression for Bible references.

A canonical passage is referenced using the following syntax.

`BOOK/STARTCHAP:STARTVERSE-ENDCHAP:ENDVERSE/VERSION'

It is possible to omit specific chapter/verse references, or version
information.  For example, the following are valid.

 - Genesis/1:1-3:16/NRSV
 - Genesis/1:1-3:16
 - Genesis/1:1
 - Genesis/1:1/NRSV
 - Genesis/1-3
 - Genesis/1-3/NRSV

Group numbers are assigned as follows:

 - Group 1 is the Book
 - Group 2 is the Start Chapter
 - Group 3 is the Start Verse
 - Group 4 is the End Chapter
 - Group 5 is the End Verse
 - Group 6 is the Version")

(defun ol-bible-parse (path)
  "Convert PATH into a bible reference object."
  (save-match-data
    (when (string-match ol-bible-reference-regexp path)
      (let ((book (match-string-no-properties 1 path))
            (start-chapter (when (stringp (match-string-no-properties 2 path))
                             (string-to-number (match-string-no-properties 2 path))))
            (start-verse (when (stringp (match-string-no-properties 3 path))
                           (string-to-number (match-string-no-properties 3 path))))
            (end-chapter (when (stringp (match-string-no-properties 4 path))
                           (string-to-number (match-string-no-properties 4 path))))
            (end-verse (when (stringp (match-string-no-properties 5 path))
                         (string-to-number (match-string-no-properties 5 path))))
            (version (match-string-no-properties 6 path)))
        (if (not (cl-member book
                            (append (mapcar #'car ol-bible-books-chapters-verses)
                                    (mapcar #'car ol-bible-abbreviations))
                            :test #'string=))
            (user-error "Book is invalid: `%s' is not a known book" book)
          (when-let* ((new-book (alist-get book ol-bible-abbreviations nil nil #'string=)))
            (setf book new-book)))
        (when (and end-chapter (> start-chapter end-chapter))
          (user-error "Chapter range is invalid: start %d comes after end %d" start-chapter end-chapter))
        (when-let* ((verse start-verse)
                    (book-data (alist-get book ol-bible-books-chapters-verses nil nil #'string=))
                    (max-verses (nth (1- start-chapter) book-data))
                    (test (not (<= 1 start-verse max-verses))))
          (ignore test)
          (user-error "Start verse (%s) is invalid: Chapter %d of %s only has %s verses" start-verse start-chapter book max-verses))
        (when-let* ((verse end-verse)
                    (book-data (alist-get book ol-bible-books-chapters-verses nil nil #'string=))
                    (max-verses (nth (1- end-chapter) book-data))
                    (test (not (<= 1 end-verse max-verses))))
          (ignore test)
          (user-error "End verse (%s) is invalid: Chapter %d of %s only has %s verses" end-verse start-chapter book max-verses))
        (when (and end-verse
                   (= start-chapter end-chapter)
                   (> start-verse end-verse))
          (user-error "Verse range is invalid: Start verse %d comes after %d" start-verse end-verse))
        (make-ol-bible-reference :book book
                                 :start-chapter start-chapter
                                 :start-verse start-verse
                                 :end-chapter end-chapter
                                 :end-verse end-verse
                                 :version version)))))

(defun ol-bible-format-reference (reference &optional as-link)
  "Format REFERENCE.  Optionally AS-LINK.

When AS-LINK is non-nil, produce an un-abbreviated human description."
  (if as-link
      (format "%s%s%s"
              (ol-bible-reference-book reference)
              (let ((start-chapter (ol-bible-reference-start-chapter reference))
                    (start-verse (ol-bible-reference-start-verse reference))
                    (end-chapter (ol-bible-reference-end-chapter reference))
                    (end-verse (ol-bible-reference-end-verse reference)))
                (cond
                 (end-verse (format "/%d:%d-%d:%d" start-chapter start-verse end-chapter end-verse))
                 (end-chapter (format "/%d-%d" start-chapter end-chapter))
                 (start-verse (format "/%d:%d" start-chapter start-verse))
                 (start-chapter (format "/%d" start-chapter))
                 (t "")))
              (if-let* ((version (ol-bible-reference-version reference)))
                  (format "/%s" version)
                ""))
    (format "%s%s%s"
            (ol-bible-reference-book reference)
            (let ((start-chapter (ol-bible-reference-start-chapter reference))
                  (start-verse (ol-bible-reference-start-verse reference))
                  (end-chapter (ol-bible-reference-end-chapter reference))
                  (end-verse (ol-bible-reference-end-verse reference)))
              (cond
               (end-verse
                (format " %d%s%d%s%d%s%d"
                        start-chapter
                        (car ol-bible-description-format)
                        start-verse
                        (cdr ol-bible-description-format)
                        end-chapter
                        (car ol-bible-description-format)
                        end-verse))
               (end-chapter (format " %d%s%d"
                                    start-chapter
                                    (cdr ol-bible-description-format)
                                    end-chapter))
               (start-verse (format " %d%s%d"
                                    start-chapter
                                    (car ol-bible-description-format)
                                    start-verse))
               (start-chapter (format " %d" start-chapter))
               (t "")))
            (if-let ((version (ol-bible-reference-version reference)))
                (format " (%s)" version)
              ""))))


;;; Open

(defvar bible-mode-book-chapters)
(declare-function bible-mode--get-book-global-chapter "bible-mode.el")
(declare-function bible-open "bible-mode.el")

(defun ol-bible-open (bible-ref _)
  "Open BIBLE-REF at point, using `ol-bible-viewer' settings."
  (let ((reference (ol-bible-parse bible-ref)))
    (pcase ol-bible-viewer
      ('bible-mode
       (let* ((chapter-data (assoc (ol-bible-reference-book reference) bible-mode-book-chapters))
              (book-chapter (bible-mode--get-book-global-chapter (nth 0 chapter-data)))
              (chapter (if book-chapter (ol-bible-reference-start-chapter reference))))
         (bible-open (+ book-chapter chapter) (or (ol-bible-reference-start-verse reference) 1))))
      ('oremus
       (let ((ol-bible-http-provider 'oremus))
         (eww (ol-bible--to-link reference))))
      ('link (browse-url (ol-bible--to-link reference)))
      (`(functionp ,ol-bible-viewer) (funcall ol-bible-viewer reference)))))


;;; Export

(defun ol-bible--to-link (reference)
  "Convert REFERENCE to HTTP link using `ol-bible-http-provider'."
  (pcase ol-bible-http-provider
    (`biblegateway
     (format "https://www.biblegateway.com/passage/?search=%s%s"
             (let ((ol-bible-description-format (cons ":" "-"))
                   (new-reference (make-ol-bible-reference
                                   :book (ol-bible-reference-book reference)
                                   :start-chapter (ol-bible-reference-start-chapter reference)
                                   :start-verse (ol-bible-reference-start-verse reference)
                                   :end-chapter (ol-bible-reference-end-chapter reference)
                                   :end-verse (ol-bible-reference-end-verse reference))))
               (url-encode-url (ol-bible-format-reference new-reference)))
             (if-let* ((version (ol-bible-reference-version reference)))
                 (format "&version=%s" (url-encode-url version))
               "")))
    (`bible-dot-com
     (format "https://www.bible.com/search/bible?query=%s"
             (let ((ol-bible-description-format (cons ":" "-"))
                   (new-reference (make-ol-bible-reference
                                   :book (ol-bible-reference-book reference)
                                   :start-chapter (ol-bible-reference-start-chapter reference)
                                   :start-verse (ol-bible-reference-start-verse reference)
                                   :end-chapter (ol-bible-reference-end-chapter reference)
                                   :end-verse (ol-bible-reference-end-verse reference))))
               (url-encode-url (ol-bible-format-reference new-reference)))))
    (`bible-hub
     (if-let* ((version (ol-bible-reference-version reference)))
         (format "https://biblehub.com/%s/%s/%d.htm"
                 (downcase version)
                 (downcase (ol-bible-reference-book reference))
                 (or (ol-bible-reference-start-chapter reference) 1))
       (format "https://biblehub.com/%s/%d.htm"
               (downcase (ol-bible-reference-book reference))
               (or (ol-bible-reference-start-chapter reference) 1))))
    (`oremus
     (let ((version (or (ol-bible-reference-version reference) "NRSV")))
       (format "https://bible.oremus.org/?version=%s&passage=%s"
               version
               (let ((ol-bible-description-format (cons "." "-"))
                     (new-reference (make-ol-bible-reference
                                     :book (ol-bible-reference-book reference)
                                     :start-chapter (ol-bible-reference-start-chapter reference)
                                     :start-verse (ol-bible-reference-start-verse reference)
                                     :end-chapter (ol-bible-reference-end-chapter reference)
                                     :end-verse (ol-bible-reference-end-verse reference))))
                 (url-encode-url (ol-bible-format-reference new-reference))))))
    (`(functionp ,ol-bible-http-provider)
     (funcall ol-bible-http-provider reference))))

(defun ol-bible--bibleref (reference)
  "Generate a bibleref compatible version of REFERENCE for LaTeX export."
  (format "{\\bibleverse{%s}%s}%s"
          (ol-bible-reference-book reference)
          (let ((start-chapter (ol-bible-reference-start-chapter reference))
                (start-verse (ol-bible-reference-start-verse reference))
                (end-chapter (ol-bible-reference-end-chapter reference))
                (end-verse (ol-bible-reference-end-verse reference)))
            (cond
             (end-verse (format "(%d:%d)-(%d:%d)" start-chapter start-verse end-chapter end-verse))
             (end-chapter (format "(%d-%d:)" start-chapter end-chapter))
             (start-verse (format "(%d:%d)" start-chapter start-verse))
             (start-chapter (format "(%d:)" start-chapter))
             (t "")))
          (if-let*  ((version (ol-bible-reference-version reference)))
              (format "~(%s)" version)
            "")))

(defun ol-bible-export (path description backend _)
  "Export Bible link PATH, given DESCRIPTION to BACKEND."
  (let* ((reference (ol-bible-parse path))
         (description (or description
                          (let ((ol-bible-description-format (if (and (eq backend 'html)
                                                                      (string= "--" (cdr ol-bible-description-format)))
                                                                 (cons (car ol-bible-description-format) "&ndash;")
                                                               ol-bible-description-format)))
                            (ol-bible-format-reference reference)))))
    (pcase backend
      (`html (format "<a target=\"_blank\" href=\"%s\">%s</a>"
                     (ol-bible--to-link reference)
                     description))
      (`latex
       (pcase ol-bible-latex-export-type
         (`bibleref
          (ol-bible--bibleref reference))
         (`bibleref-in-link
          (format "\\href{%s}{%s}"
                  (ol-bible--to-link reference)
                  (ol-bible--bibleref reference)))
         (`link (format "\\href{%s}{%s}"
                        (ol-bible--to-link reference)
                        description))
         (`text description)))
      (`texinfo (format "@uref{%s,%s}"
                        (ol-bible--to-link reference)
                        description))
      (`ascii description)
      (_ description))))


;;; Completion

(defun ol-bible--read-number-in-range (prompt min max)
  "Read number between (inclusive) MIN and MAX, with PROMPT."
  (let* ((err-prompt (format "%s (*%s-%s*, -1 for none): " prompt min max))
         (prompt (format "%s (%s-%s, -1 for none): " prompt min max))
         (number (read-number prompt)))
    (while (and (not (<= min number max))
                (not (= number -1)))
      (setf number (read-number err-prompt)))
    (if (= -1 number)
        nil
      number)))

(defun ol-bible-complete (&optional _)
  "Complete a Bible passage for insertion."
  (let* ((book (completing-read "Book: " (mapcar #'car ol-bible-books-chapters-verses)))
         (book-data (alist-get book ol-bible-books-chapters-verses nil nil #'string=))
         (start-chapter (ol-bible--read-number-in-range "Start Chapter" 1 (length book-data)))
         (start-verse (and start-chapter
                           (ol-bible--read-number-in-range "Start Verse" 1 (nth (1- start-chapter) book-data))))
         (end-chapter (and start-chapter
                           (ol-bible--read-number-in-range "End Chapter" start-chapter (length book-data))))
         (end-verse (and end-chapter
                         (if (= end-chapter start-chapter)
                             (ol-bible--read-number-in-range "End Verse" (1+ start-verse) (nth (1- start-chapter) book-data))
                           (ol-bible--read-number-in-range "End Verse" 1 (nth (1- end-chapter) book-data)))))
         (version (let ((read-in-string (read-string "Version: ")))
                    (if (string-empty-p read-in-string)
                        nil
                      read-in-string))))
    (format "bible:%s"
            (ol-bible-format-reference
             (make-ol-bible-reference :book book
                                      :start-chapter start-chapter
                                      :start-verse start-verse
                                      :end-chapter end-chapter
                                      :end-verse end-verse
                                      :version version)
             t))))


;;; Preview

(defun ol-bible-preview (overlay path _node)
  "Preview link specified by PATH using OVERLAY.

PATH is parsed as normal (using `ol-bible-parse'), and the formatting is
done through `ol-bible-format-reference', which see."
  (when ol-bible-display-formatted-references
    (when-let* ((reference (condition-case-unless-debug _err
                               (ol-bible-parse path)
                             (t nil)))
                (preview (ol-bible-format-reference reference)))
      (overlay-put overlay 'display preview)
      preview)))


;;; Define Link

(org-link-set-parameters "bible"
                         :follow #'ol-bible-open
                         :export #'ol-bible-export
                         :complete #'ol-bible-complete
                         :preview #'ol-bible-preview)

(provide 'ol-bible)

;;; ol-bible.el ends here
