#lang racket/base
(require ffi/unsafe
         ffi/unsafe/objc
         racket/class
         racket/path
         "../../lock.rkt"
         "utils.rkt"
         "types.rkt"
         "queue.rkt"
         "frame.rkt")

(provide (protect-out file-selector))

(import-class NSOpenPanel NSSavePanel NSURL NSArray)

(define (nsurl->string url)
  (string->path (tell #:type _NSString url path)))

(define (file-selector message directory filename 
                       extension
                       filters style parent)
  (promote-to-gui!)
  (let ([ns (as-objc-allocation-with-retain
             (if (memq 'put style)
                (tell NSSavePanel savePanel)
                (tell NSOpenPanel openPanel)))]
        [parent (and parent
                     (not (send parent get-sheet))
                     parent)])

    (let* ([globs (apply append
                         (map (lambda (f) (regexp-split #rx" *; *" (cadr f)))
                              filters))]
           ;; get suffixes from "*.foo" globs (and *only* such globs)
           [extensions
            (for/list ([g (in-list globs)]
                       #:when (and (regexp-match #rx"[*][.][^.]+$" g)
                                   (not (equal? g "*.*"))))
              (car (regexp-match #rx"[^.]+$" g)))]
           [extensions
            (if (memq 'packages style) (cons "app" extensions) extensions)]
           [extensions
            (if (and extension (not (equal? "" extension)))
              (cons extension extensions) extensions)])
      (unless (null? extensions)
        (when (memq 'put style)
          (tellv ns setCanSelectHiddenExtension: #:type _BOOL #t))
        (let ([allow-any? (member "*.*" globs)])
          (when (or (not allow-any?)
                    (memq 'put style))
            (let ([a (tell NSArray
                           arrayWithObjects: #:type (_list i _NSString) extensions
                           count: #:type _NSUInteger (length extensions))])
              (tellv ns setAllowedFileTypes: a))
            (tellv ns setAllowsOtherFileTypes: #:type _BOOL allow-any?)))))

    (cond
     [(memq 'multi style)
      (tellv ns setAllowsMultipleSelection: #:type _BOOL #t)]
     [(memq 'dir style)
      (tellv ns setCanChooseDirectories: #:type _BOOL #t)
      (tellv ns setCanChooseFiles: #:type _BOOL #f)])

    (when (or (memq 'put style)
              (memq 'dir style))
      (tellv ns setCanCreateDirectories: #:type _BOOL #t))

    (when message
      (tellv ns setMessage: #:type _NSString message))
    (when directory
      (let ([dir (if (string? directory)
                     directory
                     (path->string directory))])
        (if (version-10.6-or-later?)
            (tellv ns setDirectoryURL: (tell NSURL 
                                             fileURLWithPath: #:type _NSString dir
                                             isDirectory: #:type _BOOL #t))
            (tellv ns setDirectory: #:type _NSString dir))))
    (when filename
      (when (version-10.6-or-later?)
        (tellv ns setNameFieldStringValue: #:type _NSString (path->string
                                                             (file-name-from-path filename)))))
    
    (when (memq 'enter-packages style)
      (tellv ns setTreatsFilePackagesAsDirectories: #:type _BOOL #t))

    (let ([result 
           ;; We run the file dialog completely modally --- shutting out
           ;; all other eventspaces and threads. It would be nice to improve
           ;; on this, but it's good enough.
           (atomically
            (let ([front (get-front)]
                  [parent (and (version-10.6-or-later?)
                               parent)]
                  [completion (and (version-10.10-or-later?)
                                   parent
                                   ;; retain until done:
                                   (box null))]
                  [completion-result 0])
              (when parent
                (tellv ns beginSheetModalForWindow: (send parent get-cocoa-window)
                       completionHandler: #:type _pointer (and completion
                                                               (objc-block
                                                                (_fun #:atomic? #t #:keep completion _pointer _int -> _void)
                                                                (lambda (blk val)
                                                                  (set! completion-result val)
                                                                  (tellv app stopModal))
                                                                #:keep completion))))
              (begin0
               (if completion
                   ;; For 10.10, using `runModal` centers the sheet before
                   ;; running the model loop, so we have to use a completion
                   ;; handler as installed above plus `runModalForWindow:`
                   ;; (and this works despite the docs's claim that
                   ;; `runModalForWindow:` centers its argument).
                   (begin
                     (tell app runModalForWindow: ns)
                     (set-box! completion #f)
                     completion-result)
                   ;; For 10.9 and earlier, runModel will do the hard part
                   ;; for us:
                   (tell #:type _NSInteger ns runModal))
               (when parent (tell app endSheet: ns))
               (when front (tellv (send front get-cocoa-window)
                                  makeKeyAndOrderFront: #f)))))])
      (begin0
       (if (zero? result)
           #f
           (atomically
            (if (memq 'multi style)
                (let ([urls (tell ns URLs)])
                  (for/list ([i (in-range (tell #:type _NSUInteger urls count))])
                    (nsurl->string (tell urls objectAtIndex: #:type _NSUInteger i))))
                (let ([url (tell ns URL)])
                  (nsurl->string url)))))
       (release ns)))))
