#lang syndicate/monolithic

(require racket/set)
(require syndicate/comprehensions)

(struct book-quote (title price) #:prefab)
(struct order (title price id delivery-date) #:prefab)

(struct split-proposal (title price contribution accepted) #:prefab)

(define (while-relevant-assert P #:noisy? [noisy #f])
  (define name (format "(while-relevant-assert ~a)" P))
  (actor/stateless
   #:name name
   (lambda (e)
     (match e
       [(scn t)
        (when noisy
          (log-info "~v receives ~v" name (trie->pretty-string t)))
        (if (trie-empty? (trie-project t (observe P)))
            #f
            (quit))]
       [_ #f]))
   (list (scn/union (assertion (observe (observe P)))
                    (assertion P)))))

;; AssertionSet
(define seller-interests
  (assertion-set-union (assertion (observe (observe (book-quote ? ?))))
                       (assertion (observe (observe (order ? ? ? ?))))))

;; Event Inventory -> AssertionSet
(define (answer-quotes e inv)
  (match e
    [(scn t)
     (for-trie/fold ([quotes trie-empty])
                    ([(observe (book-quote $title _)) t])
       (assertion-set-union quotes (assertion (book-quote title (hash-ref inv title #f)))))]
    [_ trie-empty]))

;; Event Inventory -> (Values Inventory (ListOf actor))
(define (answer-orders e inv)
  (match e
    [(scn t)
     (define-values (actions new-inv)
       (for-trie/fold ([actions '()]
                       [inv inv])
                      ([(observe (order $title $offer-price _ _)) t])
         (define asking-price (hash-ref inv title #f))
         (cond
           [(or (not asking-price) (< offer-price asking-price))
            ;; We cannot sell a book we do not have, and we will not sell for less
            ;; than our asking price.
            ;; (make sure this is retracted somewhere)
            (values (cons (while-relevant-assert (order title offer-price #f #f)) actions)
                    inv)]
           [else
            ;; Allocate an order ID.
            ;;
            (define order-id next-order-id)
            (set! next-order-id (+ order-id 1))
            
            ;; Remove the book from our shelves.
            ;;
            (define new-inv (hash-remove inv title))
            
            ;; Tell the ordering party their order ID and delivery date.
            (values (cons (while-relevant-assert (order title offer-price order-id "March 9th"))
                          actions)
                    new-inv)])))
     (values new-inv actions)]
    [_ (values inv '())]))

;; Seller
;; Respond to requests for quotes
;; Respond to proposed offers
;; STATE: (Hashof string -> number)
(define next-order-id 10001483)
(define (seller-behavior e inv)
  (cond
    [(scn? e)
     (define quotes (answer-quotes e inv))
     (define-values (new-inv actions) (answer-orders e inv))
     (transition new-inv (cons (scn/union seller-interests quotes) actions))]
    [else #f]))

(define (seller inv)
  (actor #:name 'seller
         seller-behavior
         inv
         (scn seller-interests)))

(define (buyer-a titles budget)
  (define (log-offer title contrib)
    (log-info "A makes an offer to split the price of ~v, contributing ~a"
              title
              contrib))
  (define (try-to-buy titles budget)
    (match titles
      ['() (log-info "A has bought everything they wanted!") (scn trie-empty)]
      [(cons title remaining-titles)
       (actor/stateless
        #:name (format "(try-to-buy ~a)" title)
        (lambda (e)
          (match e
            [(scn t)
             (define maybe-price (set-first (trie-project/set/single t (book-quote title (?!)))))
             (cond
               [maybe-price
                (log-info "A learns that the price of ~v is ~a" title maybe-price)
                (quit (negotiate-split title maybe-price remaining-titles budget))]
               [else
                (log-info "A learns that ~v is out-of-stock." title)
                (quit (try-to-buy remaining-titles budget))])]
            [_ #f]))
        (list (scn (assertion (observe (book-quote title ?))))))]))
  (define (negotiate-split title price remaining-titles budget)
    (define initial-offer (min budget (/ price 2)))
    (cond
      [(> initial-offer budget)
       ;; Don't have enough money
       (log-info "A does not have enough money for ~v." title)
       (try-to-buy remaining-titles budget)]
      [else
       (log-offer title initial-offer)
       (actor
        #:name (format "(negotiate-split ~a ~a)" title price)
        (lambda (e my-contribution)
          (match e
            [(scn (and t (? (compose not trie-empty?))))
             (define accepted?
               (set-first (trie-project/set/single t (split-proposal title price ? (?!)))))
             (cond
               [accepted?
                (define remaining-budget (- budget my-contribution))
                (log-info "A learns that the split-proposal for ~v was accepted, leaving them with ~v remaining" title remaining-budget)
                (quit (try-to-buy remaining-titles remaining-budget))]
               [else
                (log-info "A learns that the split-proposal for ~v was rejected" title)
                (let ([my-contribution (+ my-contribution (/ (- price my-contribution) 2))])
                  (cond
                    [(> my-contribution (- price 0.10))
                     ;; Not worth bothering to split the price. May as well buy it ourselves.
                     ;; TODO: could perform BUYER here
                     ;;
                     (log-info "A gives up on ~v." title)
                     (quit (try-to-buy remaining-titles budget))]
                    [(> my-contribution budget)
                     ;; Don't have enough money
                     (log-info "A does not have enough money for ~v." title)
                     (quit (try-to-buy remaining-titles budget))]
                    [else
                     (log-offer title my-contribution)
                     (transition my-contribution
                                 (list (scn (assertion (observe (split-proposal title price my-contribution ?))))))]))])]
            [_ #f]))
        initial-offer
        (list (scn (assertion (observe (split-proposal title price initial-offer ?))))))]))
  (try-to-buy titles budget))

(define (buyer-b funds)
  (define (complete-purchase title price contrib)
    (actor/stateless
     #:name (format "(complete-purchase ~a ~a ~a)" title price contrib)
     (lambda (e)
       (match e
         [(scn t)
          (match-define (list order-id delivery-date)
            (set-first (trie-project/set #:take 2 t (order title price (?!) (?!)))))
          (log-info "The order for ~v has id ~a, and will be delivered on ~a"
                                  title
                                  order-id
                                  delivery-date)
          (quit)]
         [_ #f]))
     (list (scn/union (assertion (split-proposal title price contrib #t))
                      (assertion (observe (order title price ? ?)))))))
  (actor
   #:name 'buyer-b
   (lambda (e funds)
     (match e
       [(scn t)
        (define-values (remaining-funds actions)
          (for-trie/fold ([funds funds]
                          [actions (list)])
                         ([(observe (split-proposal $title $price $contrib _)) t])
            (define my-contribution (- price contrib))
            (log-info "B is being asked to contribute ~a toward ~v at price ~a"
                      my-contribution
                      title
                      price)
            (cond
              [(> my-contribution funds)
               (log-info "B hasn't enough funds (~a remaining)" funds)
               (values funds
                       (cons (while-relevant-assert (split-proposal title price contrib #f))
                             actions))]
              [else
               (define remaining-funds (- funds my-contribution))
               (log-info "B accepts the offer, leaving them with ~a remaining funds"
                         remaining-funds)
               (values remaining-funds
                       (cons (complete-purchase title price contrib)
                             actions))])))
          (transition remaining-funds actions)]
       [_ #f]))
   funds
   (list (scn (assertion (observe (observe (split-proposal ? ? ? ?))))))))

(seller (hash "The Wind in the Willows" 3.95
              "Catch 22" 2.22
              "Candide" 34.95))

(buyer-a (list "Catch 22"
               "Encyclopaedia Brittannica"
               "Candide"
               "The Wind in the Willows")
         35.00)

(buyer-b 5.00)
