#lang racket/base
;; Cross-layer relaying between adjacent dataspaces
;; TODO: protocol for *clean* shutdown of a dataspace

;; TODO: Actually elide the need for relays entirely, by allowing an
;; actor to manifest in multiple dataspaces (multiple
;; points-of-attachment), and by placing assertions and subscriptions
;; directly in the dataspace concerned. (Done naively, this would
;; avoid manifesting observed assertions in intermediate nested
;; dataspaces; but then, if anyone cared, they'd be observing the
;; tuples themselves - right?? Oh, maybe observing the observers would
;; be an, er, observable difference.)

(provide quit-dataspace!
         dataspace)

(require racket/match)
(require racket/set)
(require "assertions.rkt")
(require "dataspace.rkt")
(require "syntax.rkt")
(require "skeleton.rkt")
(require "term.rkt")
(require "bag.rkt")

(require (for-syntax racket/base))
(require (for-syntax syntax/parse))
(require "syntax-classes.rkt")

(struct *quit-dataspace* () #:transparent)

;; TODO: inbound^n, outbound^n -- protocol/standard-relay, iow

(define (quit-dataspace!)
  (send! (*quit-dataspace*)))

(define-syntax (dataspace stx)
  (syntax-parse stx
    [(_ name:name form ...)
     (syntax/loc stx
       (let ((ds-name name.N))
         (spawn #:name ds-name
                (define outer-facet (current-facet))
                (begin/dataflow (void)) ;; eww. dummy endpoint to keep the root facet alive
                (define (schedule-inner!)
                  (push-script!
                   (facet-actor outer-facet)
                   (lambda ()
                     (with-current-facet [outer-facet]
                       (when (facet-live? outer-facet)
                         (defer-turn! (lambda ()
                                        (when (run-scripts! inner-ds)
                                          (schedule-inner!)))))))))
                (define inner-ds (make-dataspace
                                  (lambda ()
                                    (schedule-script!
                                     (current-actor)
                                     (lambda ()
                                       (spawn #:name (list 'ds-link ds-name)
                                              (boot-relay schedule-inner!
                                                          outer-facet))
                                       (spawn* form ...))))))
                (on-start (schedule-inner!)))))]))

(define (boot-relay schedule-inner! outer-facet)
  (define inbound-endpoints (make-hash))
  (define outbound-endpoints (make-hash))

  (define inner-facet (current-facet))
  (define inner-actor (current-actor))
  (define inner-ds (actor-dataspace inner-actor))

  (on (asserted (observe (inbound $x)))
      ;; (log-info "~a (asserted (observe (inbound ~v)))" inner-actor x)
      (with-current-facet [outer-facet]
        (with-non-script-context
          (define outer-capture-proj (term->capture-proj x))
          (define inner-capture-proj (map (lambda (p) (cons 0 p)) outer-capture-proj))
          ;; ^ inner-capture-proj accounts for the extra (inbound ...) layer around assertions
          (define i (term->skeleton-interest
                     x
                     #:capture-projection outer-capture-proj
                     (lambda (op . captured-values)
                       (define term (inbound (instantiate-term->value x captured-values)))
                       (define assertion (visibility-restriction inner-capture-proj term))
                       ;; (log-info "~a => ~a ~a ~v"
                       ;;           outer-facet
                       ;;           inner-facet
                       ;;           op
                       ;;           assertion)
                       (match op
                         ['+ (apply-patch! inner-ds inner-actor (bag assertion +1))]
                         ['- (apply-patch! inner-ds inner-actor (bag assertion -1))]
                         ['! (send-assertion! (dataspace-routing-table inner-ds) assertion)])
                       (schedule-inner!))
                     #:cleanup
                     (lambda (cache)
                       (apply-patch!
                        inner-ds
                        inner-actor
                        (for/bag/count [(captured-values (in-bag cache))]
                          ;; (log-info "~a (cleanup) ~v" inner-actor term)
                          (values (visibility-restriction
                                   inner-capture-proj
                                   (inbound (instantiate-term->value x captured-values)))
                                  -1)))
                       (schedule-inner!))))
          (add-endpoint-if-live! outer-facet
                                 inbound-endpoints
                                 x
                                 "dataspace-relay (observe (inbound ...))"
                                 (lambda () (values (observe x) i))))))

  (on (message (*quit-dataspace*))
      (with-current-facet [outer-facet]
        (stop-current-facet)))

  (on (retracted (observe (inbound $x)))
      ;; (log-info "~a (retracted (observe (inbound ~v)))" inner-actor x)
      (with-current-facet [outer-facet]
        (with-non-script-context
          (remove-endpoint! outer-facet (hash-ref inbound-endpoints x))
          (hash-remove! inbound-endpoints x))))

  (on (asserted (outbound $x))
      ;; (log-info "~a (asserted (outbound ~v))" inner-actor x)
      (with-current-facet [outer-facet]
        (with-non-script-context
          (add-endpoint-if-live! outer-facet
                                 outbound-endpoints
                                 x
                                 "dataspace-relay (outbound ...)"
                                 (lambda () (values x #f))))))

  (on (retracted (outbound $x))
      ;; (log-info "~a (retracted (outbound ~v))" inner-actor x)
      (with-current-facet [outer-facet]
        (with-non-script-context
          (remove-endpoint! outer-facet (hash-ref outbound-endpoints x))
          (hash-remove! outbound-endpoints x))))

  (on (message (outbound $x))
      ;; (log-info "~a (message (outbound ~v))" inner-actor x)
      (with-current-facet [outer-facet]
        (send! x))))

(define (add-endpoint-if-live! f table key desc update-fn)
  (when (facet-live? f)
    ;;
    ;; ^ Check that `f` is still alive, because we're (carefully!!)
    ;; violating an invariant of `dataspace.rkt` by adding an endpoint
    ;; well after the construction of the facet we're in. We may be
    ;; executing this handler just after clean shutdown of the facet
    ;; by a `quit-dataspace!` request, and in that case we MUST NOT
    ;; add any further endpoints because their assertions will not get
    ;; removed, because cleanup (as part of `(quit)` processing) has
    ;; already been done.
    ;;
    ;; We don't have to do a similar check before calling
    ;; `remove-endpoint!`, because shortly after all (both) calls to
    ;; `destroy-endpoint!`, all destroyed endpoints are removed from
    ;; the `facet-endpoints` table, ensuring they won't be processed
    ;; again.
    ;;
    (hash-set! table key (add-endpoint! f desc #t update-fn))))
