#lang racket/base
(require ffi/unsafe/objc
         ffi/unsafe
         racket/class
         (only-in racket/list take drop)
          "../../syntax.rkt"
          "../../lock.rkt"
         "item.rkt"
         "utils.rkt"
         "types.rkt"
         "const.rkt"
         "window.rkt"
         "font.rkt"
         "../common/event.rkt")

(provide 
 (protect-out list-box%))

;; ----------------------------------------

(import-class NSScrollView NSTableView NSTableColumn NSCell NSIndexSet NSFont)
(import-protocol NSTableViewDataSource)

(define NSLineBreakByTruncatingTail 4)

(define during-selection-set? (make-parameter #f))

;; 11.0 and up:
(define NSTableViewStyleAutomatic 0)
(define NSTableViewStyleFullWidth 1)
(define NSTableViewStyleInset 2)
(define NSTableViewStyleSourceList 3)
(define NSTableViewStylePlain 4)

(define default-cell-font
  (and (version-11.0-or-later?)
       (atomically
        (let ([f (tell NSFont controlContentFontOfSize: #:type _CGFloat 0.0)])
          (tellv f retain)
          f))))

(define-objc-class RacketTableView NSTableView
  #:mixins (FocusResponder KeyMouseResponder CursorDisplayer)
  [wxb]
  [-a _void (doubleClicked: [_id sender])
      (queue-window*-event wxb (lambda (wx) (send wx clicked 'list-box-dclick)))]
  [-a _void (tableViewSelectionDidChange: [_id aNotification])
      (unless (during-selection-set?)
        (queue-window*-event wxb (lambda (wx) (send wx clicked 'list-box))))]
  [-a _void (tableView: [_id view] didClickTableColumn: [_id col])
      (queue-window*-event wxb (lambda (wx) (send wx clicked-column col)))]
  [-a _void (tableViewColumnDidMove: [_id view])
      (let ([wx (->wx wxb)])
        (when wx (send wx reset-column-order)))])

(define-objc-class RacketDataSource NSObject
  #:protocols (NSTableViewDataSource)
  [wxb]
  [-a _NSInteger (numberOfRowsInTableView: [_id view])
      (let ([wx (->wx wxb)])
        (send wx number))]
  [-a _id (tableView: [_id aTableView]
                      objectValueForTableColumn: [_id aTableColumn]
                      row: [_NSInteger rowIndex])
      (define wx (->wx wxb))
      (define text (if wx (send wx get-cell aTableColumn rowIndex) "???"))
      (define cell (tell (tell NSCell alloc) initTextCell: #:type _NSString text))
      (define font (and wx (send wx get-cell-font)))
      (tellv cell setLineBreakMode: #:type _NSUInteger NSLineBreakByTruncatingTail)
      (when font (tellv cell setFont: font))
      (tell cell autorelease)])

(define (remove-nth data i)
  (cond
   [(zero? i) (cdr data)]
   [else (cons (car data) (remove-nth (cdr data) (sub1 i)))]))

(defclass list-box% item%
  (init parent cb
        label kind x y w h
        choices style
        font label-font 
        columns column-order)
  (inherit set-size init-font
           register-as-child)

  (define source (as-objc-allocation
                  (tell (tell RacketDataSource alloc) init)))
  (set-ivar! source wxb (->wxb this))

  (define itemss (cons choices
                       (for/list ([i (in-list (cdr columns))])
                         (for/list ([i choices])
                           ""))))
  (define num-columns (length columns))
  (define data (map (lambda (x) (box #f)) choices))
  (define count (length choices))

  (define cocoa (as-objc-allocation
                 (tell (tell NSScrollView alloc) init)))
  (define-values (content-cocoa column-cocoas)
    (let ([content-cocoa 
           (as-objc-allocation
            (tell (tell RacketTableView alloc) init))])
      (tellv content-cocoa setDelegate: content-cocoa)
      (tellv content-cocoa setDataSource: source)
      (define cols
        (for/list ([title (in-list columns)])
          (let ([col (as-objc-allocation
                      (tell (tell NSTableColumn alloc) initWithIdentifier: #:type _NSString title))])
            (tellv content-cocoa addTableColumn: col)
            (tellv (tell col headerCell) setStringValue: #:type _NSString title)
            col)))
      (init-font content-cocoa font)
      (when (version-11.0-or-later?)
        (tellv content-cocoa setStyle: #:type _NSInteger NSTableViewStyleFullWidth)
        (tellv content-cocoa setIntercellSpacing: #:type _NSSize (make-NSSize 1.0 1.0)))
      (values content-cocoa cols)))
  (set-ivar! content-cocoa wxb (->wxb this))

  (tellv cocoa setDocumentView: content-cocoa)
  (tellv cocoa setHasVerticalScroller: #:type _BOOL #t)
  (tellv cocoa setHasHorizontalScroller: #:type _BOOL #t)
  (unless (memq 'column-headers style)
    (tellv content-cocoa setHeaderView: #f))
  (define allow-multi? (not (eq? kind 'single)))
  (when allow-multi?
    (tellv content-cocoa setAllowsMultipleSelection: #:type _BOOL #t))
  (unless (memq 'reorderable-headers style)
    (tellv content-cocoa setAllowsColumnReordering: #:type _BOOL #f))

  (when column-order
    (set-column-order column-order))
  (define/public (set-column-order column-order)
    (atomically
     (for ([c (in-list column-cocoas)])
       (tellv c retain)
       (tellv content-cocoa removeTableColumn: c))
     (for ([pos (in-list column-order)])
       (let ([c (list-ref column-cocoas pos)])
         (tellv content-cocoa addTableColumn: c)
         (tellv c release)))
     (reset-column-order)))

  (define/public (set-column-label i s)
    (let ([col (list-ref column-cocoas i)])
      (tellv (tell col headerCell) setStringValue: #:type _NSString s)
      (reset)))

  (define/public (set-column-size i w min-w max-w)
    (let ([col (list-ref column-cocoas i)])
      (tellv col setMinWidth: #:type _CGFloat min-w)
      (tellv col setMaxWidth: #:type _CGFloat max-w)
      (tellv col setWidth: #:type _CGFloat w)))

  (define/public (get-column-size i)
    (let ([col (list-ref column-cocoas i)]
          [int (lambda (v) (inexact->exact (round v)))])
      (values
       (int (tell #:type _CGFloat col width))
       (int (tell #:type _CGFloat col minWidth))
       (min 10000 (int (tell #:type _CGFloat col maxWidth))))))

  (define/override (get-cocoa-content) content-cocoa)
  (define/override (get-cocoa-control) content-cocoa)

  (super-new [parent parent]
             [cocoa cocoa]
             [no-show? (memq 'deleted style)]
             [callback cb])

  (set-size 0 0 32 50)
  ; (tellv content-cocoa sizeToFit)

  (tellv content-cocoa setTarget: content-cocoa)
  (tellv content-cocoa setDoubleAction: #:type _SEL (selector doubleClicked:))

  (def/public-unimplemented get-label-font)

  (define cell-font (or (and font (font->NSFont font))
                        default-cell-font))
  (when cell-font
    (tellv content-cocoa setRowHeight: #:type _CGFloat
           (+ (tell #:type _CGFloat cell-font defaultLineHeightForFont) 2)))

  (define/public (get-cell-font)
    cell-font)

  (define/public (get-selection)
    (if allow-multi?
        (let ([l (get-selections)])
          (if (null? l)
              -1
              (car l)))
        (tell #:type _NSInteger content-cocoa selectedRow)))
  (define/public (get-selections)
    (atomically
     (with-autorelease
      (let ([v (tell content-cocoa selectedRowIndexes)])
        (begin0
         (let loop ([i (tell #:type _NSInteger v firstIndex)])
           (cond
            [(= i NSNotFound) null]
            [else (cons i (loop (tell #:type _NSInteger v 
                                      indexGreaterThanIndex: #:type _NSInteger i)))])))))))
    
  (define/private (header-height)
    (let ([hv (tell content-cocoa headerView)])
      (if hv
          (NSSize-height (NSRect-size (tell #:type _NSRect hv frame)))
          0)))

  (define/public (number-of-visible-items)
    (define doc (tell #:type _NSRect cocoa documentVisibleRect))
    (define h (+ (tell #:type _CGFloat content-cocoa rowHeight)
                 (NSSize-height (tell #:type _NSSize content-cocoa intercellSpacing))))
    (define doc-h (- (NSSize-height (NSRect-size doc))
                     (header-height)))
    (define n (floor (/ doc-h h)))
    (if (rational? n)
        (max 1 (inexact->exact n))
        1))
  (define/public (get-first-item)
    (define doc (tell #:type _NSRect cocoa documentVisibleRect))
    (define h (header-height))
    (NSRange-location (tell #:type _NSRange content-cocoa 
                            rowsInRect: #:type _NSRect 
                            (if (zero? h)
                                doc
                                (make-NSRect (NSRect-origin doc)
                                             (make-NSSize (NSSize-width (NSRect-size doc))
                                                          (- (NSSize-height (NSRect-size doc)) h)))))))

  (define/public (set-first-visible-item i)
    (define num-vis (number-of-visible-items))
    (define start (max 0 (min i (- count num-vis))))
    (tellv content-cocoa scrollRowToVisible: #:type _NSInteger start)
    (tellv content-cocoa scrollRowToVisible: #:type _NSInteger (+ start (sub1 num-vis))))

  (define/private (replace items i s)
    (append (take items i)
            (list s)
            (drop items (add1 i))))

  (define/public (set-string i s [col 0])
    (let ([new-itemss (replace 
                       itemss
                       col
                       (replace (list-ref itemss col)
                                i
                                s))])
      (set! itemss new-itemss))
    (reset))

  (define/public (number)
    ;; Can be called by event-handling thread
    count)
  (define/public (get-cell col n)
    ;; Can be called by event-handling thread
    (let ([col (if (number? col)
                   (order->number col)
                   (col->number col))])
      (if (col . > . num-columns) ; can happen as column is deleted
          ""
          (list-ref (list-ref itemss col) n))))

  (define/private (col->number col)
    (let loop ([l column-cocoas] [pos 0])
      (cond
       [(null? l) #f]
       [(ptr-equal? (car l) col) pos]
       [else (loop (cdr l) (add1 pos))])))

  ;; When columns are rearranged, we have to be able to map
  ;; from current column numbers to original column numbers
  (define order-vector #f)
  (define/private (order->number col)
    (prep-order-vector)
    (vector-ref order-vector col))
  (define/private (prep-order-vector)
    (unless order-vector
      (let ([vec (make-vector (length column-cocoas))])
        (let ([array (tell content-cocoa tableColumns)])
          (for/list ([i (in-range (tell #:type _NSUInteger array count))])
            (let ([col (tell array objectAtIndex: #:type _NSUInteger i)])
              (vector-set! vec i (col->number col)))))
        (set! order-vector vec))))
  (define/public (reset-column-order)
    (set! order-vector #f))

  (define/public (get-column-order)
    (prep-order-vector)
    (vector->list order-vector))

  (define/public (append-column title)
    (atomically
     (let ([col (as-objc-allocation
                 (tell (tell NSTableColumn alloc) initWithIdentifier: #:type _NSString title))])
       (tellv content-cocoa addTableColumn: col)
       (tellv (tell col headerCell) setStringValue: #:type _NSString title)
       (set! column-cocoas (append column-cocoas (list col)))
       (set! itemss (append itemss
                            (list (for/list ([i (in-list (car itemss))])
                                    ""))))
       (set! num-columns (add1 num-columns))
       (reset-column-order)))
    (reset))

  (define/public (delete-column i)
    (atomically
     (let ([c (list-ref column-cocoas i)])
       (define (drop-nth l i)
         (cond
          [(zero? i) (cdr l)]
          [else (cons (car l) (drop-nth (cdr l) (sub1 i)))]))
       (set! num-columns (sub1 num-columns))
       (tellv content-cocoa removeTableColumn: c)
       (set! column-cocoas (drop-nth column-cocoas i))
       (set! itemss (drop-nth itemss i))
       (reset-column-order)))
    (reset))

  (define callback cb)
  (define/public (clicked event-type)
    (unless (zero? count)
      (callback this (new control-event%
                          [event-type event-type]
                          [time-stamp (current-milliseconds)]))))

  (define can-click-column? (memq 'clickable-headers style))
  (define/public (clicked-column col)
    (when can-click-column?
      (let ([pos (col->number col)])
        (callback this (new column-control-event%
                            [event-type 'list-box-column]
                            [time-stamp (current-milliseconds)]
                            [column pos])))))

  (define/public (set-data i v) (set-box! (list-ref data i) v))
  (define/public (get-data i) (unbox (list-ref data i)))

  (define/public (selected? i)
    (tell #:type _BOOL content-cocoa isRowSelected: #:type _NSInteger i))

  (define/public (select i [on? #t] [extend? #t])
    (parameterize ([during-selection-set? #t])
      (if on?
          (atomically
           (with-autorelease
             (let ([index (tell (tell NSIndexSet alloc) initWithIndex: #:type _NSUInteger i)])
               (tellv content-cocoa 
                      selectRowIndexes: index
                      byExtendingSelection: #:type _BOOL (and extend? allow-multi?)))))
          (tellv content-cocoa deselectRow: #:type _NSInteger i))))
  (define/public (set-selection i)
    (select i #t #f))

  (define/public (delete i)
    (atomically
     (set! count (sub1 count))
     (set! itemss (for/list ([items (in-list itemss)])
                    (remove-nth items i)))
     (set! data (remove-nth data i)))
    (reset))
  (define/public (clear)
    (atomically
     (set! count 0)
     (set! itemss (for/list ([items (in-list itemss)])
                    null))
     (set! data null))
    (reset))
  (define/public (set choices . more-choices)
    (atomically
     (set! itemss (cons choices more-choices))
     (set! data (map (lambda (x) (box #f)) choices))
     (set! count (length choices)))
    (reset))

  (public [append* append])
  (define (append* s [v #f])
    (atomically
     (set! count (add1 count))
     (set! itemss (cons (append (car itemss) (list s))
                        (for/list ([items (in-list (cdr itemss))])
                          (append items (list "")))))
     (set! data (append data (list (box v)))))
    (reset))

  (define/public (reset)
    (tellv content-cocoa noteNumberOfRowsChanged)
    (tellv content-cocoa reloadData))

  (define/override (maybe-register-as-child parent on?)
    (register-as-child parent on?)))
