#lang syndicate
;; Ethernet driver

(provide (struct-out ethernet-packet)
	 zero-ethernet-address
	 broadcast-ethernet-address
	 interface-names
	 spawn-ethernet-driver
	 ethernet-packet-pattern
	 lookup-ethernet-hwaddr)

(require/activate syndicate/drivers/timer)
(require racket/set)
(require racket/match)
(require racket/async-channel)

(require packet-socket)
(require bitsyntax)

(require "configuration.rkt")
(require "dump-bytes.rkt")

(struct ethernet-packet (interface from-wire? source destination ethertype body) #:prefab)

(define zero-ethernet-address (bytes 0 0 0 0 0 0))
(define broadcast-ethernet-address (bytes 255 255 255 255 255 255))

(define interface-names (raw-interface-names))
(log-info "Device names: ~a" interface-names)

(define (spawn-ethernet-driver)
  (spawn #:name 'ethernet-driver
   (during/spawn
    (observe (ethernet-packet (ethernet-interface $interface-name _) #t _ _ _ _))
    #:name (list 'ethernet-interface interface-name)

    (define h (raw-interface-open interface-name))
    (when (not h) (error 'ethernet "Couldn't open interface ~v" interface-name))
    (log-info "Opened interface ~a, yielding handle ~v" interface-name h)

    (define interface (ethernet-interface interface-name (raw-interface-hwaddr h)))
    (assert interface)

    (define control-ch (make-async-channel))
    (thread (lambda () (interface-packet-read-loop interface h control-ch)))

    (on-start (flush!) ;; ensure all subscriptions are in place
              (async-channel-put control-ch 'unblock)
              (spawn #:name (list 'ethernet-interface-quit-monitor interface-name)
                     (on (retracted interface)
                         (async-channel-put control-ch 'quit))))

    (on (message (inbound ($ p (ethernet-packet interface #t _ _ _ _))))
        ;; (log-info "Interface ~a inbound packet ~a -> ~a (type 0x~a)"
        ;;           (ethernet-interface-name (ethernet-packet-interface p))
        ;;           (pretty-bytes (ethernet-packet-source p))
        ;;           (pretty-bytes (ethernet-packet-destination p))
        ;;           (number->string (ethernet-packet-ethertype p) 16))
        ;; (log-info "~a" (dump-bytes->string (ethernet-packet-body p)))
        (send! p))

    (on (message ($ p (ethernet-packet interface #f _ _ _ _)))
        ;; (log-info "Interface ~a OUTBOUND packet ~a -> ~a (type 0x~a)"
        ;;           (ethernet-interface-name (ethernet-packet-interface p))
        ;;           (pretty-bytes (ethernet-packet-source p))
        ;;           (pretty-bytes (ethernet-packet-destination p))
        ;;           (number->string (ethernet-packet-ethertype p) 16))
        ;; (log-info "~a" (dump-bytes->string (ethernet-packet-body p)))
        (raw-interface-write h (encode-ethernet-packet p))))))

(define (interface-packet-read-loop interface h control-ch)
  (define (blocked)
    (match (async-channel-get control-ch)
      ['unblock (unblocked)]
      ['quit (void)]))
  (define (unblocked)
    (match (async-channel-try-get control-ch)
      ['unblock (unblocked)]
      ['quit (void)]
      [#f
       (define p (raw-interface-read h))
       (define decoded (decode-ethernet-packet interface p))
       (when decoded (send-ground-message decoded))
       (unblocked)]))
  (blocked)
  (raw-interface-close h))

(define (decode-ethernet-packet interface p)
  (bit-string-case p
    ([ (destination :: binary bytes 6)
       (source :: binary bytes 6)
       (ethertype :: integer bytes 2)
       (body :: binary) ]
     (ethernet-packet interface
		      #t
		      (bit-string->bytes source)
		      (bit-string->bytes destination)
		      ethertype
		      (bit-string->bytes body)))
    (else #f)))

(define (encode-ethernet-packet p)
  (match-define (ethernet-packet _ _ source destination ethertype body) p)
  (bit-string->bytes
   (bit-string (destination :: binary bytes 6)
	       (source :: binary bytes 6)
	       (ethertype :: integer bytes 2)
	       (body :: binary))))

(define (ethernet-packet-pattern interface-name from-wire? ethertype)
  (ethernet-packet (ethernet-interface interface-name ?) from-wire? ? ? ethertype ?))

(define (lookup-ethernet-hwaddr interface-name)
  (define timer-id (gensym 'lookup-ethernet-hwaddr))
  (react/suspend (k)
                 (on-start (send! (set-timer timer-id 5000 'relative)))
                 (stop-when (message (timer-expired timer-id _))
                            (log-info "Lookup of ethernet interface ~v failed" interface-name)
                            (k #f))
                 (stop-when (asserted (ethernet-interface interface-name $hwaddr))
                            (k hwaddr))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(spawn-ethernet-driver)
