;; multithreading for CLISP

(defpackage "THREADS"
  (:nicknames "MT" "MP")
  (:use "COMMON-LISP" "EXT")
  (:export "THREAD" "MAKE-THREAD" "THREADP" "THREAD-YIELD"
           "THREAD-INTERRUPT" "THREAD-NAME" "THREAD-ACTIVE-P"
           "THREAD-JOIN" "CURRENT-THREAD" "LIST-THREADS"
           "MUTEX" "MUTEXP" "MAKE-MUTEX" "MUTEX-LOCK" "MUTEX-UNLOCK"
           "MUTEX-OWNER" "MUTEX-RECURSIVE-P" "WITH-MUTEX-LOCK" "MUTEX-NAME"
           "EXEMPTION" "EXEMPTIONP" "MAKE-EXEMPTION" "EXEMPTION-SIGNAL"
           "EXEMPTION-WAIT" "EXEMPTION-BROADCAST" "EXEMPTION-NAME"
           "Y-OR-N-P-TIMEOUT" "YES-OR-NO-P-TIMEOUT" "WITH-TIMEOUT"
           "SYMBOL-VALUE-THREAD" "*DEFAULT-SPECIAL-BINDINGS*"
           "WITH-DEFERRED-INTERRUPTS"))

(in-package "MT")

(use-package '("MT") "EXT")
(re-export "MT" "EXT")

;; definitions

;; default value (lisp stack) size is very small -  1MB
;; 0 - leaves the OS to decide (basically bad option)
(defvar *DEFAULT-CONTROL-STACK-SIZE* 1048576 "C stack in bytes")
;; the vstack size will be inherited from the parent thread.
;; this is the number of gcv_object_t on the stack
;; 0 - means - inherit from creation thread.
;; the value will be initialized from the runtime
(defvar *DEFAULT-VALUE-STACK-SIZE*)

;; deferred interrupts.
;; in other implementations it is called without-interrupts
(defvar *defer-interrupts* nil)
(defvar *deferred-interrupts* '()) ; list of pending interrupts

;; TODO: add more variables (something should be done about the
;; standard input/output streams).
(defvar *DEFAULT-SPECIAL-BINDINGS*
  `((sys::*active-restarts*
     . '(,(sys::make-restart
           :name 'abort
           :invoke-function
           (lambda ()
             (thread-interrupt
              (current-thread)
              :function t
              :arguments (list :abort
                               (get-internal-real-time)))))))
    (*random-state* . *random-state*)
    (ext::*command-index* . ext::*command-index*)
    (*print-base* . *print-base*)
    (*print-length* . *print-length*)
    (*print-level* . *print-level*)
    (*print-circle* . *print-circle*)
    (*print-radix* . *print-radix*)
    (*print-case* . *print-case*)
    (*print-gensym* . *print-gensym*)
    (*print-pretty* . *print-pretty*)
    (*print-readably* . *print-readably*)
    (*read-suppress* . *read-suppress*)
    (*read-default-float-format* . *read-default-float-format*)
    (*readtable* . (copy-readtable))))

(defmacro with-deferred-interrupts (&body body)
  (let ((intr (gensym "WDI-")))
    `(unwind-protect
          (let ((*defer-interrupts* t)) ,@body)
       (unless *defer-interrupts*
         (loop while *deferred-interrupts* do
              (let ((,intr (pop *deferred-interrupts*)))
                (apply (car ,intr) (nreverse (cdr ,intr)))))))))

(defsetf SYMBOL-VALUE-THREAD MT::SET-SYMBOL-VALUE-THREAD)

(defmacro with-timeout ((seconds &body timeout-forms) &body body)
  "Execute BODY; if execution takes more than SECONDS seconds,
terminate and evaluate TIMEOUT-FORMS."
  `(call-with-timeout ,seconds (lambda () ,@timeout-forms) (lambda () ,@body)))

(defun timeout-message (default localinfo)
  (write-string (SYS::TEXT "[Timed out] "))
  (write-string (string (car (funcall (if default #'cdr #'car) localinfo))))
  (terpri)
  default)

(defun y-or-n-p-timeout (seconds default &rest args)
  "Y-OR-N-P with timeout."
  (declare (ignorable seconds default))
  (with-timeout (seconds (timeout-message default (localized 'sys::y-or-n)))
    (apply #'y-or-n-p args)))

(defun yes-or-no-p-timeout (seconds default &rest args)
  "YES-OR-NO-P with timeout."
  (declare (ignorable seconds default))
  (with-timeout (seconds (timeout-message default (localized 'sys::yes-or-no)))
    (apply #'yes-or-no-p args)))

;;; locks

(defmacro with-mutex-lock ((mutex) &body body)
  "Execute BODY with MUTEX locked. "
  (let ((lk (gensym "WL-"))
        (cthr (gensym "THR-"))
        (prev-owner (gensym "PO-"))
        (owner (gensym "CO-"))
        (prev-rec-count (gensym "PRC-"))
        (rec-count (gensym "CRC-")))
    `(let ((,lk ,mutex)
           (,cthr (current-thread)))
       (multiple-value-bind (,prev-owner ,prev-rec-count)
           (mutex-owner ,lk)
         (unwind-protect (progn (mutex-lock ,lk) ,@body)
           (with-deferred-interrupts ;; defer interrupts while cleanup
             (multiple-value-bind (,owner ,rec-count)
                 (mutex-owner ,lk)
               ;; release the mutex only if has been acquired.
               ;; thread-interrupt with non-local exit may bring us here
               ;; before we got the mutex
               (when (and (eq ,cthr ,owner)
                          (or (not (eq ,cthr ,prev-owner))
                              (> ,rec-count ,prev-rec-count)))
                 (mutex-unlock ,lk)))))))))


;; helper function for thread interruption
(defun %throw-tag (tag &optional result)
  (throw tag result))
