#lang racket/base

(provide big-bang-dataspace
         big-bang-dataspace/universe
         (struct-out window)
         (struct-out to-server)
         (struct-out from-server)
         (struct-out tick-event)
         (struct-out key-event)
         (struct-out pad-event)
         (struct-out release-event)
         (struct-out mouse-event)
         (struct-out mouse-state)
         (struct-out active-window)
         (struct-out frame-dimensions)
         update-window
         (all-from-out 2htdp/image))

(require racket/set)
(require racket/match)
(require 2htdp/image)
(require (except-in 2htdp/universe
                    key-event?
                    pad-event?
                    mouse-event?))
(require (only-in 2htdp/private/check-aux SQPORT))
(require (only-in racket/list flatten))

(require "main.rkt")
(require "trie.rkt")

;;---------------------------------------------------------------------------

(struct window (id x y z image) #:transparent) ;; image must be sealed

(struct to-server (message) #:transparent)
(struct from-server (message) #:transparent)
(struct tick-event () #:transparent)
(struct key-event (key window) #:transparent)
(struct pad-event (key window) #:transparent)
(struct release-event (key window) #:transparent)
(struct mouse-event (x y window type) #:transparent)
(struct mouse-state (x y window) #:transparent)
(struct active-window (id) #:transparent)
(struct frame-dimensions (width height) #:transparent)

(define (update-window id x y image #:z [z 0])
  (patch-seq (retract (outbound (window id ? ? ? ?)))
             (assert (outbound (window id x y z (seal image))))))

;;---------------------------------------------------------------------------

(struct bb (proc windows inbound outbound halted? x y width height) #:transparent)

(define window-projection (?! (window ? ? ? ? ?)))

(define (inject b es)
  (interpret-actions (struct-copy bb b [inbound (append (bb-inbound b)
                                                        es
                                                        (list (update-active-window
                                                               (find-active-window b))))])
                     #f
                     #t))

(define (incorporate-patch b p)
  (define-values (added removed) (patch-project/set/single p window-projection))
  (struct-copy bb b
               [windows (sort (set->list (set-union added
                                                    (set-subtract (list->set (bb-windows b))
                                                                  removed)))
                              (lambda (w1 w2) (< (window-z w1) (window-z w2))))]
               [halted? (if (or (and (bb-halted? b)
                                     (not (trie-lookup (patch-removed p) 'stop #f)))
                                (trie-lookup (patch-added p) 'stop #f))
                            #t
                            #f)]))

(define (deliver b e)
  (clean-transition ((process-behavior (bb-proc b)) e (process-state (bb-proc b)))))

(define (interpret-actions b txn need-poll?)
  (match txn
    [#f ;; inert
     (match (bb-inbound b)
       ['()
        (if need-poll?
            (interpret-actions b (deliver b #f) #f)
            (let ((outbound (reverse (bb-outbound b))))
              (if (null? outbound)
                  b
                  (make-package (struct-copy bb b [outbound '()]) outbound))))]
       [(cons e rest)
        (let ((b (struct-copy bb b [inbound rest])))
          (interpret-actions b (deliver b e) #t))])]
    [(<quit> _ _)
     (struct-copy bb b [halted? #t])]
    [(transition new-state actions)
     (let process-actions ((b (struct-copy bb b
                                           [proc (update-process-state (bb-proc b) new-state)]))
                           (actions actions))
       (match actions
         ['() (interpret-actions b #f #t)]
         [(cons a actions)
          (process-actions (match a
                             [(? patch? p)
                              (incorporate-patch b p)]
                             [(message (to-server sexp))
                              (struct-copy bb b
                                           [outbound (cons sexp (bb-outbound b))])]
                             [_ b])
                           actions)]))]))

(define (inside? mx my x y image)
  (and (>= mx x)
       (>= my y)
       (< (- mx x) (image-width image))
       (< (- my y) (image-height image))))

(define (find-active-window b)
  (define mx (bb-x b))
  (define my (bb-y b))
  (let loop ((ws (bb-windows b)))
    (match ws
      ['() #f]
      [(cons (window id x y _ (seal image)) ws)
       (if (inside? mx my x y image) id (loop ws))])))

(define (render b)
  (for/fold [(scene empty-image)] [(w (bb-windows b))]
    (match-define (window _ x y z (seal image)) w)
    (overlay/xy scene x y image)))

(define (update-active-window active-id)
  (patch-seq (retract (active-window ?))
             (assert (active-window active-id))))

(define-syntax-rule (big-bang-dataspace* width height boot-actions extra-clause ...)
  (let-values (((proc initial-transition _initial-assertions-always-empty)
                (actor->process+transition/assertions (dataspace-actor boot-actions))))
    (big-bang (interpret-actions (bb proc
                                     '()
                                     (list (assert (frame-dimensions width height)))
                                     '()
                                     #f
                                     0
                                     0
                                     width
                                     height)
                                 initial-transition
                                 #t)
              (on-tick (lambda (b)
                         (inject b (list (message (tick-event))))))
              (on-key (lambda (b k)
                        (inject b (list (message (key-event k (find-active-window b)))))))
              ;; (on-pad (lambda (b p)
              ;;           (inject b (list (message (pad-event p (find-active-window b)))))))
              (on-release (lambda (b k)
                            (inject b (list (message (release-event k (find-active-window b)))))))
              (on-mouse (lambda (b0 x y e)
                          (define b (struct-copy bb b0 [x x] [y y]))
                          (define active-id (find-active-window b))
                          (inject b (list (patch-seq (retract (mouse-state ? ? ?))
                                                     (assert (mouse-state x y active-id)))
                                          (message (mouse-event x y active-id e))))))
              (stop-when bb-halted?)
              extra-clause ...)))

(define-syntax-rule (big-bang-dataspace** fullscreen? width height exit? boot-actions
                                          extra-clause ...)
  (begin
    (cond
      [fullscreen?
       (big-bang-dataspace* width height boot-actions (to-draw render)
                            (display-mode 'fullscreen
                                          (lambda (b w h)
                                            (inject (struct-copy bb b [width w] [height h])
                                                    (list
                                                     (patch-seq (retract (frame-dimensions ? ?))
                                                                (assert (frame-dimensions w h)))))))
                            extra-clause ...)]
      [(and width height)
       (big-bang-dataspace* width height boot-actions (to-draw render width height)
                            extra-clause ...)]
      [else
       (big-bang-dataspace* width height boot-actions (to-draw render)
                            extra-clause ...)])
    (when exit? (exit 0))))

(define ((big-bang-dataspace #:width [width #f]
                             #:height [height #f]
                             #:exit? [exit? #t]
                             #:fullscreen? [fullscreen? #f])
         . boot-actions)
  (big-bang-dataspace** fullscreen? width height exit? boot-actions))

(define ((big-bang-dataspace/universe #:width [width #f]
                                      #:height [height #f]
                                      #:exit? [exit? #t]
                                      #:register [ip LOCALHOST]
                                      #:port [port-number SQPORT]
                                      #:name [world-name (gensym 'syndicate)]
                                      #:fullscreen? [fullscreen? #f])
         . boot-actions)
  (big-bang-dataspace** fullscreen? width height exit? boot-actions
                        (on-receive (lambda (b sexps)
                                      (inject b (for/list ((m sexps)) (message (from-server m))))))
                        (register ip)
                        (port port-number)
                        (name world-name)))
