#lang racket/base
;; DSL syntax over the API of dataspace.rkt

(provide spawn
         spawn*

         react
         react/suspend
         until

         field
         assert
         stop-facet
         stop-current-facet
         stop-when
         stop-when-true
         on-start
         on-stop
         on
         during
         during/spawn
         begin/dataflow
         define/dataflow

         asserted
         retracted
         message

         let-event

         query-value
         query-set
         query-hash
         ;; query-hash-set
         query-count
         query-value*
         query-set*
         query-hash*
         ;; query-hash-set*
         query-count*
         define/query-value
         define/query-set
         define/query-hash
         ;; define/query-hash-set
         define/query-count
         immediate-query

         send!
         defer-turn!
         flush!
         assert!
         retract!
         current-adhoc-assertions

         ;;

         ;; current-action-transformer
         )

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

(require "assertions.rkt")
(require "dataspace.rkt")
(require (submod "dataspace.rkt" priorities))
(require "event-expander.rkt")
(require "skeleton.rkt")
(require "pattern.rkt")

(require racket/match)
(require racket/set)
(require syndicate/dataflow)
(require syndicate/protocol/instance)

(begin-for-syntax
  (define-splicing-syntax-class actor-wrapper
    (pattern (~seq #:spawn wrapper))
    (pattern (~seq) #:attr wrapper #'spawn))

  (define-splicing-syntax-class on-crash-option
    (pattern (~seq #:on-crash expr))
    (pattern (~seq) #:attr expr #f))

  (define-splicing-syntax-class let-option
    (pattern (~seq #:let clauses))
    (pattern (~seq) #:attr clauses #'()))

  (define-splicing-syntax-class when-pred
    (pattern (~seq #:when Pred))
    (pattern (~seq) #:attr Pred #'#t))

  (define-splicing-syntax-class priority
    (pattern (~seq #:priority level))
    (pattern (~seq) #:attr level #'*normal-priority*)))

(define-syntax (spawn stx)
  (syntax-parse stx
    [(_ (~or (~optional (~seq #:name name-expr) #:defaults ([name-expr #'#f])
                        #:name "#:name")
             (~optional (~seq #:assertions [assertion-exprs ...])
                        #:name "#:assertions")
             (~optional (~seq #:linkage [linkage-expr ...]) #:defaults ([(linkage-expr 1) '()])
                        #:name "#:linkage"))
        ...
        O ...)
     (quasisyntax/loc stx
       (spawn** #:name name-expr
                #:assertions #,(cond [(attribute assertion-exprs) #'[assertion-exprs ...]]
                                     [else #'[]])
                linkage-expr ... O ...))]))

(define-syntax (spawn* stx)
  (syntax-parse stx
    [(_ name:name assertions:assertions script ...)
     (quasisyntax/loc stx
       (spawn** #:name name.N
                #:assertions [assertions.exprs ...]
                (on-start script ...)))]))

(define-syntax (spawn** stx)
  (syntax-parse stx
    [(_ name:name assertions:assertions script ...)
     (quasisyntax/loc stx
       (begin
         (ensure-in-script! 'spawn!)
         (spawn!
          (current-actor)
          name.N
          (lambda () (begin/void-default script ...))
          (set assertions.exprs ...))))]))

(define-syntax (begin/void-default stx)
  (syntax-parse stx
    [(_)                (syntax/loc stx (void))]
    [(_ expr0 expr ...) (syntax/loc stx (begin expr0 expr ...))]))

(define (react* where boot-proc)
  (add-facet! where
              (current-actor)
              (current-facet)
              boot-proc))

(define-syntax (react stx)
  (syntax-parse stx
    [(_ O ...)
     (quasisyntax/loc stx
       (react* #,(source-location->string stx)
               (lambda () (begin/void-default O ...))))]))

(define-syntax (react/suspend stx)
  (syntax-parse stx
    [(_ (resume-parent) O ...)
     (quasisyntax/loc stx
       (suspend-script* #,(source-location->string stx)
                        (lambda (resume-parent)
                          (react* #,(source-location->string stx)
                                  (lambda () (begin/void-default O ...))))))]))

(define-syntax (until stx)
  (syntax-parse stx
    [(_ E O ...)
     (syntax/loc stx
       (react/suspend (continue)
                      (stop-when E (continue (void)))
                      O ...))]))

(define (make-field name init)
  (let ((ac (current-actor)))
    (field-handle name (generate-id! (actor-dataspace ac)) ac init)))

(define-syntax (define-field stx)
  (syntax-parse stx
    [(_ id init)
     #'(define id (make-field 'id init))]))

(define-syntax (field stx)
  (syntax-parse stx
    [(_ [id:id init] ...)
     (quasisyntax/loc stx
       (begin (define-field id init)
              ...))]))

(define-syntax (assert stx)
  (syntax-parse stx
    [(_ w:when-pred snapshot:snapshot P)
     (quasisyntax/loc stx
       (add-endpoint! (current-facet)
                      #,(source-location->string stx)
                      snapshot.dynamic?
                      (lambda () (values (when w.Pred P) #f))))]))

(define-syntax (stop-facet stx)
  (syntax-parse stx
    [(_ f-expr script ...)
     (quasisyntax/loc stx
       (let ((f f-expr))
         (when (not (equal? (facet-actor f) (current-actor)))
           (error 'stop-facet "Attempt to stop unrelated facet ~a from actor ~a" f (current-actor)))
         (stop-facet! f (lambda () (begin/void-default script ...)))))]))

(define-syntax-rule (stop-current-facet script ...)
  (stop-facet (current-facet) script ...))

(define-syntax-rule (stop-when-true condition script ...)
  (begin/dataflow
    (when condition
      (stop-facet (current-facet) script ...))))

(define-syntax (on-start stx)
  (syntax-parse stx
    [(_ script ...)
     (quasisyntax/loc stx
       (schedule-script! (current-actor)
                         (lambda () (begin/void-default script ...))))]))

(define-syntax (on-stop stx)
  (syntax-parse stx
    [(_ script ...)
     (quasisyntax/loc stx
       (add-stop-script! (current-facet)
                         (lambda () (begin/void-default script ...))))]))

(define-syntax (stop-when stx)
  (syntax-parse stx
    [(_ w:when-pred E prio:priority script ...)
     (analyse-event stx
                    #'w.Pred
                    #'E
                    (syntax/loc stx (stop-current-facet script ...))
                    #'prio.level)]))

(define-syntax (on stx)
  (syntax-parse stx
    [(_ w:when-pred E prio:priority script ...)
     (analyse-event stx
                    #'w.Pred
                    #'E
                    (syntax/loc stx (begin/void-default script ...))
                    #'prio.level)]))

(define-syntax (begin/dataflow stx)
  (syntax-parse stx
    [(_ prio:priority expr ...)
     (quasisyntax/loc stx
       (let ()
         (add-endpoint! (current-facet)
                        #,(source-location->string stx)
                        #t
                        (lambda ()
                          (define subject-id (current-dataflow-subject-id))
                          (schedule-script!
                           #:priority prio.level
                           (current-actor)
                           (lambda ()
                             (parameterize ((current-dataflow-subject-id subject-id))
                               expr ...)))
                          (values (void) #f)))))]))

(define-syntax (define/dataflow stx)
  (syntax-parse stx
    [(_ fieldname expr)
     (quasisyntax/loc stx (define/dataflow fieldname expr #:default #f))]
    [(_ fieldname expr #:default default-expr)
     (quasisyntax/loc stx
       (begin
         (field [fieldname default-expr])
         (begin/dataflow (fieldname expr))))]))

(define-syntax (asserted stx) (raise-syntax-error #f "asserted: Used outside event spec" stx))
(define-syntax (retracted stx) (raise-syntax-error #f "retracted: Used outside event spec" stx))
(define-syntax (message stx) (raise-syntax-error #f "message: Used outside event spec" stx))

(define-syntax (suspend-script stx)
  (syntax-parse stx
    [(_ proc)
     (quasisyntax/loc stx
       (suspend-script* #,(source-location->string stx) proc))]))

(define-syntax (let-event stx)
  (syntax-parse stx
    [(_ [e ...] body ...)
     (syntax/loc stx
       ((react/suspend (k)
          (on-start (-let-event [e ...] (stop-current-facet (k (lambda () body ...))))))))]))

(define-syntax (-let-event stx)
  (syntax-parse stx
    [(_ [] expr) #'expr]
    [(_ [e es ...] expr) (quasisyntax/loc #'e (react (stop-when e (-let-event [es ...] expr))))]))

(define-for-syntax orig-insp
  (variable-reference->module-declaration-inspector (#%variable-reference)))

(define-for-syntax (analyse-event outer-expr-stx
                                  when-pred-stx
                                  armed-event-stx
                                  script-stx
                                  priority-stx)
  (define event-stx (syntax-disarm armed-event-stx orig-insp))
  (syntax-parse event-stx
    #:literals [message asserted retracted]
    [(expander args ...) #:when (event-expander-id? #'expander)
     (event-expander-transform event-stx
                               (lambda (result)
                                 (analyse-event outer-expr-stx
                                                when-pred-stx
                                                (syntax-rearm result event-stx)
                                                script-stx
                                                priority-stx)))]
    [(message snapshot:snapshot P)
     (define desc (analyse-pattern #'P))
     (quasisyntax/loc outer-expr-stx
       (add-endpoint! (current-facet)
                      #,(source-location->string outer-expr-stx)
                      snapshot.dynamic?
                      (lambda ()
                        (if #,when-pred-stx
                            (values (observe #,(desc->assertion-stx desc))
                                    (skeleton-interest #,(desc->skeleton-stx desc)
                                                       '#,(desc->skeleton-proj desc)
                                                       (list #,@(desc->key desc))
                                                       '#,(desc->capture-proj desc)
                                                       (capture-facet-context
                                                        (lambda (op #,@(desc->capture-names desc))
                                                          (when (eq? op '!)
                                                            ;; (log-info "~a ~a ~v"
                                                            ;;           (current-facet)
                                                            ;;           op
                                                            ;;           (list #,@(desc->capture-names desc)))
                                                            (schedule-script!
                                                             #:priority #,priority-stx
                                                             (current-actor)
                                                             #,(quasisyntax/loc script-stx
                                                                 (lambda ()
                                                                   #,script-stx))))))
                                                       #f))
                            (values (void) #f)))))]
    [(asserted snapshot:snapshot P)
     (analyse-asserted/retracted outer-expr-stx
                                 #'snapshot.dynamic?
                                 when-pred-stx
                                 script-stx
                                 #t
                                 #'P
                                 priority-stx)]
    [(retracted snapshot:snapshot P)
     (analyse-asserted/retracted outer-expr-stx
                                 #'snapshot.dynamic?
                                 when-pred-stx
                                 script-stx
                                 #f
                                 #'P
                                 priority-stx)]))

(define-for-syntax (analyse-asserted/retracted outer-expr-stx
                                               snapshot-dynamic?-stx
                                               when-pred-stx
                                               script-stx
                                               asserted?
                                               P-stx
                                               priority-stx)
  (define desc (analyse-pattern P-stx))
  (quasisyntax/loc outer-expr-stx
    (add-endpoint! (current-facet)
                   #,(source-location->string outer-expr-stx)
                   #,snapshot-dynamic?-stx
                   (lambda ()
                     (if #,when-pred-stx
                         (values (observe #,(desc->assertion-stx desc))
                                 (skeleton-interest #,(desc->skeleton-stx desc)
                                                    '#,(desc->skeleton-proj desc)
                                                    (list #,@(desc->key desc))
                                                    '#,(desc->capture-proj desc)
                                                    (capture-facet-context
                                                     (lambda (op #,@(desc->capture-names desc))
                                                       (when (eq? op #,(if asserted? #''+ #''-))
                                                         ;; (log-info "~a ~a ~v"
                                                         ;;           (current-facet)
                                                         ;;           op
                                                         ;;           (list #,@(desc->capture-names desc)))
                                                         (schedule-script!
                                                          #:priority #,priority-stx
                                                          (current-actor)
                                                          #,(quasisyntax/loc script-stx
                                                              (lambda ()
                                                                #,script-stx))))))
                                                    #f))
                         (values (void) #f))))))

(define-syntax (during stx)
  (syntax-parse stx
    [(_ P O ...)
     (quasisyntax/loc stx
       (on (asserted P)
           (react (stop-when (retracted #:snapshot #,(instantiate-pattern->pattern #'P)))
                  O ...)))]))

(define-syntax (during/spawn stx)
  (syntax-parse stx
    [(_ P w:actor-wrapper name:name assertions:assertions parent-let:let-option
        oncrash:on-crash-option
        O ...)
     (define Q-stx (instantiate-pattern->pattern #'P))
     (quasisyntax/loc stx
       (on (asserted P)
           (let* ((id (gensym 'during/spawn))
                  (inst (instance id #,(instantiate-pattern->value #'P)))
                  ;; ^ this is the assertion representing supply
                  )
             (react (stop-when (asserted inst)
                      ;; Supply (inst) appeared before demand (p) retracted.
                      ;; Transition to a state where we monitor demand, but also
                      ;; express interest in supply: this latter acts as a signal
                      ;; to the supply that it should stick around. We react to
                      ;; retraction of supply before retraction of demand by
                      ;; invoking the on-crash expression, if supplied. Once
                      ;; demand is retracted, this facet terminates, retracting
                      ;; its interest in supply, thereby signalling to the supply
                      ;; that it is no longer wanted.
                      (react (stop-when (retracted inst) ;; NOT OPTIONAL
                               #,@(if (attribute oncrash.expr)
                                      #'(oncrash.expr)
                                      #'()))
                             (stop-when (retracted #:snapshot #,Q-stx))))
                    (stop-when (retracted #:snapshot #,Q-stx)
                      ;; Demand (p) retracted before supply (inst) appeared. We
                      ;; MUST wait for the supply to fully appear so that we can
                      ;; reliably tell it to shut down. We must maintain interest
                      ;; in supply until we see supply, and then terminate, thus
                      ;; signalling to supply that it is no longer wanted.
                      (react (stop-when (asserted inst)))))
             (let parent-let.clauses
               (w.wrapper #:linkage [(assert inst)
                                     (stop-when (retracted (observe inst)))]
                          #:name name.N
                          #:assertions [inst
                                        (observe (observe inst))
                                        assertions.exprs ...]
                          O ...)))))]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Queries

(begin-for-syntax
  (define-splicing-syntax-class on-add
    (pattern (~optional (~seq #:on-add expr) #:defaults ([expr #f]))))
  (define-splicing-syntax-class on-remove
    (pattern (~optional (~seq #:on-remove expr) #:defaults ([expr #f]))))

  (define (schedule-query-handler-stxs maybe-expr-stx)
    (if maybe-expr-stx
        (quasisyntax/loc maybe-expr-stx
          ((schedule-script! #:priority *query-handler-priority*
                             (current-actor)
                             (lambda () #,maybe-expr-stx))))
        #'())))

(define-syntax (query-value stx)
  (syntax-parse stx
    [(_ field-name absent-expr args ...)
     (quasisyntax/loc stx
       (let ()
         (field [field-name absent-expr])
         (query-value* field-name absent-expr args ...)))]))

(define-syntax (query-value* stx)
  (syntax-parse stx
    [(_ field-name absent-expr P expr on-add:on-add on-remove:on-remove)
     (quasisyntax/loc stx
       (let ((F field-name))
         (on (asserted P) #:priority *query-priority*
             #,@(schedule-query-handler-stxs (attribute on-add.expr))
             (F expr))
         (on (retracted P) #:priority *query-priority-high*
             #,@(schedule-query-handler-stxs (attribute on-remove.expr))
             (F absent-expr))
         F))]))

(define-syntax (query-set stx)
  (syntax-parse stx
    [(_ field-name args ...)
     (quasisyntax/loc stx
       (let ()
         (field [field-name (set)])
         (query-set* field-name args ...)))]))

(define-syntax (query-set* stx)
  (syntax-parse stx
    [(_ field-name P expr on-add:on-add on-remove:on-remove)
     (quasisyntax/loc stx
       (let ((F field-name))
         (on (asserted P) #:priority *query-priority*
             (let ((V expr))
               (when (not (set-member? (F) V))
                 #,@(schedule-query-handler-stxs (attribute on-add.expr))
                 (F (set-add (F) V)))))
         (on (retracted P) #:priority *query-priority-high*
             (let ((V expr))
               (when (set-member? (F) V)
                 #,@(schedule-query-handler-stxs (attribute on-remove.expr))
                 (F (set-remove (F) V)))))
         F))]))

(define-syntax (query-hash stx)
  (syntax-parse stx
    [(_ field-name args ...)
     (quasisyntax/loc stx
       (let ()
         (field [field-name (hash)])
         (query-hash* field-name args ...)))]))

(define-syntax (query-hash* stx)
  (syntax-parse stx
    [(_ field-name P key-expr value-expr on-add:on-add on-remove:on-remove)
     (quasisyntax/loc stx
       (let ((F field-name))
         (on (asserted P) #:priority *query-priority*
             (let ((key key-expr))
               (when (hash-has-key? (F) key)
                 (log-warning "query-hash: field ~v with pattern ~v: overwriting existing entry ~v"
                              'field-name
                              'P
                              key))
               #,@(schedule-query-handler-stxs (attribute on-add.expr))
               (F (hash-set (F) key value-expr))))
         (on (retracted P) #:priority *query-priority-high*
             #,@(schedule-query-handler-stxs (attribute on-remove.expr))
             (F (hash-remove (F) key-expr)))
         F))]))

;; (define-syntax (query-hash-set stx)
;;   (syntax-parse stx
;;     [(_ field-name args ...)
;;      (quasisyntax/loc stx
;;        (let ()
;;          (field [field-name (hash)])
;;          (query-hash-set* field-name args ...)))]))

;; (define-syntax (query-hash-set* stx)
;;   (syntax-parse stx
;;     [(_ field-name P key-expr value-expr on-add:on-add on-remove:on-remove)
;;      (quasisyntax/loc stx
;;        (let ((F field-name))
;;          (on (asserted P) #:priority *query-priority*
;;              (let ((K key-expr) (V value-expr))
;;                (when (not (hashset-member? (F) K V))
;;                  #,@(schedule-query-handler-stxs (attribute on-add.expr))
;;                  (F (hashset-add (F) K V)))))
;;          (on (retracted P) #:priority *query-priority-high*
;;              (let ((K key-expr) (V value-expr))
;;                (when (hashset-member? (F) K V)
;;                  #,@(schedule-query-handler-stxs (attribute on-remove.expr))
;;                  (F (hashset-remove (F) K V)))))
;;          F))]))

(define-syntax (query-count stx)
  (syntax-parse stx
    [(_ field-name args ...)
     (quasisyntax/loc stx
       (let ()
         (field [field-name (hash)])
         (query-count* field-name args ...)))]))

(define-syntax (query-count* stx)
  (syntax-parse stx
    [(_ field-name P expr on-add:on-add on-remove:on-remove)
     (quasisyntax/loc stx
       (let ((F field-name))
         (on (asserted P) #:priority *query-priority*
             (let ((E expr))
               #,@(schedule-query-handler-stxs (attribute on-add.expr))
               (F (hash-set (F) E (+ 1 (hash-ref (F) E 0))))))
         (on (retracted P) #:priority *query-priority-high*
             (let ((E expr))
               #,@(schedule-query-handler-stxs (attribute on-remove.expr))
               (let ((F0 (F)))
                 (F (match (hash-ref F0 E 0)
                      [0 F0] ;; huh
                      [1 (hash-remove F0 E)]
                      [n (hash-set F0 E (- n 1))])))))
         F))]))

(define-syntax-rule (define/query-value id ae P x ...) (define id (query-value id ae P x ...)))
(define-syntax-rule (define/query-set id P x ...) (define id (query-set id P x ...)))
(define-syntax-rule (define/query-hash id P x ...) (define id (query-hash id P x ...)))
;; (define-syntax-rule (define/query-hash-set id P x ...) (define id (query-hash-set id P x ...)))
(define-syntax-rule (define/query-count id P x ...) (define id (query-count id P x ...)))

(define-syntax (immediate-query stx)
  (syntax-case stx ()
    [(_ [op args ...] ...)
     (with-syntax [((query-result ...) (generate-temporaries #'(op ...)))]
       (syntax/loc stx
         (react/suspend (k)
                        (define query-result (op query-result args ...)) ...
                        (on-start (flush!) (k (query-result) ...)))))]))

(define (send! m)
  (ensure-in-script! 'send!)
  (enqueue-send! (current-actor) m))

(define (defer-turn! k)
  (ensure-in-script! 'defer-turn!)
  (enqueue-deferred-turn! (current-actor) k))

(define (flush!)
  (ensure-in-script! 'flush!)
  (define ack (gensym 'flush!))
  (until (message ack)
    (on-start (send! ack))))

(define (assert! a [count 1])
  (ensure-in-script! 'assert!)
  (adhoc-assert! (current-actor) a count))

(define (retract! a [count 1])
  (ensure-in-script! 'retract!)
  (adhoc-retract! (current-actor) a count))

(define (current-adhoc-assertions)
  (actor-adhoc-assertions (current-actor)))
