;;; syd-handle-repl.el -*- lexical-binding: t; -*-

(eval-when-compile (require 'cl-lib))
(require 'syd-prelude)
(require 'syd-project)



(syd-add-hook 'on-init-ui-hook
  (defun syd--set-popup-rules-for-repls-h ()
    (require 'doom-popup)
    (set-popup-rule!
      (lambda (bufname _)
        (when (boundp 'syd-repl-mode)
          (buffer-local-value 'syd-repl-mode (get-buffer bufname))))
      :ttl (lambda (buf)
             (unless (plist-get syd-repl-plist :persist)
               (when-let (process (get-buffer-process buf))
                 (set-process-query-on-exit-flag process nil)
                 (kill-process process)
                 (kill-buffer buf))))
      :size 0.25
      :quit nil)))


;;; State & settings

(defvar +syd-major-mode-repl-alist '()
  "TODO: An alist pairing major-modes (symbols) with plists describing REPLs.")

(defvar +syd-repl-buffers (make-hash-table :test 'equal)
  "A hashmap mapping pairs (MAJOR-MODE . PROJECT-ROOT) to their corresponding
buffers.  Indeed, this implies a single REPL per language per project.  Is this
limitation worth overcoming?  I'm not sure!  I've yet to butt heads with it.")

(defvar-local syd-repl-plist nil
  "A plist describing the repl associated with the current buffer.

This is little more than a cache.  Its value can almost always be equivalently
derived from `+syd-major-mode-repl-alist'.")

(defun set-repl-handler! (modes command &rest plist)
  "Defines a REPL for MODES.

MODES is either a single major mode symbol or a list of them. COMMAND is a
function that creates and returns the REPL buffer.

COMMAND can either be a function that takes no arguments, or an interactive
command that will be called interactively. COMMANDS must return either the repl
buffer or a function that takes no arguments and returns the repl buffer.

PLIST is a property list that map special attributes to this repl. These are
recognized:

  :persist BOOL
    If non-nil, this REPL won't be killed when its window is closed.
  :send-region FUNC
    A function that accepts a BEG and END, and sends the contents of the region
    to the REPL. Defaults to `+eval/send-region-to-repl'.
  :send-buffer FUNC
    A function of no arguments that sends the contents of the buffer to the REPL.
    Defaults to `+eval/region', which will run the :send-region specified function
    or `+eval/send-region-to-repl'."
  (declare (indent defun))
  (dolist (mode (ensure-list modes))
    (setf (alist-get mode +syd-major-mode-repl-alist)
          (cons command plist))))


;;; Repls

;;;###autoload
(define-minor-mode syd-repl-mode
  "A minor mode for repl buffers.  One use is to universally customise the
display of all repl buffers."
  :after-hook (format "syd-repl-mode after %s" (current-buffer)))

(defun syd--repl-from-major-mode ()
  "TODO:"
  (pcase-let ((`(_ ,fn . ,plist) (assq major-mode +syd-major-mode-repl-alist)))
    (list fn plist)))

(defun syd--clean-repl-buffers ()
  "Remove any key/value pairs from `+syd-repl-buffers' whose values involve a
not-alive buffer."
  (maphash (lambda (repl-key buffer)
             (unless (buffer-live-p buffer)
               (remhash repl-key +syd-repl-buffers)))
           +syd-repl-buffers))

(defun syd--get-repl-key ()
  (cons major-mode (syd-project-root)))

(defun syd--goto-end-of-repl ()
  "Try to move point to the last comint prompt or the end of the buffer."
  (unless (or (derived-mode-p 'term-mode)
              (eq (current-local-map) (bound-and-true-p term-raw-map)))
    (goto-char (if (and (derived-mode-p 'comint-mode)
                        (cdr comint-last-prompt))
                   (cdr comint-last-prompt)
                 (point-max)))))

(cl-defun syd--call-repl-handler (repl-handler &key plist repl-key)
  "Spawn a new repl buffer using REPL-HANDLER.  REPL-HANDLER's return value will
be returned.

If REPL-HANDLER fails to return a buffer, this `syd--call-repl-handler' will
throw an error.  `syd-repl-mode' will be enabled in the new buffer, and the
buffer will be cached in `+syd-repl-buffers'.

REPL-HANDLER will be called interactively if supported."
  (let ((repl-buffer (save-window-excursion
                           (if (commandp repl-handler)
                               (call-interactively repl-handler)
                             (funcall repl-handler)))))
    (unless repl-buffer
      (error "REPL handler %S couldn't open the REPL buffer" fn))
    (unless (bufferp repl-buffer)
      (error "REPL handler %S failed to return a buffer" fn))
    (with-current-buffer repl-buffer
      ;; It is important that `syd-repl-mode' is enabled before the buffer is
      ;; displayed by `display-fn'.
      (syd-repl-mode 1)
      (when plist
        ;; Cache the plist at `syd-repl-plist'.
        (setq syd-repl-plist plist)))
    (puthash repl-key repl-buffer +syd-repl-buffers)
    repl-buffer))

;; #+begin_src dot :file /tmp/repl.svg :results file graphics
;; digraph {
;;   bgcolor="transparent"
;;
;;   node [
;;     fillcolor=gray95
;;     color=black
;;     shape=record
;;   ]
;;
;;   "Start" [shape=diamond]
;;   "Start" -> x1
;;   x1 [label="Is the user currently in the repl buffer,\nOR has a repl handler NOT been provided?"]
;;
;;   x1 -> x2 [label="Yes"]
;;   x1 -> x3 [label="No"]
;;   x2 [label="Find entry in +syd-repl-buffers;\nis it a live buffer?"]
;;   x2 -> x4 [label="Yes"]
;;   x4 [label="Call display-fn on the\n+syd-repl-buffers entry, and use the result"]
;;   x2 -> x5 [label="No"]
;;   x5 [label="Call the provided repl-handler. Ensure it returns\na valid buffer, and pass the resultto display-fn.\nSet the plist, enable repl mode, update\n+syd-repl-buffers. Use the buffer returned by display-fn"]
;;   x3 [label="Use entry found in +syd-repl-buffers"]
;; }
;; #+end_src

(cl-defun syd--ensure-in-repl-buffer
    (&key repl-handler plist (display-fn #'get-buffer-create))
  "Display the repl buffer associated with the current major mode and project.
A repl buffer will be created (using REPL-HANDLER) if necessary.

If an active repl buffer is found in `+syd-repl-buffers', it will be displayed
by the given DISPLAY-FN.

PLIST is a plist of repl-specific options."
  (syd--clean-repl-buffers)
  (let* ((repl-key (syd--get-repl-key))
         (maybe-repl-buffer (gethash repl-key +syd-repl-buffers)))
    (cl-check-type maybe-repl-buffer (or buffer null))
    (let ((repl-buffer
           (if (or (eq maybe-repl-buffer (current-buffer))
                   (null repl-handler))
               ;; * If the current buffer is the repl buffer, we can be sure
               ;;   that it is not nil and can be returned as-is.
               ;; * If we were not given a repl-handler, there's nothing else we
               ;;   can do.  Return what was found in `+syd-repl-buffers', and
               ;;   hope it's the right thing.
               maybe-repl-buffer
             ;; If the repl buffer found in `+syd-repl-buffers' is live and
             ;; well, we can return that.  If not, we're going to have to spawn
             ;; a new repl buffer with `repl-handler' and `display-fn'.
             (if (buffer-live-p maybe-repl-buffer)
                 (funcall display-fn maybe-repl-buffer)
               (funcall display-fn
                        (syd--call-repl-handler repl-handler
                                                :plist plist
                                                :repl-key repl-key))))))
      (when (bufferp repl-buffer)
        (with-current-buffer repl-buffer
          (syd--goto-end-of-repl))
        repl-buffer))))

(defun syd--known-repls ()
  "Return a list of all known mode-repl pairs, each as a two-element list.

More precisely, the return value is a list of mode-repl pairs, where each
mode-repl pair is a two-element list (MAJOR-MODE HANDLER) where MAJOR-MODE is a
symbol, and HANDLER is a (possibly interactive) procedure.

See also: `+syd-major-mode-repl-alist'."
  (mapcar (lambda (xs) (list (car xs) (cadr xs)))
          +syd-major-mode-repl-alist))

(defun syd--pretty-mode-name (mode)
  "Convert MODE (a symbol or string) into a string appropriate for human
presentation."
  (let ((mode* (if (symbolp mode) (symbol-name mode) mode)))
    (if (not (string-match "^\\([a-z-]+\\)-mode$" mode*))
        (error "Given string/symbol is not a major mode: %s" mode*)
      (string-join (split-string
                    (capitalize (match-string-no-properties 1 mode*))
                    "-")
                   " "))))

(defun syd-prompt-for-repl ()
  "Prompt the user for a repl to open.  Returns the chosen repl-handler
function."
  ;; REVIEW: Doom scans all interned symbols for anything that looks like
  ;; "open-XXXX-repl."  Is this worth doing?
  (let* ((repls (mapcar (lambda (xs)
                               (pcase-let ((`(,mode ,fn) xs))
                                 (list (syd--pretty-mode-name mode) fn)))
                             (syd--known-repls)))
         (choice (or (completing-read "Open a REPL for: "
                                      (mapcar #'car repls))
                     (user-error "Aborting"))))
    (cadr (assoc choice repls))))

(defun syd-send-region-to-repl (beg end)
  (interactive "r")
  (let ((selection (buffer-substring-no-properties beg end))
        (buffer (syd--ensure-in-repl-buffer)))))

(cl-defun +syd-open-repl (&key prompt-p display-fn)
  "TODO: Open a repl via DISPLAY-FN.  When PROMPT-P, the user will be
unconditionally prompted for a repl choice.

If Evil-mode is active, insert state will be enabled."
  (pcase-let* ((`(,major-mode-fn ,plist) (syd--repl-from-major-mode))
               (repl-handler (if (or prompt-p (not major-mode-fn))
                                 (syd-prompt-for-repl)
                               major-mode-fn))
               (region (when (use-region-p)
                         (buffer-substring-no-properties (region-beginning)
                                                         (region-end)))))
    (unless (commandp repl-handler)
      (error "Couldn't find a REPL for %s" major-mode))
    (with-current-buffer (syd--ensure-in-repl-buffer :repl-handler repl-handler
                                                     :plist plist
                                                     :display-fn display-fn)
      ;; Start the user in insert mode at the end of the input line.
      (when (bound-and-true-p evil-mode)
        (call-interactively #'evil-append-line))
      (when region
        (insert region))
      t)))

;;;###autoload
(defun +syd/open-repl-other-window (prompt-p)
  "Like `+syd-open-repl', but opens in a different window.  The repl
corresponding to the current major mode and project will be opened, unless a
prefix argument is given, in which case the user will be prompted for a repl."
  (interactive "P")
  (+syd-open-repl :prompt-p prompt-p
                  :display-fn #'pop-to-buffer))



(provide 'syd-handle-repl)
