; feed/fanout — THE SHOWCASE. Fan activities out to followers via the APL outer
; product (∘.×). activities ∘.× audience → an (activity × follower) matrix of
; inbox events; flatten to a vector; guard-keep only real follow edges.
;
; Requires: lib/apl/runtime.sx, lib/feed/normalize.sx, lib/feed/stream.sx.
;
; NOTE: apl-outer's combiner result is run through (if (scalar? r) (disclose r) r).
; A bare dict counts as a scalar (shape ()) and disclose nils it — so the combiner
; must (enclose ...) its event dict; apl-outer then discloses it back intact.

; --- graph: {followee -> (list of followers)} -------------------------------

(define feed/followers (fn (graph user) (get graph user (list))))

; build a graph from (follower followee) edges: "follower follows followee"
(define
  feed/follow-graph
  (fn
    (edges)
    (reduce
      (fn
        (g e)
        (let
          ((follower (first e)) (followee (nth e 1)))
          (assoc
            g
            followee
            (append (feed/followers g followee) (list follower)))))
      {}
      edges)))

; --- helpers ----------------------------------------------------------------

; unwrap an apl-scalar (has :ravel) back to its value; pass activities through
(define
  feed/-val
  (fn
    (x)
    (if (and (= (type-of x) "dict") (has-key? x :ravel)) (disclose x) x)))

(define feed/-elem? (fn (x lst) (some (fn (y) (equal? x y)) lst)))

(define
  feed/-distinct
  (fn
    (lst)
    (if
      (= (len lst) 0)
      (list)
      (get (apl-unique (make-array (list (len lst)) lst)) :ravel))))

; rank-2 matrix -> rank-1 stream of its ravel
(define feed/-flatten (fn (arr) (feed/stream (get arr :ravel))))

; distinct receivers across the whole graph, sorted for determinism
; (dict key order is unspecified, so sort to pin audience/recipient ordering)
(define
  feed/audience
  (fn
    (graph)
    (sort
      (feed/-distinct
        (reduce
          (fn (acc k) (append acc (feed/followers graph k)))
          (list)
          (keys graph))))))

; --- the outer product ------------------------------------------------------

; one (activity, follower) inbox event, enclosed so apl-outer keeps the dict
(define feed/-mk-event (fn (a f) (enclose {:activity (feed/-val a) :to (feed/-val f)})))

; keep events where :to actually follows the activity's actor
(define
  feed/-edge?
  (fn
    (graph)
    (fn
      (ev)
      (feed/-elem?
        (get ev :to)
        (feed/followers graph (get (get ev :activity) :actor))))))

; fanout — activities ∘.× audience, flatten, guard-keep real edges
(define
  feed/fanout
  (fn
    (stream graph)
    (let
      ((matrix (apl-outer feed/-mk-event stream (feed/stream (feed/audience graph)))))
      (feed/filter (feed/-flatten matrix) (feed/-edge? graph)))))

; --- inbox queries ----------------------------------------------------------

(define
  feed/inbox-for
  (fn
    (inbox user)
    (feed/filter inbox (fn (ev) (equal? (get ev :to) user)))))

(define
  feed/recipients
  (fn
    (inbox)
    (feed/-distinct (map (fn (ev) (get ev :to)) (feed/items inbox)))))

; the activities (unwrapped) destined for a user
(define
  feed/inbox-activities
  (fn
    (inbox user)
    (map
      (fn (ev) (get ev :activity))
      (feed/items (feed/inbox-for inbox user)))))
