;;; top-level.lisp
;;;
;;; Copyright (C) 2003-2006 Peter Graves
;;; $Id$
;;;
;;; This program is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU General Public License
;;; as published by the Free Software Foundation; either version 2
;;; of the License, or (at your option) any later version.
;;;
;;; This program is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with this program; if not, write to the Free Software
;;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
;;;
;;; As a special exception, the copyright holders of this library give you
;;; permission to link this library with independent modules to produce an
;;; executable, regardless of the license terms of these independent
;;; modules, and to copy and distribute the resulting executable under
;;; terms of your choice, provided that you also meet, for each linked
;;; independent module, the terms and conditions of the license of that
;;; module.  An independent module is a module which is not derived from
;;; or based on this library.  If you modify this library, you may extend
;;; this exception to your version of the library, but you are not
;;; obligated to do so.  If you do not wish to do so, delete this
;;; exception statement from your version.

;;; Adapted from SB-ACLREPL (originally written by Kevin Rosenberg).

(in-package #:top-level)

(require 'inspect)

(defvar *null-cmd* (gensym))
(defvar *handled-cmd* (gensym))

(defvar *command-char* #\:)

(defvar *cmd-number* 1
  "Number of the next command")

(defun prompt-package-name ()
  (let ((result (package-name *package*)))
    (dolist (nickname (package-nicknames *package*))
      (when (< (length nickname) (length result))
        (setf result nickname)))
    result))

(defun repl-prompt-fun (stream)
  (fresh-line stream)
  (when (> *debug-level* 0)
    (sys::%format stream "[~D~A] "
             *debug-level*
             (if sys::*inspect-break* "i" "")))
  (sys::%format stream "~A(~D): " (prompt-package-name) *cmd-number*))

(defparameter *repl-prompt-fun* #'repl-prompt-fun)

(defun peek-char-non-whitespace (stream)
  (loop
    (let ((c (read-char stream nil)))
      (when (null c) ; control d
        (quit))
      (unless (eql c #\space)
        (unread-char c stream)
        (return c)))))

(defun apropos-command (args)
  (when args (apropos args)))

(defun continue-command (args)
  (when args
    (let ((n (read-from-string args)))
      (let ((restarts (compute-restarts)))
        (when (< -1 n (length restarts))
          (invoke-restart-interactively (nth n restarts)))))))

(defun describe-command (args)
  (let ((obj (eval (read-from-string args))))
    (describe obj)))

(defun error-command (ignored)
  (declare (ignore ignored))
  (when *debug-condition*
    (let* ((s (sys::%format nil "~A" *debug-condition*))
           (len (length s)))
      (when (plusp len)
        (setf (schar s 0) (char-upcase (schar s 0)))
        (unless (eql (schar s (1- len)) #\.)
          (setf s (concatenate 'string s "."))))
      (sys::%format *debug-io* "~A~%" s))
    (show-restarts (compute-restarts) *debug-io*)))

(defun print-frame (frame stream &key prefix)
  (when prefix
    (write-string prefix stream))
  (etypecase frame
    (sys::lisp-stack-frame
     (let ((frame (sys:frame-to-list frame)))
       (pprint-logical-block (stream nil :prefix "(" :suffix ")")
         (ignore-errors
           (prin1 (car frame) stream)
           (let ((args (cdr frame)))
             (if (listp args)
                 (format stream "~{ ~_~S~}" args)
                 (format stream " ~S" args)))))))
    (sys::java-stack-frame
     (write-string (sys:frame-to-string frame) stream))))


(defun backtrace-command (args)
  (let ((count (or (and args (ignore-errors (parse-integer args)))
                   8))
        (n 0))
    (with-standard-io-syntax
      (let ((*print-pretty* t)
            (*print-readably* nil)
            (*print-structure* nil)
            (*print-array* nil))
        (dolist (frame *saved-backtrace*)
          (fresh-line *debug-io*)
          (print-frame frame *debug-io* :prefix (format nil "~3D: " n))
          (incf n)
          (when (>= n count)
            (fresh-line *debug-io*)
            (return))))))
  (fresh-line *debug-io*)
  (values))

(defun frame-command (args)
  (let* ((n (or (and args (ignore-errors (parse-integer args)))
                0))
         (frame (nth n *saved-backtrace*)))
    (when frame
      (with-standard-io-syntax
        (let ((*print-pretty* t)
              (*print-readably* nil)
              (*print-structure* nil))
          (fresh-line *debug-io*)
          (print-frame frame *debug-io*)))
      (setf *** **
            ** *
            * frame)))
  (values))

(defun inspect-command (args)
  (let ((obj (eval (read-from-string args))))
    (inspect obj)))

(defun istep-command (args)
  (sys::istep args))

(defun macroexpand-command (args)
  (let ((s (with-output-to-string (stream)
             (pprint (macroexpand (read-from-string args)) stream))))
    (write-string (string-left-trim '(#\return #\linefeed) s)))
  (values))

(defvar *old-package* nil)

(defun package-command (args)
  (cond ((null args)
         (sys::%format *standard-output* "The ~A package is current.~%"
                  (package-name *package*)))
        ((and *old-package* (string= args "-") (null (find-package "-")))
         (rotatef *old-package* *package*))
        (t
         (when (and (plusp (length args)) (eql (char args 0) #\:))
           (setf args (subseq args 1)))
         (setf args (nstring-upcase args))
         (let ((pkg (find-package args)))
           (if pkg
               (setf *old-package* *package*
                     *package* pkg)
               (sys::%format *standard-output* "Unknown package ~A.~%" args))))))

(defun reset-command (ignored)
  (declare (ignore ignored))
  (invoke-restart 'top-level))

(defun exit-command (ignored)
  (declare (ignore ignored))
  (exit))

(defvar *old-pwd* nil)

(defun cd-command (args)
  (cond ((null args)
         (setf args (if (featurep :windows)
                        "C:\\"
                        (namestring (user-homedir-pathname)))))
        ((string= args "-")
         (if *old-pwd*
             (setf args (namestring *old-pwd*))
             (progn
               (sys::%format t "No previous directory.")
               (return-from cd-command))))
        ((and (> (length args) 1) (string= (subseq args 0 2) "~/")
              (setf args (concatenate 'string
                                      (namestring (user-homedir-pathname))
                                      (subseq args 2))))))
  (let ((dir (probe-directory args)))
    (if dir
        (progn
          (unless (equal dir *default-pathname-defaults*)
            (setf *old-pwd* *default-pathname-defaults*
                  *default-pathname-defaults* dir))
          (sys::%format t "~A" (namestring *default-pathname-defaults*)))
        (sys::%format t "Error: no such directory (~S).~%" args))))

(defun ls-command (args)
  (let ((args (if (stringp args) args ""))
        (ls-program (if (featurep :windows) "dir" "ls")))
    (run-shell-command (concatenate 'string ls-program " " args)
                       :directory *default-pathname-defaults*))
  (values))

(defun tokenize (string)
  (do* ((res nil)
        (string (string-left-trim " " string)
                (string-left-trim " " (subseq string end)))
        (end (position #\space string) (position #\space string)))
       ((zerop (length string)) (nreverse res))
    (unless end
      (setf end (length string)))
    (push (subseq string 0 end) res)))

(defvar *last-files-loaded* nil)

(defun ld-command (args)
  (let ((files (if args (tokenize args) *last-files-loaded*)))
    (setf *last-files-loaded* files)
    (dolist (file files)
      (load file))))

(defun cf-command (args)
  (let ((files (tokenize args)))
    (dolist (file files)
      (compile-file file))))

(defvar *last-files-cloaded* nil)

(defun cload-command (args)
  (let ((files (if args (tokenize args) *last-files-cloaded*)))
    (setf *last-files-cloaded* files)
    (dolist (file files)
      (load (compile-file file)))))

(defun rq-command (args)
  (let ((modules (tokenize (string-upcase args))))
    (dolist (module modules)
      (require module))))

(defun pwd-command (ignored)
  (declare (ignore ignored))
  (sys::%format t "~A~%" (namestring *default-pathname-defaults*)))

(defun trace-command (args)
  (if (null args)
    (sys::%format t "~A~%" (sys::list-traced-functions))
    (dolist (f (tokenize args))
      (sys::trace-1 (read-from-string f)))))

(defun untrace-command (args)
  (if (null args)
    (sys::untrace-all)
    (dolist (f (tokenize args))
      (sys::untrace-1 (read-from-string f)))))

(defconstant spaces (make-string 32 :initial-element #\space))

(defun pad (string width)
  (if (< (length string) width)
      (concatenate 'string string (subseq spaces 0 (- width (length string))))
      string))

(defparameter *command-table*
  '(("apropos" "ap" apropos-command "apropos")
    ("bt" nil backtrace-command "backtrace n stack frames (default 8)")
    ("cd" nil cd-command "change default directory")
    ("cf" nil cf-command "compile file(s)")
    ("cload" "cl" cload-command "compile and load file(s)")
    ("continue" "cont" continue-command "invoke restart n")
    ("describe" "de" describe-command "describe an object")
    ("error" "err" error-command "print the current error message")
    ("exit" "ex" exit-command "exit lisp")
    ("frame" "fr" frame-command "set the value of cl:* to be frame n (default 0)")
    ("help" "he" help-command "print this help")
    ("inspect" "in" inspect-command "inspect an object")
    ("istep" "i" istep-command "navigate within inspection of an object")
    ("ld" nil ld-command "load a file")
    ("ls" nil ls-command "list directory")
    ("macroexpand" "ma" macroexpand-command "macroexpand an expression")
    ("package" "pa" package-command "change *PACKAGE*")
    ("pwd" "pw" pwd-command "print current directory")
    ("reset" "res" reset-command "return to top level")
    ("rq" nil rq-command "require a module")
    ("trace" "tr" trace-command "trace function(s)")
    ("untrace" "untr" untrace-command "untrace function(s)")))

(defun %help-command (prefix)
  (let ((prefix-len (length prefix)))
    (when (and (> prefix-len 0)
               (eql (schar prefix 0) *command-char*))
      (setf prefix (subseq prefix 1))
      (decf prefix-len))
    (sys::%format t "~%  COMMAND     ABBR DESCRIPTION~%")
    (dolist (entry *command-table*)
      (when (or (null prefix)
                (and (<= prefix-len (length (entry-name entry)))
                     (string-equal prefix (subseq (entry-name entry) 0 prefix-len))))
        (sys::%format t "  ~A~A~A~%"
                 (pad (entry-name entry) 12)
                 (pad (entry-abbreviation entry) 5)
                 (entry-help entry))))
    (sys::%format t "~%Commands must be prefixed by the command character, which is '~A'~A.~%~%"
             *command-char* (if (eql *command-char* #\:) " by default" ""))))

(defun help-command (&optional ignored)
  (declare (ignore ignored))
  (%help-command nil))

(defun entry-name (entry)
  (first entry))

(defun entry-abbreviation (entry)
  (second entry))

(defun entry-command (entry)
  (third entry))

(defun entry-help (entry)
  (fourth entry))

(defun find-command (string)
  (let ((len (length string)))
    (when (and (> len 0)
               (eql (schar string 0) *command-char*))
      (setf string (subseq string 1)
            len (1- len)))
    (dolist (entry *command-table*)
      (when (or (string-equal string (entry-abbreviation entry))
                (string-equal string (entry-name entry)))
        (return (entry-command entry))))))

(defun process-cmd (form)
  (when (eq form *null-cmd*)
    (return-from process-cmd t))
  (when (and (stringp form)
             (> (length form) 1)
             (eql (char form 0) *command-char*))
    (let* ((pos (or (position #\space form)
                    (position #\return form)))
           (command-string (subseq form 0 pos))
           (args (if pos (subseq form (1+ pos)) nil)))
      (let ((command (find-command command-string)))
        (cond ((null command)
               (sys::%format t "Unknown top-level command \"~A\".~%" command-string)
               (sys::%format t "Type \"~Ahelp\" for a list of available commands." *command-char*))
              (t
               (when args
                 (setf args (string-trim (list #\space #\return) args))
                 (when (zerop (length args))
                   (setf args nil)))
               (funcall command args)))))
      t))

(defun read-cmd (stream)
  (let ((c (peek-char-non-whitespace stream)))
    (cond ((eql c *command-char*)
           (let* ((input (read-line stream))
                  (name (symbol-name (read-from-string input))))
             (if (find-command name)
                 (progn (process-cmd input) *handled-cmd*)
               (read-from-string (concatenate 'string ":" name)))))
          ((eql c #\newline)
           (read-line stream)
           *null-cmd*)
          (t
           (read stream nil *null-cmd*)))))

(defun repl-read-form-fun (in out)
  (loop
    (funcall *repl-prompt-fun* out)
    (finish-output out)
    (let ((form (read-cmd in)))
      (setf (charpos out) 0)
      (unless (eq form *null-cmd*)
        (incf *cmd-number*))
      (cond ((or (eq form *null-cmd*)
                 (eq form *handled-cmd*)))
            ((and (> *debug-level* 0)
                  (fixnump form))
             (let ((n form)
                   (restarts (compute-restarts)))
               (if (< -1 n (length restarts))
                   (invoke-restart-interactively (nth n restarts))
                   (return form))))
            (t
             (return form))))))

(defparameter *repl-read-form-fun* #'repl-read-form-fun)

(defun repl (&optional (in *standard-input*) (out *standard-output*))
    (loop
       (let* ((form (funcall *repl-read-form-fun* in out))
              (results (multiple-value-list (sys:interactive-eval form))))
         (dolist (result results)
           (fresh-line out)
           (prin1 result out)))))

(defun top-level-loop ()
  (fresh-line)
  (unless sys:*noinform*
    (sys::%format t "Type \"~Ahelp\" for a list of available commands.~%" *command-char*))
  (loop
    (setf *inspected-object* nil
          *inspected-object-stack* nil
          *inspect-break* nil)
    (with-simple-restart (top-level
                          "Return to top level.")
      (if (featurep :j)
          (handler-case
              (repl)
            (stream-error (c) (declare (ignore c)) (return-from top-level-loop)))
          (repl)))))
