"engine.ss"
;;; engine.ss
;;; Copyright 1984-2017 Cisco Systems, Inc.
;;; 
;;; Licensed under the Apache License, Version 2.0 (the "License");
;;; you may not use this file except in compliance with the License.
;;; You may obtain a copy of the License at
;;; 
;;; http://www.apache.org/licenses/LICENSE-2.0
;;; 
;;; Unless required by applicable law or agreed to in writing, software
;;; distributed under the License is distributed on an "AS IS" BASIS,
;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;;; See the License for the specific language governing permissions and
;;; limitations under the License.

;;; Notes:
;;; The engine code defines three functions: make-engine,
;;; engine-block, and engine-return.

;;; Keyboard interrupts are caught while an engine is running
;;; and the engine disabled while the handler is running.

;;; All of the engine code is defined within local state
;;; containing the following variables:
;;;   *active*  true iff an engine is running
;;;   *exit*    the continuation to the engine invoker
;;;   *keybd*   the saved keyboard interrupt handler
;;;   *timer*   the saved timer interrupt handler


(define make-engine)
(define engine-block)
(define engine-return)

(let ()

(define-threaded *exit*)
(define-threaded *keybd*)
(define-threaded *timer*)
(define-threaded *active* #f)

(define cleanup
  (lambda (who)
    (unless *active* ($oops who "no engine active"))
    (set! *active* #f)
    (keyboard-interrupt-handler *keybd*)
    (timer-interrupt-handler *timer*)
    (set! *keybd* (void))
    (set! *exit* (void))
    (set! *timer* (void))))

(define setup
  (lambda (exit)
    (set! *active* #t)
    (set! *keybd* (keyboard-interrupt-handler))
    (keyboard-interrupt-handler (exception *keybd*))
    (set! *timer* (timer-interrupt-handler))
    (timer-interrupt-handler block)
    (set! *exit* exit)))

(define block
 ; disable engine and return the continuation
  (lambda ()
    (let ([exit *exit*])
      (cleanup 'engine-block)
      (set-timer (call/cc (lambda (k) (exit (lambda () k))))))))

(define return
 ; disable engine and return list (ticks value ...)
  (lambda (args)
    (let ([n (set-timer 0)])
      (let ([exit *exit*])
        (cleanup 'engine-return)
        (exit (lambda () (cons n args)))))))

(define exception
 ; disable engine while calling the handler
  (lambda (handler)
    (lambda args
      (let ([ticks (set-timer 0)])
        (let ([exit *exit*])
          (cleanup 'engine-exception)
          (apply handler args)
          (setup exit)
          (if (= ticks 0) (block) (set-timer ticks)))))))

(define run-engine
 ; run a continuation as an engine
  (lambda (k ticks)
    ((call/cc
       (lambda (exit)
         (set-timer 0)
         (when *active* ($oops 'engine "cannot nest engines"))
         (setup exit)
         (k ticks))))))

(define eng
 ; create an engine from a procedure or continuation
  (lambda (k)
    (lambda (ticks complete expire)
      (unless (and (fixnum? ticks) (not (negative? ticks)))
        ($oops 'engine "invalid ticks ~s" ticks))
      (unless (procedure? complete)
        ($oops 'engine "~s is not a procedure" complete))
      (unless (procedure? expire)
        ($oops 'engine "~s is not a procedure" expire))
      (if (= ticks 0)
          (expire (eng k))
          (let ([x (run-engine k ticks)])
            (if (procedure? x)
                (expire (eng x))
                (apply complete x)))))))

(set! engine-return (lambda args (return args)))

(set! engine-block (lambda () (set-timer 0) (block)))

(set! make-engine
  (lambda (x)
    (unless (procedure? x) ($oops 'make-engine "~s is not a procedure" x))
    (eng (lambda (ticks) 
           (with-exception-handler
             (lambda (c)
               (let ([ticks (set-timer 0)])
                 (let ([exit *exit*])
                   (cleanup 'raise)
                   (call/cc
                     (lambda (k)
                       (exit
                         (lambda ()
                           (let-values ([vals (raise-continuable c)])
                             (setup exit)
                             (if (= ticks 0) (block) (set-timer ticks))
                             (apply k vals)))))))))
             (lambda ()
               (set-timer ticks)
               (call-with-values x (lambda args (return args)))))))))
)
