;; Given a .aux file, create a .bib file which contains the references
;; it requires, as extracted from the one or more .bib files it
;; references.
;;
;; With options -Ocrossref and -Oyaml, produce the output in
;; (rough) crossref format, or YAML, respectively.
;;
;; Usage:
;;
;;      beastie [-O format] [-p] extract-bib.scm foo.aux
;;
;; Just to show how (and to support \cite{*}) we also output the
;; result sorted in order of increasing date+authorname.
;;
;; This file primarily illustrates calling `call-with-aux-file` with a
;; suitable worker procedure, but it also illustrates calling
;; `getopt`.  The worker procedure here calls one of three procedures,
;; depending on the -O option, whcih illustrate
;; `with-fields-from-entry` and basic use of the `bibtex` module.
;;
;; The use of ustring->ustring illustrates getting rid of TeXisms in a
;; case where the output is not expected to be fed to LaTeX later.
;;
;; This works with beastie v0.14.
;; The functions it uses may change with later versions.
;;
;; This file is part of Beastie <https://purl.org/nxg/dist/beastie>
;; SPDX-FileCopyrightText: 2023 Norman Gray <https://nxg.me.uk>
;; SPDX-License-Identifier: BSD-2-Clause

(module 'aux 'bibtex 'authors 'xexpr 'subtex)

;; plain.scm exports assemble-bibliography
(load "plain.scm")

;; Pick a format
;(define *name-format* '((von "~") (last) (", " junior) (", " first)))
(define *name-format* '((first " ") (von "~") (last) (", " junior)))

(define *prettyprint?* #t)

;; Given a list of citation? objects, extract the BibTeX entry
;; from each one, and print it as a .bib entry.
(define (print/bibtex! citation-list)
  (for-each (λ (c)
              (entry-print! (citation-entry c)))
            citation-list))

;; Similarly, printing as a Crossref <citation>...</citation> element.
;; For crossref details, see
;; <https://www.crossref.org/documentation/schema-library/markup-guide-metadata-segments/references/>
;;
;; This isn't a particularly careful crossref-writer, so there may be
;; a variety of crossref subtleties being omitted/ignored here.
;;
;; Thanks to Karl Berry for the (implicit) suggestion here.
(define (print/crossref! citation-list)
  (define (first-page page-range/str)
    ;; Return the first page in the range.
    ;; However, if page-range/str is #f, return that;
    ;; or if it's present but not a valid/expected page-range,
    ;; then return that, also.
    (cond ((string->page-range page-range/str) => car)
          (else page-range/str)))
  (define (citation/default c)
    (with-fields-from-entry (citation-entry c)
        ()
        (title year author doi isbn)
      (list
       (and author
            (let ((authors (parse-author-list author)))
              `(author ,(untexify-string-or-list (format-name *name-format* (car authors))))))
       (and title `(volume_title ,(untexify-string-or-list title)))
       (and year `(cYear ,year))
       (and doi `(doi ,doi))
       (and isbn `(isbn ,isbn))
       `(unstructured_citation ,(xexpr-text (citation-html c))))))
  (define (citation/article c)
    (with-fields-from-entry (citation-entry c)
        ()
        (journal title booktitle year volume number pages author doi isbn)
      (list
       (and journal `(journal_title ,journal))
       (and title `(article_title ,(untexify-string-or-list title)))
       (and booktitle `(volume_title ,(untexify-string-or-list booktitle)))
       (and author
            (let ((authors (parse-author-list author)))
              `(author ,(format-name *name-format* (car authors)))))
       (and volume `(volume ,volume))
       (and number `(issue ,number))
       (and pages `(first_page ,(first-page pages)))
       (and year `(cYear ,year))
       (and doi `(doi ,doi))
       (and isbn `(isbn ,isbn))
       `(unstructured_citation ,(xexpr-text (citation-html c))))))
  (define (citation->crossref-citation c)
    (let ((e (citation-entry c)))
      ;; Given an entry?, produce a <citation> as xexpr?.
      ;;
      ;; Each of the citation/foo functions returns a list of (or/c xexpr? #f)
      `(citation ((key ,(symbol->string (entry-key e))))
                 . ,(filter values
                            (case (entry-type e)
                              ((article incollection inproceedings) (citation/article c))
                              (else (citation/default c)))))))
  (xexpr-write/xml!
   `(citation_list . ,(map citation->crossref-citation citation-list))
   :expand-entities? #t
   :prettyprint? *prettyprint?*))

;; ...or printing as YAML, with keys matching BibTeX field names.
;;
;; Here, note the call to ustring->ustring,
;; to remove TeXisms "~" and "{...}" from the output.
(define (print/yaml! citation-list)
  (define (print-field/yaml! field entry)
    (case field
      ((author editor translator)
       (let ((authors (entry-field/authorlist entry field)))
         (printf "  ~a:~%" field)
         (for-each (λ (a)
                     (printf "    - ~a~%"
                             (ustring->ustring
                              (format-name *name-format* a)
                              :nbsp " " :braces? #f)))
                   authors)))
      (else
       (printf "  ~a: \"~a\"~%" field (entry-field entry field)))))
  (define (print-entry/yaml! c)
    (let ((e (citation-entry c)))
      (printf "- key: ~a~%  type: ~a~%"
              (entry-key e)
              (entry-type e))
      (for-each (λ (f)
                  (print-field/yaml! f e))
                (entry-fields e))))
  (for-each print-entry/yaml! citation-list))

(define (exit/usage)
  (eprintf "Usage:~%    beastie [-O fmt] [-p] extract-bib.scm foo.aux~%~%Respects the value of $BIBINPUTS.~%fmt can be bibtex|crossref|yaml (default bibtex/.bib output).~%Option -p does (limited) prettyprinting.~%")
  (exit 1))

;; Parse the argument list.
;;
;; This defines values *AUX-FILE* and PRINT-ENTRIES! which are respectively
;; the .aux file to be processed, and the function -- one of print/bibtex!,
;; print/crossref! or print/yaml! -- to write them out.
(define-values (*aux-file* print-entries!)
  (receive (cmd opts args)
      (getopt '((#\O format
                 "Output format: bibtex | crossref | yaml (default bibtex)"
                 (string->symbol format))
                (#\p
                 "Prettyprint output (currently only has an effect for 'crossref' output)."
                 (set! *prettyprint?* #t))))
    (values
     (case (length args)
       ((0) (exit/usage))
       ((1) (car args))
       (else
        (eprintf "Too many arguments.~%")
        (exit/usage)))
     (cond ((assq #\O opts)
            => (λ (format-option)
                 (case (cdr format-option)
                   ((bibtex) print/bibtex!)
                   ((crossref) print/crossref!)
                   ((yaml) print/yaml!)
                   (else (eprintf "Unexpected -O option: ~a~%" (cdr format-option))
                         (exit/usage)))))
           (else                        ;default: output .bib
            print/bibtex!)))))

;; Do the work
(print-entries!
 (call-with-aux-file
     *aux-file*
   (λ (citations bibfiles _ignored)
     ;; call-with-aux-file guarantees citations is a list
     (unless (list? bibfiles)
       (eprintf "extract-bib.scm: I didn't find any \\bibdata{foo} in the .aux file, ~a~%" *aux-file*)
       (exit 1))
     (assemble-bibliography citations bibfiles))))

;; The above uses assemble-bibliography, from plain.scm.
;;
;; An alternative way of doing most of the above is below.
;; This works fine, but doesn't make it easy to include the 'unstructured-citation'
;; elements of the writers above.
;;
;; Below, the extract-aux function returns a sorted list of entry? objects,
;; which can be printed by a slightly different version of print/bibtex! and friends
;; which take an entry? rather than a citation?.

;; Extract the requested citations (may be 'all) from the one or more bibfiles
;; (bibstyle is ignored).
;; (define (extract-aux citations bibfiles bibstyle)
;;   (unless (and (or (eqv? citations 'all)
;;                    (list? citations))
;;                (list? bibfiles))
;;     (define (list/printing l)
;;       (cond ((not l) "#f")
;;             ((null? l) "()")
;;             (else (sprintf "(~s...)" (car l)))))
;;     (eprintf "extract-aux: citation list = ~a, and bibfiles list = ~a must both be lists~%"
;;              (list/printing citations) (list/printing bibfiles))
;;     (exit 1))
;;
;;   (let ((citations/set            ;the list of citations as a set/hash
;;          (if (eqv? citations 'all)
;;              (λ (key) #t)    ;dummy: include all entries in the output
;;              (make-set/eqv citations)))
;;         (allbib                 ;parse all of the mentioned .bib files
;;          (apply append (map parse-bibtex-file bibfiles))))
;;     (sort! (fold (lambda (e knil)
;;                    ;; include only those entries whose key appears in citations/set
;;                    (if (citations/set (entry-key e))
;;                        (cons e knil)
;;                        knil))
;;                  '()
;;                  allbib)
;;            entry<?)))
;;
;; Wrap parse-aux-file:
;; discard any .bst file found in the aux file
;; (we don't need it, and so get an error we don't care about, if it's not findable).
;; (define (parse-aux-file/nobst fn)
;;   (cond ((parse-aux-file fn)
;;          => (lambda (result)
;;               (list (car result) (cadr result) #f)))
;;         (else #f)))
;;
;; (print-entries!
;;  (call-with-aux-file *aux-file*
;;                      extract-aux
;;                      :parse-aux-file parse-aux-file/nobst))
