;; Provide a minor mode to start a build process in the background, collect its
;; outputs, and associate them with the current buffer.
;;
;; Copyright 2025--2026 Klaus Aehlig, License: Apache License, Version 2.0

;; Configuration: the command to run bufbuild, becomes buffer-local.
;; Set this to some appropriate value before enabling bufbuild-mode.
(defcustom bufbuild-cmd '("just-mr" "build" "-p")
  "The command to run the bufbuild target for the current project.
Has to be a list of strings describing argv. Is used by the
`after-save-hook' set by `bufbuild-mode'; usually set before
enabling this minor mode.")
(make-variable-buffer-local 'bufbuild-cmd)

;; Result variables, set on the bufer that requested the build.
;; Used by bufbuild-show (bound to f6 in bufbuild-mode) to fill
;; the result buffer.
(defvar bufbuild-status nil
  "A short status string if the last bufbuild run for the current buffer
did not succeed, nil otherwise. Used by the bufbuild minor mode as lighter.")
(make-variable-buffer-local 'bufbuild-status)
(defvar bufbuild-msg nil
  "The full bufbuild message for this buffer, if any, nil otherwise.")
(make-variable-buffer-local 'bufbuild-msg)

;; Variables of the process buffer; used by bufbuild keep track of what
;; to do once the build process finishes.
(defvar bufbuild-err-buffer nil
  "The buffer for bufbuild stderr of the process for which this is stdout")
(make-variable-buffer-local 'bufbuild-err-buffer)
(defvar bufbuild-initiating-buffer nil
  "The buffer that requested bufbuild run")
(make-variable-buffer-local 'bufbuild-initiating-buffer)

(defun bufbuild-sentinel (proc msg)
  "Sentinel for the bufbuild process.

Upon completion of the process, set the buffer-local variables `bufbuild-status'
and `bufbuild-msg' on the buffer that requested the build. Afterwards, clean up
the buffers used for collecting stdout and stderr of the build process."
  (unless (process-live-p proc)
    (let* ((status (process-status proc))
           (code (process-exit-status proc))
           (buffer (process-buffer proc))
           (err-buffer (with-current-buffer buffer bufbuild-err-buffer))
           (req-buffer (with-current-buffer buffer
                         bufbuild-initiating-buffer))
           )
      (message "Building finished for %s, %s %s" req-buffer status code)
      (cond
       ((not (eq status 'exit))
        (let ((stderr (with-current-buffer err-buffer (buffer-string))))
          (message "Bufbuilding %s did not terminate regularly:\n%s"
                   proc stderr)
          (when (buffer-live-p req-buffer)
            (with-current-buffer req-buffer
              (setq bufbuild-status "PROC")
              (setq bufbuild-msg stderr)))))
       ((eq code 0)
        (when (buffer-live-p req-buffer)
          (let ((report (with-current-buffer buffer (buffer-string))))
            (with-current-buffer req-buffer
              (setq bufbuild-status nil)
              (setq bufbuild-msg report)))))
       ((eq code 2)
        (let ((report (with-current-buffer buffer (buffer-string))))
          (message "Bufbuilding found test:\n%s" report)
          (when (buffer-live-p req-buffer)
            (with-current-buffer req-buffer
              (setq bufbuild-status "TEST")
              (setq bufbuild-msg report)))
          ))
       ((eq code 1)
        (let ((stderr (with-current-buffer err-buffer (buffer-string))))
          (message "Bufbuilding had build errors:\n%s" stderr)
          (when (buffer-live-p req-buffer)
            (setq bufbuild-status "BUILD")
            (setq bufbuild-msg stderr))))
       (t (let ((stderr (with-current-buffer err-buffer (buffer-string))))
            (message "Failed to bufbuild:\n%s" stderr)
            (when (buffer-live-p req-buffer)
              (with-current-buffer req-buffer
                (setq bufbuild-status "BUILD-internal")
                (setq bufbuild-msg stderr))))))
      (kill-buffer err-buffer)
      (kill-buffer buffer))))

(defun bufbuild-start ()
  "Start a build process with the command set by the buffer-local
variable `bufbuild-cmd'. Usually triggered by the `after-save-hook'
set by the `bufbuild-mode' minor mode."
  (interactive)
  (let ((out-buffer (generate-new-buffer "*bufbuild* report"))
        (err-buffer (generate-new-buffer "*bufbuild* progress"))
        (req-buffer (current-buffer))
        (cmd bufbuild-cmd)
        )
    (with-current-buffer out-buffer
      (setq bufbuild-err-buffer err-buffer)
      (setq bufbuild-initiating-buffer req-buffer)
      )
    (message "Starting build for %s" req-buffer)
    (make-process
     :name "bufbuilding"
     :command cmd
     :buffer out-buffer
     :stderr err-buffer
     :sentinel #'bufbuild-sentinel)))

(defun bufbuild-show ()
  "Switch to the bufbuild result buffer *BUFBUILD* and set its
content to the build message associated with current buffer.
f6 buries."
  (interactive)
  (let ((out (get-buffer-create "*BUFBUILD*"))
        (status bufbuild-status)
        (msg bufbuild-msg))
    (with-current-buffer out
      (erase-buffer)
      (when status (insert status))
      (when msg (insert "\n\n" msg))
      (use-local-map (copy-keymap global-map))
      (local-set-key [f6] 'bury-buffer)
      )
    (switch-to-buffer out)))

;; Minor mode to provide a key binidng to show the last build result
(define-minor-mode bufbuild-mode
  "Minor mode to provide a build status well as a key binding to show result.

If enabled, a local `after-save-hook' triggers a build, with the build
command taken from `bufbuild-cmd'. The status of the last-finished build
for this buffer is shown in the status line; the result as reported on
stdout (exit code 0 or 2), or failure message (stderr, otherwise) can
be requested by `bufbuild-show' which is bound to f6; f6 also buries the
result buffer."
  :lighter (:eval
            (if bufbuild-status
                (propertize (format " %s" bufbuild-status)
                            'face '(:foreground "red" :weight bold))
              " BB"))
  :keymap (let ((map (make-sparse-keymap)))
            (define-key map [f6] 'bufbuild-show)
            map))
(add-hook 'bufbuild-mode-hook
          (lambda ()
            (if bufbuild-mode
                (add-hook 'after-save-hook #'bufbuild-start nil 'local)
              (remove-hook 'after-save-hook #'bufbuild-start 'local))))

;; Infrastructure for autmotically enabling bufbuild-mode on find-file

(defcustom bufbuild-file-assoc '()
  "Assoc list specifying default `bufbuild-cmd' for given files.
If the value for a file is non-nil, `bufbuild-mode' is enabled on
`find-file'. Comparison is by `string-prefix-p', so a command can
also be set for all files in a certain subtree.")

(defun bufbuild-load-file-assoc (fname)
  "Load value for `buildbuild-file-assoc' from specified file name as sexp.
Before assigning, the value is sorted in reverse order; in that way, the
most-specific prefix takes precedence, regardless of the order in which the
entries are on disk."
  (if (file-readable-p fname)
      (progn
        (save-excursion
          (find-file fname)
          (goto-char 0)
          (let ((value (setq bufbuild-file-assoc
                             (sort (read (current-buffer)) :reverse t))))
            (bury-buffer)
            value)))))

(defun bufbuild-find-file-hook ()
  "Enable `bufbuild-mode' if the file name of the current buffer, or a prefix
thereof, is specified in `bufbuild-file-assoc'."
  (let ((entry (assoc buffer-file-name bufbuild-file-assoc 'string-prefix-p)))
    (if entry
        (progn
          (setq bufbuild-cmd (cdr entry))
          (bufbuild-mode)
          (bufbuild-start)))))

(add-hook 'find-file-hook 'bufbuild-find-file-hook)
