
#lang scheme/base
(require scheme/class
         scheme/unit
         scheme/list
         scheme/match
         scheme/gui
         framework/framework
         syntax/boundmap
         "interfaces.ss"
         "prefs.ss"
         "extensions.ss"
         "warning.ss"
         "hiding-panel.ss"
         "term-record.ss"
         (prefix-in s: "../syntax-browser/widget.ss")
         (prefix-in s: "../syntax-browser/params.ss")
         "../model/deriv.ss"
         "../model/deriv-util.ss"
         "../model/deriv-find.ss"
         "../model/trace.ss"
         "../model/reductions.ss"
         "../model/steps.ss"
         "cursor.ss"
         "../util/notify.ss")
(provide macro-stepper-widget%
         macro-stepper-widget/process-mixin)

;; Macro Stepper

;; macro-stepper-widget%
(define macro-stepper-widget%
  (class* object% ()
    (init-field parent)
    (init-field config)

    ;; Terms

    ;; all-terms : (list-of TermRecord)
    ;; (Reversed)
    (define all-terms null)

    ;; terms : (Cursor-of TermRecord)
    ;; Contains visible terms of all-terms
    (define terms (cursor:new null))

    ;; focused-term : -> TermRecord or #f
    (define (focused-term)
      (cursor:next terms))

    ;; add-deriv : Deriv -> void
    (define/public (add-deriv d)
      (let ([trec (new term-record% (stepper this) (raw-deriv d))])
        (add trec)))

    ;; add-trace : (list-of event) -> void
    (define/public (add-trace events)
      (let ([trec (new term-record% (stepper this) (events events))])
        (add trec)))

    ;; add : TermRecord -> void
    (define/public (add trec)
      (set! all-terms (cons trec all-terms))
      (let ([display-new-term? (cursor:at-end? terms)]
            [invisible? (send trec get-deriv-hidden?)])
        (unless invisible?
          (cursor:add-to-end! terms (list trec))
          (trim-navigator)
          (if display-new-term?
              (refresh)
              (update)))))

    ;; remove-current-term : -> void
    (define/public (remove-current-term)
      (cursor:remove-current! terms)
      (trim-navigator)
      (refresh))

    (define/public (get-config) config)
    (define/public (get-controller) sbc)
    (define/public (get-view) sbview)
    (define/public (get-warnings-area) warnings-area)
    (define/public (get-macro-hiding-prefs) macro-hiding-prefs)

    (define/public (reset-primary-partition)
      (send sbc reset-primary-partition)
      (update/preserve-view))

    (define area (new vertical-panel% (parent parent)))
    (define supernavigator
      (new horizontal-panel%
           (parent area)
           (stretchable-height #f)
           (alignment '(center center))))
    (define navigator
      (new horizontal-panel%
           (parent supernavigator)
           (stretchable-width #f)
           (stretchable-height #f)
           (alignment '(left center))))
    (define extra-navigator
      (new horizontal-panel%
           (parent supernavigator)
           (stretchable-width #f)
           (stretchable-height #f)
           (alignment '(left center))
           (style '(deleted))))
    
    (define warnings-area (new stepper-warnings% (parent area)))
    
    (define sbview (new stepper-syntax-widget% 
                        (parent area)
                        (macro-stepper this)))
    (define sbc (send sbview get-controller))
    (define control-pane
      (new vertical-panel% (parent area) (stretchable-height #f)))
    (define macro-hiding-prefs
      (new macro-hiding-prefs-widget%
           (parent control-pane)
           (stepper this)
           (config config)))

    (send config listen-show-syntax-properties?
          (lambda (show?) (send sbview show-props show?)))
    (send config listen-show-hiding-panel?
          (lambda (show?) (show-macro-hiding-prefs show?)))
    (send sbc listen-selected-syntax
          (lambda (stx) (send macro-hiding-prefs set-syntax stx)))
    (send config listen-highlight-foci?
          (lambda (_) (update/preserve-view)))
    (send config listen-highlight-frontier?
          (lambda (_) (update/preserve-view)))
    (send config listen-show-rename-steps?
          (lambda (_) (refresh/re-reduce)))
    (send config listen-one-by-one?
          (lambda (_) (refresh/re-reduce)))
    (send config listen-force-letrec-transformation?
          (lambda (_) (refresh/resynth)))
    (send config listen-extra-navigation?
          (lambda (show?) (show-extra-navigation show?)))

    (define nav:up
      (new button% (label "Previous term") (parent navigator)
           (callback (lambda (b e) (navigate-up)))))
    (define nav:start
      (new button% (label "<-- Start") (parent navigator)
           (callback (lambda (b e) (navigate-to-start)))))
    (define nav:previous
      (new button% (label "<- Step") (parent navigator)
           (callback (lambda (b e) (navigate-previous)))))
    (define nav:next
      (new button% (label "Step ->") (parent navigator)
           (callback (lambda (b e) (navigate-next)))))
    (define nav:end
      (new button% (label "End -->") (parent navigator)
           (callback (lambda (b e) (navigate-to-end)))))
    (define nav:down
      (new button% (label "Next term") (parent navigator)
           (callback (lambda (b e) (navigate-down)))))

    (define/private (trim-navigator)
      (if (> (length (cursor->list terms)) 1)
          (send navigator change-children
                (lambda _
                  (list nav:up
                        nav:start
                        nav:previous
                        nav:next
                        nav:end
                        nav:down)))
          (send navigator change-children
                (lambda _
                  (list nav:start
                        nav:previous
                        nav:next
                        nav:end)))))

    (define/public (show-macro-hiding-prefs show?)
      (send area change-children
            (lambda (children)
              (if show?
                  (append (remq control-pane children) (list control-pane))
                  (remq control-pane children)))))

    (define/private (show-extra-navigation show?)
      (send supernavigator change-children
            (lambda (children)
              (if show?
                  (list navigator extra-navigator)
                  (list navigator)))))

    ;; Navigation

    (define/public-final (at-start?)
      (send (focused-term) at-start?))
    (define/public-final (at-end?)
      (send (focused-term) at-end?))

    (define/public-final (navigate-to-start)
      (send (focused-term) navigate-to-start)
      (update/save-position))
    (define/public-final (navigate-to-end)
      (send (focused-term) navigate-to-end)
      (update/save-position))
    (define/public-final (navigate-previous)
      (send (focused-term) navigate-previous)
      (update/save-position))
    (define/public-final (navigate-next)
      (send (focused-term) navigate-next)
      (update/save-position))

    (define/public-final (navigate-up)
      (when (focused-term)
        (send (focused-term) on-lose-focus))
      (cursor:move-prev terms)
      (refresh/move))
    (define/public-final (navigate-down)
      (when (focused-term)
        (send (focused-term) on-lose-focus))
      (cursor:move-next terms)
      (refresh/move))

    ;; Update

    ;; update/save-position : -> void
    (define/private (update/save-position)
      (update/preserve-lines-view))

    ;; update/preserve-lines-view : -> void
    (define/public (update/preserve-lines-view)
      (define text (send sbview get-text))
      (define start-box (box 0))
      (define end-box (box 0))
      (send text get-visible-line-range start-box end-box)
      (update)
      (send text scroll-to-position
            (send text line-start-position (unbox start-box))
            #f
            (send text line-start-position (unbox end-box))
            'start))
    
    ;; update/preserve-view : -> void
    (define/public (update/preserve-view)
      (define text (send sbview get-text))
      (define start-box (box 0))
      (define end-box (box 0))
      (send text get-visible-position-range start-box end-box)
      (update)
      (send text scroll-to-position (unbox start-box) #f (unbox end-box) 'start))

    ;; update : -> void
    ;; Updates the terms in the syntax browser to the current step
    (define/private (update)
      (define text (send sbview get-text))
      (define position-of-interest 0)
      (define multiple-terms? (> (length (cursor->list terms)) 1))
      (send text begin-edit-sequence)
      (send sbview erase-all)
      
      (update:show-prefix)
      (when multiple-terms? (send sbview add-separator))
      (set! position-of-interest (send text last-position))
      (update:show-current-step)
      (when multiple-terms? (send sbview add-separator))
      (update:show-suffix)
      (send text end-edit-sequence)
      (send text scroll-to-position
            position-of-interest
            #f
            (send text last-position)
            'start)
      (enable/disable-buttons))

    ;; update:show-prefix : -> void
    (define/private (update:show-prefix)
      ;; Show the final terms from the cached synth'd derivs
      (for-each (lambda (trec) (send trec display-final-term))
                (cursor:prefix->list terms)))

    ;; update:show-current-step : -> void
    (define/private (update:show-current-step)
      (when (focused-term)
        (send (focused-term) display-step)))

    ;; update:show-suffix : -> void
    (define/private (update:show-suffix)
      (let ([suffix0 (cursor:suffix->list terms)])
        (when (pair? suffix0)
          (for-each (lambda (trec)
                      (send trec display-initial-term))
                    (cdr suffix0)))))

    ;; enable/disable-buttons : -> void
    (define/private (enable/disable-buttons)
      (define term (focused-term))
      (send nav:start enable (and term (send term has-prev?)))
      (send nav:previous enable (and term (send term has-prev?)))
      (send nav:next enable (and term (send term has-next?)))
      (send nav:end enable (and term (send term has-next?)))
      (send nav:up enable (cursor:has-prev? terms))
      (send nav:down enable (cursor:has-next? terms)))

    ;; --

    ;; refresh/resynth : -> void
    ;; Macro hiding policy has changed; invalidate cached parts of trec
    (define/public (refresh/resynth)
      (for-each (lambda (trec) (send trec invalidate-synth!))
                (cursor->list terms))
      (refresh))

    ;; refresh/re-reduce : -> void
    ;; Reduction config has changed; invalidate cached parts of trec
    (define/private (refresh/re-reduce)
      (for-each (lambda (trec) (send trec invalidate-steps!))
                (cursor->list terms))
      (refresh))

    ;; refresh/move : -> void
    ;; Moving between terms; clear the saved position
    (define/private (refresh/move)
      (refresh))

    ;; refresh : -> void
    (define/public (refresh)
      (send warnings-area clear)
      (when (focused-term)
        (send (focused-term) on-get-focus))
      (update))

    ;; delayed-recache-errors : (list-of (cons exn string))
    (define delayed-recache-errors null)

    ;; handle-recache-error : exception string -> void
    (define/private (handle-recache-error exn part)
      (if (send config get-debug-catch-errors?)
          (begin
            (set! delayed-recache-errors 
                  (cons (cons exn part) delayed-recache-errors))
            (queue-callback
             (lambda ()
               (when (pair? delayed-recache-errors)
                 (message-box
                  "Error"
                  (string-append 
                   "Internal errors in macro stepper:\n"
                   (if (memq 'macro-hiding (map cdr delayed-recache-errors))
                       (string-append 
                        "Macro hiding failed on one or more terms. "
                        "The macro stepper is showing the terms "
                        "with macro hiding disabled.\n")
                       "")
                   (if (memq 'reductions (map cdr delayed-recache-errors))
                       (string-append
                        "The macro stepper failed to compute the reduction sequence "
                        "for one or more terms.\n")
                       "")))
                 (set! delayed-recache-errors null)))))
          (raise exn)))

    (define/private (foci x) (if (list? x) x (list x)))

    ;; Hiding policy
    
    (define/public (get-show-macro?)
      (send macro-hiding-prefs get-policy))

    ;; Derivation pre-processing

    (define/public (get-preprocess-deriv) (lambda (d) d))

    ;; Initialization

    (super-new)
    (send sbview show-props (send config get-show-syntax-properties?))
    (show-macro-hiding-prefs (send config get-show-hiding-panel?))
    (show-extra-navigation (send config get-extra-navigation?))
    (refresh/move)
    ))

(define (macro-stepper-widget/process-mixin %)
  (class %
    (super-new)
    (define/override (get-preprocess-deriv)
      (lambda (d) (get-original-part d)))

    ;; get-original-part : Deriv -> Deriv/#f
    ;; Strip off mzscheme's #%top-interaction
    ;; Careful: the #%top-interaction node may be inside of a lift-deriv
    (define/private (get-original-part deriv)
      (let ([deriv* (adjust-deriv/lift deriv)])
        deriv*))

    ;; adjust-deriv/lift : Derivation -> (list-of Derivation)
    (define/private (adjust-deriv/lift deriv)
      (match deriv
        [(Wrap lift-deriv (e1 e2 first lifted-stx second))
         (let ([first (adjust-deriv/top first)])
           (and first
                (let ([e1 (wderiv-e1 first)])
                  (make-lift-deriv e1 e2 first lifted-stx second))))]
        [else (adjust-deriv/top deriv)]))

    ;; adjust-deriv/top : Derivation -> Derivation
    (define/private (adjust-deriv/top deriv)
      (if (or (syntax-source (wderiv-e1 deriv))
              (p:module? deriv))
          deriv
          ;; It's not original...
          ;; Strip out mzscheme's top-interactions
          ;; Keep anything that is a non-mzscheme top-interaction
          ;; Drop everything else (not original program)
          (cond [(not (mrule? deriv)) #f]
                [(for/or ([x (base-resolves deriv)]) (top-interaction-kw? x))
                 ;; Just mzscheme's top-interaction; strip it out
                 (adjust-deriv/top (mrule-next deriv))]
                [(equal? (map syntax-e (base-resolves deriv)) '(#%top-interaction))
                 ;; A *different* top interaction; keep it
                 deriv]
                [else
                 ;; Not original and not tagged with top-interaction
                 #f])))

    (define/public (top-interaction-kw? x)
      (free-identifier=? x #'#%top-interaction))

    ))
