;;; swank-mrepl.lisp
;;
;; Licence: public domain

(in-package :swank)
(eval-when (:compile-toplevel :load-toplevel :execute)
  (let ((api '(
	       *emacs-connection*
	       channel 
	       channel-id
	       define-channel-method
	       defslimefun 
	       dcase
	       log-event
	       process-requests
	       send-to-remote-channel
	       use-threads-p
	       wait-for-event
	       with-bindings
	       with-connection
	       with-top-level-restart
	       with-slime-interrupts
	       )))
    (eval `(defpackage #:swank-api
	     (:use)
	     (:import-from #:swank . ,api)
	     (:export . ,api)))))

(defpackage :swank-mrepl
  (:use :cl :swank-api)
  (:export #:create-mrepl))

(in-package :swank-mrepl)

(defclass listener-channel (channel)
  ((remote :initarg :remote)
   (env :initarg :env)
   (mode :initform :eval)
   (tag :initform nil)))

(defun package-prompt (package)
  (reduce (lambda (x y) (if (<= (length x) (length y)) x y))
	  (cons (package-name package) (package-nicknames package))))

(defslimefun create-mrepl (remote)
  (let* ((pkg *package*)
         (conn *emacs-connection*)
	 (thread (if (use-threads-p)
		     (spawn-listener-thread conn)
		     nil))
         (ch (make-instance 'listener-channel :remote remote :thread thread)))
    (setf (slot-value ch 'env) (initial-listener-env ch))
    (when thread
      (swank/backend:send thread `(:serve-channel ,ch)))
    (list (channel-id ch)
	  (swank/backend:thread-id (or thread (swank/backend:current-thread)))
	  (package-name pkg)
	  (package-prompt pkg))))

(defun initial-listener-env (listener)
  `((*package* . ,*package*)
    (*standard-output* . ,(make-listener-output-stream listener))
    (*standard-input* . ,(make-listener-input-stream listener))))

(defun spawn-listener-thread (connection)
  (swank/backend:spawn 
   (lambda ()
     (with-connection (connection)
       (dcase (swank/backend:receive)
	 ((:serve-channel c)
	  (loop
	   (with-top-level-restart (connection (drop-unprocessed-events c))
	     (process-requests nil)))))))
   :name "mrepl thread"))

(defun drop-unprocessed-events (channel)
  (with-slots (mode) channel
    (let ((old-mode mode))
      (setf mode :drop)
      (unwind-protect
	   (process-requests t)
	(setf mode old-mode)))
    (send-prompt channel)))

(define-channel-method :process ((c listener-channel) string)
  (log-event ":process ~s~%" string)
  (with-slots (mode remote) c
    (ecase mode
      (:eval (mrepl-eval c string))
      (:read (mrepl-read c string))
      (:drop))))

(defun mrepl-eval (channel string)
  (with-slots (remote env) channel
    (let ((aborted t))
      (with-bindings env
	(unwind-protect 
	     (let ((result (with-slime-interrupts (read-eval-print string))))
	       (send-to-remote-channel remote `(:write-result ,result))
	       (setq aborted nil))
	  (setf env (loop for (sym) in env
			  collect (cons sym (symbol-value sym))))
	  (cond (aborted
		 (send-to-remote-channel remote `(:evaluation-aborted)))
		(t
		 (send-prompt channel))))))))

(defun send-prompt (channel)
  (with-slots (env remote) channel
    (let ((pkg (or (cdr (assoc '*package* env)) *package*))
	  (out (cdr (assoc '*standard-output* env)))
	  (in (cdr (assoc '*standard-input* env))))
      (when out (force-output out))
      (when in (clear-input in))
      (send-to-remote-channel remote `(:prompt ,(package-name pkg)
					       ,(package-prompt pkg))))))
  
(defun mrepl-read (channel string)
  (with-slots (tag) channel
    (assert tag)
    (throw tag string)))

(defun read-eval-print (string)
  (with-input-from-string (in string)
    (setq / ())
    (loop
       (let* ((form (read in nil in)))
	 (cond ((eq form in) (return))
	       (t (setq / (multiple-value-list (eval (setq + form))))))))
    (force-output)
    (if /
	(format nil "~{~s~%~}" /) 
	"; No values")))

(defun make-listener-output-stream (channel)
  (let ((remote (slot-value channel 'remote)))
    (swank/backend:make-output-stream 
     (lambda (string)
       (send-to-remote-channel remote `(:write-string ,string))))))

(defun make-listener-input-stream (channel)
  (swank/backend:make-input-stream (lambda () (read-input channel))))

(defun set-mode (channel new-mode)
  (with-slots (mode remote) channel
    (unless (eq mode new-mode)
      (send-to-remote-channel remote `(:set-read-mode ,new-mode)))
    (setf mode new-mode)))

(defun read-input (channel)
  (with-slots (mode tag remote) channel
    (force-output)
    (let ((old-mode mode)
	  (old-tag tag))
      (setf tag (cons nil nil))
      (set-mode channel :read)
      (unwind-protect 
	   (catch tag (process-requests nil))
	(setf tag old-tag)
	(set-mode channel old-mode)))))

(provide :swank-mrepl)