#lang racket/base

(require racket/match)
(require (prefix-in udp: racket/udp))
(require "../main.rkt")
(require "../demand-matcher.rkt")

(provide (struct-out udp-remote-address)
	 (struct-out udp-handle)
	 (struct-out udp-listener)
	 udp-address?
	 udp-local-address?
	 (struct-out udp-packet)
	 spawn-udp-driver)

;; A UdpAddress is one of
;; -- a (udp-address String Uint16), representing a remote socket
;; -- a (udp-handle Any), representing a local socket on a kernel-assigned port
;; -- a (udp-listener Uint16), representing a local socket on a user-assigned port
;; Note that udp-handle-ids must be chosen carefully: they are scoped
;; to the local VM, i.e. shared between processes in that VM, so
;; processes must make sure not to accidentally clash in handle ID
;; selection.
(struct udp-remote-address (host port) #:prefab)
(struct udp-handle (id) #:prefab)
(struct udp-listener (port) #:prefab)

(define (udp-address? x)
  (or (udp-remote-address? x)
      (udp-local-address? x)))

(define (udp-local-address? x)
  (or (udp-handle? x)
      (udp-listener? x)))

;; A UdpPacket is a (udp-packet UdpAddress UdpAddress Bytes), and
;; represents a packet appearing on our local "subnet" of the full UDP
;; network, complete with source, destination and contents.
(struct udp-packet (source destination body) #:prefab)

;; -> Action
;; Spawns a process acting as a UDP socket factory.
(define (spawn-udp-driver)
  (spawn-demand-matcher (observe (udp-packet ? (?! (udp-listener ?)) ?))
                        (advertise (udp-packet ? (?! (udp-listener ?)) ?))
                        spawn-udp-socket))

;; UdpLocalAddress -> Action
(define (spawn-udp-socket local-addr)
  (define socket (udp:udp-open-socket #f #f))

  (match local-addr
    [(udp-listener port) (udp:udp-bind! socket #f port)]
    [(udp-handle _) (udp:udp-bind! socket #f 0)]) ;; kernel-allocated port number

  (define control-ch (make-channel))
  (thread (lambda () (udp-receiver-thread local-addr socket control-ch)))

  (spawn (lambda (e s)
	   (match e
             [(scn r)
              (cond [(trie-non-empty? r) #f] ;; peer hasn't quit yet: do nothing.
                    [else (channel-put control-ch 'quit)
                          (quit)])]
	     [(message (at-meta (? udp-packet? p)))
	      (transition s (message p))]
	     [(message (udp-packet _ (udp-remote-address host port) body))
	      (udp:udp-send-to socket host port body)
	      #f]
	     [_ #f]))
	 (void)
         (scn/union (subscription (udp-packet ? local-addr ?) #:meta-level 1)
                    (subscription (udp-packet local-addr (udp-remote-address ? ?) ?))
                    (advertisement (udp-packet (udp-remote-address ? ?) local-addr ?))
                    (subscription (observe (udp-packet (udp-remote-address ? ?) local-addr ?))))))

;; UdpLocalAddress UdpSocket Channel -> Void
(define (udp-receiver-thread local-addr socket control-ch)
  (define buffer (make-bytes 65536))
  (let loop ()
    (sync (handle-evt control-ch (match-lambda ['quit (void)]))
	  (handle-evt (udp:udp-receive!-evt socket buffer)
		      (lambda (receive-results)
			(match-define (list len source-hostname source-port) receive-results)
			(send-ground-message
			 (udp-packet (udp-remote-address source-hostname source-port)
				     local-addr
				     (subbytes buffer 0 len)))
			(loop)))))
  (udp:udp-close socket))
