#lang racket/base
;; owner: ryanc
(require (for-syntax racket/base syntax/parse unstable/syntax)
         racket/list
         racket/class)
(provide define-notify
         notify-box%
         notify-box/pref)

;; Non-gui parts of notify-boxes
;; Worth splitting into two libraries?
;; Probably not, very few non-gui uses of classes.

(define-for-syntax (mk-init name)
  (format-id name "init-~a" (syntax-e name)))
(define-for-syntax (mk-get name)
  (format-id name "get-~a" (syntax-e name)))
(define-for-syntax (mk-set name)
  (format-id name "set-~a" (syntax-e name)))
(define-for-syntax (mk-listen name)
  (format-id name "listen-~a" (syntax-e name)))

(define-syntax (define-notify stx)
  (syntax-parse stx
    [(define-notify name:id
       (~optional value:expr
                  #:defaults ([value #'(new notify-box% (value #f))]))
       (~optional (~and #:init-method init-method)))
     (with-syntax ([init-name (mk-init #'name)]
                   [get-name (mk-get #'name)]
                   [set-name (mk-set #'name)]
                   [listen-name (mk-listen #'name)])
       (with-syntax ([(init-expr init-method-decl)
                      (if (attribute init-method)
                          (list #'(init-name)
                                #'(define/public (init-name) value))
                          (list #'value
                                #'(begin)))])
         (quasisyntax/loc stx
           (begin (field [name init-expr])
                  init-method-decl
                  (define/public-final (get-name)
                    (send name get))
                  (define/public-final (set-name new-value)
                    (send name set new-value))
                  (define/public-final (listen-name listener)
                    (send name listen listener))))))]))

(define notify-box%
  (class object%
    (init value)
    (define v value)
    (define listeners null)

    ;; get : -> value
    ;; Fetch current value
    (define/public (get)
      v)

    ;; set : value -> void
    ;; Update value and notify listeners
    (define/public (set nv)
      (set! v nv)
      (for-each (lambda (p) (p nv)) listeners))

    ;; listen : (value -> void) -> void
    ;; Add a listener
    (define/public (listen p)
      (set! listeners (cons p listeners)))

    ;; remove-listener : (value -> void) -> void
    (define/public (remove-listener p)
      (set! listeners (remq p listeners)))

    ;; remove-all-listeners : -> void
    (define/public (remove-all-listeners)
      (set! listeners null))

    (super-new)))


(define (notify-box/pref pref #:readonly? [readonly? #f])
  (define nb (new notify-box% (value (pref))))
  (send nb listen pref)
  nb)
