;; should cache the count of new snips -- dont
;; use `count-snips'; use something associated with the
;; equal hash-table

(module gui mzscheme
  (require (lib "")
           (lib "" "mrlib")
           (lib "" "mred")
           (lib "" "framework")
           (lib "")
           (lib "")
           (lib "")
           (lib ""))

   [traces (opt-> (compiled-lang?
                   (listof red?)
   [traces/pred (opt-> (compiled-lang?
                        (listof red?)
                        (listof any/c)
                        (any/c . -> . boolean?))
   [traces/multiple (opt-> (compiled-lang?
                            (listof red?)
                            (listof any/c))
  (provide reduction-steps-cutoff initial-font-size initial-char-width
           dark-pen-color light-pen-color dark-brush-color light-brush-color)

  (preferences:set-default 'plt-reducer:show-bottom #t boolean?)
  (define dark-pen-color (make-parameter "blue"))
  (define light-pen-color (make-parameter "lightblue"))
  (define dark-brush-color (make-parameter "lightblue"))
  (define light-brush-color (make-parameter "white"))
  ;; after (about) this many steps, stop automatic, initial reductions
  (define reduction-steps-cutoff (make-parameter 20))
  (define initial-font-size
     (send (send (send (editor:get-standard-style-list) 
  (define initial-char-width (make-parameter 30))
  ;; the initial spacing between row and columns of the reduction terms
  (define x-spacing 15)
  (define y-spacing 15)
  (define (default-pp v port w spec)
    (parameterize ([pretty-print-columns w])
      (pretty-print v port)))
  (define traces
    (opt-lambda (lang reductions expr [pp default-pp])
      (traces/multiple lang reductions (list expr) pp)))
  (define traces/multiple
    (opt-lambda (lang reductions exprs [pp default-pp])
      (traces/pred lang reductions exprs (lambda (x) #t) pp)))
  (define traces/pred
    (opt-lambda (lang reductions exprs pred [pp default-pp])
      (define main-eventspace (current-eventspace))
      (define graph-pb (make-object graph-pasteboard%))
      (define f (instantiate red-sem-frame% ()
                  (label "Reduction Graph")
                  (graph-pb graph-pb)
                  (width 600)
                  (height 400)
                   (lambda ()
                     (send remove-my-contents-panel
                           (lambda (l)
                             (preferences:set 'plt-reducer:show-bottom (null? l))
                             (if (null? l)
                                 (list bottom-panel)
      (define ec (make-object editor-canvas% (send f get-area-container) graph-pb))
      (define remove-my-contents-panel (new vertical-panel%
                                            (parent (send f get-area-container))
                                            (stretchable-height #f)))
      (define bottom-panel (new vertical-panel%
                                (parent remove-my-contents-panel)
                                (stretchable-height #f)))
      (define font-size (instantiate slider% ()
                          (label "Font Size")
                          (min-value 1)
                          (init-value (initial-font-size))
                          (max-value 127)
                          (parent bottom-panel)
                          (callback (lambda (slider evt) (set-font-size (send slider get-value))))))
      (define lower-panel (instantiate horizontal-panel% ()
                             (parent bottom-panel)
                             (stretchable-height #f)))
      (define reduce-button (make-object button% 
                              (lambda (x y)
      (define status-message (instantiate message% ()
                               (label "")
                               (parent lower-panel)
                               (stretchable-width #t)))
      (define snip-cache (make-hash-table 'equal))
      ;; call-on-eventspace-main-thread : (-> any) -> any
      ;; =reduction thread=
      (define (call-on-eventspace-main-thread thnk)
        (parameterize ([current-eventspace main-eventspace])
          (let ([s (make-semaphore 0)]
                [ans #f])
             (lambda ()
               (set! ans (thnk))
               (semaphore-post s)))
            (semaphore-wait s)
      ;; only changed on the reduction thread
      ;; frontier : (listof (is-a?/c graph-editor-snip%))
      (define frontier (map (lambda (expr) (build-snip snip-cache #f expr pred pp)) exprs))

      ;; set-font-size : number -> void
      ;; =eventspace main thread=
      (define (set-font-size size)
        (let* ([scheme-standard (send (editor:get-standard-style-list) find-named-style
               [scheme-delta (make-object style-delta%)])
          (send scheme-standard get-delta scheme-delta)
          (send scheme-delta set-size-mult 0)
          (send scheme-delta set-size-add size)
          (send scheme-standard set-delta scheme-delta)))
      ;; reduce-frontier : -> void
      ;; =reduction thread=
      ;; updates frontier with the new snip after a single reduction
      (define (reduce-frontier)
        (let ([col #f])
          (let loop ([snips frontier]
                     [new-frontier null]
                     [y 0])
              [(null? snips)
               (set! frontier new-frontier)]
               (let* ([snip (car snips)]
                        (lambda (x) x)
                        (map (lambda (sexp) 
                                (λ ()
                                  (build-snip snip-cache snip sexp pred pp))))
                             (reduce reductions (send snip get-expr))))]
                        (lambda () ; =eventspace main thread=
                          (send graph-pb begin-edit-sequence)
                          (unless col  ;; only compute col here, incase user moves snips
                            (set! col (+ x-spacing (find-rightmost-x graph-pb))))
                            (insert-into col y graph-pb new-snips)
                            (send graph-pb end-edit-sequence)
                            (send status-message set-label
                                  (string-append (term-count (count-snips)) "...")))))])
                 (loop (cdr snips)
                       (append new-frontier new-snips)
      ;; count-snips : -> number
      ;; =eventspace main thread=
      ;; counts the snips in `pb'. If connections?
      ;; is #t, also counts the number of connections
      ;; and returns the sum of the connections and snips
      (define (count-snips)
        (let loop ([n 0]
                   [snip (send graph-pb find-first-snip)])
            [snip (loop (+ n 1) (send snip next))]
            [else n])))
      ;; reduce-button-callback : -> void
      ;; =eventspace main thread=
      (define (reduce-button-callback)
        (send reduce-button enable #f)
        (send reduce-button set-label "Reducing...")
         (lambda ()
            (lambda () ;; =eventspace main thread=
              (send reduce-button set-label "Reduce")
                [(null? frontier)
                 (send status-message set-label (term-count (count-snips)))]
                 (send status-message set-label 
                       (string-append (term-count (count-snips))
                                      "(possibly more to find)"))
                 (send reduce-button enable #t)]))))))
      (define (term-count n)
        (format "found ~a term~a" n (if (equal? n 1) "" "s")))
      ;; do-some-reductions : -> void
      ;; =reduction thread=
      ;; reduces some number of times,
      ;; adding at least reduction-steps-cutoff steps
      ;; before stopping (unless there aren't that many left)
      (define (do-some-reductions)
        (let ([initial-size (call-on-eventspace-main-thread count-snips)])
          (let loop ()
              [(null? frontier) (void)]
              [((call-on-eventspace-main-thread count-snips) . >= . (+ initial-size (reduction-steps-cutoff)))
      ;; scroll-to-rightmost-snip : -> void
      ;; =eventspace main thread=
      (define (scroll-to-rightmost-snip)
        (let ([rightmost-snip (send graph-pb find-first-snip)])
          (let loop ([rightmost-snip rightmost-snip]
                     [rightmost-y (get-right-edge rightmost-snip)]
                     [snip (send rightmost-snip next)])
              [(not snip) (make-snip-visible rightmost-snip)]
               (let ([snip-y (get-right-edge snip)])
                 (if (<= rightmost-y snip-y)
                     (loop snip snip-y (send snip next))
                     (loop rightmost-snip rightmost-y (send snip next))))]))))

      ;; make-snip-visisble : snip -> void
      ;; =eventspace-main-thread=
      (define (make-snip-visible snip)
        (let ([bl (box 0)]
              [bt (box 0)]
              [br (box 0)]
              [bb (box 0)])
          (send graph-pb get-snip-location snip bl bt #f)
          (send graph-pb get-snip-location snip br bb #t)
          (send graph-pb scroll-to 
                (- (unbox br) (unbox bl))
                (- (unbox bb) (unbox bt))
      ;; get-right-edge : snip -> void
      ;; =eventspace-main-thread=
      (define (get-right-edge snip)
        (let ([br (box 0)])
          (send graph-pb get-snip-location snip br #f #t)
          (unbox br)))
      (send remove-my-contents-panel
            (lambda (l)
              (if (preferences:get 'plt-reducer:show-bottom)
                  (list bottom-panel)
      (insert-into init-rightmost-x 0 graph-pb frontier)
      (set-font-size (initial-font-size))
      (send f show #t)))
  (define red-sem-frame%
    (class (frame:standard-menus-mixin (frame:basic-mixin frame%))
      (init-field graph-pb toggle-panel-callback)
      (define/override (file-menu:create-save?) #f)
      (define/override (file-menu:between-save-as-and-print file-menu)
        (make-object menu-item% "Print..."
          (lambda (item evt) (send graph-pb print)))
        (make-object menu-item% "Export as Encapsulted PostScript..."
          (lambda (item evt) (send graph-pb print #t #f 'postscript this #f)))
        (make-object menu-item% "Export as PostScript..."
          (lambda (item evt) (send graph-pb print #t #f 'postscript this)))
        (make-object menu-item% 
          "Toggle bottom stuff"
          (lambda (item evt) (toggle-panel-callback))))
      (super-instantiate ())))
  (define (resizing-pasteboard-mixin pb%)
    (class pb%
      (define/augment (on-interactive-resize snip)
        (when (is-a? snip reflowing-snip<%>)
          (send snip reflow-program))
        #;(super on-interactive-resize snip))
      (define/augment (after-interactive-resize snip)
        (when (is-a? snip reflowing-snip<%>)
          (send snip reflow-program))
        #;(super after-interactive-resize snip))
      (define/override (interactive-adjust-resize snip w h)
        (super interactive-adjust-resize snip w h)
        (when (is-a? snip reflowing-snip<%>)
          (send snip reflow-program)))
      (super-instantiate ())))
  (define reflowing-snip<%>
    (interface ()
  (define graph-pasteboard% 
     (graph-pasteboard-mixin pasteboard%)))
  (define graph-editor-snip%
    (class* (graph-snip-mixin editor-snip%) (reflowing-snip<%>)
      (init-field expr pp char-width bad?)
      (define/public (get-expr) expr)
      (inherit get-editor)
      (define/public (reflow-program)
        (let ([ed (get-editor)])
          (when ed
            (let ([ed-ad (send ed get-admin)])
              (when ed-ad
                (let ([dc (send ed get-dc)]
                      [wb (box 0)]
                      [std-style (send (editor:get-standard-style-list) find-named-style "Standard")])
                  (send ed-ad get-view #f #f wb #f)
                  (let-values ([(tw _1 _2 _3) (send dc get-text-extent "w"
                                                    (and std-style
                                                         (send std-style get-font)))])
                    (let ([new-width (max 1 (inexact->exact (floor (/ (- (unbox wb) 2) tw))))])
                      (unless (equal? new-width char-width)
                        (set! char-width new-width)
      (inherit get-extent)
      (define/override (draw dc x y left top right bottom dx dy draw-caret)
        (when bad?
          (let ([bw (box 0)]
                [bh (box 0)]
                [pen (send dc get-pen)]
                [brush (send dc get-brush)])
            (get-extent dc x y bw bh #f #f #f #f)
            (send dc set-pen (send the-pen-list find-or-create-pen bad-color 1 'solid))
            (send dc set-brush (send the-brush-list find-or-create-brush bad-color 'solid))
            (send dc draw-rectangle x y (unbox bw) (unbox bh))
            (send dc set-pen pen)
            (send dc set-brush brush)))
        (super draw dc x y left top right bottom dx dy draw-caret))
      (define/public (format-expr)
        (let* ([text (get-editor)]
               [port (make-output-port
                      (lambda (bytes start end buffering? enable-breaks?)
                        (send text insert (bytes->string/utf-8 (subbytes bytes start end))
                              (send text last-position)
                              (send text last-position))
                        (- end start))
          (send text begin-edit-sequence)
          (send text thaw-colorer)
          (send text set-styles-sticky #f)
          (send text erase)
          (pp expr port char-width text)
          (when (char=? #\newline (send text get-character (- (send text last-position) 1)))
            (send text delete (- (send text last-position) 1) (send text last-position)))
          (send text freeze-colorer)
          (when bad?
            (send text change-style bad-style-delta 0 (send text last-position)))
          (send text end-edit-sequence)))

      (super-instantiate ())))
  (define program-text%
    (class scheme:text%
      (init-field bad?)
      (define/override (on-paint before? dc left top right bottom dx dy draw-caret)
        (when (and bad? before?)
          (let ([pen (send dc get-pen)]
                [brush (send dc get-brush)])
            (send dc set-pen (send the-pen-list find-or-create-pen bad-color 1 'solid))
            (send dc set-brush (send the-brush-list find-or-create-brush bad-color 'solid))
            (send dc draw-rectangle (+ dx left) (+ dy top) (- right left) (- bottom top))
            (send dc set-pen pen)
            (send dc set-brush brush)))
        (super on-paint before? dc left top right bottom dx dy draw-caret))
  (define lines-pen (send the-pen-list find-or-create-pen "black" 1 'solid))
  (define bad-style-delta (make-object style-delta% 'change-italic))
  (define bad-color "pink")
  ;; where the first snips are inserted
  (define init-rightmost-x 25)
  ;; insert-into : number number pasteboard (listof snip%) -> number
  ;; inserts the snips into the pasteboard vertically
  ;; aligned, starting at (x,y). Returns
  ;; the y coordinate where another snip might be inserted.
  (define (insert-into x y pb exprs)
    (let loop ([exprs exprs]
               [y y])
        [(null? exprs) y]
         (let ([es (car exprs)])
           (send pb insert es x y)
           (loop (cdr exprs)
                 (+ y (find-snip-height pb es) y-spacing)))])))

  ;; build-snip : hash-table
  ;;              (union #f (is-a?/c graph-snip<%>))
  ;;              sexp
  ;;              sexp -> boolean
  ;;              (any port number -> void)
  ;;           -> (union #f (is-a?/c graph-editor-snip%))
  ;; returns #f if a snip corresponding to the expr has already been created.
  ;; also adds in the links to the parent snip
  ;; =eventspace main thread=
  (define (build-snip cache parent-snip expr pred pp)
    (let-values ([(snip new?)
                  (let/ec k
                      (lambda ()
                        (let ([new-snip (make-snip parent-snip expr pred pp)])
                          (hash-table-put! cache expr new-snip)
                          (k new-snip #t))))
      (when parent-snip
        (add-links parent-snip snip
                   (send the-pen-list find-or-create-pen (dark-pen-color) 0 'solid)
                   (send the-pen-list find-or-create-pen (light-pen-color) 0 'solid)
                   (send the-brush-list find-or-create-brush (dark-brush-color) 'solid)
                   (send the-brush-list find-or-create-brush (light-brush-color) 'solid)))
      (and new? snip)))
  ;; make-snip : (union #f (is-a?/c graph-snip<%>))
  ;;             sexp
  ;;             sexp -> boolean
  ;;             (any port number -> void)
  ;;          -> (is-a?/c graph-editor-snip%)
  ;; unconditionally creates a new graph-editor-snip
  ;; =eventspace main thread=
  (define (make-snip parent-snip expr pred pp)
    (let* ([bad? (not (pred expr))]
           [text (new program-text% (bad? bad?))]
           [es (instantiate graph-editor-snip% ()
                 (char-width (initial-char-width))
                 (editor text)
                  (if (procedure-arity-includes? pp 4)
                      (lambda (v port w spec) (display (pp v) port))))
                 (expr expr)
                 (bad? bad?))])
      (send text set-autowrap-bitmap #f)
      (send text freeze-colorer)
      (send es format-expr)
  ;; find-rightmost-x : pasteboard -> number
  (define (find-rightmost-x pb) 
    (let ([first-snip (send pb find-first-snip)])
      (if first-snip
          (let loop ([snip first-snip]
                     [max-x (find-snip-right-edge pb first-snip)])
               (loop (send snip next)
                     (max max-x (find-snip-right-edge pb snip)))]
              [else max-x]))
  ;; find-snip-right-edge : editor snip -> number
  (define (find-snip-right-edge ed snip)
    (let ([br (box 0)])
      (send ed get-snip-location snip br #f #t)
      (unbox br)))
  ;; find-snip-height : editor snip -> number
  (define (find-snip-height ed snip)
    (let ([bt (box 0)]
          [bb (box 0)])
      (send ed get-snip-location snip #f bt #f)
      (send ed get-snip-location snip #f bb #t)
      (- (unbox bb)
         (unbox bt)))))