;;; edij.el --- Emacs Does Interactive J

; These definitions let you interact with J in all buffers.
;; You can load J scripts and evaluate embedded expressions.

;; Installation
;; ============
;;
;; Copy edij.el to your load-path and add to your .emacs:
;;
;;     (require 'edij)
;;     (global-set-key [f11] 'edij-dwim)

;; Usage
;; =====
;;
;; The central function is `edij-dwim' (Do What I Mean), which is
;; bound to F11. Depending on the content at point, `edij-dwim' does
;; the "appropriate" thing: If point is on a J expression, F11 sends
;; the expression to a J process, and you interact with the process
;; in the current buffer.

;;; Code:

(defconst edij-version "1.0")

(defgroup edij nil
  "Transparent interaction with J."
  :group 'languages
  :group 'processes)

(defcustom edij-program
  (or
   (executable-find "jconsole")
   (executable-find "ijconsole")
   (executable-find "j")
   "jconsole")
  "Program name of the J executable."
  :group 'edij
  :type 'string)

(defcustom edij-program-switches nil
  "List of switches passed to the J process."
  :group 'edij
  :type '(repeat string))

(defcustom edij-prefix "NB. "
  "String to prepend when inserting output from the J process into the buffer."
  :group 'edij
  :type 'string)

(defcustom edij-max-history 80000
  "Maximal size of history buffers storing recent interactions, or
nil to never truncate the history."
  :group 'edij
  :type 'sexp)

(defvar edij-process               nil "A J process.")
(defvar edij-temp-buffer           nil "Buffer that temporarily saves process output ")
(defvar edij-seen-prompt           nil "Whether a prompt was (recently) emitted by the J process.")
(defvar edij-read-term             nil "Whether the J process waits for the user to enter a term.")
(defvar edij-indent-prefix         "" "Any whitespace occurring before the most recently executed expression.")
(defvar edij-temp-file             nil "File name of a temporary file used for loading the buffer.")
(defvar edij-load-buffer "*edij-load*" "Buffer used to display load output.")
(defvar edij-load-window        nil "Window used to show load output.")
(defvar edij-history-buffer        nil "Buffer that stores recent interactions.")
(defvar edij-interrupted           nil "True iff waiting for the previous expression was interrupted with C-g.")
(defvar edij-current-command       nil "The command currently being executed.")

(defun edij-prompt ()
  "Prompt used in the J session."
  "   ")

(defmacro edij-wait-for-prompt-after (&rest forms)
  "Evaluate FORMS and wait for prompt."
  `(progn
     (setq edij-seen-prompt nil)
     (edij-ensure-buffer "temp")
     (with-current-buffer edij-temp-buffer
       (let (buffer-read-only)
         (erase-buffer)))
     ;; execute forms with default-directory etc. from invocation buffer
     ,@forms
     (while (not edij-seen-prompt)
       (discard-input)
       (sit-for 0.1))))

(defmacro edij-remember-interruption (form)
  "Set `edij-interrupted' if evaluation of FORM was interrupted."
  `(condition-case nil
       ,form
     (quit (setq edij-interrupted t))))

(defun edij-sentinel (proc str)
  (when (and str (stringp str) (buffer-live-p (process-buffer proc)))
    (with-current-buffer (process-buffer proc)
      (let ((status (with-temp-buffer
                      (insert str)
                      (while (search-backward "\n" nil t)
                        (replace-match ""))
                      (buffer-string))))
        (edij-log
         (format "%s: %s.\n"
                 (substring (current-time-string) 4 -5) status) "green" t))
      (when (string-match "^\\(?:finished\n\\|exited abnormally\\|killed\n\\)"
                          str)
        (setq edij-seen-prompt t)))))

(defun edij-ensure-buffer (name)
  (let ((str (format "*edij-%s*" name))
        (var (intern (format "edij-%s-buffer" name))))
    (unless (buffer-live-p (symbol-value var))
      (set var (generate-new-buffer str))
      (with-current-buffer (symbol-value var)
        (buffer-disable-undo)
        (setq buffer-read-only t)))))

(defun edij-log (str &optional col nl)
  (edij-ensure-buffer "history")
  (with-current-buffer edij-history-buffer
    (let (buffer-read-only)
      (goto-char (point-max))
      (let ((s (format "%s%s" (if (and nl (not (bolp))) "\n" "") str)))
        (insert (if col (propertize s 'face `(:background ,col)) s)))
      (let ((size (- (point-max) (point-min))))
        (when (and edij-max-history
                   (> size edij-max-history))
          (delete-region (point-min) (+ (point-min) (/ size 2))))))))

(defun edij-run-j ()
  "Start a J process."
  (let ((args (cons edij-program edij-program-switches)))
    (edij-log (format "%s: starting: %S\n"
                      (substring (current-time-string) 4 -5) args)
              "green" t)
    (condition-case nil
        (progn
          (setq edij-process
                (apply #'start-file-process "edij" (current-buffer) args))
          (set-process-sentinel edij-process 'edij-sentinel)
          (set-process-filter edij-process 'edij-wait-for-prompt-filter)
          ;; Give J a moment to start up
          (sit-for 1.0)
          (setq edij-seen-prompt t))
      ((error quit)
       (edij-log "Could not start J process." "red" t)
       (error "Could not start: %s" edij-program)))))

(defun edij-kill-j ()
  "Kill the J process and run the process sentinel."
  (when (edij-running)
    (delete-process edij-process)))

(defun edij-show-load-output (str)
  (with-current-buffer (get-buffer-create edij-load-buffer)
    (setq buffer-read-only t)
    (let (buffer-read-only)
      (erase-buffer)
      (insert str)
      (goto-char (point-min)))
    (setq str (buffer-string)))
  ;; show load output in a separate window unless it is empty or just whitespace
  (unless (and (stringp str) (string-match "^[\t\n ]*$" str))
    (setq edij-load-window (display-buffer edij-load-buffer))
    (set-window-dedicated-p edij-load-window t)
    (fit-window-to-buffer edij-load-window nil nil nil nil t)))

(defun edij-load-filter (proc str)
  "Filter used when loading a file, showing load output."
  (when (and str (stringp str))  ; Check str is not nil and is string
    (with-current-buffer (edij-temp-buffer proc)
      (goto-char (point-max))
      (let (buffer-read-only)
        (insert str))
      (with-current-buffer (process-buffer proc)
        (edij-log str))
      ;; J shows prompt after processing (whitespace-based)
      (when (string-match "^[ \t]+$" str)
        (with-current-buffer (process-buffer proc)
          (setq edij-seen-prompt t)))
      (skip-chars-backward "\n")
      (edij-show-load-output (buffer-substring (point-min) (point))))))

(defun edij-wait-for-prompt-filter (proc str)
  "Filter that only waits until prompt appears."
  (when (and str (stringp str))  ; Check str is not nil and is string
    (with-current-buffer (edij-temp-buffer proc)
      (goto-char (point-max))
      (let (buffer-read-only)
        (insert str))
      (with-current-buffer (process-buffer proc)
        (edij-log str))
      ;; Check for J prompt (whitespace-based)
      (when (string-match "^[ \t]+$" str)
        (with-current-buffer (process-buffer proc)
          (setq edij-seen-prompt t))))))

;;;###autoload
(defun edij-dwim (&optional arg)
  "Load current buffer into J or evaluate expression (Do What I Mean)."
  (interactive "P")
  (cond ((eq arg 0)
         (unless (edij-running)
           (error "No J process running"))
         (edij-kill-j)
         ;;(message "J process killed.")
         )
        ((eq arg 1) (edij-load))
        ((eq arg 2) (edij-load t))
        ((eq arg 7)
         (unless (edij-more-solutions)
           (error "No expression in progress"))
         (edij-toplevel))
        ((equal arg '(4)) (edij-load) (edij-expression))
        ((equal arg '(16)) (edij-load t) (edij-expression))
        ((null arg) (unless (edij-expression) (edij-load)))
        (t (edij-remove-interactions))))

(defun edij-process-ready ()
  "Error if the previous expression is still in progress."
  (when (and edij-interrupted
             (edij-running)
             (edij-more-solutions))
    (error "Previous expression still in progress, see `edij-toplevel'"))
  (setq edij-interrupted nil))

(defun edij-find-expression-bounds ()
  "Find the bounds of the J expression at point.
Returns (START . END) or nil if no expression found."
  (save-excursion
    (let ((orig-point (point))
          start end)
      ;; Find the start of the expression
      (beginning-of-line)

      ;; Special handling: if we're on a comment line, look for verb definition start first
      (when (looking-at "^[ \t]*NB\\. ")
        ;; Look backward for verb definition start
        (let ((found-verb-start nil)
              (max-search-lines 50))
          (save-excursion
            (dotimes (i max-search-lines)
              (when (and (not found-verb-start) (not (bobp)))
                (beginning-of-line)
                (when (looking-at "^[ \t]*[a-zA-Z_][a-zA-Z0-9_]*[ \t]*=:[ \t]*[0-4][ \t]*:[ \t]*0[ \t]*\\(?:''\\)?[ \t]*$")
                  (setq found-verb-start (point)))
                (forward-line -1))))
          ;; If we found a verb start, go there
          (when found-verb-start
            (goto-char found-verb-start))))


      ;; Regular backward search for expression start
      ;; For non-comment lines, just skip empty lines and line continuations
      (while (and (not (bobp))
                  (or (looking-at "^[ \t]*$")           ; empty line
                      (looking-at "^.*\\\\[ \t]*$")))   ; line ending with \
        (forward-line -1))

      ;; Check if we're on a valid J expression line
      (when (and (not (looking-at "^[ \t]*$"))         ; not empty
                 (not (looking-at "^[ \t]*NB\\. "))    ; not comment (handled above)
                 (looking-at "^\\([ \t]*\\)\\(.*\\)$")) ; has content
        (setq start (point))
        (setq edij-indent-prefix (match-string 1))

        ;; Find the end of the expression
        (end-of-line)

        ;; Check for different multiline definition types
        (let ((is-multiline nil))
          (cond
           ;; 1. Explicit definition: n : 0 ... ) or n : 0 '' ... )
           ((save-excursion
              (beginning-of-line)
              (let ((line-text (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
                (let ((regex-result (looking-at "^[ \t]*[a-zA-Z_][a-zA-Z0-9_]*[ \t]*=:[ \t]*[0-4][ \t]*:[ \t]*0[ \t]*\\(?:''\\)?[ \t]*$")))
                  regex-result)))
            (setq is-multiline t)
            (while (and (not (eobp))
                        (not (save-excursion
                               (forward-line 1)
                               (looking-at "^[ \t]*)[ \t]*$"))))
              (forward-line 1))
            (when (not (eobp))
              (forward-line 1)  ; include the ) line
              (end-of-line)))

           ;; 2. Direct definition: {{ ... }}
           ((save-excursion
              (beginning-of-line)
              (looking-at ".*{{"))
            ;; Check if it's a single-line {{ }} definition first
            (let ((line-content (buffer-substring-no-properties
                                 (line-beginning-position)
                                 (line-end-position))))
              (if (and (string-match "{{" line-content)
                       (string-match "}}" line-content)
                       (> (string-match "}}" line-content)
                          (string-match "{{" line-content)))
                  ;; Single-line {{ }} - don't set multiline, just go to end of line
                  nil
                ;; Multi-line {{ }} definition
                (setq is-multiline t)
                (let ((brace-count 1))  ; we've seen one {{
                  (while (and (> brace-count 0) (not (eobp)))
                    (forward-line 1)
                    (beginning-of-line)
                    (while (not (eolp))
                      (cond
                       ;; Skip comments inside verb definitions
                       ((looking-at "NB\\. ")
                        (end-of-line))
                       ((looking-at "{{")
                        (setq brace-count (1+ brace-count))
                        (forward-char 2))
                       ((looking-at "}}")
                        (setq brace-count (1- brace-count))
                        (forward-char 2))
                       (t (forward-char 1))))
                    (end-of-line))))))

           ;; 3. Regular multi-line expressions ending with \
           (t
            (while (and (not (eobp))
                        (save-excursion
                          (beginning-of-line)
                          (looking-at ".*\\\\[ \t]*$")))
              (forward-line 1)
              (end-of-line))))

          ;; Don't skip empty lines - keep expression bounds tight
          )

        (setq end (point))

        ;; Only return bounds if original point was within this range
        (when (<= start orig-point end)
          (cons start end))))))

(defun edij-find-next-expression ()
  "Find the start of the next valid J expression after point.
Skips over comments, empty lines, and output lines."
  (save-excursion
    (let ((found nil)
          (start-pos (point)))
      ;; Move past current line
      (forward-line 1)
      (beginning-of-line)

      ;; Search for next valid expression
      (while (and (not (eobp)) (not found))
        (cond
         ;; Skip empty lines
         ((looking-at "^[ \t]*$")
          (forward-line 1))
         ;; Skip comment lines (including NB. output lines)
         ((looking-at "^[ \t]*NB\\. ")
          (forward-line 1))
         ;; Found a valid expression line
         ((looking-at "^\\([ \t]*\\)\\(.*\\)$")
          (let ((content (match-string 2)))
            (when (and (> (length content) 0)
                       (not (string-match "^[ \t]*$" content)))
              (setq found (point)))))
         ;; Skip other lines
         (t (forward-line 1))))
      found)))

(defun edij-expression ()
  "If point is on an expression, send it to the process and start interaction."
  (edij-process-ready)
  (when (and (not (and transient-mark-mode mark-active)))
    (let ((bounds (edij-find-expression-bounds)))
      (when bounds
        ;; Save current position to mark ring (for '' in Evil mode)
        (push-mark (point) t)

        (let* ((start (car bounds))
               (end (cdr bounds))
               (expression (buffer-substring-no-properties start end))
               ;; Clean up the expression - remove line continuations
               (clean-expr (if (stringp expression)
                               (replace-regexp-in-string "\\\\[ \t]*\n[ \t]*" " " expression)
                             expression))
               (handle (and (fboundp 'prepare-change-group)
                            (fboundp 'undo-amalgamate-change-group)
                            (cons t (prepare-change-group)))))
          ;; Evaluate the expression with cursor positioned for output insertion
          (save-excursion
            (goto-char end)
            (end-of-line)
            (edij-interact (format "%s\n" clean-expr)))
          (when handle
            (undo-amalgamate-change-group (cdr handle)))

          ;; Move to next valid expression if found
          (let ((next-expr-pos (save-excursion
                                 (goto-char end)
                                 (end-of-line)
                                 (edij-find-next-expression))))
            (when next-expr-pos
              (goto-char next-expr-pos)))
          t)))))

;;;###autoload
(defun edij-interact (expression)
  "Send EXPRESSION to J process and interact as on a terminal."
  (unless (edij-running)
    (edij-run-j))
  (set-process-buffer edij-process (current-buffer))
  (set-process-filter edij-process 'edij-interact-filter)
  (edij-ensure-buffer "temp")
  (with-current-buffer edij-temp-buffer
    (let (buffer-read-only)
      (erase-buffer)))
  (setq edij-seen-prompt nil
        edij-read-term nil)
  ;; Store the command being executed for display purposes
  (setq-local edij-current-command (if (stringp expression) (string-trim expression) expression))
  (edij-send-string expression)
  (edij-toplevel))

(defun edij-send-string (str)
  "Send string to J process and log it."
  (edij-log str "cyan")
  (process-send-string edij-process str))

(defun edij-toplevel ()
  "Start or resume J toplevel interaction in the buffer."
  (interactive)
  (when edij-process
    (select-window (display-buffer (process-buffer edij-process))))
  (edij-remember-interruption
   (while (edij-more-solutions)
     ;; Wait for output and process it
     (accept-process-output edij-process 0.1)
     (sit-for 0.05))))

;;;###autoload
(defun edij-remove-interactions ()
  "Remove all lines starting with `edij-prefix' from buffer."
  (interactive)
  (save-excursion
    (save-restriction
      (when (and transient-mark-mode mark-active)
        (narrow-to-region (region-beginning) (region-end)))
      (goto-char (point-min))
      (flush-lines (concat "^[\t ]*" (regexp-quote edij-prefix)))))
  (message "Interactions removed."))

;;;###autoload
(defun edij-load (&optional new-process)
  "Buffer is loaded into a J process."
  (interactive)
  (when (string= (buffer-name) edij-load-buffer)
    (error "Cannot load the load buffer"))
  (when (window-live-p edij-load-window)
    (condition-case nil
        (delete-window edij-load-window)
      (error nil)))
  (when (buffer-live-p edij-load-buffer)
    (bury-buffer edij-load-buffer))
  (when new-process
    (edij-kill-j))
  (unless (edij-running)
    (edij-run-j))
  (edij-process-ready)
  (set-process-buffer edij-process (current-buffer))
  (when (or (null edij-temp-file)
            (and buffer-file-name
                 (not (equal (file-remote-p edij-temp-file)
                             (file-remote-p buffer-file-name)))))
    (setq edij-temp-file
          (funcall (if (fboundp 'make-nearby-temp-file)
                       'make-nearby-temp-file
                     'make-temp-file)
                   "edij" nil ".ijs")))
  (let ((start (if (and transient-mark-mode mark-active)
                   (region-beginning) (point-min)))
        (end (if (and transient-mark-mode mark-active)
                 (region-end) (point-max))))
    (write-region start end edij-temp-file nil 'silent))
  (set-process-filter edij-process 'edij-load-filter)
  (edij-remember-interruption
   (edij-wait-for-prompt-after
    (edij-send-string (format "load '%s'\n"
                              (if (fboundp 'file-local-name)
                                  (file-local-name edij-temp-file)
                                edij-temp-file)))))
  (message "%s loaded." (if (and transient-mark-mode mark-active)
                            "Region" "Buffer")))

(defun edij-running ()
  "True iff `edij-process' is a running process."
  (and (processp edij-process)
       (eq (process-status edij-process) 'run)))

(defun edij-more-solutions ()
  "True iff there could be more solutions from the process."
  (not edij-seen-prompt))

(defun edij-interact-filter (proc string)
  "Insert output from the process and update the state."
  (when (and string  ; Check that string is not nil
             (stringp string)  ; Check that string is actually a string
             (buffer-live-p (edij-temp-buffer proc))
             (buffer-live-p (process-buffer proc)))
    (with-current-buffer (edij-temp-buffer proc)
      (goto-char (point-max))
      (let (buffer-read-only)
        (insert string))
      (with-current-buffer (process-buffer proc)
        (edij-log string))

      ;; Only process output when we see the J prompt (indicating completion)
      (goto-char (point-max))
      (when (re-search-backward "^[ \t]+$" nil t)
        ;; Found the prompt - J is done processing
        (with-current-buffer (process-buffer proc)
          (setq edij-seen-prompt t))
        (set-process-filter proc 'edij-ignore-filter)

        ;; Get all output up to (but not including) the prompt line
        (forward-line 0)  ; go to beginning of prompt line
        (let ((str (string-trim-right (buffer-substring (point-min) (point)))))
          ;; Clear the temp buffer now that we've processed the output
          (let (buffer-read-only)
            (erase-buffer))

          ;; Process the complete output
          (if (and str
                   (stringp str)  ; Check str is actually a string
                   (> (length (string-trim str)) 0))
              ;; Handle case with output
            (progn
              (with-temp-buffer
                ;; Add the J output
                (insert str)
                (goto-char (point-min))

                ;; Check if the output already contains the command (J echo)
                (let ((last-cmd (buffer-local-value 'edij-current-command (process-buffer proc)))
                      (already-has-command nil))
                  (when last-cmd
                    ;; Look for the command in the first few lines (allow for whitespace variations)
                    (save-excursion
                      (dotimes (_ 3) ; Check first 3 lines
                        (when (and (not (eobp))
                                   ;; Match the command with flexible whitespace
                                   (looking-at (concat "^[ \t]*" (regexp-quote (string-trim last-cmd)) "[ \t]*$")))
                          (setq already-has-command t))
                        (when (not (eobp))
                          (forward-line 1)))))

                  ;; Only add command prefix if not already present
                  (unless already-has-command
                    (goto-char (point-min))
                    (let ((indent (buffer-local-value 'edij-indent-prefix (process-buffer proc)))
                          (prefix (buffer-local-value 'edij-prefix (process-buffer proc))))
                      (when (and indent last-cmd)
                        (insert indent last-cmd "\n"))))

                ;; Add prefix to each output line
                (goto-char (point-min))
                (while (not (eobp))
                  ;; Add prefix to ALL lines, including empty ones (important for J array structure)
                  (let ((indent (buffer-local-value 'edij-indent-prefix (process-buffer proc)))
                        (prefix (buffer-local-value 'edij-prefix (process-buffer proc))))
                    (when (and indent prefix)  ; Check both are not nil
                      (insert indent prefix)))
                  (forward-line 1)))
                (setq str (buffer-string)))
              (with-current-buffer (process-buffer proc)
                (when (stringp str)  ; Final check before inserting
                  (edij-insert-at-marker str))))
          ;; Handle case with no output - show minibuffer message
          (with-current-buffer (process-buffer proc)
            (let ((last-cmd (buffer-local-value 'edij-current-command (process-buffer proc))))
              (when (and last-cmd (stringp last-cmd))
                (message "Executed: %s" (string-trim last-cmd)))))))))))

(defun edij-insert-at-marker (&rest args)
  "Insert strings ARGS below the current line."
  (save-excursion
    ;; Go to end of current line
    (end-of-line)
    (insert "\n")
    ;; Insert the J output
    (apply #'insert args)
    ))

(defun edij-ignore-filter (proc str)
  "Log and then ignore all process output."
  (when (and str (stringp str))  ; Check str is not nil and is string
    (with-current-buffer (process-buffer proc)
      (edij-log str "gray"))))

(defun edij-temp-buffer (proc)
  (with-current-buffer (process-buffer proc)
    edij-temp-buffer))

(defun edij-map-variables (func)
  "Call FUNC with all edij variables that can become buffer-local."
  (mapc func '(edij-process
               edij-program
               edij-program-switches
               edij-temp-buffer
               edij-history-buffer
               edij-seen-prompt
               edij-interrupted
               edij-read-term
               edij-indent-prefix
               edij-prefix
               edij-temp-file
               edij-current-command)))

;;;###autoload
(defun edij-localize ()
  "After `edij-localize', any J process started from
this buffer becomes buffer-local."
  (interactive)
  (unless (local-variable-p 'edij-process)
    (edij-map-variables #'make-local-variable)
    (setq edij-temp-file nil
          edij-process nil
          edij-history-buffer nil
          edij-temp-buffer nil)))

(defun edij-kill-j ()
  "Kill the running J process."
  (interactive)
  (when (and edij-process (processp edij-process))
    ;; Set a custom sentinel to suppress the default "Process killed" message
    (set-process-sentinel edij-process (lambda (proc event) nil))
    ;; Also prevent query on exit
    (set-process-query-on-exit-flag edij-process nil)
    ;; Now kill the process
    (delete-process edij-process)
    (setq edij-process nil)
    (when (and edij-temp-buffer (buffer-live-p edij-temp-buffer))
      (kill-buffer edij-temp-buffer)
      (setq edij-temp-buffer nil))
    (when (and edij-history-buffer (buffer-live-p edij-history-buffer))
      (kill-buffer edij-history-buffer)
      (setq edij-history-buffer nil))
    (message "J process killed.")))

(defun edij-run-j ()
  "Start a new J process."
  (interactive)
  (unless (edij-running)
    (setq edij-process
          (start-process "J" "*J*" edij-program))
    (setq edij-temp-buffer (generate-new-buffer " *edij-temp*"))
    (setq edij-history-buffer (generate-new-buffer " *edij-history*"))
    (message "J process started.")))

(defun edij-unlocalize ()
  "Revert the effect of `edij-localize'."
  (interactive)
  (when (local-variable-p 'edij-process)
    (edij-kill-j)
    (edij-map-variables #'kill-local-variable)))

(provide 'edij)

;; ~byakuren
;;    ^ ^
;;  =(o.o)=
