#lang racket/base

  (require string-constants
           racket/match
           racket/class
           "sig.rkt"
           "../preferences.rkt"
           "../gui-utils.rkt"
           mred/mred-sig
           racket/path
           racket/unit)

;; for use in the test suite
(define pay-attention-to-current-eventspace-has-standard-menus?
  (make-parameter #t))

(provide pay-attention-to-current-eventspace-has-standard-menus?
         group@)

(define-unit group@
  (import mred^
          [prefix application: framework:application^]
          [prefix frame: framework:frame^]
          [prefix text: framework:text^]
          [prefix canvas: framework:canvas^]
          [prefix menu: framework:menu^]
          [prefix exit: framework:exit^])
  (export framework:group^)
  
  (define-struct frame (frame id))
  
  (define mdi-parent #f)
  
  (define extra-windows-menus-proc void)
  (define (add-to-windows-menu f)
    (let ([old extra-windows-menus-proc])
      (set! extra-windows-menus-proc
            (λ (menu)
              (f menu)
              (old menu)))))
  
  (define windows-menu-label
    (case (system-type)
      [(macosx) (string-constant windows-menu-label)]
      [else (string-constant tabs-menu-label)]))
  
  (define-local-member-name update-windows-menu)
  
  (define %
    (class object%
      
      [define most-recent-window-box (make-weak-box #f)]
      [define frame-counter 0]
      [define frames null]
      [define todo-to-new-frames void]
      
      [define windows-menus null]
      
      ;; get-windows-menu : (is-a?/c frame%) -> (union false? (is-a?/c menu%))
      (define/private (get-windows-menu frame)
        (let ([menu-bar (send frame get-menu-bar)])
          (and menu-bar
               (let ([menus (send menu-bar get-items)])
                 (ormap (λ (x)
                          (if (or (string=? (string-constant windows-menu)
                                            (send x get-plain-label))
                                  (string=? (string-constant tabs-menu)
                                            (send x get-plain-label)))
                              x
                              #f))
                        menus)))))
      
      (define/private (insert-windows-menu frame)
        (let ([menu (get-windows-menu frame)])
          (when menu
            (set! windows-menus (cons menu windows-menus)))))
      
      (define/private (remove-windows-menu frame)
        (let ([menu (get-windows-menu frame)])
          
          (when menu
            ;; to help the gc.
            (for-each (λ (i) (send i delete)) (send menu get-items))
            
            (set! windows-menus
                  (remove
                   menu
                   windows-menus
                   eq?)))))
      
      (define/public (update-windows-menu menu)
        (let* ([windows (length windows-menus)]
               [default-name (string-constant untitled)]
               [get-name 
                (λ (frame)
                  (let ([label (send frame get-label)])
                    (if (string=? label "")
                        (if (method-in-interface? 'get-entire-label (object-interface frame))
                            (let ([label (send frame get-entire-label)])
                              (if (string=? label "")
                                  default-name
                                  label))
                            default-name)
                        label)))]
               [sorted/visible-frames
                (sort
                 (filter (λ (x) (send (frame-frame x) is-shown?)) frames)
                 (λ (f1 f2)
                   (string-ci<=? (get-name (frame-frame f1))
                                 (get-name (frame-frame f2)))))])
          (for-each (λ (item) (send item delete)) (send menu get-items))
          (when (eq? (system-type) 'macosx)
            (new menu:can-restore-menu-item%
                 [label (string-constant minimize)]
                 [parent menu]
                 [callback (λ (x y) (send (send (send menu get-parent) get-frame) iconize #t))]
                 [shortcut #\m])
            (new menu:can-restore-menu-item%
                 [label (string-constant zoom)]
                 [parent menu]
                 [callback (λ (x y) 
                             (let ([frame (send (send menu get-parent) get-frame)])
                               (send frame maximize (not (send frame is-maximized?)))))]) 
            (instantiate menu:can-restore-menu-item% ()
              (label (string-constant bring-frame-to-front...))
              (parent menu)
              (callback (λ (x y) (choose-a-frame (send (send menu get-parent) get-frame))))
              (shortcut #\j))
            (instantiate menu:can-restore-menu-item% ()
              (label (string-constant most-recent-window))
              (parent menu)
              (callback (λ (x y) (most-recent-window-to-front)))
              (shortcut #\'))
            (make-object separator-menu-item% menu))
          
          (extra-windows-menus-proc menu)
          
          (when (eq? (system-type) 'macosx)
            (for-each
             (λ (frame)
               (let ([frame (frame-frame frame)])
                 (make-object menu-item% 
                   (regexp-replace*
                    #rx"&"
                    (gui-utils:trim-string (get-name frame) 200)
                    "&&")
                   menu
                   (λ (_1 _2)
                     (send frame show #t)))))
             sorted/visible-frames))))
      
      ;; most-recent-window-to-front : -> void?
      ;; brings the most recent window to the front
      (define/private (most-recent-window-to-front)
        (let ([most-recent-window (weak-box-value most-recent-window-box)])
          (when most-recent-window
            (send most-recent-window show #t))))                              
      
      (define/private (update-close-menu-item-state)
        (let* ([set-close-menu-item-state! 
                (λ (frame state)
                  (when (is-a? frame frame:standard-menus<%>)
                    (let ([close-menu-item (send frame file-menu:get-close-menu)])
                      (when close-menu-item
                        (send close-menu-item enable state)))))])
          (if (eq? (length frames) 1)
              (set-close-menu-item-state! (car frames) #f)
              (for-each (λ (a-frame)
                          (set-close-menu-item-state! a-frame #t))
                        frames))))
      
      (define/public (get-mdi-parent)
        (when (and (eq? (system-type) 'windows)
                   (preferences:get 'framework:windows-mdi)
                   (not mdi-parent))
          (set! mdi-parent (make-object frame% (application:current-app-name)
                             #f #f #f #f #f
                             '(mdi-parent)))
          (send mdi-parent show #t))
        mdi-parent)
      
      (define/public (get-frames) (map frame-frame frames))
      
      (define/public (frame-label-changed frame)
        (void))
      
      (define/public (frame-shown/hidden frame)
        (void))
      
      (define/public (for-each-frame f)
        (for-each (λ (x) (f (frame-frame x))) frames)
        (set! todo-to-new-frames
              (let ([old todo-to-new-frames])
                (λ (frame) (old frame) (f frame)))))
      
      (define/public (get-active-frame)
        (cond
          [(null? frames) #f]
          [else (frame-frame (car frames))]))
      
      (define/public (set-active-frame f)
        (define active-frame (get-active-frame))
        (when (and active-frame
                   (not (eq? active-frame f)))
          (set! most-recent-window-box (make-weak-box active-frame)))
        (move-to-front f))

      (define/public (insert-frame new-frame)
        (define already-has-new-frame? (move-to-front new-frame))
        (unless already-has-new-frame?
          (set! frame-counter (add1 frame-counter))
          (set! frames (cons (make-frame new-frame frame-counter) frames))
          (update-close-menu-item-state)
          (insert-windows-menu new-frame)
          (todo-to-new-frames new-frame)))

      ;; move-to-front : (is-a?/c frame%) -> boolean
      ;; effect: changes `frames` so that the frame struct
      ;;         containing `f` is at the front, if there is one
      ;; returns #t if `f` was in `frames` and #f otherwise
      (define/private (move-to-front f)
        (define-values (fst rst)
          (let loop ([frames frames])
            (match frames
              ['() (values #f '())]
              [(cons fr frames)
               (cond
                 [(object=? (frame-frame fr) f)
                  (values fr frames)]
                 [else
                  (define-values (fst more) (loop frames))
                  (values fst (cons fr more))])])))
        (cond
          [fst
           (set! frames (cons fst rst))
           #t]
          [else
           #f]))
      
      (define/public (remove-frame f)
        (let ([new-frames
               (remove
                f frames
                (λ (f fr) (eq? f (frame-frame fr))))])
          (set! frames new-frames)
          (update-close-menu-item-state)
          (remove-windows-menu f)))
      
      (define/public (clear)
        (set! frames null)
        #t)
      
      (define/public (on-close-all)
        (for-each (λ (f)
                    (let ([frame (frame-frame f)])
                      (send frame on-close) 
                      (send frame show #f)))
                  frames))
      
      (define/public (can-close-all?)
        (andmap (λ (f)
                  (let ([frame (frame-frame f)])
                    (send frame can-close?)))
                frames))
      
      (define/public (locate-file name)
        (let* ([normalized
                ;; allow for the possibility of filenames that are urls
                (with-handlers ([exn:fail?
                                 (λ (x) name)])
                  (normal-case-path
                   (normalize-path name)))]
               [test-frame
                (λ (frame)
                  (and (is-a? frame frame:basic<%>)
                       (send frame editing-this-file? normalized)))])
          (let loop ([frames frames])
            (cond
              [(null? frames) #f]
              [else
               (let* ([frame (frame-frame (car frames))])
                 (if (test-frame frame)
                     frame
                     (loop (cdr frames))))]))))

      (define/public (get-all-open-files)
        (define the-frames (map frame-frame frames))
        (for/list ([frame (in-list the-frames)])
          (send frame get-all-open-files)))
      
      (super-new)))

  (define (create-windows-menu mb)
    (new menu:can-restore-underscore-menu%
         [label windows-menu-label]
         [demand-callback (λ (menu) (send (get-the-frame-group) update-windows-menu menu))]
         [parent mb]))
  
  (define (can-close-check)
    (let ([number-of-frames (length (send (get-the-frame-group) get-frames))])
      (or (not (preferences:get 'framework:exit-when-no-frames))
          (exit:exiting?)
          (not (= 1 number-of-frames))
          (and (pay-attention-to-current-eventspace-has-standard-menus?)
               (current-eventspace-has-standard-menus?))
          (exit:user-oks-exit))))

  (define (on-close-action)
    (when (preferences:get 'framework:exit-when-no-frames)
      (unless (exit:exiting?)
        (when (and (null? (send (get-the-frame-group) get-frames))
                   (not (and (pay-attention-to-current-eventspace-has-standard-menus?)
                             (current-eventspace-has-standard-menus?))))
          (exit:exit)))))
  
  (define (choose-a-frame parent)
    (define sorted-frames
      (sort
       (send (get-the-frame-group) get-frames)
       (λ (x y) (string-ci<=? (send x get-label) (send y get-label)))))
    (define d
      (make-object dialog% (string-constant bring-frame-to-front) parent 400 600))
    (define lb
      (new list-box%
           (label #f)
           (choices (map (λ (x) (gui-utils:trim-string (send x get-label) 200)) sorted-frames))
           (callback (λ (x y) (listbox-callback y)))
           (parent d)))
    (define t (new text:hide-caret/selection%))
    (define ec (new canvas:basic%
                    (parent d)
                    (stretchable-height #f)))
    (define bp (new horizontal-panel%
                    (parent d)
                    (stretchable-height #f)
                    (alignment '(right center))))
    (define cancelled? #t)
    (define (listbox-callback evt)
      (case (send evt get-event-type)
        [(list-box)
                          
         (send ok enable (pair? (send lb get-selections)))
                          
         (let ([full-name
                (let ([sels (send lb get-selections)])
                  (and (pair? sels)
                       (let ([fr (list-ref sorted-frames (car sels))])
                         (and (is-a? fr frame:basic%)
                              (send fr get-filename)))))])
           (send t begin-edit-sequence)
           (send t erase)
           (when full-name
             (send t insert (path->string full-name)))
           (send t end-edit-sequence))]
        [(list-box-dclick)
         (set! cancelled? #f)
         (send d show #f)]))
    (define-values (ok cancel)
      (gui-utils:ok/cancel-buttons
       bp
       (λ (x y)
         (set! cancelled? #f)
         (send d show #f))
       (λ (x y)
         (send d show #f))))
    (send ec set-line-count 3)
    (send ec set-editor t)
    (send t auto-wrap #t)
    (let ([fr (car sorted-frames)])
      (when (and (is-a? fr frame:basic<%>)
                 (send fr get-filename))
        (send t insert (path->string (send (car sorted-frames) get-filename))))
      (send lb set-selection 0))
    (send d show #t)
    (unless cancelled?
      (let ([sels (send lb get-selections)])
        (unless (null? sels)
          (send (list-ref sorted-frames (car sels)) show #t)))))

  
  (define (internal-get-the-frame-group)
    (let ([the-frame-group (make-object %)])
      (set! internal-get-the-frame-group (λ () the-frame-group))
      (internal-get-the-frame-group)))
  
  (define (get-the-frame-group)
    (internal-get-the-frame-group)))
