#lang racket/base
(require racket/class
         racket/gui/base
         racket/port
         racket/set
         racket/format
         string-constants
         net/url
         pkg/lib
         pkg
         (prefix-in db: pkg/db)
         "filter-panel.rkt"
         "common.rkt")

(provide by-list-panel%)

(define sc-pkg-update-package-list (string-constant install-pkg-update-package-list))
(define sc-pkg-stop-update (string-constant install-pkg-stop-update))

(define (pkg-install-status info scope default-scope)
  (~a (cond 
       [(pkg-info-auto? info) "*"]
       [else check-mark])
      (cond
       [(equal? scope default-scope) ""]
       [else "!"])
      (case (car (pkg-info-orig-pkg info))
        [(catalog) ""]
        [(link) "="]
        [(static-link) "="]
        [(clone) "="]
        [(url) "@"])))

(define by-list-panel%
  (class vertical-panel%
    (init-field [in-terminal in-terminal])

    (super-new)

    (inherit get-top-level-window)

    (define tool-panel
      (new horizontal-panel%
           [parent this]
           [alignment '(left center)]
           [stretchable-height #f]))

    (define/private (list-pkg-keep? a)
      (define keep-rx (send filter-panel get-rx))
      (or (regexp-match? keep-rx (db:pkg-name a))
          (regexp-match? keep-rx (db:pkg-author a))
          (regexp-match? keep-rx (db:pkg-desc a))
          (regexp-match? keep-rx (format-tags (pkg-tags a)))
          (regexp-match? keep-rx (db:pkg-source a))
          (regexp-match? keep-rx (db:pkg-catalog a))))

    (define filter-panel (make-filter-panel tool-panel
                                            (lambda () (sort-pkg-list!))))
  
    (define updating? #f)

    (define update-button
      (new button%
           [label sc-pkg-update-package-list]
           [parent tool-panel]
           [font small-control-font]
           [callback
            (lambda (b e)
              (if updating?
                  (interrupt-task!)
                  (update-db-package-list)))]))

    (define status-text
      (new message%
           [parent this]
           [label install-status-desc]
           [font small-control-font]
           [stretchable-width #t]))

    (define sort-column -1)
    (define list-pkg<? (lambda (a b) #f))

    (define/private (sort-by! col)
      (define sel (case col
                    [(0) (lambda (p)
                           (define i
                             (hash-ref installed (db:pkg-name p) #f))
                           (if i
                               (pkg-install-status (cdr i) (car i) default-scope)
                               ""))]
                    [(1) db:pkg-name]
                    [(2) db:pkg-author]
                    [(3) db:pkg-desc]
                    [(4) (lambda (p) (format-tags (pkg-tags p)))]
                    [(5) db:pkg-checksum]
                    [(6) db:pkg-source]
                    [(7) db:pkg-catalog]))
      (define empty-last? (not (= col 0)))
      (define switch (if (= sort-column col)
                         not
                         values))
      (set! sort-column (if (= sort-column col)
                            -1
                            col))
      (set! list-pkg<?
            (lambda (a b)
              (switch
               (cond
                [(and empty-last?
                      (not (string=? (sel a) ""))
                      (string=? (sel b) ""))
                 #t]
                [(and empty-last?
                      (string=? (sel a) "")
                      (not (string=? (sel b) "")))
                 #f]
                [(string<? (sel a) (sel b)) #t]
                [(string=? (sel a) (sel b))
                 (cond
                  [(string<? (db:pkg-name a) (db:pkg-name b)) #t]
                  [(string=? (db:pkg-name a) (db:pkg-name b))
                   (string<? (db:pkg-catalog a) (db:pkg-catalog b))]
                  [else #f])]
                [else #f])))))
    (sort-by! 7)

    (define pkg-list
      (new list-box%
           [parent this]
           [label #f]
           [choices null]
           [columns (list check-mark
		      (string-constant install-pkg-package)
		      (string-constant install-pkg-author)
		      (string-constant install-pkg-description)
		      (string-constant install-pkg-tags)
		      (string-constant install-pkg-checksum)
		      (string-constant install-pkg-source)
		      (string-constant install-pkg-catalog))]
           [style '(multiple column-headers clickable-headers)]
           [callback (lambda (lb e)
                       (when (e . is-a? . column-control-event%)
                         (sort-by! (send e get-column))
                         (sort-pkg-list!))
                       (adjust-buttons!))]))
    
    (send pkg-list set-column-width 0 30 2 1000)
    (send pkg-list set-column-width 3 300 2 1000)

    (define button-line
      (new horizontal-panel%
           [parent this]
           [alignment '(center center)]
           [stretchable-height #f]))

    (define install-button
      (new button%
           [label (pick-wider normal-control-font
			      (string-constant install-pkg-install)
			      (string-constant install-pkg-update))]
           [parent button-line]
           [style '(border)]
           [callback (lambda (b e)
                       (define pkgs (selected-pkgs))
                       (define names (map db:pkg-name pkgs))
                       (define first-inst
                         (hash-ref installed (db:pkg-name (car pkgs)) #f))
                       (define s (queue-scroll!))
                       (in-terminal
                        (if first-inst
                            (string-constant install-pkg-abort-update)
                            (string-constant install-pkg-abort-install))
                        (lambda ()
                          (apply
                           (if first-inst pkg-update-command pkg-install-command)
                           #:deps 'search-auto
                           #:scope (and first-inst
                                        (car first-inst))
                           names)))
                       (set-box! s #f)
                       (refresh-installed-list!))]))
    (define remove-button
      (new button%
           [label sc-install-pkg-remove]
           [parent button-line]
           [callback (lambda (b e)
                       (define pkgs (selected-pkgs))
                       (define names (map db:pkg-name pkgs))
                       (when (really-remove? names #:parent (get-top-level-window))
                         (define scope
                           (car (hash-ref installed (db:pkg-name (car pkgs)) #f)))
                         (define s (queue-scroll!))
                         (in-terminal
                          (string-constant install-pkg-abort-remove)
                          (lambda ()
                            (apply
                             pkg-remove-command
                             #:scope scope
                             names)))
                         (set-box! s #f)
                         (refresh-installed-list!)))]))

    ;; When a terminal panel appears, it may shrink the
    ;; list box enough that the selected item is not visible,
    ;; so re-sort to adjust scrolling:
    (define (queue-scroll!)
      (define b (box #t))
      (queue-callback (lambda ()
                        (when (unbox b)
                          (sort-pkg-list!)))
                      #f)
      b)

    (define/private (selected-pkgs)
      (for/list ([i (in-list (send pkg-list get-selections))])
        (send pkg-list get-data i)))

    (define/private (adjust-buttons!)
      (define sels (for/list ([p (selected-pkgs)])
                     (hash-ref installed (db:pkg-name p) #f)))
      (define all-installed? (and (pair? sels) 
                                  (andmap values sels)
                                  ;; must be all in the same scope:
                                  (for/and ([p (cdr sels)])
                                    (eq? (car p) (caar sels)))))
      (define none-installed? (not (ormap values sels)))
      (send install-button enable (and (pair? sels)
                                       (or (and all-installed?
                                                (not (for/or ([p (in-list sels)])
                                                       (define kind (car (pkg-info-orig-pkg (cdr p))))
                                                       (or (eq? 'link kind)
                                                           (eq? 'static-link kind)))))
                                           none-installed?)))
      (send install-button set-label (if all-installed?
                                         (string-constant install-pkg-update)
                                         (string-constant install-pkg-install)))
      (send remove-button enable all-installed?))

    (adjust-buttons!)

    (define/private (check-catalogs)
      (define user-catalogs (pkg-config-catalogs))
      (define db-catalogs (db:get-catalogs))
      (unless (equal? (list->set db-catalogs)
                      (list->set user-catalogs))
        (when (equal? 1 (message-box/custom (string-constant install-pkg-catalogs)
                                            (~a
                                             (string-constant install-pkg-update-catalogs?)
                                             "\n\n"
                                             (string-constant install-pkg-currently-configured-are) ":\n"
                                             (apply ~a
                                                    (for/list ([url user-catalogs])
                                                      (~a "  " url "\n")))
                                             "\n"
                                             (string-constant install-pkg-database-recorded-are) ":\n"
                                             (apply ~a
                                                    (for/list ([url db-catalogs])
                                                      (~a "  " url "\n"))))
                                            (string-constant install-pkg-update-catalogs)
                                            (string-constant install-pkg-do-not-update-catalogs)
                                            #f
                                            (get-top-level-window)
                                            '(caution default=1)))
          (db:set-catalogs! user-catalogs)
          (update-db-package-list))))

    (define/private (check-init)
      (unless (file-exists? (db:current-pkg-catalog-file))
        (db:set-catalogs! (pkg-config-catalogs))
        (update-db-package-list)))

    (define task #f)
    (define finalize void)
    
    (define/private (task! thunk given-finalize)
      (define finalized? #f)
      (set! finalize (lambda (ok?)
                       (unless finalized?
                         (set! finalized? #t)
                         (given-finalize ok?))))
      (set! task (thread 
                  (lambda ()
                    (with-handlers ([exn:break? void])
                      (with-handlers ([exn:fail? (lambda (exn)
                                                   (queue-callback
                                                    (lambda ()
                                                      (message-box
                                                       (string-constant error)
                                                       (exn-message exn)
                                                       (get-top-level-window)
                                                       '(ok stop)))))])
                        (thunk))
                      (let ([f finalize])
                        (queue-callback/wait (lambda () (f #t)))))))))

    (define/private (queue-callback/wait thunk)
      (define s (make-semaphore))
      (define ok? #t)
      (queue-callback (lambda ()
                        (when ok?
                          (thunk)
                          (semaphore-post s))))
      (dynamic-wind
          void
          (lambda () (semaphore-wait s))
          (lambda () (set! ok? #f))))

    (define/private (interrupt-task!)
      (when task
        (break-thread task)
        (sync task)
        (finalize #f))
      (set! task #f)
      (send status-text set-label install-status-desc))

    (define/private (update-db-package-list)
      (interrupt-task!)
      (set! updating? #t)
      (send update-button set-label sc-pkg-stop-update)
      (send status-text set-label (string-constant install-pkg-updating))
      (task!
       (lambda ()
         (for ([catalog (in-list (db:get-catalogs))])
           (queue-callback/wait
            (lambda ()
              (send status-text set-label (format (string-constant install-pkg-updating-from) catalog))))
           (pkg-catalog-update-local #:catalogs (list catalog)
                                     #:set-catalogs? #f
                                     #:quiet? #t)))
       (lambda (finished?)
         (send status-text set-label install-status-desc)
         (set! updating? #f)
         (send update-button set-label sc-pkg-update-package-list)
         (refresh-pkg-list!))))

    (define/private (background-pkg-details)
      (task!
       (lambda ()
         (define catalog-ht (make-hash))
         (for ([a-pkg (in-vector pkgs)]
               [pos (in-naturals)])
           (define name (db:pkg-name a-pkg))
           (define catalog (db:pkg-catalog a-pkg))
           (send status-text set-label 
                 (format (string-constant install-pkg-details-from) name catalog))
           (define all-ht 
             (or (hash-ref catalog-ht catalog #f)
                 (let ([all-ht (parameterize ([current-pkg-catalogs (list (string->url catalog))])
                                 (get-all-pkg-details-from-catalogs))])
                   (hash-set! catalog-ht catalog all-ht)
                   all-ht)))
           (define ht (hash-ref all-ht name))
           (define author (hash-ref ht 'author ""))
           (define source (hash-ref ht 'source ""))
           (define checksum (hash-ref ht 'checksum ""))
           (define desc (hash-ref ht 'description ""))
           (define tags (hash-ref ht 'tags '()))
           (define new-pkg (db:pkg name catalog author source checksum desc))
           (unless (and (equal? new-pkg a-pkg)
                        (equal? tagss (pkg-tags a-pkg)))
             (db:set-pkg! name catalog author source checksum desc)
             (db:set-pkg-tags! name catalog tags)
             (queue-callback/wait
              (lambda ()
                (vector-set! pkgs pos new-pkg)
                (set! tagss (hash-set (hash-remove tagss a-pkg)
                                      new-pkg
                                      tags))
                (define lpos (vector-ref posns pos))
                (when lpos
                  (send pkg-list set-string lpos (->label-string author) 2)
                  (send pkg-list set-string lpos (->label-string desc) 3)
                  (send pkg-list set-string lpos (->label-string (format-tags tags)) 4)
                  (send pkg-list set-string lpos (->label-string checksum) 5)
                  (send pkg-list set-string lpos (->label-string source) 6)))))))
       (lambda (ok?)
         (send status-text set-label install-status-desc))))

    (define/private (->label-string s)
      (let ([s (regexp-replace* #rx"[\r\n]+" s " ")])
	(substring s 0 (min 200 (string-length s)))))

    (define pkgs '#())
    (define tagss #(hash))
    (define posns '#())
    (define installed '#())
    (define default-scope 'user)
    
    (define/private (refresh-pkg-list!)
      (define pkg-list (db:get-pkgs))
      (define tags-list (map (lambda (p)
                               (db:get-pkg-tags (db:pkg-name p)
                                                (db:pkg-catalog p)))
                             pkg-list))
      (set! pkgs (list->vector pkg-list))
      (set! tagss (for/hash ([p (in-list pkg-list)]
                             [t (in-list tags-list)])
                    (values p t)))
      (refresh-installed-list! #:always? #t))

    (define/private (refresh-installed-list! #:always? [always? #f])
      (define new-default-scope (default-pkg-scope))
      (define new-installed
        (for*/hash ([scope (in-list (get-scope-list))]
                    [(k v) (in-hash (installed-pkg-table #:scope scope))])
          (values k (cons scope v))))
      (when (or always?
                (not (equal? installed new-installed))
                (not (eq? default-scope new-default-scope)))
        (set! installed new-installed)
        (set! default-scope new-default-scope)
        (sort-pkg-list!)))

    (define/private (pkg-tags p)
      (hash-ref tagss p '()))
    
    (define/private (sort-pkg-list!)
      (define sels (for/list ([i (in-list (send pkg-list get-selections))])
                     (define p (send pkg-list get-data i))
                     (cons (db:pkg-name p) (db:pkg-catalog p))))
      (set! posns (make-vector (vector-length pkgs) #f))
      (define list-pkg+poses
        (sort
         (filter
          (lambda (p) (list-pkg-keep? (car p)))
          (for/list ([p pkgs]
                     [i (in-naturals)])
            (cons p i)))
         list-pkg<?
         #:key car))
      (for ([p (in-list list-pkg+poses)]
            [j (in-naturals)])
        (vector-set! posns (cdr p) j))
      (define list-pkgs (map car list-pkg+poses))
      (send filter-panel set-result (length list-pkgs) (vector-length pkgs))
      (send pkg-list set
            (for/list ([p list-pkgs])
              (define v (hash-ref installed (db:pkg-name p) #f))
              (cond
               [(not v) ""]
               [else
                (pkg-install-status (cdr v) (car v) default-scope)]))
            (for/list ([p list-pkgs]) (->label-string (db:pkg-name p)))
            (for/list ([p list-pkgs]) (->label-string (db:pkg-author p)))
            (for/list ([p list-pkgs]) (->label-string (db:pkg-desc p)))
            (for/list ([p list-pkgs]) (->label-string (format-tags (pkg-tags p))))
            (for/list ([p list-pkgs]) (->label-string (db:pkg-checksum p)))
            (for/list ([p list-pkgs]) (->label-string (db:pkg-source p)))
            (for/list ([p list-pkgs]) (->label-string (db:pkg-catalog p))))
      (let ([ht (for/hash ([p list-pkgs]
                           [i (in-naturals)])
                  (send pkg-list set-data i p)
                  (values (cons (db:pkg-name p) (db:pkg-catalog p))
                          i))])
        (for/fold ([did? #f]) ([sel (in-list sels)])
          (define i (hash-ref ht sel #f))
          (when i
            (if did?
                (send pkg-list select i)
                (begin
                  (send pkg-list set-selection i)
                  (let ([f (send pkg-list get-first-visible-item)]
                        [n (send pkg-list number-of-visible-items)])
                    (unless (<= f i (+ f n -1))
                      (send pkg-list set-first-visible-item
                            (if (i . < . f)
                                i
                                (max 0 (add1 (- i n))))))))))
          (or did? i)))
      (adjust-buttons!))

    (define/private (format-tags tags)
      (apply ~a #:separator ", " tags))

    (define prepared? #f)

    (define/override (on-superwindow-show on?)
      (if on?
          (cond
           [prepared? (refresh-installed-list!)]
           [else
            (check-init)
            (refresh-pkg-list!)
            (check-catalogs)
            (set! prepared? #t)])
          (interrupt-task!)))))
