;;; forth-interaction-mode.el --- Interaction mode Forth -*-lexical-binding: t-*-

(eval-when-compile (byte-compile-disable-warning 'cl-functions))

(require 'comint)
(require 'forth-mode)

(defvar forth-interaction-buffer nil)
(defvar forth-interaction-source-buffer nil)
(defvar forth-interaction-callback nil)
(defvar forth-words-cache nil)
(defvar forth-implementation nil)
(defvar forth-banner "")
(defvar forth-backend-dir
  (concat (file-name-directory load-file-name) "backend"))

(defvar forth-implementation-matches
  '(("Gforth" . gforth)
    ("SP-FORTH" . spforth)
    ("PForth" . pforth)
    ("VFX Forth" . vfxforth)
    ("SwiftForth" . swiftforth)
    ("lbForth" . lbforth)))

(defvar forth-interaction-mode-map
  (let ((map (copy-keymap forth-mode-map)))
    (set-keymap-parent map comint-mode-map)
    (define-key map (kbd "C-c C-f") 'forth-restart)
    (define-key map (kbd "C-c C-z") 'forth-switch-to-source-buffer)
    map)
  "Keymap for Forth interaction.")

(define-derived-mode forth-interaction-mode comint-mode "Forth Interaction"
  "Major mode for interacting with Forth."
  :syntax-table forth-mode-syntax-table
  (use-local-map forth-interaction-mode-map))

(defvar forth-interaction-init-backend-hook '())

(defun forth-interaction-preoutput-filter (text)
  (unless forth-implementation
    (setq forth-banner (concat forth-banner text))
    (dolist (x forth-implementation-matches)
      (when (string-match (car x) forth-banner)
	(setq forth-implementation (cdr x))
	(let ((load-path (cons forth-backend-dir load-path)))
	  (require forth-implementation))
	(run-hook-with-args 'forth-interaction-init-backend-hook
			    forth-implementation
			    (get-buffer-process (current-buffer))))))
  (if forth-interaction-callback
      (funcall forth-interaction-callback text)
      text))

;;;###autoload
(defun forth-kill (&optional buffer)
  (interactive)
  (setq buffer (or buffer forth-interaction-buffer))
  (when (get-buffer-process buffer)
    (set-process-query-on-exit-flag (get-buffer-process buffer) nil))
  (kill-buffer buffer)
  (setq forth-interaction-buffer nil))

(defun forth-interaction-sentinel (proc arg)
  (message "Forth: %s" arg)
  ;;FIXME: Can't do this because it calls process-mark, which
  ;; errors out in killed processes.  Still, would be nice to see
  ;; something in the *forth* buffer.
  ;;(comint-output-filter proc (format "\nForth: %s\n" arg))
  )

(defvar forth-executable nil)

(defvar run-forth-hooks)

;;;###autoload
(defun run-forth ()
  "Start an interactive forth session."
  (interactive)
  (setq forth-implementation nil)
  (setq forth-banner "")
  (unless forth-executable
    (setq forth-executable
	  (read-string "Forth executable: ")))
  (let ((buffer (get-buffer-create "*forth*")))
    (pop-to-buffer buffer)
    (unless (comint-check-proc buffer)
      (run-hooks 'run-forth-hooks)
      (make-comint-in-buffer "forth" buffer forth-executable)
      (set-process-window-size (get-buffer-process buffer)
			       (window-height) (window-width))
      (set-process-sentinel (get-buffer-process buffer)
			    'forth-interaction-sentinel)
      (forth-interaction-mode)
      (add-hook 'comint-preoutput-filter-functions
		'forth-interaction-preoutput-filter nil t)
      (setq forth-interaction-buffer buffer))))

;;;###autoload
(defun forth-restart ()
  (interactive)
  (forth-kill)
  (run-forth))

(defun forth-ensure ()
  (unless (buffer-live-p forth-interaction-buffer)
    (run-forth))
  (get-buffer-process forth-interaction-buffer))

(defun forth-scrub (string &optional keep-ok)
  "Remove terminal escape sequences from STRING."
  (let ((n 0))
    (while (setq n (string-match "[?[0-9;]*[a-z]" string n))
      (setq string (replace-match "" t t string))))
  (setq string (replace-regexp-in-string "\\`[[:space:]\n]*" "" string))
  (setq string (replace-regexp-in-string "[[:space:]\n]*\\'" "" string))
  (if keep-ok
      string
    (setq string (replace-regexp-in-string "ok\\'" "" string))
    (setq string (replace-regexp-in-string "[[:space:]\n]*\\'" "" string))))

(defun forth-interaction-send-raw-result (&rest strings)
  (let* ((proc (forth-ensure))
	 (forth-result nil)
	 (forth-interaction-callback (lambda (x)
				       (setq forth-result (concat forth-result x))
				       ""))
	 (end-time (+ (float-time) .4)))
    (dolist (s strings)
      (comint-send-string proc s))
    (comint-send-string proc "\n")
    (while (< (float-time) end-time)
      (accept-process-output proc 0.1))
    (setq forth-words-cache nil)
    forth-result))

;;;###autoload
(defun forth-interaction-send (&rest strings)
  (forth-scrub (apply #'forth-interaction-send-raw-result strings)))

;;;###autoload
(defun forth-words ()
  (when forth-interaction-buffer
    (or forth-words-cache
	(setq forth-words-cache
	      (split-string (forth-interaction-send "words"))))))

;;;###autoload
(defun forth-eval (string)
  (interactive "sForth expression: ")
  (message "%s" (forth-interaction-send string)))

;;;###autoload
(defun forth-eval-region (start end)
  (interactive "r")
  (forth-eval (buffer-substring start end)))

;;;###autoload
(defun forth-eval-defun ()
  (interactive)
  (save-excursion
    (mark-defun)
    (forth-eval-region (point) (mark))))

;;;###autoload
(defun forth-load-file (file)
  (interactive (list (buffer-file-name (current-buffer))))
  (save-some-buffers)
  (let ((result (forth-interaction-send-raw-result (format "s\" %s\" included" file))))
    (setq result (forth-scrub result t))
    (if (< (cl-count ?\n result) 2)
	(message "%s" result)
      (pop-to-buffer forth-interaction-buffer))
    (comint-output-filter (get-buffer-process forth-interaction-buffer)
			  (concat result "\n"))))

;;;###autoload
(defun forth-see (word)
  (interactive (list (forth-word-at-point)))
  (let ((buffer (get-buffer-create "*see*")))
    (pop-to-buffer buffer)
    (let ((inhibit-read-only t))
      (erase-buffer)
      (insert (forth-interaction-send "see " word)))
    (special-mode)))

(defun forth-switch-to-buffer (buffer)
  ;; If buffer is visible, switch to that window.  Otherwise, display
  ;; buffer in current window.
  (select-window (display-buffer buffer
				 '((display-buffer-reuse-window
				    display-buffer-same-window)))))

;;;###autoload
(defun forth-switch-to-output-buffer ()
  (interactive)
  (if forth-interaction-buffer
      (progn
	(setq forth-interaction-source-buffer (current-buffer))
	(forth-switch-to-buffer forth-interaction-buffer))
      (message "Forth not started.")))

;;;###autoload
(defun forth-switch-to-source-buffer ()
  (interactive)
  (if forth-interaction-source-buffer
      (forth-switch-to-buffer forth-interaction-source-buffer)
    (message "Don't know which buffer to switch to.")))

;;;###autoload
(defun forth-eval-last-expression ()
  (interactive)
  (save-excursion
    (backward-sexp)
    (let ((start (point)))
      (forward-sexp)
      (forth-eval-region start (point)))))

;;;###autoload
(defun forth-eval-last-expression-display-output ()
  (interactive)
  (if forth-interaction-buffer
      (save-excursion
	(backward-sexp)
	(let ((start (point)))
	  (forward-sexp)
	  (let ((string (buffer-substring start (point))))
	    (forth-switch-to-output-buffer)
	    (insert (forth-interaction-send string)))))
      (message "Forth not started.")))

(provide 'forth-interaction-mode)
