#lang scheme/base

(require scheme/pretty
         (for-syntax scheme/base))

(provide trace untrace
         current-trace-print-args trace-call
         current-trace-notify
         current-prefix-out current-prefix-in)

(define max-dash-space-depth 10)
(define number-nesting-depth 6)
(define current-prefix-out (make-parameter "<"))
(define current-prefix-in (make-parameter ">"))

(define (as-spaces s)
  (make-string (string-length s) #\space))

(define-struct prefix-entry (for-first for-rest))

(define prefixes (make-hash))

(define (lookup-prefix n label)
  (hash-ref prefixes (cons n label) (lambda () #f)))

(define (insert-prefix n label first rest)
  (hash-set! prefixes (cons n label) (make-prefix-entry first rest)))

(define (construct-prefixes level label)
  (let loop ([n level]
             [first (list label)]
             [rest '(" ")])
    (if (>= n max-dash-space-depth)
        (let-values ([(pre-first pre-rest)
                      (build-prefixes number-nesting-depth label)])
          (let ((s (number->string level)))
            (values
             (string-append pre-first "[" s "] ")
             (string-append pre-rest " " (as-spaces s) " "))))
        (cond
          [(= n 0) (values (apply string-append (reverse first))
                           (apply string-append (reverse rest)))]
          [(= n 1) (loop (- n 1)
                         (cons '" " first)
                         (cons '" " rest))]
          [else (loop (- n 2)
                      (cons (format " ~a" label) first)
                      (cons "  " rest))]))))

(define (build-prefixes level label)
  (let ([p (lookup-prefix level label)])
    (if p
        (values (prefix-entry-for-first p)
                (prefix-entry-for-rest p))
        (let-values (((first rest)
                      (construct-prefixes level label)))
          (insert-prefix level label first rest)
          (values first rest)))))

(define current-trace-notify
  (make-parameter (lambda (s)
                    (display s)
                    (newline))
                  (lambda (p)
                    (unless (and (procedure? p)
                                 (procedure-arity-includes? p 1))
                      (raise-type-error 'current-trace-notify
                                        "procedure (arity 1)"
                                        p))
                    p)))

(define (as-trace-notify thunk)
  (let ([p (open-output-bytes)])
    (parameterize ([current-output-port p])
      (thunk))
    (let ([b (get-output-bytes p #t 0 
                               ;; drop newline:
                               (sub1 (file-position p)))])
      ((current-trace-notify) (bytes->string/utf-8 b)))))

(define -:trace-print-args
  (lambda (name args kws kw-vals level)
    (as-trace-notify
     (lambda ()
       ((current-trace-print-args) name args kws kw-vals level)))))

(define current-trace-print-args
  (make-parameter
   (lambda (name args kws kw-vals level)
     (let-values (((first rest)
                   (build-prefixes level (current-prefix-in))))
       (parameterize ((pretty-print-print-line
                       (lambda (n port offset width)
                         (display
                          (if n
                            (if (zero? n) first (format "\n~a" rest))
                            "\n")
                          port)
                         (if n
                             (if (zero? n)
                                 (string-length first)
                                 (string-length rest))
                             0))))
         (pretty-print (append (cons name args)
                               (apply append (map list kws kw-vals)))))))))

(define -:trace-print-results
  (lambda (name results level)
    (as-trace-notify
     (lambda ()
       (trace-print-results name results level)))))

(define trace-print-results
  (lambda (name results level)
    (let-values (((first rest)
                  (build-prefixes level (current-prefix-out))))
      (parameterize ((pretty-print-print-line
                      (lambda (n port offset width)
                        (display
                         (if n
                           (if (zero? n) first (format "\n~a" rest))
                           "\n")
                         port)
                        (if n
                            (if (zero? n)
                                (string-length first)
                                (string-length rest))
                            0))))
        (cond
          ((null? results)
           (pretty-display "*** no values ***"))
          ((null? (cdr results))
           (pretty-print (car results)))
          (else
           (pretty-print (car results))
           (parameterize ((pretty-print-print-line
                           (lambda (n port offset width)
                             (display
                              (if n
                                (if (zero? n) rest (format "\n~a" rest))
                                "\n")
                              port)
                             (if n
                                 (string-length rest)
                                 0))))
             (for-each pretty-print (cdr results)))))))))


;; A traced-proc struct instance acts like a procedure,
;;  but preserves the original, too.
(define-values (struct:traced-proc make-traced-proc traced-proc? traced-proc-ref traced-proc-set!)
  (make-struct-type 'traced-proc #f 2 0 #f null (current-inspector) 0))

;; Install traced versions of a given set of procedures.  The traced
;;  versions are also given, so that they can be constructed to have
;;  a nice name.
(define (do-trace ids procs setters traced-procs)
  (for-each (lambda (id proc)
              (unless (procedure? proc)
                (error 'trace
                       "the value of ~s is not a procedure: ~e" id proc)))
            ids procs)
  (for-each (lambda (proc setter traced-proc)
              (unless (traced-proc? proc)
                (setter (make-traced-proc 
                         (let-values ([(a) (procedure-arity proc)]
                                      [(req allowed) (procedure-keywords proc)])
                           (procedure-reduce-keyword-arity traced-proc
                                                           a
                                                           req
                                                           allowed))
                         proc))))
            procs setters traced-procs))

;; Key used for a continuation mark to indicate
;;  the nesting depth:
(define -:trace-level-key (gensym))

(define trace-call
  (make-keyword-procedure
   (lambda (id f kws vals . args)
     (do-traced id args kws vals f))
   (lambda (id f . args)
     (do-traced id args '() '() f))))

;; Apply a traced procedure to arguments, printing arguments
;; and results. We set and inspect the -:trace-level-key continuation
;; mark a few times to detect tail calls.
(define (do-traced id args kws kw-vals real-value)
  (let* ([levels (continuation-mark-set->list
                  (current-continuation-marks)
                  -:trace-level-key)]
         [level (if (null? levels) 0 (car levels))])
    ;; Tentatively push the new depth level:
    (with-continuation-mark -:trace-level-key (add1 level)
      ;; Check for tail-call => car of levels replaced,
      ;;  which means that the first two new marks are
      ;;  not consecutive:
      (let ([new-levels (continuation-mark-set->list
                         (current-continuation-marks)
                         -:trace-level-key)])
        (if (and (pair? (cdr new-levels))
                 (> (car new-levels) (add1 (cadr new-levels))))
            ;; Tail call: reset level and just call real-value.
            ;;  (This is in tail position to the call to `do-traced'.)
            ;;  We don't print the results, because the original
            ;;  call will.
            (begin
              (-:trace-print-args id args kws kw-vals (sub1 level))
              (with-continuation-mark -:trace-level-key (car levels)
                (if (null? kws)
                    (apply real-value args)
                    (keyword-apply real-value kws kw-vals args))))
            ;; Not a tail call; push the old level, again, to ensure
            ;;  that when we push the new level, we have consecutive
            ;;  levels associated with the mark (i.e., set up for 
            ;;  tail-call detection the next time around):
            (begin
              (-:trace-print-args id args kws kw-vals level)
              (with-continuation-mark -:trace-level-key level
                (call-with-values
                    (lambda ()
                      (with-continuation-mark -:trace-level-key (add1 level)
                        (if (null? kws)
                          (apply real-value args)
                          (keyword-apply real-value kws kw-vals args))))
                    (lambda results
                      (flush-output)
                      ;; Print the results:
                      (-:trace-print-results id results level)
                      ;; Return the results:
                      (apply values results))))))))))

(define-for-syntax (check-ids stx ids)
  (for ([id (in-list (syntax->list ids))])
    (unless (identifier? id)
      (raise-syntax-error #f "not an identifier" stx id)))
  #t)

(define-syntax (trace stx)
  (syntax-case stx ()
    [(_ id ...) (check-ids stx #'(id ...))
     (with-syntax ([(tid ...)
                    (for/list ([id (in-list (syntax->list #'(id ...)))])
                      (let ([tid (format "traced-~a" (syntax-e id))])
                        (datum->syntax id (string->symbol tid) #f)))])
       #'(do-trace
          '(id ...)
          (list id ...)
          (list (lambda (v) (set! id v)) ...)
          (list (let* ([real-value id]
                       [tid (make-keyword-procedure
                             (lambda (kws vals . args)
                               (do-traced 'id args kws vals real-value))
                             (lambda args
                               (do-traced 'id args null null real-value)))])
                  tid)
                ...)))]))

(define-syntax (untrace stx)
  (syntax-case stx ()
    [(_ id ...) (check-ids stx #'(id ...))
     #'(begin (when (traced-proc? id)
                (set! id (traced-proc-ref id 1)))
              ...)]))
