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

(provide
 (protect-out menu%))

(define-user32 CreatePopupMenu (_wfun -> _HMENU))
(define-user32 AppendMenuW (_wfun _HMENU _UINT _pointer _string/utf-16 -> (r : _BOOL)
                                  -> (unless r (failed 'AppendMenuW))))
(define-user32 EnableMenuItem (_wfun _HMENU _UINT _UINT -> _BOOL))

(define-user32 TrackPopupMenu(_wfun _HMENU _UINT _int _int _int _HWND (_or-null _RECT-pointer)
                                    -> _int))

(define TPM_LEFTBUTTON  #x0000)
(define TPM_RIGHTBUTTON #x0002)
(define TPM_NONOTIFY    #x0080)
(define TPM_RETURNCMD   #x0100)

(defclass menu% object%
  (init lbl
        cb
        font)

  (define label lbl)
  (define parent #f)
  (define items null)

  (define callback cb)

  (define hmenu (CreatePopupMenu))

  (define/public (get-hmenu) hmenu)

  (define/public (set-parent p lbl parent-hmenu)
    (set! label lbl)
    (set! parent p)
    (AppendMenuW parent-hmenu 
                 (bitwise-ior MF_POPUP MF_STRING)
                 hmenu
                 lbl))

  (define/public (select mb)
    (when parent
      (let ([m (regexp-match #rx"&[^&]" label)])
        (when m
          (send parent popup-menu-with-char (string-ref (car m) 1)))))
    #t)

  (def/public-unimplemented get-font)
  (def/public-unimplemented set-width)
  (def/public-unimplemented set-title)

  (define/public (popup gx gy hwnd call-callback)
    (let ([cmd (TrackPopupMenu hmenu
                               (bitwise-ior
                                TPM_LEFTBUTTON
                                TPM_RIGHTBUTTON
                                TPM_NONOTIFY
                                TPM_RETURNCMD)
                               (->screen gx) (->screen gy)
                               0 hwnd #f)])
      (let* ([e (new popup-event% [event-type 'menu-popdown])])
        (unless (zero? cmd)
          (send e set-menu-id cmd))
        (call-callback (lambda () (callback this e))))))

  (define/private (with-item id proc)
    (let loop ([items items] [pos 0])
      (cond
       [(null? items) (void)]
       [(and (car items)
             (eq? id (send (car items) id)))
        (proc (car items) pos)]
       [else (loop (cdr items) (add1 pos))])))

  (define/public (set-menu-label bar-hmenu pos str)
    (ModifyMenuW bar-hmenu pos
                 (bitwise-ior MF_BYPOSITION MF_STRING MF_POPUP)
                 (cast hmenu _HMENU _UINT_PTR)
                 str))

  (define/public (set-label id str)
    (with-item
     id
     (lambda (i pos)
       (send i set-label hmenu pos str))))

  (define/public (set-help-string id str)
    (void))

  (define/public (number) (length items))

  (define/public (enable id on?)
    (with-item
     id
     (lambda (i pos)
       (void
        (EnableMenuItem hmenu pos 
                        (bitwise-ior MF_BYPOSITION
                                     (if on? MF_ENABLED MF_GRAYED)))))))

  (define/public (enable-self parent-hmenu pos on?)
    (EnableMenuItem parent-hmenu pos  
                    (bitwise-ior MF_BYPOSITION
                                 (if on? MF_ENABLED MF_GRAYED))))

  (define/public (check id on?)
    (with-item
     id
     (lambda (i pos)
       (send i set-check hmenu pos on?))))

  (define/public (checked? id)
    (with-item
     id
     (lambda (i pos)
       (send i get-check hmenu pos))))

  (define/public (auto-check id)
    (with-item
     id
     (lambda (i pos)
       (send i set-check hmenu pos (not (send i get-check hmenu pos))))))

  (define/private (remove-item! pos)
    (set! items 
          (append (take items pos)
                  (drop items (add1 pos)))))

  (define/public (delete-by-position pos)
    (atomically
     (remove-item! pos)
     (RemoveMenu hmenu pos MF_BYPOSITION)))

  (define/public (delete id)
    (with-item
     id
     (lambda (i pos)
       (atomically
        (remove-item! pos)
        (RemoveMenu hmenu pos MF_BYPOSITION)))))

  (public [append-item append])
  (define (append-item id label help-str-or-submenu chckable?)
    (let ([i (id-to-menu-item id)])
      (when i
        (let* ([submenu (and (help-str-or-submenu . is-a? . menu%)
                             help-str-or-submenu)]
               [id (send i set-parent this label chckable?
                         submenu)])
          (atomically
           (set! items (append items (list i)))
           (if submenu
               (AppendMenuW hmenu (bitwise-ior MF_POPUP MF_STRING) (send submenu get-hmenu) label)
               (AppendMenuW hmenu (bitwise-ior MF_STRING) (cast id _intptr _pointer) label)))))))

  (define/public (append-separator) 
    (atomically
     (set! items (append items (list #f)))
     (AppendMenuW hmenu MF_SEPARATOR #f #f)))

  (super-new))
