#lang racket/base

(provide ide-dataspace
         install-ide-dataspace!)

(require racket/async-channel)
(require (only-in racket/list flatten))
(require racket/match)
(require (only-in racket/math pi sqr))
(require racket/set)
(require 2htdp/image)

(require (only-in syndicate seal process-name process-behavior process))
(require (only-in syndicate/dataspace dataspace?))
(require (only-in syndicate/relay relay))
(require syndicate/actor)
(require (only-in syndicate/lang current-ground-dataspace))
(require syndicate/patch)
(require syndicate/protocol/standard-relay)
(require syndicate/ground)
(require syndicate/trace)
(require syndicate/store)
(require syndicate-gl/2d)

(require "hsv.rkt")

(struct view-position (pid pos) #:prefab) ;; assertion
(struct influence (subject object) #:prefab) ;; message

;; A SpringType is one of
;; - tension - only pulls the ends together when stretched
;; - compression - only pushes the ends apart when compressed
;; - both - both tension and compression

(struct spring (type subject object strength length) #:prefab) ;; assertion
(struct push-view (pid delta) #:prefab) ;; message

(define (random-in-range lo hi)
  (+ lo (* (random) (- hi lo))))

(define (tooltip touching? x y w h label-strings0)
  (define label-strings (flatten label-strings0))
  (define label-text (apply above (map (lambda (s) (text s 22 "black")) label-strings)))
  (define label (overlay label-text (empty-scene (+ (image-width label-text) 10)
                                                 (+ (image-height label-text) 10))))
  (define (x-pos window-width)
    (define left (- (x) (image-width label) 10))
    (define right (+ (x) w 10))
    (cond
      [(not (negative? left)) left]
      [(<= (+ right (image-width label)) window-width) right]
      [else (+ (x) (* 1/2 w) (* -1/2 (image-width label)))]))
  (react (define/query-value window-width 0 (inbound (window $w _)) w)
         (assert #:when (touching?)
                 (outbound (simple-sprite -10
                                          (x-pos (window-width))
                                          (+ (y) (* 1/2 h) (- (* 1/2 (image-height label))))
                                          (image-width label)
                                          (image-height label)
                                          label)))))

(define (actor-view name parent-pid pid is-dataspace?)
  (spawn #:name (list 'actor-view name pid)

         (field [pos #f])
         (define/query-value win #f (inbound (window $w $h)) (window w h)
           #:on-add (when (not (pos)) (pos (make-rectangular (random-in-range 0 w)
                                                             (random-in-range 0 h)))))

         (define color (color-by-hash pid))
         (define costume (circle (if is-dataspace? 40 20) "solid" color))
         (define extent (make-rectangular (image-width costume) (image-height costume)))

         (define (x) (real-part (pos)))
         (define (y) (imag-part (pos)))

         (define/query-value touching? #f (inbound (touching pid)) #t)
         (on-start (tooltip touching? x y (real-part extent) (imag-part extent)
                            (list (format "~a" name)
                                  (format "~a" pid))))

         (assert #:when (pos) (view-position pid (+ (pos) (* 1/2 extent))))
         (assert #:when (pos)
                 (outbound (simple-sprite #:touchable-id pid
                                          #:touchable-predicate in-unit-circle?
                                          0
                                          (x)
                                          (y)
                                          (real-part extent)
                                          (imag-part extent)
                                          costume)))

         (on (message (push-view pid $delta))
             (when (pos)
               (pos (+ (pos) delta))))

         (on (message (trace-notification pid _ 'action _))
             (log-info "~v acting" pid)
             (void))

         (on (message (influence pid _))
             (log-info "~v influencing" pid)
             (void))

         (on (message (influence _ pid))
             (log-info "~v influenced" pid)
             (void))

         (stop-when (message (trace-notification _ pid 'exit _)))))

(define 1x1-white-rectangle (rectangle 1 1 "solid" "white"))
(define (compute-link-line start-pos end-pos width)
  (define delta (- end-pos start-pos))
  (define heading (angle delta))
  (define displacement (make-polar 10 (- heading (* pi 1/2))))
  (outbound
   (simple-sprite 1
                  #:rotation (* -180 (/ heading pi))
                  (real-part (+ start-pos displacement))
                  (imag-part (+ start-pos displacement))
                  (magnitude delta)
                  width
                  1x1-white-rectangle)))

(define (spawn-influence-view-factory)
  (spawn #:name 'influence-view-factory
         (field [pairs (set)])
         (on (message (influence $subject $object))
             (define entry (cons subject object))
             (when (not (set-member? (pairs) entry))
               (pairs (set-add (pairs) entry))
               (react
                (stop-when (retracted (view-position subject _)) (pairs (set-remove (pairs) entry)))
                (stop-when (retracted (view-position object _)) (pairs (set-remove (pairs) entry))))
               (spawn #:name (list 'influence-view entry)
                      (field [strength 1] [line-width 0])
                      (define/query-value subject-pos #f (view-position subject $p) p)
                      (define/query-value object-pos #f (view-position object $p) p)
                      (stop-when (retracted (view-position subject _)))
                      (stop-when (retracted (view-position object _)))
                      (assert #:when (and (subject-pos) (object-pos) (>= (strength) 0.1))
                              (compute-link-line (subject-pos) (object-pos) (line-width)))
                      (assert #:when (>= (strength) 0.1)
                              (spring 'tension subject object 1 (+ 50 (/ 100 (strength)))))
                      (begin/dataflow
                        (define new-width (min 10 (floor (strength))))
                        (when (not (= new-width (line-width))) (line-width new-width)))
                      (on (message (inbound (frame-event $counter _ _ $target-frame-rate)))
                          (define update-every (inexact->exact (floor (/ target-frame-rate 3))))
                          (when (zero? (modulo counter update-every))
                            (strength (/ (strength) (expt 2 (* update-every (/ target-frame-rate)))))))
                      (on (message (influence subject object))
                          (strength (+ (strength) 1))))))))

(define (spawn-layout-engine)
  (spawn #:name 'layout-engine
         (define/query-value win #f (inbound (window $w $h)) (window w h))
         (define/query-hash positions (view-position $pid $pos) pid pos)
         (define/query-set springs ($ s (spring _ _ _ _ _)) s)
         (on #:when (win) (message (inbound (frame-event _ _ _ _)))
             (define midpoint (/ (make-rectangular (window-width (win)) (window-height (win))) 2))
             (for [((this-pid pos) (in-hash (positions)))]
               (send! (push-view this-pid
                                 (* 1/10
                                    (+ (- midpoint pos)
                                       (for/sum [((other-pid other-pos) (in-hash (positions)))
                                                 #:when (not (equal? other-pid this-pid))]
                                         (define delta (- pos other-pos))
                                         (when (< (magnitude delta) 10)
                                           (set! delta (make-polar 10 (random-in-range (- pi) pi))))
                                         (make-polar (/ 1000000 (sqr (magnitude delta)))
                                                     (angle delta)))
                                       (for/sum [(s (in-set (springs)))]
                                         (define other-pid (match s
                                                             [(spring _ p (== this-pid) _ _) p]
                                                             [(spring _ (== this-pid) p _ _) p]
                                                             [_ #f]))
                                         (or (and other-pid
                                                  (not (equal? this-pid other-pid))
                                                  (let ((other-pos (hash-ref (positions) other-pid #f)))
                                                    (and other-pos
                                                         (let ()
                                                           (define heading (angle (- other-pos pos)))
                                                           (define current-length (magnitude (- other-pos pos)))
                                                           (define ideal-length (spring-length s))
                                                           (define force (* (spring-strength s)
                                                                            (- current-length ideal-length)))
                                                           (make-polar ((case (spring-type s)
                                                                          [(both) (lambda (z v) v)]
                                                                          [(tension) max]
                                                                          [(compression) min]) 0 force)
                                                                       heading)))))
                                             0))))))))
         ))

(define (process-is-dataspace? p)
  (match p
    [(process _name _beh (? dataspace? _)) #t]
    [(process _name _beh (relay _ _ _ _ _ (process _inner-name _inner-beh (? dataspace? _)))) #t]
    [_ #f]))

(define ((ide-dataspace #:exit? [exit? #t]) . boot-actions)
  (define from-user-thread-ch (make-async-channel))

  (define user-thread
    (thread (lambda ()
              (with-store ((current-trace-procedures
                            (cons (lambda (n) (async-channel-put from-user-thread-ch n))
                                  (current-trace-procedures))))
                (run-ground boot-actions)))))

  (signal-background-activity! #t)
  (with-store ((current-trace-procedures '()))
    ((2d-dataspace #:label "Syndicate IDE" #:exit? exit?)

     (spawn #:name 'user-thread-death-monitor
            (field [user-thread-running? #t])
            (assert #:when (user-thread-running?) 'user-thread-running)
            (on (message (inbound (? frame-event? _)))
                (when (thread-dead? user-thread) (user-thread-running? #f))))

     ;; Ground dataspace
     (actor-view 'ground #f '() #t)

     (spawn #:name 'notification-relay
            (on (message (inbound (? frame-event? _)))
                (let loop ()
                  (define n (async-channel-try-get from-user-thread-ch))
                  (match n
                    [#f
                     (void)]
                    [(trace-notification _ new-pid 'spawn (list parent-pid p))
                     (actor-view (process-name p)
                                 (match parent-pid
                                   [(cons 'meta _) #f]
                                   [_ parent-pid])
                                 new-pid
                                 (process-is-dataspace? p))]
                    [(trace-notification s o 'influence (? patch? p))
                     (for [(source-pid (in-set (extract-patch-pids p)))]
                       (send! (influence (cons source-pid (cdr s)) o)))]
                    [(trace-notification s o 'influence (message _))
                     (send! (influence s o))]
                    [(? trace-notification? n)
                     (send! n)])
                  (when n (loop)))))

     (spawn #:name 'quit-listener
            (on (message (inbound (key-event #\q #t _)))
                (assert! (outbound 'stop))))

     (spawn-influence-view-factory)
     (spawn-layout-engine)

     ;; (spawn #:name 'debug
     ;;        (on (message (? trace-notification? $n))
     ;;            (log-info "INBOUND: ~v --~v--> ~v"
     ;;                      (trace-notification-source n)
     ;;                      (trace-notification-type n)
     ;;                      (trace-notification-sink n))))

     )))

(define install-ide-dataspace!
  (make-keyword-procedure
   (lambda (ks vs . positionals)
     (define installed-dataspace (current-ground-dataspace))
     (current-ground-dataspace (keyword-apply ide-dataspace ks vs positionals)))))
