;;; citar-typst.el --- Typst support for citar -*- lexical-binding: t; -*-

;; Copyright © 2026 Ashton Wiersdorf

;; Author: Ashton Wiersdorf <https://lambdaland.org/>
;; Maintainer: Ashton Wiersdorf <https://lambdaland.org/>
;; Created: February 10, 2026
;; Package-Version: 1.0.0
;; Package-Revision: 5ce419484e3b
;; Homepage: https://codeberg.org/ashton314/citar-typst
;; SPDX-FileCopyrightText: 2026 Ashton Wiersdorf
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Package-Requires: ((emacs "29.1") (citar "1.4"))

;; This file is not part of GNU Emacs.

;;; Commentary:

;; This is a small package to make Citar usable for Typst documents.

;;; Code:

(require 'citar)
(require 'thingatpt)
(require 'treesit)

(defgroup citar-typst nil
  "Extension to make Citar work with Typst files"
  :group 'citar)

(defcustom citar-typst-prompt-for-extra-arguments t
  "Whether to prompt for additional arguments when inserting a citation."
  :group 'citar-typst
  :type 'boolean)

(defconst citar-typst-citation-key-regexp
  ;;    #cite(<refKeyHere>, ...)    #cite(label("refKeyHere"), ...)       @refKeyHere[...]
  "\\(?:#cite(<\\([^>]+\\)>[^)]*)\\|#cite(label(\"\\([^\"]+\\)\")[^)]*)\\|@\\([-_[:alnum:]]+\\)\\(?:\\[[^]]*\\]\\)?\\)"
  "Regex matching Typst citation keys.

Matches both the #cite(<key>) and @key forms, along with optional args.")

;;;###autoload
(defun citar-typst-local-bib-files ()
  "Look for #bibliography(...) and load the files it specifies."
  (let ((query (treesit-query-compile
                'typst
                '((call
                   item: (_) @fn
                   (group (string) @bib-file)
                   (:equal @fn "bibliography"))))))
    (when-let* ((local-bib
                 (treesit-node-text
                  (cdr (assoc 'bib-file
                              (treesit-query-capture (treesit-buffer-root-node) query)))))
                ((and (> (length local-bib) 2)
                  (string-prefix-p "\"" local-bib)
                  (string-suffix-p "\"" local-bib))))
      (substring-no-properties local-bib 1 -1))))

;;;###autoload
(defun citar-typst-insert-keys (keys)
  "Insert space-separated and @-prefixed KEYS in a Typst buffer."
  (insert (mapconcat (lambda (k) (concat "@" k)) keys " ")))

(defun citar-typst--format-citation (key supplement &optional form style)
  "Format a citation given the key and optional arguments.

Tries to use a minimal citation. For example, if just given the KEY,
formats as @refHere1942. If KEY and SUPPLEMENT given, format as
@refHere1942[supplement here like p. 99]. If FORM or STYLE are
specified, uses the more verbose #cite(...) syntax to format it."
  (if (or (and form (not (string-empty-p form)))
          (and style (not (string-empty-p style))))
      (format "#cite(<%s>%s%s%s)"
              key
              (or (and supplement (not (string-empty-p supplement))
                       (format ", supplement: \"%s\"" supplement))
                  "")
              (or (and form (not (string-empty-p form)) form
                       (format ", form: \"%s\"" form))
                  "")
              (or (and style (not (string-empty-p style)) style
                       (format ", style: \"%s\"" style))
                  ""))
      ;; Use compact style for just key w/ optional supplement
      (if (and supplement (not (string-empty-p supplement)))
          (format "@%s[%s]" key supplement)
        (format "@%s" key))))

;;;###autoload
(defun citar-typst-insert-citation (keys &optional invert-prompt)
  "Insert a citation consisting of KEYS.

If INVERT-PROMPT is non-nil, invert the meaning of
`citar-typst-prompt-for-extra-arguments'."
  (let* ((citation (citar-typst-citation-at-point))
         (keys (if citation (seq-difference keys (car citation)) keys))
         (prompt (xor invert-prompt citar-typst-prompt-for-extra-arguments))
         (sup (when prompt (read-from-minibuffer "Supplement: ")))
         (style (when prompt (read-from-minibuffer "Style: ")))
         (form (when prompt (completing-read "Form: " '("" "normal" "prose" "full" "author" "year")))))
    (when keys
      (let ((keys-fmt (mapconcat
            (lambda (k) (citar-typst--format-citation k sup form style))
            keys
            " ")))
        (if (or (not citation)
                (= (point) (cadr citation))
                (= (point) (cddr citation)))
            ;; true case: outside a citation
            (insert keys-fmt)
          ;; false case: inside a citation
          (progn
            (goto-char (cddr citation))
            (insert " " keys-fmt)))))))

;;;###autoload
(defun citar-typst-insert-edit (&optional _arg)
  "Prompt for keys and call `citar-typst-insert-citation."
  (citar-typst-insert-citation (citar-select-refs)))

;;;###autoload
(defun citar-typst-key-at-point ()
  "Return the citation key at point or nil if none."
  (save-excursion
    (skip-chars-backward "^@#")
    (backward-word)               ; backward-char errors if moving past beginning of buffer
    (when (re-search-forward citar-typst-citation-key-regexp nil 'noerror)
      (or (match-string-no-properties 1)
          (match-string-no-properties 2)
          (match-string-no-properties 3)))))

;;;###autoload
(defun citar-typst-citation-at-point ()
  "Return citation key at point (with its bounds) for Typst citations.
Returns (KEY . BOUNDS), where KEY is the citation key at point and
BOUNDS is a pair of buffer positions. Citation keys are found using
`citar-typst-citation-key-regexp'. Returns nil if there is no key at
point."
  (interactive)
  (when (thing-at-point-looking-at citar-typst-citation-key-regexp)
    (cons (or (match-string-no-properties 1)
              (match-string-no-properties 2)
              (match-string-no-properties 3))
          (cons (match-beginning 0) (match-end 0)))))

;;;###autoload
(defun citar-typst-list-keys ()
  "Return a list of all citation keys in the current buffer."
  (save-excursion
    (let (matches)
      (goto-char (point-min))
      (while (re-search-forward citar-typst-citation-key-regexp nil t)
        (push (or (match-string-no-properties 1)
                  (match-string-no-properties 2)
                  (match-string-no-properties 3))
              matches))
      (delete-dups (nreverse matches)))))

;; Plug these functions into Citar
(defvar citar-major-mode-functions)

;;;###autoload
(define-minor-mode citar-typst-mode
  "Support for using Citar in Typst files."
  :global t
  (if citar-typst-mode
      (unless (assoc '(typst-ts-mode) citar-major-mode-functions)
        (push '((typst-ts-mode) .
                ((local-bib-files . citar-typst-local-bib-files)
                 (insert-keys . citar-typst-insert-keys)
                 (insert-citation . citar-typst-insert-citation)
                 (insert-edit . citar-typst-insert-edit)
                 (key-at-point . citar-typst-key-at-point)
                 (citation-at-point . citar-typst-citation-at-point)
                 (list-keys . citar-typst-list-keys)))
              citar-major-mode-functions))
    (setopt citar-major-mode-functions
            (assoc-delete-all '(typst-ts-mode) citar-major-mode-functions))))

(provide 'citar-typst)
;;; citar-typst.el ends here
