(defpackage :swank-trace-dialog
  (:use :cl)
  (:import-from :swank :defslimefun :from-string :to-string)
  (:export #:clear-trace-tree
           #:dialog-toggle-trace
           #:dialog-trace
           #:dialog-traced-p
           #:dialog-untrace
           #:dialog-untrace-all
           #:inspect-trace-part
           #:report-partial-tree
           #:report-specs
           #:report-total
           #:report-trace-detail
           #:report-specs
           #:trace-format
           #:still-inside
           #:exited-non-locally
           #:*record-backtrace*
           #:*traces-per-report*
           #:*dialog-trace-follows-trace*
           #:find-trace-part
           #:find-trace))

(in-package :swank-trace-dialog)

(defparameter *record-backtrace* nil
  "Record a backtrace of the last 20 calls for each trace.

Beware that this may have a drastic performance impact on your
program.")

(defparameter *traces-per-report* 150
  "Number of traces to report to emacs in each batch.")


;;;; `trace-entry' model
;;;;
(defvar *traces* (make-array 1000 :fill-pointer 0
                                  :adjustable t))

(defvar *trace-lock* (swank/backend:make-lock :name "swank-trace-dialog lock"))

(defvar *current-trace-by-thread* (make-hash-table))

(defclass trace-entry ()
  ((id         :reader   id-of)
   (children   :accessor children-of :initform nil)
   (backtrace  :accessor backtrace-of :initform (when *record-backtrace*
                                                  (useful-backtrace)))

   (spec       :initarg  :spec      :accessor spec-of
               :initform (error "must provide a spec"))
   (args       :initarg  :args      :accessor args-of
               :initform (error "must provide args"))
   (parent     :initarg  :parent    :reader   parent-of
               :initform (error "must provide a parent, even if nil"))
   (retlist    :initarg  :retlist   :accessor retlist-of
               :initform 'still-inside)))

(defmethod initialize-instance :after ((entry trace-entry) &rest initargs)
  (declare (ignore initargs))
  (if (parent-of entry)
      (nconc (children-of (parent-of entry)) (list entry)))
  (swank/backend:call-with-lock-held
   *trace-lock*
   #'(lambda ()
       (setf (slot-value entry 'id) (fill-pointer *traces*))
       (vector-push-extend entry *traces*))))

(defmethod print-object ((entry trace-entry) stream)
  (print-unreadable-object (entry stream)
    (format stream "~a: ~a" (id-of entry) (spec-of entry))))

(defun completed-p (trace) (not (eq (retlist-of trace) 'still-inside)))

(defun find-trace (id)
  (when (<= 0 id (1- (length *traces*)))
    (aref *traces* id)))

(defun find-trace-part (id part-id type)
  (let* ((trace (find-trace id))
         (l (and trace
                 (ecase type
                   (:arg (args-of trace))
                   (:retval (swank::ensure-list (retlist-of trace)))))))
    (values (nth part-id l)
            (< part-id (length l)))))

(defun useful-backtrace ()
  (swank/backend:call-with-debugging-environment
   #'(lambda ()
       (loop for i from 0
             for frame in (swank/backend:compute-backtrace 0 20)
             collect (list i (swank::frame-to-string frame))))))

(defun current-trace ()
  (gethash (swank/backend:current-thread) *current-trace-by-thread*))

(defun (setf current-trace) (trace)
  (setf (gethash (swank/backend:current-thread) *current-trace-by-thread*)
        trace))


;;;; Control of traced specs
;;;
(defvar *traced-specs* '())

(defslimefun dialog-trace (spec)
  (flet ((before-hook (args)
           (setf (current-trace) (make-instance 'trace-entry
                                                :spec      spec
                                                :args      args
                                                :parent    (current-trace))))
         (after-hook (retlist)
           (let ((trace (current-trace)))
             (when trace
               ;; the current trace might have been wiped away if the
               ;; user cleared the tree in the meantime. no biggie,
               ;; don't do anything.
               ;;
               (setf (retlist-of trace) retlist
                     (current-trace) (parent-of trace))))))
    (when (dialog-traced-p spec)
      (warn "~a is apparently already traced! Untracing and retracing." spec)
      (dialog-untrace spec))
    (swank/backend:wrap spec 'trace-dialog
                        :before #'before-hook
                        :after #'after-hook)
    (pushnew spec *traced-specs*)
    (format nil "~a is now traced for trace dialog" spec)))

(defslimefun dialog-untrace (spec)
  (swank/backend:unwrap spec 'trace-dialog)
  (setq *traced-specs* (remove spec *traced-specs* :test #'equal))
  (format nil "~a is now untraced for trace dialog" spec))

(defslimefun dialog-toggle-trace (spec)
  (if (dialog-traced-p spec)
      (dialog-untrace spec)
      (dialog-trace spec)))

(defslimefun dialog-traced-p (spec)
  (find spec *traced-specs* :test #'equal))

(defslimefun dialog-untrace-all ()
  (untrace)
  (mapcar #'dialog-untrace *traced-specs*))

(defparameter *dialog-trace-follows-trace* nil)

(setq swank:*after-toggle-trace-hook*
      #'(lambda (spec traced-p)
          (when *dialog-trace-follows-trace*
            (cond (traced-p
                   (dialog-trace spec)
                   "traced for trace dialog as well")
                  (t
                   (dialog-untrace spec)
                   "untraced for the trace dialog as well")))))


;;;; A special kind of trace call
;;;
(defun trace-format (format-spec &rest format-args)
  "Make a string from FORMAT-SPEC and FORMAT-ARGS and as a trace."
  (let* ((line (apply #'format nil format-spec format-args)))
    (make-instance 'trace-entry :spec line
                                :args format-args
                                :parent (current-trace)
                                :retlist nil)))


;;;; Reporting to emacs
;;;
(defparameter *visitor-idx* 0)

(defparameter *visitor-key* nil)

(defvar *unfinished-traces* '())

(defun describe-trace-for-emacs (trace)
  `(,(id-of trace)
    ,(and (parent-of trace) (id-of (parent-of trace)))
    ,(spec-of trace)
    ,(loop for arg in (args-of trace)
           for i from 0
           collect (list i (swank::to-line arg)))
    ,(loop for retval in (swank::ensure-list (retlist-of trace))
           for i from 0
           collect (list i (swank::to-line retval)))))

(defslimefun report-partial-tree (key)
  (unless (equal key *visitor-key*)
    (setq *visitor-idx* 0
          *visitor-key* key))
  (let* ((recently-finished
           (loop with i = 0
                 for trace in *unfinished-traces*
                 while (< i *traces-per-report*)
                 when (completed-p trace)
                   collect trace
                   and do
                     (incf i)
                     (setq *unfinished-traces*
                           (remove trace *unfinished-traces*))))
         (new (loop for i
                      from (length recently-finished)
                        below *traces-per-report*
                    while (< *visitor-idx* (length *traces*))
                    for trace = (aref *traces* *visitor-idx*)
                    collect trace
                    unless (completed-p trace)
                      do (push trace *unfinished-traces*)
                    do (incf *visitor-idx*))))
    (list
     (mapcar #'describe-trace-for-emacs
             (append recently-finished new))
     (- (length *traces*) *visitor-idx*)
    key)))

(defslimefun report-trace-detail (trace-id)
  (swank::call-with-bindings
   swank::*inspector-printer-bindings*
   #'(lambda ()
       (let ((trace (find-trace trace-id)))
         (when trace
           (append
            (describe-trace-for-emacs trace)
            (list (backtrace-of trace)
                  (swank::to-line trace))))))))

(defslimefun report-specs ()
  (sort (copy-list *traced-specs*)
        #'string<
        :key #'princ-to-string))

(defslimefun report-total ()
  (length *traces*))

(defslimefun clear-trace-tree ()
  (setf *current-trace-by-thread* (clrhash *current-trace-by-thread*)
        *visitor-key* nil
        *unfinished-traces* nil)
  (swank/backend:call-with-lock-held
   *trace-lock*
   #'(lambda () (setf (fill-pointer *traces*) 0)))
  nil)

;; HACK: `swank::*inspector-history*' is unbound by default and needs
;; a reset in that case so that it won't error `swank::inspect-object'
;; before any other object is inspected in the slime session.
;;
(unless (boundp 'swank::*inspector-history*)
  (swank::reset-inspector))

(defslimefun inspect-trace-part (trace-id part-id type)
  (multiple-value-bind (obj found)
      (find-trace-part trace-id part-id type)
    (if found
        (swank::inspect-object obj)
        (error "No object found with ~a, ~a and ~a" trace-id part-id type))))

(provide :swank-trace-dialog)