#lang racket/base
(require '#%flfxnum
         (only-in '#%foreign cpointer? ptr-add)
         (only-in '#%place place-shared?)
         racket/prefab)

(provide place-message-allowed?
         place-message-allowed-direct?

         message-ize
         un-message-ize
         
         prop:place-message)

;; A prop:place-message value is a procedure that takes self
;; and returns either #f [=> not a place message after all] or
;; (-> (-> v))
(define-values (prop:place-message place-message? place-message-ref)
  (make-struct-type-property 'place-message))

(struct message-ized (unmessage)
  #:authentic)

(define (allowed? v #:direct? direct?)
  (let loop ([v v] [graph #hasheq()])
    (or (number? v)
        (char? v)
        (boolean? v)
        (keyword? v)
        (void? v)
        (symbol? v)
        (and (or (string? v)
                 (bytes? v))
             (or (not direct?)
                 (immutable? v)
                 (and (bytes? v)
                      (place-shared? v))))
        (null? v)
        (and (pair? v)
             (or (hash-ref graph v #f)
                 (let ([graph (hash-set graph v #t)])
                   (and (loop (car v) graph)
                        (loop (cdr v) graph)))))
        (and (box? v)
             (or (not direct?)
                 (and (immutable? v)
                      (not (impersonator? v))))
             (or (hash-ref graph v #f)
                 (let ([graph (hash-set graph v #t)])
                   (loop (unbox v) graph))))
        (and (vector? v)
             (or (not direct?)
                 (and (immutable? v)
                      (not (impersonator? v))))
             (or (hash-ref graph v #f)
                 (let ([graph (hash-set graph v #t)])
                   (for/and ([e (in-vector v)])
                     (loop e graph)))))
        (and (immutable-prefab-struct-key v)
             (or (hash-ref graph v #f)
                 (let ([graph (hash-set graph v #t)])
                   (for/and ([e (in-vector (struct->vector v))])
                     (loop e graph)))))
        (and (hash? v)
             (or (not direct?)
                 (and (immutable? v)
                      (not (impersonator? v))))
             (or (hash-ref graph v #f)
                 (let ([graph (hash-set graph v #t)])
                   (for/and ([(k v) (in-hash v)])
                     (and (loop k graph)
                          (loop v graph))))))
        (and (not direct?)
             (or (cpointer? v)
                 (and (or (fxvector? v)
                          (flvector? v)
                          (bytes? v))
                      (place-shared? v))
                 (and (place-message? v)
                      ((place-message-ref v) v)
                      #t))))))

(define (place-message-allowed-direct? v)
  (allowed? v #:direct? #t))
  
(define (place-message-allowed? v)
  (allowed? v #:direct? #f))

;; Convert a message to a form suitable to keep in a channel, but
;; simultaneously check whether the message is ok, since a message
;; might start out with mutable elements that are changed while
;; the conversion is in progress (but we convert enough to avoid
;; problems afterward)
(define (message-ize v fail)
  (define graph #f)
  (define used #f)
  (define (maybe-ph ph v new-v)
    (cond
      [(and used (hash-ref used ph #f))
       (placeholder-set! ph new-v)
       ph]
      [else
       (hash-remove! graph v)
       new-v]))
  (define new-v
    (let loop ([v v])
      (cond
        [(or (number? v)
             (char? v)
             (boolean? v)
             (keyword? v)
             (void? v)
             (symbol? v)
             (null? v))
         v]
        [(string? v)
         (string->immutable-string v)]
        [(bytes? v)
         (if (place-shared? v)
             v
             (bytes->immutable-bytes v))]
        [else
         (unless graph (set! graph (make-hasheq)))
         (cond
           [(hash-ref graph v #f)
            => (lambda (v)
                 (unless used (set! used (make-hasheq)))
                 (hash-set! used v #t)
                 v)]
           [(pair? v)
            (define ph (make-placeholder #f))
            (hash-set! graph v ph)
            (maybe-ph ph v (cons (loop (car v))
                                 (loop (cdr v))))]
           [(box? v)
            (define ph (make-placeholder #f))
            (hash-set! graph v ph)
            (maybe-ph ph v (box (loop (unbox v))))]
           [(vector? v)
            (define ph (make-placeholder #f))
            (hash-set! graph v ph)
            (maybe-ph ph v (for/vector #:length (vector-length v) ([e (in-vector v)])
                             (loop e)))]
           [(immutable-prefab-struct-key v)
            => (lambda (k)
                 (define ph (make-placeholder #f))
                 (hash-set! graph v ph)
                 (maybe-ph
                  ph
                  v
                  (apply make-prefab-struct
                         k
                         (for/list ([e (in-vector (struct->vector v) 1)])
                           (loop e)))))]
           [(hash? v)
            (define ph (make-placeholder #f))
            (hash-set! graph v ph)
            (maybe-ph
             ph
             v
             (hash-map/copy v
                            (lambda (k v)
                              (values (loop k) (loop v)))
                            #:kind 'immutable))]
           [(cpointer? v)
            (ptr-add v 0)]
           [(and (or (fxvector? v)
                     (flvector? v))
                 (place-shared? v))
            v]
           [(place-message? v)
            (define make-unmessager ((place-message-ref v) v))
            (if make-unmessager
                (message-ized (make-unmessager))
                (fail))]
           [else (fail)])])))
  (message-ized new-v))

(define (un-message-ize v)
  (if (message-ized? v)
      (make-reader-graph (do-un-message-ize (message-ized-unmessage v)))
      v))

(define (do-un-message-ize v)
  (define graph #f)
  (let loop ([v v])
    (cond
      [(placeholder? v)
       (define ph (make-placeholder #f))
       (unless graph (set! graph (make-hasheq)))
       (cond
         [(hash-ref graph v #f) => (lambda (ph) ph)]
         [else
          (hash-set! graph v ph)
          (placeholder-set! ph (loop (placeholder-get v)))
          ph])]
      [(pair? v)
       (cons (loop (car v)) (loop (cdr v)))]
      [(box? v)
       (box-immutable
        (loop (unbox v)))]
      [(vector? v)
       (vector->immutable-vector
        (for/vector #:length (vector-length v) ([e (in-vector v)])
          (loop e)))]
      [(immutable-prefab-struct-key v)
       => (lambda (k)
            (apply make-prefab-struct
                   k
                   (for/list ([e (in-vector (struct->vector v) 1)])
                     (loop e))))]
      [(hash? v)
       (hash-map/copy v
                      (lambda (k v)
                        (values (loop k) (loop v)))
                      #:kind 'immutable)]
      [(and (cpointer? v)
            v ; not #f
            (not (bytes? v)))
       (ptr-add v 0)]
      [(message-ized? v)
       ((message-ized-unmessage v))]
      [else v])))

(module+ test
  (define-syntax-rule (test expect actual)
    (let ([v actual])
      (unless (equal? expect v)
        (error 'test "failed: ~s = ~s" 'actual v))))

  (struct external (a)
    #:property prop:place-message (lambda (self)
                                    (lambda ()
                                      (define a (external-a self))
                                      (lambda () (external a)))))

  (test #t (place-message-allowed? "apple"))
  (test #t (place-message-allowed-direct? "apple"))
  (test #f (place-message-allowed-direct? (string-copy "apple")))
  (test #f (place-message-allowed-direct? (cons 1 (string-copy "apple"))))

  (test #t (place-message-allowed? (box "car")))

  (test #t (place-message-allowed-direct? '(a . b)))
  (test #t (place-message-allowed-direct? '#(a b)))
  (test #t (place-message-allowed-direct? '#hasheq((a . b))))
  (test #t (place-message-allowed-direct? '#s(pre 1 2 3)))

  (define direct-cyclic (read (open-input-string "#0=(1 #0# 2)")))
  (test #t (place-message-allowed-direct? direct-cyclic))

  (test #t (place-message-allowed? (make-prefab-struct 'bx (box #f) (box #f))))
  (test #f (place-message-allowed-direct? (make-prefab-struct 'bx (box #f) (box #f))))

  (struct unallowed
    (a b))
  (test #f (place-message-allowed? (unallowed 1 2)))
  (test #f (place-message-allowed? (box (unallowed 1 2))))
  
  (define stateful-cyclic (make-reader-graph
                           (let ([ph (make-placeholder #f)]
                                 [ph2 (make-placeholder #f)]
                                 [ph3 (make-placeholder #f)])
                             (define (as ph v) (placeholder-set! ph v) v)
                             (as ph2 (vector (as ph (cons ph (string-copy "apple")))
                                             (box (box 2))
                                             (box (vector "string" (box 'symbol)))
                                             ph2
                                             (as ph3 (hasheq 'a 1 'b ph3))
                                             '#s(pre 4 5)))
                             ph)))
  (test #f (place-message-allowed-direct? stateful-cyclic))
  (test #t (place-message-allowed? stateful-cyclic))
  (test stateful-cyclic (un-message-ize (message-ize stateful-cyclic
                                                     (lambda ()
                                                       (raise-argument-error #f
                                                                             "test-error"
                                                                             stateful-cyclic)))))

  (define ext (external 'x))
  (test #t (place-message-allowed? ext))
  (test #f (place-message-allowed-direct? ext))
  (define ext2 (un-message-ize (message-ize ext
                                            (lambda ()
                                              (raise-argument-error #f
                                                                    "test-error"
                                                                    ext)))))
  (test #t (external? ext2)) 
  (test #f (eq? ext ext2))
  (test 'x (external-a ext2))

  (void))
