(define (with-lock lock thunk)
  (obtain-lock lock)
  (let ((res (thunk)))
    (release-lock lock)
    res))

;; Thread-safe event queue *******************************************

(define-record-type sync-x-event :sync-x-event
  (really-make-sync-x-event event next)
  sync-x-event?
  (event sync-x-event-event)
  (next really-next-sync-x-event really-set-next-sync-x-event))

(define (make-sync-x-event event)
  (really-make-sync-x-event event (make-placeholder)))

(define (next-sync-x-event sync-x-event pred)
  (let ((next (placeholder-value (really-next-sync-x-event sync-x-event))))
    (if (pred (sync-x-event-event next))
	next
	(next-sync-x-event next pred))))

(define (set-next-sync-x-event! sync-x-event next-sync-x-event)
  (placeholder-set! 
   (really-next-sync-x-event sync-x-event) 
   next-sync-x-event))

(define *most-recent-sync-x-event* (make-sync-x-event 'no-event))
(define *most-recent-lock* (make-lock))

(define (init-sync-x-events dpy)
  (spawn
   (lambda ()
     (let lp ()
       (let ((next (wait-event dpy)))
	 (with-lock *most-recent-lock*
		    (lambda ()
		      (set-next-sync-x-event! *most-recent-sync-x-event*
					      (make-sync-x-event next))
		      (set! *most-recent-sync-x-event*
			    (placeholder-value (really-next-sync-x-event
						*most-recent-sync-x-event*)))))
	 (lp))))
   'init-sync-x-events))

(define (most-recent-sync-x-event)
  *most-recent-sync-x-event*)

;; High-Level Event-Dispatcher ***************************************

;; contains (display window event-mask) triples
(define *event-requests* '())
(define *event-requests-lock* (make-lock))

(define (make-request display window event-mask)
  (list display window event-mask))

(define (add-request! req)
  (with-lock *event-requests-lock*
	     (lambda ()
	       (set! *event-requests*
		     (cons req *event-requests*))
	       (select-requests))))

(define (remove-request! req)
  (with-lock *event-requests-lock*
	     (lambda ()
	       (set! *event-requests*
		     (filter (lambda (r) (not (eq? r req))) *event-requests*))
	       (select-requests))))

(define request:display car)
(define request:window cadr)
(define request:event-mask caddr)

(define (event-masks-union masks)
  (fold-right (lambda (m res)
		(enum-set-union m res))
	      (event-mask)
	      masks))

(define (select-requests)
  (let loop ((rest *event-requests*))
    (if (not (null? rest))
	(let ((r (car rest)))
	  (call-with-values
	   (lambda ()
	     (partition (lambda (r2)
			  ;; find all with the same display and window
			  (and (eq? (request:display r2) (request:display r))
			       (eq? (request:window r2) (request:window r))))
			(cdr rest)))
	   (lambda (same rest)
	     (let ((mask (event-masks-union (map request:event-mask
						 (cons r same)))))
	       (if (window-exists? (request:display r) (request:window r))
		   (display-select-input (request:display r) (request:window r)
					 mask)))
	     (loop rest)))))))

(define (call-with-event-channel display window event-mask fun)
  (let ((r (make-request display window event-mask))
	(x-event-channel (make-channel))
        (filter-control-channel (make-channel))
        (dead?-box (cons #f #f)))
    (spawn-event-filter x-event-channel filter-control-channel 
                        display window event-mask fun dead?-box)
    ;; we send the first sync-event to the thread to really have the
    ;; most recent one, without keeping it in an environment.
    (send filter-control-channel (most-recent-sync-x-event))
    (let ((first? #t))
      (dynamic-wind
       (lambda () 
         (add-request! r)
         (if first?
             (set! first? #f)
             (warn "throwing back into call-with-event-channel")))
       (lambda () (call-with-values 
                   (lambda () (fun x-event-channel))
                   (lambda args
                     (apply values args))))
       (lambda ()
         (set-car! dead?-box #t)
         (remove-request! r))))))

(define (true x) #t)

(define (spawn-event-filter out-channel control-channel display window event-mask fun dead?-box)
  (let ((pred (lambda (e)
		(and (eq? (any-event-display e) display)
		     (matches-event-mask? window event-mask e)))))
    (spawn (lambda ()
	     ;; the first sync-event is send to us through the channel
	     (let loop ((se (receive control-channel)))
               (if (not (car dead?-box))
                   (let ((nse (next-sync-x-event se true)))
                     (if (not (car dead?-box))
                         (if (pred (sync-x-event-event nse))
                             (begin (send out-channel (sync-x-event-event nse))
                                    (loop nse))
                             (loop nse)))))))
           (cons 'spawn-event-filter fun))))

(define (matches-event-mask? window event-mask event)
  (let ((type (any-event-type event)))
    (cond
     ;; keymap-event has no window element
     ((eq? type (event-type keymap-notify))
      (enum-set-member? event-mask (event-mask-item keymap-state)))
     ;; other events must have at least the correct window
     ((not (eq? window (any-event-window event)))
      #f)
     ;; these event are send always because they do not depend on a mask
     ((or (eq? type (event-type client-message))
	  (eq? type (event-type mapping-notify))
	  (eq? type (event-type selection-clear))
	  (eq? type (event-type selection-notify))
	  (eq? type (event-type selection-request)))
      #t)
     ;; these do not depend an an event-mask too, but on a flag in GC,
     ;; so we sent it too
     ((or (eq? type (event-type graphics-expose))
	  (eq? type (event-type no-expose)))
      #t)

     ;; OwnerGrabButtonMask only generates extra events between a
     ;; ButtonPress and ButtonRelease event and does not be respected
     ;; here

     ;; PointerMotionHintMask only has an effect if one of the
     ;; ButtonMotion Masks or PointerMotionMask is selected, so we
     ;; don't have to take a look at it here.

     ;; for the rest one of the event-mask items must match the type
     ((any (lambda (mask-item)
	     (matches-event-mask-2? type window event mask-item))
	   (enum-set->list event-mask))
      #t)
     (else #f))))

(define (matches-event-mask-2? type window event mask-item)
  (cond
   ((or (eq? mask-item (event-mask-item button-motion))
	(eq? mask-item (event-mask-item button-1-motion))
	(eq? mask-item (event-mask-item button-2-motion))
	(eq? mask-item (event-mask-item button-3-motion))
	(eq? mask-item (event-mask-item button-4-motion))
	(eq? mask-item (event-mask-item button-5-motion)))
    (eq? type (event-type motion-notify)))
   ((eq? mask-item (event-mask-item button-press))
    (eq? type (event-type button-press)))
   ((eq? mask-item (event-mask-item button-release))
    (eq? type (event-type button-release)))
   ((eq? mask-item (event-mask-item colormap-change))
    (eq? type (event-type colormap-notify)))
   ((eq? mask-item (event-mask-item enter-window))
    (eq? type (event-type enter-notify)))
   ((eq? mask-item (event-mask-item leave-window))
    (eq? type (event-type leave-notify)))
   ((eq? mask-item (event-mask-item exposure))
    (eq? type (event-type expose)))
   ((eq? mask-item (event-mask-item focus-change))
    (or (eq? type (event-type focus-in))
	(eq? type (event-type focus-out))))
   ((eq? mask-item (event-mask-item keymap-state))
    (eq? type (event-type keymap-notify)))
   ((eq? mask-item (event-mask-item key-press))
    (eq? type (event-type key-press)))
   ((eq? mask-item (event-mask-item key-release))
    (eq? type (event-type key-release)))
   ((eq? mask-item (event-mask-item pointer-motion))
    (eq? type (event-type motion-notify)))
   ((eq? mask-item (event-mask-item property-change))
    (eq? type (event-type property-notify)))
   ((eq? mask-item (event-mask-item resize-redirect))
    (eq? type (event-type resize-request)))
   ((eq? mask-item (event-mask-item structure-notify))
    (or (and (eq? type (event-type circulate-notify))
	     (eq? window (circulate-event-event event))
	     (eq? window (circulate-event-window event)))
	(and (eq? type (event-type configure-notify))
	     (eq? window (configure-event-event event))
	     (eq? window (configure-event-window event)))
	(and (eq? type (event-type destroy-notify))
	     (eq? window (destroy-window-event-event event))
	     (eq? window (destroy-window-event-window event)))
	(and (eq? type (event-type gravity-notify))
	     (eq? window (gravity-event-event event))
	     (eq? window (gravity-event-window event)))
	(and (eq? type (event-type map-notify))
	     (eq? window (map-event-event event))
	     (eq? window (map-event-window event)))
	(and (eq? type (event-type reparent-notify))
	     (eq? window (reparent-event-event event))
	     (eq? window (reparent-event-window event)))
	(and (eq? type (event-type unmap-notify))
	     (eq? window (unmap-event-event event))
	     (eq? window (unmap-event-window event)))))
   ((eq? mask-item (event-mask-item substructure-notify))
    (or (and (eq? type (event-type circulate-notify))
	     (eq? window (circulate-event-event event))
	     (not (eq? window (circulate-event-window event))))
	(and (eq? type (event-type configure-notify))
	     (eq? window (configure-event-event event))
	     (not (eq? window (configure-event-window event))))
	(and (eq? type (event-type create-notify))
	     (eq? window (create-window-event-parent event))
	     (not (eq? window (create-window-event-window event))))
	(and (eq? type (event-type destroy-notify))
	     (eq? window (destroy-window-event-event event))
	     (not (eq? window (destroy-window-event-window event))))
	(and (eq? type (event-type gravity-notify))
	     (eq? window (gravity-event-event event))
	     (not (eq? window (gravity-event-window event))))
	(and (eq? type (event-type map-notify))
	     (eq? window (map-event-event event))
	     (not (eq? window (map-event-window event))))
	(and (eq? type (event-type reparent-notify))
	     (eq? window (reparent-event-event event))
	     (not (eq? window (reparent-event-window event))))
	(and (eq? type (event-type unmap-notify))
	     (eq? window (unmap-event-event event))
	     (not (eq? window (unmap-event-window event))))))
   ((eq? mask-item (event-mask-item substructure-redirect))
    (or (eq? type (event-type circulate-request))
	(eq? type (event-type configure-request))
	(eq? type (event-type map-request))))
   ((eq? mask-item (event-mask-item visibility-change))
    (eq? type (event-type visibility-notify)))
   (else #f)))