; Copyright (c) 1993, 1994 Richard Kelsey and Jonathan Rees.  See file COPYING.


; This is file command.scm.

; Command processor.

; The command processor's state is of three kinds:
; 1. User context - preserved across dump commands.
;    This includes the designated user and configuration environments.
; 2. Session state - one per "login"; not preserved across dump commands.
;    This includes ## and the command loop's interactive ports.
; 3. Command levels - one for each different command level.
;    This includes any condition being handled, and continuations.

(define command-prefix #\,)


; User context.

(define (make-user-context thunk)
  (let ((t (make-table)))
    (let-fluid $user-context t
      (lambda ()
        (for-each (lambda (name+thunk)
                    (table-set! t (car name+thunk) ((cdr name+thunk))))
                  *user-context-initializers*)
        (thunk)
        t))))

(define *user-context-initializers* '())

(define $user-context (make-fluid #f))
; (set-fluid! $user-context (make-user-context unspecific)) ;Bad for GC
(define (user-context) (fluid $user-context))

(define (user-context-accessor name initializer)
  (set! *user-context-initializers*
        (append *user-context-initializers*
                (list (cons name initializer))))
  (let ((probe (fluid $user-context)))
    (if probe (table-set! probe name (initializer))))
  (lambda ()
    (table-ref (user-context) name)))

(define (user-context-modifier name)
  (lambda (new)
    (table-set! (user-context) name new)))


; Session state.

(define session-type
  (make-record-type 'session '(input output values batch-mode? bow?)))
(define make-session
  (record-constructor session-type
                      '(input output values batch-mode? bow?)))
(define $session
  (make-fluid (make-session (current-input-port)
                            (current-output-port)
                            '() #f #f)))
(define (session-accessor name)
  (let ((a (record-accessor session-type name)))
    (lambda () (a (fluid $session)))))
(define (session-modifier name)
  (let ((m (record-modifier session-type name)))
    (lambda (new) (m (fluid $session) new))))
(define command-input (session-accessor 'input))
(define command-output (session-accessor 'output))
(define focus-values (session-accessor 'values))
(define set-focus-values! (session-modifier 'values))
(define batch-mode? (session-accessor 'batch-mode?))
(define set-batch-mode?! (session-modifier 'batch-mode?))
(define break-on-warnings? (session-accessor 'bow?))
(define set-break-on-warnings?! (session-modifier 'bow?))



; Command levels.

(define $command-levels (make-fluid '()))
(define (command-level) (car (fluid $command-levels)))

(define :command-level
  (make-record-type 'command-level
                    '(throw vm-cont condition interrupts
			    ;; env
			    )))
(define make-command-level
  (record-constructor :command-level
                      '(throw vm-cont condition interrupts
			      ;; env
			      )))
(define command-level? (record-predicate :command-level))
(define command-level-throw
  (record-accessor :command-level 'throw))
(define command-level-vm-cont
  (record-accessor :command-level 'vm-cont))
(define command-level-condition
  (record-accessor :command-level 'condition))
(define command-level-interrupts
  (record-accessor :command-level 'interrupts))
;(define command-level-env
;  (record-accessor :command-level 'env))
;(define set-command-level-env!
;  (record-modifier :command-level 'env))

(define environment-for-commands interaction-environment)

; --------------------
; Main entry point.

(define (with-user-context context thunk)
  (let-fluid $user-context context thunk))	; Log in.


(define (with-new-session context iport oport resume-args batch? thunk)
  (let-fluids $user-context context
	      $command-levels '()
	      $session (make-session iport oport resume-args batch? #f)
    thunk))


;; The double-paren around the w-n-s is because it returns a continuation
;; which is the thing to do after the command-processor exits.

(define (start-command-processor resume-args context
				 ;; initial-env
				 start-thunk)
  (interrupt-before-heap-overflow!)
  ((with-new-session context
		     (current-input-port) (current-output-port)
		     resume-args (and (pair? resume-args)
				      (equal? (car resume-args) "batch"))
     (lambda ()
       (command-loop start-thunk #f
		     ;; initial-env
		     )))))

; Entry for initialization & testing.

(define (command-processor command-env args) 
  (start-command-processor args
                           (make-user-context
			    (lambda ()
			      (set-user-command-environment! command-env)))
                           ;; (interaction-environment)
                           unspecific))

; Command loop
; Uses:
;  1. startup, 2. condition handler, 3. abort-to-level, 4. breakpoint

(define (command-loop start-thunk condition
		      ;; env
		      )
  (call-with-command-level condition ;; env
    (lambda (level)
      (start-command-level start-thunk level))))

(define (call-with-command-level condition
				 ;; env
				 proc)
  (primitive-catch
    (lambda (vm-cont)
      ((call-with-current-continuation
         (lambda (throw)
           (proc (make-command-level throw vm-cont condition
                                     (enabled-interrupts)
                                     ;; env
				     ))))))))

(define (start-command-level start-thunk level)
  (with-handler command-loop-condition-handler
    (lambda ()
      (let-fluids $command-levels (cons level (fluid $command-levels))
                  $note-undefined #f    ;useful
        (lambda ()
          ;;(with-interaction-environment (command-level-env level)
            ;;(lambda ()
              (start-thunk)
              (let ((condition (command-level-condition level)))
                (if condition
                    (display-condition condition (command-output)))
                (if (not (= (enabled-interrupts) all-interrupts))
                    (begin (if (not (and (interrupt? condition)
                                         (= (caddr condition) all-interrupts)))
                               (write-line "(Enabling interrupts)"
                                           (command-output)))
                           (set-enabled-interrupts! all-interrupts))))
              (let loop ()
                (let ((command (read-command-carefully (command-prompt)
                                                       (form-preferred?)
                                                       (command-input))))
                  (showing-focus-object
                   (lambda ()
                     (execute-command command)))
                  (loop))))))));;))

(define form-preferred?
  (user-context-accessor 'form-preferred? (lambda () #t)))

; Command level control

(define (pop-command-level)
  (let ((levels (fluid $command-levels)))
    (if (null? (cdr levels))
        (if (or (batch-mode?)
                (y-or-n? "Exit the Scheme Shell" #t))
            (exit-command-processor (lambda () 0))
            (abort-to-command-level (car levels)))
        (abort-to-command-level (cadr levels)))))

(define (abort-to-command-level level)
  (throw-to-command-level
       level
       (lambda ()
         (start-command-level
          (lambda ()
            (cond ((command-level-condition level)
                   (display "Back to" (command-output)))
                  ((null? (fluid $command-levels))
                   (newline (command-output))
                   (write-line "Top level" (command-output)))))
          ;; Condition will be displayed.
          level))))

(define (throw-to-command-level level thunk)
  ((command-level-throw level) thunk))

(define (exit-command-processor thunk)
  (throw-to-command-level (top-command-level)
                          (lambda () thunk)))

; Condition handler

(define (command-loop-condition-handler c next-handler)
  (cond ((or (warning? c) (note? c))
         (if (break-on-warnings?)
             (deal-with-condition c)
             (begin (display-condition c (command-output))
                    (unspecific))))     ;proceed
        ((or (error? c) (interrupt? c))
         (if (batch-mode?)
             (begin (display-condition c (command-output))
                    (let ((status (if (error? c) 1 2)))
                      (exit-command-processor (lambda () status))))
             (deal-with-condition c)))
        (else                           
         (next-handler))))

(define push-command-levels?
  (user-context-accessor 'push-command-levels (lambda () #t)))
;(define set-push-command-levels?!
;  (user-context-modifier 'push-command-levels))


(define (deal-with-condition c)
  (if (push-command-levels?)
      (command-loop list c
		    ;; (interaction-environment)
		    )
      (call-with-command-level c ;; (interaction-environment)
        (lambda (level)
          (set-focus-object! level)
          (display-condition c (command-output))
          (abort-to-command-level (car (fluid $command-levels)))))))


(define-condition-type 'note '())
(define note? (condition-predicate 'note))

(define (command-prompt)
  (let ((level (- (length (fluid $command-levels)) 1))
	(id (environment-id-string (environment-for-commands))))
    (string-append (if (= level 0)
		       ""
		       (number->string level))
		   (if (or (= level 0) (= (string-length id) 0))
		       ""
		       " ")
		   id
		   "> ")))

(define-generic environment-id-string &environment-id-string (env))

(define-method &environment-id-string (env) "")


; Evaluate a form

(define (evaluate-and-select form env)
  (call-with-values (lambda ()
                      (evaluate form env))
    (lambda results
      (if (or (null? results)
              (not (null? (cdr results)))
              (not (eq? (car results) (unspecific))))
          (set-focus-values! results))
      (apply values results))))

(define-generic evaluate &evaluate (form env))

(define-method &evaluate (form env) (eval form env))


; Display the focus object if it changes (sort of like emacs's redisplay)

(define (showing-focus-object thunk)
  (let ((focus-before (focus-values)))
    (thunk)
    (let ((focus-after (focus-values)))
      (if (not (eq? focus-after focus-before))
          (show-command-results focus-after)))))


(define (focus-object)
  (let ((v (focus-values)))
    (if (and (pair? v) (null? (cdr v))) (car v) v)))

(define (set-focus-object! obj)
  (set-focus-values! (list obj)))


(define (show-command-results results)
  (cond ((null? results))
        ((not (null? (cdr results)))
         (let ((out (command-output)))
           (display "; " out)
           (write (length results) out)
           (display " values" out)
           (newline out))
         (for-each show-command-result results))
        (else ;(not (eq? (car results) (unspecific)))
         (show-command-result (car results)))))

(define (show-command-result result)
  (write-carefully (value->expression result)
                   (command-output))
  (newline (command-output)))

(define $write-depth (make-fluid -1))
(define $write-length (make-fluid -1))

(define (write-carefully x port)
  (if (error? (ignore-errors (lambda ()
                               (limited-write x port
                                              (fluid $write-depth)
                                              (fluid $write-length))
                               #f)))
      (display "<Error while printing.>" port)))


; Sentinels - run after every command.

(define *sentinels* '())
(define (run-sentinels)
  (for-each (lambda (sentinel) (sentinel)) *sentinels*))
(define (add-sentinel! sentinel)
  (if (not (memq sentinel *sentinels*))
      (set! *sentinels* (cons sentinel *sentinels*))))



; Commands.

(define command-environment
  (user-context-accessor 'command-environment interaction-environment))

;(define *command-structure* (unspecific))
;
;(define (command-structure)
;  *command-structure*)
;
;(define (set-command-structure! structure)  ; called on initial startup
;  (set! *command-structure* structure))

(define command-syntax-table (make-table))
(define *command-help* '())

(define (get-command-syntax name)
  (or (table-ref (user-command-syntax-table) name)
      (table-ref command-syntax-table name)))

(define (define-command-syntax name help1 help2 arg-descriptions)
  (table-set! command-syntax-table name arg-descriptions)
  (if help1
      (set! *command-help* (add-help *command-help* name help1 help2))))

(define (add-help help name help1 help2)  
  (insert (list name
                (string-append (symbol->string name) " " help1)
                help2)
          help
          (lambda (z1 z2)
            (string<=? (cadr z1) (cadr z2)))))

(define user-command-syntax-table
  (user-context-accessor 'user-command-syntax-table (lambda () (make-table))))

(define user-command-environment
  (user-context-accessor 'user-command-environment (lambda () #f)))

(define set-user-command-environment!
  (user-context-modifier 'user-command-environment))

(define user-command-help
  (user-context-accessor 'user-command-help (lambda () *command-help*)))

(define set-user-command-help!
  (user-context-modifier 'user-command-help))

(define (define-user-command-syntax name help1 help2 arg-descriptions)
  (table-set! (user-command-syntax-table) name arg-descriptions)
  (if help1
      (set-user-command-help!
           (add-help (user-command-help) name help1 help2))))

(define (execute-command command)
  (cond ((eof-object? command)
         (newline (command-output))
         (pop-command-level))
        ((not command))       ; error while reading
        (else
         (let* ((name (car command))
		(proc (evaluate name (user-command-environment))))
	   (dynamic-wind (lambda () #f)
			 (lambda ()
			   (apply proc (cdr command)))
			 run-sentinels)))))

; help

(define (help . maybe-id)
  (if (null? maybe-id)
      (list-commands)
      (print-command-help (car maybe-id))))

(define (print-command-help id)
  (let ((o-port (command-output)))
    (display #\space o-port)
    (cond ((assq id (user-command-help))
           => (lambda (data)
                (if (form-preferred?) (display command-prefix o-port))
                (display (cadr data) o-port)
                (display "    " o-port)
                (display (caddr data) o-port)))
          (else
           (display #\" o-port)
           (display id o-port)
           (display #\" o-port)
           (display #\space o-port)
           (display "is not a command.")))
    (newline o-port)))

(define (list-commands)
  (let ((o-port (command-output))
        (widest 28)
        (f? (form-preferred?)))
    (for-each (lambda (s)
                (write-line s o-port))
              '(
"This is an alpha-test version of Scheme 48.  You are interacting with"
"the command processor.  A command is either a Scheme form to evaluate"
"or one of the following:"
""))

    (list-command-help (user-command-help) f? o-port)
    (for-each (lambda (s)
                (write-line s o-port))
              '(
""
"Square brackets [...] indicate optional arguments."
""
"The expression ## evaluates to the last value displayed by the command"
"processor."
                ))))

(define (list-command-help data prefix? o-port)
  (let* ((strings (map (if prefix?
                           (lambda (d)
                             (string-append (command-prefix-string
                                             command-prefix)
                                            (cadr d)))
                           cadr)
                       data))
         (count (length strings))
         (back-half (list-tail strings (quotient (+ 1 count) 2))))
    (let loop ((s1 strings) (s2 back-half))
      (cond ((not (eq? s1 back-half))
             (display #\space o-port)
             (display (car s1) o-port)
             (write-spaces (max 1 (- 32 (string-length (car s1)))) o-port)
             (if (not (null? s2))
                 (display (car s2) o-port))
             (newline o-port)
             (loop (cdr s1) (if (null? s2) s2 (cdr s2))))))))
                   

; Utilities

(define (top-command-level)
  (last (fluid $command-levels)))

(define (error-form proc args)
  (cons proc (map value->expression args)))

(define (value->expression obj)         ;mumble
  (if (or (number? obj) (char? obj) (string? obj) (boolean? obj))
      obj
      `',obj))

(define (write-spaces count o-port)
  (do ((count count (- count 1)))
      ((<= count 0))
    (display #\space o-port)))

(define (command-prefix-string prefix)
  (cond ((string? prefix) prefix)
        ((char? prefix) (string prefix))
        ((symbol? prefix) (symbol->string prefix))))

(define (write-line string port)
  (display string port)
  (newline port))


(define (y-or-n? question eof-value)
  (let ((i-port (command-input))
        (o-port (command-output)))
    (let loop ((count *y-or-n-eof-count*))
      (display question o-port)
      (display " (y/n)? " o-port)
      (let ((line (read-line i-port)))
        (cond ((eof-object? line)
               (newline o-port)
               (if (= count 0)
                   eof-value
                   (begin (display "I'll only ask another " o-port)
                          (write count o-port)
                          (display " times." o-port)
                          (newline o-port)
                          (loop (- count 1)))))
              ((< (string-length line) 1) (loop count))
              ((char=? (string-ref line 0) #\y) #t)
              ((char=? (string-ref line 0) #\n) #f)
              (else (loop count)))))))

(define *y-or-n-eof-count* 100)

(define (read-line port)
  (let loop ((l '()))
    (let ((c (read-char port)))
      (if (eof-object? c)
          c
          (if (char=? c #\newline)
              (list->string (reverse l))
              (loop (cons c l)))))))


(define (greet-user info)
  (let ((port (command-output)))
    (display "Welcome to Scheme 48 " port)
    (display version-info port)
    (if info
        (begin (write-char #\space port)
               (display info port)))
    (display "." port)
    (newline port)
    (write-line "Copyright (c) 1993, 1994 by Richard Kelsey and Jonathan Rees."
		port)
    (write-line "Please report bugs to scheme-48-bugs@martigny.ai.mit.edu."
                port)
    (if (not (batch-mode?))
	(write-line "Type ,? (comma question-mark) for help." port))))


(define (command-continuation)          ;utility for debugger
  (let ((obj (focus-object)))
    (command-level-vm-cont
     (if (command-level? obj)
         obj
         (command-level)))))
