(defpackage :eval
  (:use :cl :qml)
  (:export
   #:*eval-thread*
   #:append-output
   #:eval-in-thread))

(in-package :eval)

(defvar *output-buffer* (make-string-output-stream))
(defvar *prompt*        t)
(defvar *eval-thread*   nil)
(defvar *               nil)
(defvar **              nil)
(defvar ***             nil)

(defun ini-streams ()
  (setf *standard-output* (make-broadcast-stream *standard-output*
                                                 *output-buffer*))
  (setf *trace-output* *standard-output*
        *error-output* *standard-output*))

(defun current-package-name ()
  (if (eql (find-package :cl-user) *package*)
      "CL-USER"
      (car (sort (list* (package-name *package*) (package-nicknames *package*))
                 (lambda (x y) (< (length x) (length y)))))))

(let ((n -1))
  (defun eval-in-thread (text &optional (progress t)) ; called from QML
    (let ((str (string-trim " " text)))
      (unless (x:empty-string str)
        (if *prompt*
            (let ((pkg (if (zerop n) "QML-USER" (current-package-name)))
                  (counter (princ-to-string (incf n))))
              (format t "~A [~A]~%~A"
                      pkg
                      counter
                      str))
            (format t "~%~%~A" str))
        ;; run eval in its own thread, so UI will remain responsive
        (update-output t)
        (when progress
          (show-progress-bar))
        (qsingle-shot 50 (lambda ()
                           (setf *eval-thread*
                                 (mp:process-run-function "LQML REPL top-level"
                                                          (lambda () (do-eval str))))))))))

(defvar *color-text*       "#c0c0c0")
(defvar *color-values*     "#80b0ff")
(defvar *color-read-error* "orange")
(defvar *color-error*      "#ff8080")

#+ios
(defun escape-smart-quotation (string)
  (dotimes (i (length string))
    (case (char-code (char string i))
      ((8216 8217 8218)
       (setf (char string i) #\'))
      ((171 187 8220 8221 8222)
       (setf (char string i) #\"))))
  string)

(defun do-eval (string)
  (let ((str #+ios (escape-smart-quotation string)
             #-ios string)
        (color *color-read-error*))
    (handler-case
        (let ((exp (read-from-string str)))
          (setf color *color-error*)
          (let ((vals (multiple-value-list (eval exp))))
            (setf *** ** ** * * (first vals))
            (update-output)
            (append-output (format nil "~{~S~^~%~}" vals) *color-values* t))
          (q! |clear| ui:*repl-input*)
          (history-add str))
      (condition (c)
        (show-error c color))))
  (qsingle-shot 50 'eval-exited))

(defun eval-exited ()
  (update-output)
  (show-progress-bar nil))

(defun show-error (error color)
  (let ((e1 (prin1-to-string error))
        (e2 (princ-to-string error)))
    (append-output e1 color)
    (unless (string= e1 e2)
      (append-output e2 color))))

(defun show-progress-bar (&optional (show t))
  (q> |visible| ui:*progress* show))

;;; output

(defun update-output (&optional line)
  (let ((text (get-output-stream-string *output-buffer*)))
    (unless (x:empty-string text)
      (let ((err (search "[LQML:err]" text)))
        (qjs |appendText| ui:*repl-model*
             (list :m-text  (if err (subseq text err) text)
                   :m-color (if err *color-error* *color-text*)
                   :m-bold  nil
                   :m-line  line))))))

(defun append-output (text &optional (color *color-text*) bold)
  (qjs |appendText| ui:*repl-model*
       (list :m-text  text
             :m-color color
             :m-bold  bold
             :m-line  nil)))

;;; command history

(defvar *history*       (make-array 0 :adjustable t :fill-pointer t))
(defvar *history-index* nil)
(defvar *history-file*  ".lqml-repl-history")
(defvar *max-history*   100)

(defun read-saved-history ()
  (when (probe-file *history-file*)
    (let ((i -1))
      (labels ((index ()
                 (mod i *max-history*))
               (next-index ()
                 (incf i)
                 (index)))
        (let ((tmp (make-array *max-history*))) ; ring buffer
          (with-open-file (s *history-file*)
            (x:while-it (read-line s nil nil)
              (setf (svref tmp (next-index)) x:it)))
          (let ((max (min (1+ i) *max-history*)))
            (when (< max *max-history*)
              (setf i -1))
            (dotimes (n max)
              (vector-push-extend (svref tmp (next-index))
                                  *history*))
            (setf *history-index* (length *history*)))))))) ; 1 after last

(let (out)
  (defun history-ini ()
    (read-saved-history)
    (setf out (open *history-file* :direction :output
                    :if-exists :append :if-does-not-exist :create)))
  (defun history-add (line)
    (unless out
      (history-ini))
    (let ((len (length *history*)))
      (when (or (zerop len)
                (string/= line (aref *history* (1- len))))
        (vector-push-extend line *history*)
        (write-line line out)
        (finish-output out)))
    (setf *history-index* (length *history*))) ; 1 after last
  (defun history-move (direction)
    (unless out
      (history-ini))
    (when (and *history-index*
               (plusp (length *history*)))
      (setf *history-index* (if (string= "back" direction)
                                (max (1- *history-index*) 0)
                                (min (1+ *history-index*) (1- (length *history*)))))
      (let ((text (aref *history* *history-index*)))
        (q> |text| ui:*repl-input* text)
        (q> |cursorPosition| ui:*repl-input*
            (- (length text) (if (x:ends-with ")" text) 1 0)))))))

(defun qml::help ()
  (format t "~%~
             ~% :s  (start-swank)~
             ~% :q  (quicklisp)")
  (values))

(progn
  (ini-streams)
  (in-package :qml-user)
  (eval-in-thread "(qml::help)" nil))
