#lang racket/base
(require racket/class
         ffi/unsafe
         ffi/unsafe/objc
         racket/runtime-path
         "../../syntax.rkt"
         "types.rkt"
         "utils.rkt"
         "window.rkt"
         "panel.rkt"
         "queue.rkt"
         "../common/event.rkt"
         "../common/procs.rkt"
         "../../lock.rkt"
         (for-syntax racket/base))

(provide 
 (protect-out tab-panel%))

(define-runtime-path psm-tab-bar-dir
  '(so "PSMTabBarControl.framework"))
(define-runtime-path mm-tab-bar-dir
  ;; This directory will not exist for platforms other than x86_64:
  '(so "MMTabBarView.framework"))

(define use-mm?
  (and (version-10.10-or-later?)
       64-bit?
       (directory-exists? mm-tab-bar-dir)))

;; Load MMTabBarView or PSMTabBarControl:
(if use-mm?
    (void (ffi-lib (build-path mm-tab-bar-dir "MMTabBarView")))
    (void (ffi-lib (build-path psm-tab-bar-dir "PSMTabBarControl"))))
(define NSNoTabsNoBorder 6)

(define NSDefaultControlTint 0)
(define NSClearControlTint 7)

(import-class NSView NSTabView NSTabViewItem)
(define TabBarControl
  (if use-mm?
      (let ()
        (import-class MMTabBarView)
        MMTabBarView)
      (let ()
        (import-class PSMTabBarControl)
        PSMTabBarControl)))
(import-protocol NSTabViewDelegate)

(define NSOrderedAscending -1)
(define NSOrderedSame 0)
(define NSOrderedDescending 1)
(define (order-content-first a b data)
  (cond
   [(ptr-equal? a data) NSOrderedDescending]
   [(ptr-equal? b data) NSOrderedAscending]
   [else NSOrderedSame]))
(define order_content_first (function-ptr order-content-first
                                          (_fun #:atomic? #t _id _id _id -> _int)))

(define-objc-class RacketTabView NSTabView
  #:mixins (FocusResponder KeyMouseResponder CursorDisplayer)
  [wxb]
  (-a _void (tabView: [_id cocoa] didSelectTabViewItem: [_id item-cocoa])
      (let ([wx (->wx wxb)])
        (when (and wx (send wx callbacks-enabled?))
          (queue-window*-event wxb (lambda (wx) (send wx do-callback))))))
  (-a _void (onCloseTabViewItem: item)
      (let ([wx (->wx wxb)])
        (when wx
          (queue-window*-event wxb (lambda (wx) (send wx do-tab-close item))))))
  (-a _void (onDropTabViewItem: item)
      (let ([wx (->wx wxb)])
        (when wx
          (send wx check-reorder)))))

;; The MMTabBarView widget doesn't support disabling, so we have to
;; implement it. Also, we need to override a method to disable (for now)
;; reordering tabs.
(define-objc-mixin (EnableMixin Superclass)
  [wxb]
  (-a _id (hitTest: [_NSPoint pt])
      (let ([wx (->wx wxb)])
        (if (and wx
                 (not (send wx is-enabled-to-root?)))
            #f
            (super-tell hitTest: #:type _NSPoint pt))))
  (-a _BOOL (shouldStartDraggingAttachedTabBarButton: b withMouseDownEvent: evt)
      (let ([wx (->wx wxb)])
        (cond
          [(and wx (send wx drag-enabled?))
           (super-tell  #:type _BOOL shouldStartDraggingAttachedTabBarButton: b withMouseDownEvent: evt)]
          [else #f]))))

(define-objc-class RacketTabViewDelegate NSObject
  []
  (-a _BOOL (tabView: tv shouldCloseTabViewItem: item)
      (tellv tv onCloseTabViewItem: item)
      #f)
  (-a _void (tabView: tv didMoveTabViewItem: item toIndex: pos)
      (tellv tv onDropTabViewItem: item)))

;; A no-op mixin instead of `EnableMixin` for PSMTabBarControl:
(define-objc-mixin (EmptyMixin Superclass)
  [wxb])

(define-objc-class RacketPSMTabBarControl TabBarControl
  #:mixins (FocusResponder KeyMouseResponder CursorDisplayer (if use-mm? EnableMixin EmptyMixin))
  [wxb]
  (-a _void (tabView: [_id cocoa] didSelectTabViewItem: [_id item-cocoa])
      (super-tell #:type _void tabView: cocoa didSelectTabViewItem: item-cocoa)
      (queue-window*-event wxb (lambda (wx) (send wx do-callback)))))

(defclass tab-panel% (panel-mixin window%)
  (init parent
        x y w h
        style
        labels)
  (inherit get-cocoa register-as-child
           is-window-enabled?
           block-mouse-events
           refresh)

  (define tabv-cocoa (as-objc-allocation
                      (tell (tell RacketTabView alloc) init)))
  (define cocoa (if (not (memq 'border style))
                    (as-objc-allocation
                     (tell (tell NSView alloc) init))
                    tabv-cocoa))

  (define has-close? (and (memq 'can-close style) #t))

  (define control-cocoa
    (and (not (memq 'border style))
         (let ([i (as-objc-allocation
                   (tell (tell RacketPSMTabBarControl alloc)
                         initWithFrame: #:type _NSRect (make-NSRect (make-NSPoint 0 0)
                                                                    (make-NSSize 200 22))))])
           (set-ivar! tabv-cocoa wxb (->wxb this))
           (tellv cocoa addSubview: i)
           (tellv cocoa addSubview: tabv-cocoa)
           (tellv tabv-cocoa setDelegate: i)
           (tellv tabv-cocoa setTabViewType: #:type _int NSNoTabsNoBorder)
           (tellv i setTabView: tabv-cocoa)
           (when use-mm?
             (let ([delegate (as-objc-allocation (tell (tell RacketTabViewDelegate alloc) init))])
               (tellv i setDelegate: delegate)))
           (tellv i setStyleNamed: #:type _NSString
                  (if use-mm?
                      (if (version-10.14-or-later?)
                          "Mojave"
                          "Yosemite")
                      "Aqua"))
           ;; (tellv i setSizeCellsToFit: #:type _BOOL #t)
           (if (and has-close?
                    use-mm?)
               (tellv i setOnlyShowCloseOnHover: #:type _BOOL #t)
               (tellv i setDisableTabClose: #:type _BOOL #t))
           (when use-mm?
             (tellv i setResizeTabsToFitTotalWidth: #:type _BOOL #t))
           i)))

  (define item-cocoas
    (for/list ([lbl (in-list labels)])
      (let ([item (as-objc-allocation
                   (tell (tell NSTabViewItem alloc) initWithIdentifier: #f))])
        (tellv item setLabel: #:type _NSString (label->plain-label lbl))
        (when (and has-close?
                   use-mm?)
          (tellv item setHasCloseButton: #:type _BOOL #t))
        (tellv tabv-cocoa addTabViewItem: item)
        item)))
  (if control-cocoa
      (tellv cocoa setFrame: #:type _NSRect (make-NSRect (make-init-point x y) 
                                                         (make-NSSize 50 22)))
      (let ([sz (tell #:type _NSSize tabv-cocoa minimumSize)])
        (tellv tabv-cocoa setFrame: #:type _NSRect (make-NSRect (make-init-point x y) sz))
        (tellv tabv-cocoa setDelegate: tabv-cocoa)))
  
  (define content-cocoa 
    (as-objc-allocation
     (tell (tell NSView alloc)
           initWithFrame: #:type _NSRect (tell #:type _NSRect tabv-cocoa contentRect))))
  (tellv tabv-cocoa addSubview: content-cocoa)

  (define/override (get-cocoa-content) content-cocoa)
  (define/override (get-cocoa-cursor-content) tabv-cocoa)
  (define/override (set-size x y w h)
    (super set-size x y w h)
    (when control-cocoa
      (let ([r (tell #:type _NSRect cocoa frame)])
        (tellv control-cocoa setFrame: #:type _NSRect (make-NSRect (make-NSPoint
                                                                    0
                                                                    (- (NSSize-height (NSRect-size r)) 22))
                                                                   (make-NSSize
                                                                    (NSSize-width (NSRect-size r))
                                                                    22)))
        (tellv tabv-cocoa setFrame: #:type _NSRect (make-NSRect (make-NSPoint 0 0)
                                                                (make-NSSize
                                                                 (NSSize-width (NSRect-size r))
                                                                 (- (NSSize-height (NSRect-size r)) 22))))))
    (tellv content-cocoa setFrame: #:type _NSRect (tell #:type _NSRect tabv-cocoa contentRect)))

  (define/public (set-label i str)
    (tellv (list-ref item-cocoas i) setLabel: #:type _NSString (label->plain-label str)))

  (define callbacks-ok? #t)
  (define/public (callbacks-enabled?) callbacks-ok?)
  
  (define/private (direct-set-selection i)
    (tellv tabv-cocoa selectTabViewItem: (list-ref item-cocoas i)))
  (define/public (set-selection i)
    (atomically
     (set! callbacks-ok? #f)
     (direct-set-selection i)
     (set! callbacks-ok? #t)))
  (define/public (get-selection)
    (item->index (tell tabv-cocoa selectedTabViewItem)))

  (define (item->index tv)
    (for/or ([c (in-list item-cocoas)]
             [i (in-naturals)])
      (and (ptr-equal? c tv) i)))

  (public [append* append])
  (define (append* lbl)
    (atomically
     (set! callbacks-ok? #f)
     (do-append lbl)
     (set! callbacks-ok? #t)))

  (define (do-append lbl)
    (let ([item (as-objc-allocation
                 (tell (tell NSTabViewItem alloc) initWithIdentifier: #f))])
      (tellv item setLabel: #:type _NSString (label->plain-label lbl))
      (when (and has-close?
                 use-mm?)
        (tellv item setHasCloseButton: #:type _BOOL #t))
      (tellv tabv-cocoa addTabViewItem: item)
      (set! item-cocoas (append item-cocoas (list item)))
      ;; Sometimes the sub-view for the tab buttons gets put in front
      ;; of the content view, so fix the order:
      (tellv tabv-cocoa sortSubviewsUsingFunction: #:type _fpointer order_content_first
             context: #:type _pointer content-cocoa)))

  (define/public (delete i)
    (atomically
     (set! callbacks-ok? #f)
     (let ([item-cocoa (list-ref item-cocoas i)])
       (tellv tabv-cocoa removeTabViewItem: item-cocoa)
       (set! item-cocoas (remq item-cocoa item-cocoas)))
     (set! callbacks-ok? #t)))

  (define/public (set choices)
    (atomically
     (set! callbacks-ok? #f)
     (for ([item-cocoa (in-list item-cocoas)])
       (tellv tabv-cocoa removeTabViewItem: item-cocoa))
     (set! item-cocoas null)
     (for ([lbl (in-list choices)])
       (do-append lbl))
     (set! callbacks-ok? #t)))

  (define callback void)
  (define/public (set-callback cb) (set! callback cb))
  (define/public (do-callback)
    (callback this (new control-event%
                        [event-type 'tab-panel]
                        [time-stamp (current-milliseconds)])))

  (super-new [parent parent]
             [cocoa cocoa]
             [no-show? (memq 'deleted style)])
  
  (when control-cocoa
    (set-ivar! control-cocoa wxb (->wxb this)))

  (define/override (enable-window on?)
    (super enable-window on?)
    (let ([on? (and on? (is-window-enabled?))])
      (block-mouse-events (not on?))
      (tellv tabv-cocoa setControlTint: #:type _int
             (if on? NSDefaultControlTint NSClearControlTint))
      (when control-cocoa
        (unless use-mm?
          (tellv control-cocoa setEnabled: #:type _BOOL on?)))))

  (define/override (can-accept-focus?)
    (and (not control-cocoa)
         (tell #:type _BOOL tabv-cocoa canBecomeKeyView)))
  (define/override (get-cocoa-focus)
    (if control-cocoa
        content-cocoa
        tabv-cocoa))

  (define/public (number) (length item-cocoas))
  (define/public (button-focus n)
    (if (= n -1)
        (get-selection)
        (direct-set-selection n)))

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

  (define is-drag-enabled? (and (memq 'can-reorder style)))
  (define/public (drag-enabled?) is-drag-enabled?)
  (define/public (check-reorder)
    (define rev-mapping
      (for/hash ([item-cocoa (in-list item-cocoas)]
                 [i (in-naturals)])
        (values (tell #:type _NSInteger tabv-cocoa indexOfTabViewItem: item-cocoa)
                (cons i item-cocoa))))
    (unless (for/and ([(k v) (in-hash rev-mapping)])
              (= k (car v)))
      (set! item-cocoas (for/list ([i (in-range (length item-cocoas))])
                          (cdr (hash-ref rev-mapping i))))
      (define moved-mapping
        (for/list ([i (in-range (length item-cocoas))]) (car (hash-ref rev-mapping i))))
      (refresh) ; seems to be needed sometimes to fix display
      (on-choice-reorder moved-mapping)))
  (define/public (on-choice-reorder new-positions)
    (void))

  (define/public (do-tab-close close-item-cocoa)
    (define i (for/or ([item-cocoa (in-list item-cocoas)]
                       [i (in-naturals)])
                (and (equal? item-cocoa close-item-cocoa)
                     i)))
    (when i
      (on-choice-close i)))
  (define/public (on-choice-close pos)
    (void)))
