#lang racket/base

(define oprintf
  (let ([op (current-output-port)])
    (λ args
      (apply fprintf op args))))

(require mred
         racket/class
         syntax/moddep
         syntax/toplevel
         framework/framework
         string-constants
         mrlib/graph
         "drsig.rkt"
         "eval-helpers.rkt"
         racket/unit
         racket/async-channel
         setup/private/lib-roots
         racket/port
         "rectangle-intersect.rkt")

(define-struct req (filename key))
;; type req = (make-req string[filename] (union symbol #f))

(provide module-overview@
         process-program-unit
         process-program-import^
         process-program-export^
         (struct-out req))

(define adding-file (string-constant module-browser-adding-file))
(define unknown-module-name "? unknown module name")

;; probably, at some point, the module browser should get its
;; own output ports or something instead of wrapping these ones
(define original-output-port (current-output-port))
(define original-error-port (current-error-port))

(define-unit module-overview@
  (import [prefix drracket:frame: drracket:frame^]
          [prefix drracket:eval: drracket:eval^]
          [prefix drracket:language-configuration: drracket:language-configuration/internal^]
          [prefix drracket:language: drracket:language^])
  (export drracket:module-overview^)
  
  (define filename-constant (string-constant module-browser-filename-format))
  (define font-size-gauge-label (string-constant module-browser-font-size-gauge-label))
  (define progress-label (string-constant module-browser-progress-label))
  (define laying-out-graph-label (string-constant module-browser-laying-out-graph-label))
  (define open-file-format (string-constant module-browser-open-file-format))
  (define lib-paths-checkbox-constant (string-constant module-browser-show-lib-paths))
  
  (define (set-box/f b v) (when (box? b) (set-box! b v)))
  
  (define (module-overview parent)
    (let ([filename (get-file #f parent)])
      (when filename
        (module-overview/file filename parent))))
  
  (define (find-label-font size)
    (send the-font-list find-or-create-font size 'decorative 'normal 'normal #f))
  
  (define module-overview-pasteboard<%>
    (interface ()
      set-label-font-size
      get-label-font-size
      get-hidden-paths
      show-visible-paths
      remove-visible-paths
      set-name-length
      get-name-length))
  
  (define boxed-word-snip<%>
    (interface ()
      get-filename
      get-word
      get-lines
      is-special-key-child?
      add-special-key-child
      set-found!))
  
  ;; make-module-overview-pasteboard : boolean
  ;;                                   ((union #f snip) -> void)
  ;;                                -> (union string pasteboard)
  ;; string as result indicates an error message
  ;; pasteboard as result is the pasteboard to show
  (define (make-module-overview-pasteboard vertical? mouse-currently-over)
    
    (define level-ht (make-hasheq))
    
    ;; snip-table : hash-table[sym -o> snip]
    (define snip-table (make-hash))
    (define label-font (find-label-font (preferences:get 'drracket:module-overview:label-font-size)))
    (define text-color "blue")
    
    (define search-result-text-color "white")
    (define search-result-background "forestgreen")
    
    (define dark-syntax-pen (send the-pen-list find-or-create-pen "darkorchid" 1 'solid))
    (define dark-syntax-brush (send the-brush-list find-or-create-brush "darkorchid" 'solid))
    (define light-syntax-pen (send the-pen-list find-or-create-pen "plum" 1 'solid))
    (define light-syntax-brush (send the-brush-list find-or-create-brush "plum" 'solid))
    
    (define dark-template-pen (send the-pen-list find-or-create-pen "seagreen" 1 'solid))
    (define dark-template-brush (send the-brush-list find-or-create-brush "seagreen" 'solid))
    (define light-template-pen (send the-pen-list find-or-create-pen "springgreen" 1 'solid))
    (define light-template-brush (send the-brush-list find-or-create-brush "springgreen" 'solid))
    
    (define dark-pen (send the-pen-list find-or-create-pen "blue" 1 'solid))
    (define dark-brush (send the-brush-list find-or-create-brush "blue" 'solid))
    (define light-pen (send the-pen-list find-or-create-pen "light blue" 1 'solid))
    (define light-brush (send the-brush-list find-or-create-brush "light blue" 'solid))
    
    (define (module-overview-pasteboard-mixin %)
      (class* % (module-overview-pasteboard<%>)
        
        (inherit get-snip-location
                 begin-edit-sequence
                 end-edit-sequence
                 insert
                 move-to
                 find-first-snip
                 dc-location-to-editor-location
                 find-snip
                 get-canvas)
      
        ;; require-depth-ht : hash[(list snip snip) -o> (listof integer)]
        ;; maps parent/child snips (ie, those that match up to modules that require each other) to phase differences
        (define require-depth-ht (make-hash))
        
        (define name-length 'long)
        (define/public (set-name-length nl)
          (unless (eq? name-length nl)
            (set! name-length nl)
            (re-add-snips)
            (render-snips)))
        (define/public (get-name-length) name-length)
        
        (field [max-lines #f])
        
        ;; controls if the snips should be moved
        ;; around when the font size is changed.
        ;; set to #f if the user ever moves a
        ;; snip themselves.
        (define dont-move-snips #f)
        
        (field (label-font-size (preferences:get 'drracket:module-overview:label-font-size)))
        (define/public (get-label-font-size) label-font-size)
        (define/private (get-snip-hspace) (if vertical?
                                              2
                                              (* 2 label-font-size)))
        (define/private (get-snip-vspace) (if vertical?
                                              30
                                              2))
        (define snip-height #f)
        
        (define font-label-size-callback-running? #f)
        (define new-font-size #f)
        (define/public (set-label-font-size size-to-set)
          (set! new-font-size size-to-set)
          (unless font-label-size-callback-running?
            (set! font-label-size-callback-running? #t)
            (queue-callback
             (λ ()
               (set! label-font-size new-font-size)
               (preferences:set 'drracket:module-overview:label-font-size 
                                new-font-size)
               (set! label-font (find-label-font label-font-size))
               (begin-edit-sequence)
               (let loop ([snip (find-first-snip)])
                 (when snip
                   (let ([admin (send snip get-admin)])
                     (when admin
                       (send admin resized snip #t)))
                   (loop (send snip next))))
               (unless dont-move-snips
                 (render-snips))
               (end-edit-sequence)
               (set! new-font-size #f)
               (set! font-label-size-callback-running? #f))
             #f)))
        
        (define/public (begin-adding-connections)
          (when max-lines
            (error 'begin-adding-connections "already in begin-adding-connections/end-adding-connections sequence"))
          (set! max-lines 0)
          (begin-edit-sequence)
          (let loop ()
            (let ([s (find-first-snip)])
              (when s
                (send s release-from-owner)
                (loop))))
          (set! level-ht (make-hasheq))
          (set! snip-table (make-hash)))
        
        (define/public (end-adding-connections)
          (unless max-lines
            (error 'end-adding-connections "not in begin-adding-connections/end-adding-connections sequence"))
          
          (unless (zero? max-lines)
            (let loop ([snip (find-first-snip)])
              (when snip
                (when (is-a? snip word-snip/lines%)
                  (send snip normalize-lines max-lines))
                (loop (send snip next)))))
          
          
          (set! max-lines #f)
          (compute-snip-require-phases)
          (remove-specially-linked)
          (render-snips)
          (end-edit-sequence))
        
        (define/private (compute-snip-require-phases)
          (let ([ht (make-hash)]) ;; avoid infinite loops 
            (for ([snip (in-list (get-top-most-snips))])
              (let loop ([parent snip]
                         [depth 0])    ;; depth is either an integer or #f (indicating for-label)
                (unless (hash-ref ht (cons parent depth) #f)
                  (hash-set! ht (cons parent depth) #t)
                  (send parent add-require-phase depth)
                  (for ([child (in-list (send parent get-children))])
                    (for ([delta-depth (in-list (hash-ref require-depth-ht (list parent child)))])
                      (loop child 
                            (and depth delta-depth (+ delta-depth depth))))))))))
        
        ;; add-connection : string string (union symbol #f) number -> void
        ;; name-original and name-require and the identifiers for those paths and
        ;; original-filename? and require-filename? are booleans indicating if the names
        ;; are filenames.
        (define/public (add-connection name-original name-require path-key require-depth)
          (unless max-lines
            (error 'add-connection "not in begin-adding-connections/end-adding-connections sequence"))
          (let* ([original-filename? (file-exists? name-original)]
                 [require-filename? (file-exists? name-require)]
                 [original-snip (find/create-snip name-original original-filename?)]
                 [require-snip (find/create-snip name-require require-filename?)]
                 [original-level (send original-snip get-level)]
                 [require-level (send require-snip get-level)])
            (let ([require-depth-key (list original-snip require-snip)])
              (hash-set! require-depth-ht 
                         require-depth-key
                         (cons require-depth (hash-ref require-depth-ht require-depth-key '())))) 
            (case require-depth 
              [(0)
               (add-links original-snip require-snip
                          dark-pen light-pen
                          dark-brush light-brush)]
              [else
               (add-links original-snip require-snip 
                          dark-syntax-pen light-syntax-pen
                          dark-syntax-brush light-syntax-brush)])
            (when path-key
              (send original-snip add-special-key-child path-key require-snip))
            (if (send original-snip get-level)
                (fix-snip-level require-snip (+ original-level 1))
                (fix-snip-level original-snip 0))))
        
        ;; fix-snip-level : snip number -> void
        ;; moves the snip (and any children) to at least `new-level'
        ;; doesn't move them if they are already past that level
        (define/private (fix-snip-level snip new-min-level)
          (let loop ([snip snip]
                     [new-min-level new-min-level])
            (let ([current-level (send snip get-level)])
              (when (or (not current-level)
                        (new-min-level . > . current-level))
                (send snip set-level new-min-level)
                (for-each
                 (λ (child) (loop child (+ new-min-level 1)))
                 (send snip get-children))))))
        
        ;; find/create-snip : (union path string) boolean? -> word-snip/lines
        ;; finds the snip with this key, or creates a new
        ;; ones. For the same key, always returns the same snip.
        ;; uses snip-table as a cache for this purpose.
        (define/private (find/create-snip name is-filename?)
          (hash-ref
           snip-table
           name
           (λ () 
             (let* ([snip (instantiate word-snip/lines% ()
                            (lines (if is-filename? (count-lines name) #f))
                            (word (let-values ([(_1 name _2) (split-path name)])
                                    (path->string name)))
                            (pb this)
                            (filename (if is-filename? name #f)))])
               (insert snip)
               (hash-set! snip-table name snip)
               snip))))
        
        ;; count-lines : string[filename] -> (union #f number)
        ;; effect: updates max-lines
        (define/private (count-lines filename)
          (let ([lines
                 (call-with-input-file filename
                   (λ (port)
                     (let loop ([n 0])
                       (let ([l (read-line port)])
                         (if (eof-object? l)
                             n
                             (loop (+ n 1))))))
                   #:mode 'text)])
            (set! max-lines (max lines max-lines))
            lines))
        
        ;; get-snip-width : snip -> number
        ;; exracts the width of a snip
        (define/private (get-snip-width snip)
          (let ([lb (box 0)]
                [rb (box 0)])
            (get-snip-location snip lb #f #f)
            (get-snip-location snip rb #f #t)
            (- (unbox rb)
               (unbox lb))))
        
        ;; get-snip-height : snip -> number
        ;; exracts the width of a snip
        (define/private (get-snip-height snip)
          (let ([tb (box 0)]
                [bb (box 0)])
            (get-snip-location snip #f tb #f)
            (get-snip-location snip #f bb #t)
            (- (unbox bb)
               (unbox tb))))
        
        (field [hidden-paths (preferences:get 'drracket:module-browser:hide-paths)])
        (define/public (remove-visible-paths symbol)
          (unless (memq symbol hidden-paths)
            (set! hidden-paths (cons symbol hidden-paths))
            (refresh-visible-paths)))
        (define/public (show-visible-paths symbol)
          (when (memq symbol hidden-paths)
            (set! hidden-paths (remq symbol hidden-paths))
            (refresh-visible-paths)))
        (define/public (get-hidden-paths) hidden-paths)
        
        (define/private (refresh-visible-paths)
          (begin-edit-sequence)
          (re-add-snips)
          (render-snips)
          (end-edit-sequence))
        
        (define/private (re-add-snips)
          (begin-edit-sequence)
          (remove-specially-linked)
          (end-edit-sequence))
        
        (define/private (remove-specially-linked)
          (remove-currrently-inserted)
          (cond
            [(null? hidden-paths)
             (add-all)]
            [else
             (let ([ht (make-hasheq)])
               (for ([snip (in-list (get-top-most-snips))])
                 (insert snip)
                 (let loop ([snip snip])
                   (unless (hash-ref ht snip #f)
                     (hash-set! ht snip #t)
                     (for ([child (in-list (send snip get-children))])
                       (unless (ormap (λ (key) (send snip is-special-key-child?
                                                     key child))
                                      hidden-paths)
                         (insert child)
                         (loop child)))))))]))
        
        (define/private (remove-currrently-inserted)
          (let loop ()
            (let ([snip (find-first-snip)])
              (when snip
                (send snip release-from-owner)
                (loop)))))
        
        (define/private (add-all)
          (let ([ht (make-hasheq)])
            (for-each
             (λ (snip)
               (let loop ([snip snip])
                 (unless (hash-ref ht snip (λ () #f))
                   (hash-set! ht snip #t)
                   (insert snip)
                   (for-each loop (send snip get-children)))))
             (get-top-most-snips))))
        
        (define/private (get-top-most-snips) (hash-ref level-ht 0 (λ () null)))
        
        ;; render-snips : -> void
        (define/public (render-snips)
          (begin-edit-sequence)
          (let ([max-minor 0])
            
            ;; major-dim is the dimension that new levels extend along
            ;; minor-dim is the dimension that snips inside a level extend along
            
            (hash-for-each
             level-ht
             (λ (n v)
               (set! max-minor (max max-minor (apply + (map (if vertical?
                                                                (λ (x) (get-snip-width x))
                                                                (λ (x) (get-snip-height x)))
                                                            v))))))
            
            (let ([levels (sort (hash-map level-ht list)
                                (λ (x y) (<= (car x) (car y))))])
              (let loop ([levels levels]
                         [major-dim 0])
                (cond
                  [(null? levels) (void)]
                  [else
                   (let* ([level (car levels)]
                          [n (car level)]
                          [this-level-snips (cadr level)]
                          [this-minor (apply + (map (if vertical? 
                                                        (λ (x) (get-snip-width x))
                                                        (λ (x) (get-snip-height x)))
                                                    this-level-snips))]
                          [this-major (apply max (map (if vertical? 
                                                          (λ (x) (get-snip-height x))
                                                          (λ (x) (get-snip-width x)))
                                                      this-level-snips))])
                     (let loop ([snips this-level-snips]
                                [minor-dim (/ (- max-minor this-minor) 2)])
                       (unless (null? snips)
                         (let* ([snip (car snips)]
                                [new-major-coord
                                 (+ major-dim
                                    (floor
                                     (- (/ this-major 2) 
                                        (/ (if vertical? 
                                               (get-snip-height snip)
                                               (get-snip-width snip))
                                           2))))])
                           (if vertical?
                               (move-to snip minor-dim new-major-coord)
                               (move-to snip new-major-coord minor-dim))
                           (loop (cdr snips)
                                 (+ minor-dim
                                    (if vertical?
                                        (get-snip-hspace)
                                        (get-snip-vspace))
                                    (if vertical?
                                        (get-snip-width snip)
                                        (get-snip-height snip)))))))
                     (loop (cdr levels)
                           (+ major-dim 
                              (if vertical? 
                                  (get-snip-vspace)
                                  (get-snip-hspace))
                              this-major)))]))))
          (end-edit-sequence))
        
        (define/override (on-mouse-over-snips snips)
          (mouse-currently-over snips))
        
        (define/override (on-double-click snip event)
          (cond
            [(is-a? snip boxed-word-snip<%>) 
             (let ([fn (send snip get-filename)])
               (when fn
                 (handler:edit-file fn)))]
            [else (super on-double-click snip event)]))
        
        (define/override (on-event evt)
          (cond
            [(send evt button-down? 'right)
             (let ([ex (send evt get-x)]
                   [ey (send evt get-y)])
               (let-values ([(x y) (dc-location-to-editor-location ex ey)])
                 (let ([snip (find-snip x y)]
                       [canvas (get-canvas)])
                   (let ([right-button-menu (make-object popup-menu%)])
                     (when (and snip
                                (is-a? snip boxed-word-snip<%>)
                                canvas
                                (send snip get-filename))
                       (instantiate menu-item% ()
                         (label 
                          (trim-string
                           (format open-file-format
                                   (path->string (send snip get-filename)))
                           200))
                         (parent right-button-menu)
                         (callback
                          (λ (x y)
                            (handler:edit-file
                             (send snip get-filename))))))
                     (instantiate menu-item% ()
                       (label (string-constant module-browser-open-all))
                       (parent right-button-menu)
                       (callback
                        (λ (x y)
                          (let loop ([snip (find-first-snip)])
                            (when snip
                              (when (is-a? snip boxed-word-snip<%>)
                                (let ([filename (send snip get-filename)])
                                  (handler:edit-file filename)))
                              (loop (send snip next)))))))
                     (send canvas popup-menu
                           right-button-menu
                           (+ (send evt get-x) 1)
                           (+ (send evt get-y) 1))))))]
            [else (super on-event evt)]))
        
        (super-new)))
    
    (define (trim-string str len)
      (cond
        [(<= (string-length str) len) str]
        [else (substring str (- (string-length str) len) (string-length str))]))
    
    (define (level-mixin %)
      (class %
        (field (level #f))
        (define/public (get-level) level)
        (define/public (set-level _l) 
          (when level
            (hash-set! level-ht level
                       (remq this (hash-ref level-ht level))))
          (set! level _l)
          (hash-set! level-ht level 
                     (cons this (hash-ref level-ht level (λ () null)))))
        
        (super-instantiate ())))
    
    (define (boxed-word-snip-mixin %)
      (class* % (boxed-word-snip<%>)
        (init-field word
                    filename
                    lines
                    pb)
        
        (inherit get-admin)
        
        (define require-phases '())
        (define/public (add-require-phase d)
          (unless (member d require-phases)
            (set! last-name #f)
            (set! last-size #f)
            (set! require-phases (sort (cons d require-phases) < #:key (λ (x) (or x +inf.0))))))
        
        (field [special-children (make-hasheq)])
        (define/public (is-special-key-child? key child)
          (let ([ht (hash-ref special-children key #f)])
            (and ht (hash-ref ht child #f))))
        (define/public (add-special-key-child key child)
          (hash-set! (hash-ref! special-children key make-hasheq) child #t))
        
        (define/public (get-filename) filename)
        (define/public (get-word) word)
        (define/public (get-lines) lines)
        
        (field (lines-brush #f))
        (define/public (normalize-lines n)
          (if lines
              (let* ([grey (inexact->exact (floor (- 255 (* 255 (sqrt (/ lines n))))))])
                (set! lines-brush (send the-brush-list find-or-create-brush
                                        (make-object color% grey grey grey)
                                        'solid)))
              (set! lines-brush (send the-brush-list find-or-create-brush
                                      "salmon"
                                      'solid))))
        
        (define snip-width 0)
        (define snip-height 0)
        
        (define/override (get-extent dc x y wb hb descent space lspace rspace)
          (cond
            [(equal? (name->label) "")
             (set! snip-width 15)
             (set! snip-height 15)]
            [else
             (let-values ([(w h a d) (send dc get-text-extent (name->label) label-font)])
               (set! snip-width (+ w 5))
               (set! snip-height (+ h 5)))])
          (set-box/f wb snip-width)
          (set-box/f hb snip-height)
          (set-box/f descent 0)
          (set-box/f space 0)
          (set-box/f lspace 0)
          (set-box/f rspace 0))
        
        (define/public (set-found! fh?) 
          (unless (eq? (and fh? #t) found-highlight?)
            (set! found-highlight? (and fh? #t))
            (let ([admin (get-admin)])
              (when admin
                (send admin needs-update this 0 0 snip-width snip-height)))))
        (define found-highlight? #f)
        
        (define/override (draw dc x y left top right bottom dx dy draw-caret)
          (let ([old-font (send dc get-font)]
                [old-text-foreground (send dc get-text-foreground)]
                [old-brush (send dc get-brush)]
                [old-pen (send dc get-pen)])
            (send dc set-font label-font)
            (cond
              [found-highlight?
               (send dc set-brush search-result-background 'solid)]
              [lines-brush
               (send dc set-brush lines-brush)])
            (when (rectangles-intersect? left top right bottom
                                         x y (+ x snip-width) (+ y snip-height))
              (send dc draw-rectangle x y snip-width snip-height)
              (send dc set-text-foreground (send the-color-database find-color 
                                                 (if found-highlight?
                                                     search-result-text-color
                                                     text-color)))
              (send dc draw-text (name->label) (+ x 2) (+ y 2)))
            (send dc set-pen old-pen)
            (send dc set-brush old-brush)
            (send dc set-text-foreground old-text-foreground)
            (send dc set-font old-font)))
        
        ;; name->label : path -> string
        ;; constructs a label for the little boxes in terms
        ;; of the filename.
        
        (define last-name #f)
        (define last-size #f)
        
        (define/private (name->label)
          (let ([this-size (send pb get-name-length)])
            (cond
              [(eq? this-size last-size) last-name]
              [else
               (set! last-size this-size)
               (set! last-name
                     (case last-size
                       [(short)
                        (if (string=? word "")
                            ""
                            (string (string-ref word 0)))]
                       [(medium)
                        (let ([m (regexp-match #rx"^(.*)\\.[^.]*$" word)])
                          (let ([short-name (if m (cadr m) word)])
                            (if (string=? short-name "")
                                ""
                                (let ([ms (regexp-match* #rx"-[^-]*" short-name)])
                                  (cond
                                    [(null? ms)
                                     (substring short-name 0 (min 2 (string-length short-name)))]
                                    [else
                                     (apply string-append
                                            (cons (substring short-name 0 1)
                                                  (map (λ (x) (substring x 1 2))
                                                       ms)))])))))]
                       [(long) word]
                       [(very-long)  
                        (string-append
                         word
                         ": "
                         (format "~s" require-phases))]))
               last-name])))
        
        (super-new)))
    
    (define word-snip/lines% (level-mixin (boxed-word-snip-mixin (graph-snip-mixin snip%))))
    
    (define draw-lines-pasteboard% (module-overview-pasteboard-mixin
                                    (graph-pasteboard-mixin
                                     pasteboard:basic%)))
    (new draw-lines-pasteboard% [cache-arrow-drawing? #t]))
  
  
  ;                                                                
  ;                                                                
  ;                                                                
  ;    ;;;                                      ;;;;   ;     ;  ;  
  ;   ;                                        ;    ;  ;     ;  ;  
  ;   ;                                       ;        ;     ;  ;  
  ;  ;;;;  ; ;  ;;;    ; ;;  ;;     ;;;       ;        ;     ;  ;  
  ;   ;    ;;  ;   ;   ;;  ;;  ;   ;   ;      ;        ;     ;  ;  
  ;   ;    ;       ;   ;   ;   ;  ;    ;      ;        ;     ;  ;  
  ;   ;    ;    ;;;;   ;   ;   ;  ;;;;;;      ;     ;  ;     ;  ;  
  ;   ;    ;   ;   ;   ;   ;   ;  ;           ;     ;  ;     ;  ;  
  ;   ;    ;   ;   ;   ;   ;   ;   ;           ;    ;  ;     ;  ;  
  ;   ;    ;    ;;;;;  ;   ;   ;    ;;;;        ;;;;;   ;;;;;   ;  
  ;                                                                
  ;                                                                
  ;                                                                
  
  
  (define (module-overview/file filename parent)
    (define progress-eventspace (make-eventspace))
    (define progress-frame (parameterize ([current-eventspace progress-eventspace])
                             (instantiate frame% ()
                               (parent parent)
                               (label progress-label)
                               (width 600))))
    (define progress-message (instantiate message% ()
                               (label "")
                               (stretchable-width #t)
                               (parent progress-frame)))
    
    (define thd 
      (thread
       (λ ()
         (sleep 2)
         (parameterize ([current-eventspace progress-eventspace])
           (queue-callback
            (λ ()
              (send progress-frame show #t)))))))
    
    (define text/pos 
      (let ([t (make-object text:basic%)])
        (send t load-file filename)
        (drracket:language:text/pos
         t
         0
         (send t last-position))))
    
    (define update-label void)
    
    (define (show-status str)
      (parameterize ([current-eventspace progress-eventspace])
        (queue-callback
         (λ ()
           (send progress-message set-label str)))))
    
    (define pasteboard (make-module-overview-pasteboard 
                        #f
                        (λ (x) (update-label x))))
    
    (let ([success? (fill-pasteboard pasteboard text/pos show-status void)])
      (kill-thread thd)
      (parameterize ([current-eventspace progress-eventspace])
        (queue-callback
         (λ ()
           (send progress-frame show #f))))
      (when success?
        (let ()
          (define frame (instantiate overview-frame% ()
                          (label (string-constant module-browser))
                          (width (preferences:get 'drracket:module-overview:window-width))
                          (height (preferences:get 'drracket:module-overview:window-height))
                          (alignment '(left center))))
          (define vp (instantiate vertical-panel% ()
                       (parent (send frame get-area-container))
                       (alignment '(left center))))
          (define root-message (instantiate message% ()
                                 (label 
                                  (format (string-constant module-browser-root-filename)
                                          filename))
                                 (parent vp)
                                 (stretchable-width #t)))
          (define label-message (instantiate message% ()
                                  (label "")
                                  (parent vp)
                                  (stretchable-width #t)))
          (define font/label-panel (new horizontal-panel%
                                        [parent vp]
                                        [stretchable-height #f]))
          (define font-size-gauge
            (instantiate slider% ()
              (label font-size-gauge-label)
              (min-value 1)
              (max-value 72)
              (init-value (preferences:get 'drracket:module-overview:label-font-size))
              (parent font/label-panel)
              (callback
               (λ (x y)
                 (send pasteboard set-label-font-size (send font-size-gauge get-value))))))
          (define module-browser-name-length-choice
            (new choice%
                 (parent font/label-panel)
                 (label (string-constant module-browser-name-length))
                 (choices (list (string-constant module-browser-name-long)
                                (string-constant module-browser-name-very-long)))
                 (selection (case (preferences:get 'drracket:module-browser:name-length)
                              [(0) 0]
                              [(1) 0]
                              [(2) 0]
                              [(3) 1]))
                 (callback
                  (λ (x y)
                    ;; note: the preference drracket:module-browser:name-length is also used for the View|Show Module Browser version of the module browser
                    ;; here we just treat any pref value except '3' as if it were for the long names.
                    (let ([selection (send module-browser-name-length-choice get-selection)])
                      (preferences:set 'drracket:module-browser:name-length (+ 2 selection))
                      (send pasteboard set-name-length 
                            (case selection
                              [(0) 'long]
                              [(1) 'very-long])))))))
          
          (define lib-paths-checkbox
            (instantiate check-box% ()
              (label lib-paths-checkbox-constant)
              (parent vp)
              (callback
               (λ (x y)
                 (if (send lib-paths-checkbox get-value)
                     (send pasteboard show-visible-paths 'lib)
                     (send pasteboard remove-visible-paths 'lib))))))
          
          (define ec (make-object canvas:basic% vp pasteboard))
          
          (define search-tf
            (new text-field%
                 [label (string-constant module-browser-highlight)]
                 [parent vp]
                 [callback
                  (λ (tf evt)
                    (send pasteboard begin-edit-sequence)
                    (define val (send tf get-value))
                    (define reg (and (not (string=? val ""))
                                     (regexp (regexp-quote (send tf get-value)))))
                    (let loop ([snip (send pasteboard find-first-snip)])
                      (when snip
                        (when (is-a? snip boxed-word-snip<%>)
                          (send snip set-found! (and reg (regexp-match reg (path->string (send snip get-filename))))))
                        (loop (send snip next))))
                    (send pasteboard end-edit-sequence))]))
          
          (send lib-paths-checkbox set-value (not (memq 'lib (preferences:get 'drracket:module-browser:hide-paths))))
          (set! update-label
                (λ (s)
                  (if (and s (not (null? s)))
                      (let* ([currently-over (car s)]
                             [fn (send currently-over get-filename)]
                             [lines (send currently-over get-lines)])
                        (when (and fn lines)
                          (send label-message set-label
                                (format filename-constant fn lines))))
                      (send label-message set-label ""))))
          
          (send pasteboard set-name-length 
                (case (preferences:get 'drracket:module-browser:name-length)
                  [(0) 'long]
                  [(1) 'long]
                  [(2) 'long]
                  [(3) 'very-long]))          
          ;; shouldn't be necessary here -- need to find callback on editor
          (send pasteboard render-snips)
          
          (send frame show #t)))))
  
  (define (fill-pasteboard pasteboard text/pos show-status send-user-thread/eventspace)
    
    (define progress-channel (make-async-channel))
    (define connection-channel (make-async-channel))
    
    (define-values/invoke-unit process-program-unit
      (import process-program-import^)
      (export process-program-export^))
    
    ;; =user thread=
    (define (iter sexp continue)
      (cond
        [(eof-object? sexp) 
         (custodian-shutdown-all user-custodian)]
        [else
         (add-connections sexp)
         (continue)]))
    (define init-complete (make-semaphore 0))
    
    (define user-custodian #f)
    (define user-thread #f)
    (define error-str #f)
    
    (define init-dir
      (let* ([bx (box #f)]
             [filename (send (drracket:language:text/pos-text text/pos) get-filename bx)])
        (get-init-dir 
         (and (not (unbox bx)) filename))))
    
    (define (init)
      (set! user-custodian (current-custodian))
      (set! user-thread (current-thread))
      (moddep-current-open-input-file
       (λ (filename)
         (let* ([p (open-input-file filename)]
                [wxme? (regexp-match-peek #rx#"^WXME" p)])
           (if wxme?
               (let ([t (new text%)])
                 (close-input-port p)
                 (send t load-file filename)
                 (let ([prt (open-input-text-editor t)])
                   (port-count-lines! prt)
                   prt))
               p))))
      (current-output-port (swallow-specials original-output-port))
      (current-error-port (swallow-specials original-error-port))
      (current-load-relative-directory #f)
      (current-directory init-dir)
      (error-display-handler (λ (str exn) (set! error-str str)))
      
      ;; instead of escaping when there's an error on the user thread,
      ;; we just shut it all down. This kills the event handling loop
      ;; for the eventspace and wakes up the thread below
      ;; NOTE: we cannot set this directly in `init' since the call to `init'
      ;; is wrapped in a parameterize of the error-escape-handler
      (queue-callback
       (λ ()
         (error-escape-handler
          (λ () (custodian-shutdown-all user-custodian)))
         (semaphore-post init-complete))))
    
    (define (swallow-specials port)
      (define-values (in out) (make-pipe-with-specials))
      (thread
       (λ ()
         (let loop ()
           (define c (read-char-or-special in))
           (cond
             [(char? c)
              (display c out)
              (loop)]
             [(eof-object? c)
              (close-output-port out)
              (close-input-port in)]
             [else
              (loop)]))))
      out)
    
    (define (kill-termination) (void))
    (define complete-program? #t)

    ((drracket:eval:traverse-program/multiple
      (preferences:get (drracket:language-configuration:get-settings-preferences-symbol))
      init
      kill-termination)
     text/pos
     iter
     complete-program?)
    
    (semaphore-wait init-complete)
    (send-user-thread/eventspace user-thread user-custodian)
    
    ;; this thread puts a "cap" on the end of the connection-channel
    ;; so that we know when we've gotten to the end.
    ;; this ensures that we can completely flush out the
    ;; connection-channel.
    (thread
     (λ ()
       (sync (thread-dead-evt user-thread))
       (async-channel-put connection-channel 'done)))
    
    (send pasteboard begin-adding-connections)
    (let ([evt
           (choice-evt
            (handle-evt progress-channel (λ (x) (cons 'progress x)))
            (handle-evt connection-channel (λ (x) (cons 'connect x))))])
      (let loop ()
        (let* ([evt-value (yield evt)]
               [key (car evt-value)]
               [val (cdr evt-value)])
          (case key
            [(progress) 
             (show-status val)
             (loop)]
            [(connect)
             (unless (eq? val 'done)
               (let ([name-original (list-ref val 0)]
                     [name-require (list-ref val 1)]
                     [path-key (list-ref val 2)]
                     [require-depth (list-ref val 3)])
                 (send pasteboard add-connection name-original name-require path-key require-depth))
               (loop))]))))
    (send pasteboard end-adding-connections)
    
    (custodian-shutdown-all user-custodian)
    
    (cond
      [error-str
       (message-box 
        (string-constant module-browser)
        (format (string-constant module-browser-error-expanding)
                error-str))
       #f]
      [else
       #t]))
  
  (define overview-frame%
    (class (drracket:frame:basics-mixin
            frame:standard-menus%)
      (define/override (edit-menu:between-select-all-and-find menu) (void))
      (define/override (edit-menu:between-redo-and-cut menu) (void))
      (define/override (edit-menu:between-find-and-preferences menu) (void))
      
      (define/override (edit-menu:create-cut?) #f)
      (define/override (edit-menu:create-copy?) #f)
      (define/override (edit-menu:create-paste?) #f)
      (define/override (edit-menu:create-clear?) #f)
      (define/override (edit-menu:create-select-all?) #f)
      
      (define/override (on-size w h)
        (preferences:set 'drracket:module-overview:window-width w)
        (preferences:set 'drracket:module-overview:window-height h)
        (super on-size w h))
      (super-instantiate ()))))



;                                                                                    
;                                                                                    
;                                                                                    
;                                                                                    
;                                                                                    
;                                                                                    
;   ; ;;    ; ;   ;;;     ;;;    ;;;    ;;;    ;;;       ; ;;    ; ;   ;;;     ;; ;  
;   ;;  ;   ;;   ;   ;   ;   ;  ;   ;  ;      ;          ;;  ;   ;;   ;   ;   ;  ;;  
;   ;    ;  ;   ;     ; ;      ;    ;  ;;     ;;         ;    ;  ;   ;     ; ;    ;  
;   ;    ;  ;   ;     ; ;      ;;;;;;   ;;     ;;        ;    ;  ;   ;     ; ;    ;  
;   ;    ;  ;   ;     ; ;      ;          ;      ;       ;    ;  ;   ;     ; ;    ;  
;   ;;  ;   ;    ;   ;   ;   ;  ;         ;      ;       ;;  ;   ;    ;   ;   ;  ;;  
;   ; ;;    ;     ;;;     ;;;    ;;;;  ;;;    ;;;        ; ;;    ;     ;;;     ;; ;  
;   ;                                                    ;                        ;  
;   ;                                                    ;                   ;    ;  
;   ;                                                    ;                    ;;;;   


(define-signature process-program-import^
  (progress-channel connection-channel))

(define-signature process-program-export^
  (add-connections))

(define-unit process-program-unit
  (import process-program-import^)
  (export process-program-export^)
  
  (define visited-hash-table (make-hash))
  
  ;; add-connections : (union syntax string[filename]) -> (union #f string)
  ;; recursively adds a connections from this file and
  ;; all files it requires
  ;; returns a string error message if there was an error compiling
  ;; the program
  (define (add-connections filename/stx)
    (cond
      [(path-string? filename/stx)
       (add-filename-connections filename/stx)]
      [(syntax? filename/stx)
       (add-syntax-connections filename/stx)]))
  
  ;; add-syntax-connections : syntax -> void
  (define (add-syntax-connections stx)
    (define module-codes (map compile (expand-syntax-top-level-with-compile-time-evals/flatten stx)))
    (for ([module-code (in-list module-codes)])
      (when (compiled-module-expression? module-code)
        (define name (extract-module-name stx))
        (define base 
          (build-module-filename
           (if (regexp-match #rx"^," name)
               (substring name 1 (string-length name))
               (build-path (or (current-load-relative-directory) 
                               (current-directory))
                           name))
           #f))
        (add-module-code-connections base module-code))))
  
  (define (build-module-filename pth remove-extension?)
    (define (try ext)
      (define tst (bytes->path (bytes-append 
                                (if remove-extension?
                                    (regexp-replace #rx"[.][^.]*$" (path->bytes pth) #"")
                                    (path->bytes pth))
                                ext)))
      (and (file-exists? tst)
           tst))
    (or (try #".rkt")
        (try #".ss")
        (try #".scm")
        (try #"")
        pth))
  
  ;; add-filename-connections : string -> void
  (define (add-filename-connections filename)
    (add-module-code-connections filename (get-module-code filename)))
  
  (define (add-module-code-connections module-name module-code)
    (unless (hash-ref visited-hash-table module-name (λ () #f))
      (async-channel-put progress-channel (format adding-file module-name))
      (hash-set! visited-hash-table module-name #t)
      (define import-assoc (module-compiled-imports module-code))
      (for ([line (in-list import-assoc)])
        (define level (car line))
        (define mpis (cdr line))
        (define requires (extract-filenames mpis module-name))
        (for ([require (in-list requires)])
          (add-connection module-name
                          (req-filename require)
                          (req-key require)
                          level)
          (add-filename-connections (req-filename require))))))
  
  ;; add-connection : string string (union symbol #f) number -> void
  ;; name-original and name-require and the identifiers for those paths and
  ;; original-filename? and require-filename? are booleans indicating if the names
  ;; are filenames.
  (define (add-connection name-original name-require req-sym require-depth)
    (async-channel-put connection-channel
                       (list name-original name-require req-sym require-depth)))
  
  (define (extract-module-name stx)
    (syntax-case stx ()
      [(module m-name rest ...)
       (and (eq? (syntax-e (syntax module)) 'module)
            (identifier? (syntax m-name)))
       (format "~a" (syntax->datum (syntax m-name)))]
      [else unknown-module-name]))

  ;; maps a path to the path of its "library" (see setup/private/lib-roots)
  (define get-lib-root
    (let ([t (make-hash)]) ; maps paths to their library roots
      (lambda (path)
        (hash-ref! t path (lambda () (path->library-root path))))))

  ;; extract-filenames :
  ;;   (listof (union symbol module-path-index)) string[module-name]
  ;;   -> (listof req)
  (define (extract-filenames direct-requires base)
    (define base-lib (get-lib-root base))
    (for*/list ([dr (in-list direct-requires)]
                [rkt-path (in-value (and (module-path-index? dr)
                                         (resolve-module-path-index dr base)))]
                #:when (path? rkt-path))
      (define path (build-module-filename rkt-path #t))
      (make-req (simplify-path path) (get-key dr base-lib path))))

  (define (get-key dr requiring-libroot required)
    (and (module-path-index? dr)
         ;; files in the same library => return #f as if the require
         ;; is a relative one, so any kind of require from the same
         ;; library is always displayed (regardless of hiding planet
         ;; or lib links)
         (not (equal? requiring-libroot (get-lib-root required)))
         (let-values ([(a b) (module-path-index-split dr)])
           (cond [(symbol? a) 'lib]
                 [(pair? a) (and (symbol? (car a)) (car a))]
                 [else #f])))))
