#lang syndicate
;; An extended two-buyer book-purchase protocol, based loosely on that
;; given in Honda/Yoshida/Carbone 2008, "Multiparty Asynchronous
;; Session Types".

;; SAMPLE OUTPUT:
;;---------------------------------------------------------------------------
;; A learns that the price of "Catch 22" is 2.22
;; A makes an offer to split the price of "Catch 22", contributing 1.11
;; B is being asked to contribute 1.11 toward "Catch 22" at price 2.22
;; B accepts the offer, leaving them with 3.8899999999999997 remaining funds
;; A learns that the split-proposal for "Catch 22" was accepted, leaving them with 33.89 remaining
;; The order for "Catch 22" has id 10001483, and will be delivered on March 9th
;; A learns that "Encyclopaedia Brittannica" is out-of-stock.
;; A learns that the price of "Candide" is 34.95
;; A makes an offer to split the price of "Candide", contributing 17.475
;; B is being asked to contribute 17.475 toward "Candide" at price 34.95
;; B hasn't enough funds (3.8899999999999997 remaining)
;; A learns that the split-proposal for "Candide" was rejected
;; A makes an offer to split the price of "Candide", contributing 26.212500000000002
;; B is being asked to contribute 8.7375 toward "Candide" at price 34.95
;; B hasn't enough funds (3.8899999999999997 remaining)
;; A learns that the split-proposal for "Candide" was rejected
;; A makes an offer to split the price of "Candide", contributing 30.581250000000004
;; B is being asked to contribute 4.368749999999999 toward "Candide" at price 34.95
;; B hasn't enough funds (3.8899999999999997 remaining)
;; A learns that the split-proposal for "Candide" was rejected
;; A makes an offer to split the price of "Candide", contributing 32.765625
;; B is being asked to contribute 2.184375000000003 toward "Candide" at price 34.95
;; B accepts the offer, leaving them with 1.7056249999999968 remaining funds
;; A learns that the split-proposal for "Candide" was accepted, leaving them with 1.1243750000000006 remaining
;; The order for "Candide" has id 10001484, and will be delivered on March 9th
;; A learns that the price of "The Wind in the Willows" is 3.95
;; A makes an offer to split the price of "The Wind in the Willows", contributing 1.1243750000000006
;; B is being asked to contribute 2.8256249999999996 toward "The Wind in the Willows" at price 3.95
;; B hasn't enough funds (1.7056249999999968 remaining)
;; A learns that the split-proposal for "The Wind in the Willows" was rejected
;; A does not have enough money for "The Wind in the Willows".
;; A has bought everything they wanted!
;;---------------------------------------------------------------------------

;; Role: SELLER
;;  - when interest in (book-quote $title _) appears,
;;    asserts (book-quote title (Option Float)), #f meaning not available,
;;    and otherwise an asking-price.
;;  - when interest in (order $title $offer-price _ _) appears,
;;    asserts (order title offer-price #f #f) for "no sale", otherwise
;;    (order title offer-price PositiveInteger String), an accepted sale.

;; Role: BUYER
;;  - observes (book-quote title $price) to learn prices.
;;  - observes (order title offer-price $id $delivery-date) to make orders.

;; Role: SPLIT-PROPOSER
;;  - observes (split-proposal title asking-price contribution $accepted?)
;;    to make a split-proposal and learn whether it was accepted or not.

;; Role: SPLIT-DISPOSER
;;  - when interest in (split-proposal $title $asking-price $contribution _)
;;    appears, asserts (split-proposal title asking-price contribution #t)
;;    to indicate they are willing to go through with the deal, in which case
;;    they then perform the role of BUYER for title/asking-price, or asserts
;;    (split-proposal title asking-price contribution #f) to indicate they
;;    are unwilling to go through with the deal.

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

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

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

;; Utility syntax. Under consideration for possible addition to actor.rkt.
;;
(define-syntax while-relevant-assert
  (syntax-rules ()
    [(_ P)
     (begin (stop-when (retracted (observe P)))
            (assert P))]))

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

;; SELLER
;;
(define (seller)
  (spawn (field [books (hash "The Wind in the Willows" 3.95
                             "Catch 22" 2.22
                             "Candide" 34.95)]
                [next-order-id 10001483])

         ;; Give quotes to interested parties.
         ;;
         (during (observe (book-quote $title _))
                 (assert (book-quote title (hash-ref (books) title #f))))

         ;; Respond to order requests.
         ;;
         (on (asserted (observe (order $title $offer-price _ _)))
             (define asking-price (hash-ref (books) 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.
                ;;
                (react (while-relevant-assert (order title offer-price #f #f)))]

               [else
                ;; Allocate an order ID.
                ;;
                (define order-id (next-order-id))
                (next-order-id (+ order-id 1))

                ;; Remove the book from our shelves.
                ;;
                (books (hash-remove (books) title))

                ;; Tell the ordering party their order ID and delivery date.
                ;;
                (spawn
                 (while-relevant-assert
                  (order title offer-price order-id "March 9th")))]))))

;; Serial SPLIT-PROPOSER
;;
(define (buyer-a)

  ;; Attempt to entice some SPLIT-DISPOSER to purchase each title in titles, one after another.
  ;;
  (define (try-to-buy titles budget)
    (match titles
      ['()
       (log-info "A has bought everything they wanted!")]
      [(cons title remaining-titles)

       ;; First, retrieve a quote for the title, and analyze the result.
       ;;
       (match (react/suspend (yield)
                (stop-when (asserted (book-quote title $price)) (yield price)))
         [#f
          (log-info "A learns that ~v is out-of-stock." title)
          (try-to-buy remaining-titles budget)]

         [price
          (log-info "A learns that the price of ~v is ~a" title price)

          ;; Next, repeatedly make split offers to a SPLIT-DISPOSER until either one is
          ;; accepted, or the contribution from the SPLIT-DISPOSER becomes pointlessly small.
          ;;
          (let try-to-split ((contribution (min budget (/ price 2))))
            (cond
              [(> 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)
               (try-to-buy remaining-titles budget)]
              [(> contribution budget)
               ;; Don't have enough money
               (log-info "A does not have enough money for ~v." title)
               (try-to-buy remaining-titles budget)]

              [else
               ;; Make our proposal, and wait for a response.
               ;;
               (log-info "A makes an offer to split the price of ~v, contributing ~a"
                      title
                      contribution)
               (react
                (stop-when (asserted (split-proposal title price contribution #t))
                           (define remaining-budget (- budget contribution))
                           (log-info "A learns that the split-proposal for ~v was accepted, leaving them with ~v remaining" title remaining-budget)
                           (try-to-buy remaining-titles remaining-budget))
                (stop-when (asserted (split-proposal title price contribution #f))
                           (log-info "A learns that the split-proposal for ~v was rejected" title)
                           (try-to-split (+ contribution (/ (- price contribution) 2)))))]))])]))

  (spawn* (try-to-buy (list "Catch 22"
                            "Encyclopaedia Brittannica"
                            "Candide"
                            "The Wind in the Willows")
                      35.00)))

;; Serial SPLIT-DISPOSER
;;
(define (buyer-b)
  (spawn ;; This actor maintains a record of the amount of money it has to spend.
         ;;
         (field [funds 5.00])

         (on (asserted (observe (split-proposal $title $price $their-contribution _)))

             (define my-contribution (- price their-contribution))
             (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))
                (react (while-relevant-assert (split-proposal title price their-contribution #f)))]

               [else

                ;; Spawn a small actor (TODO: when we revise actor.rkt's implementation style,
                ;; this could perhaps be a facet rather than a full actor) to handle the
                ;; actual purchase now that we have agreed on a split.
                ;;
                (spawn* (define-values (order-id delivery-date)
                          (react/suspend (yield)
                                         ;; While we are in this state, waiting for order confirmation, take
                                         ;; the opportunity to signal to our SPLIT-PROPOSER that we accepted
                                         ;; their proposal.
                                         ;;
                                         (assert (split-proposal title price their-contribution #t))

                                         (stop-when (asserted (order title price $id $date))
                                                    ;; We have received order confirmation from the SELLER.
                                                    ;;
                                                    (yield id date))))
                       (log-info "The order for ~v has id ~a, and will be delivered on ~a"
                                 title
                                 order-id
                                 delivery-date))

                ;; Meanwhile, update our records of our available funds, and continue to wait
                ;; for more split-proposals to arrive.
                ;;
                (define remaining-funds (- (funds) my-contribution))
                (log-info "B accepts the offer, leaving them with ~a remaining funds"
                          remaining-funds)
                (funds remaining-funds)]))))

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

;; Starting configuration:
;;
(seller)
(buyer-a)
(buyer-b)
