#lang racket/base
(require "base.rkt"
         "number.rkt"
         racket/dict
         racket/class
         racket/promise
         racket/match
         racket/contract
         sugar/unstable/dict)
(provide (all-defined-out))

#|
approximates
https://github.com/mbutterick/restructure/blob/master/src/Pointer.coffee
|#

(define valid-pointer-relatives '(local immediate parent global))

(define (pointer-relative-value? x)
  (and (symbol? x) (memq x valid-pointer-relatives)))

(define (find-top-parent parent)
  (cond
    [(hash-ref parent x:parent-key #f) => find-top-parent]
    [else parent]))

(define (resolve-pointer type val)
  (cond
    [type (values type val)]
    [(xvoid-pointer? val) (values (xvoid-pointer-type val) (xvoid-pointer-value val))]
    [else (raise-argument-error 'x:pointer "VoidPointer" val)]))

(define x:pointer%
  (class x:base%
    (super-new)
    (init-field [(@ptr-val-type ptr-type)]
                [(@dest-type dest-type)]
                [(@pointer-relative-to pointer-relative-to)]
                [(@allow-null? allow-null?)]
                [(@null-value null-value)]
                [(@pointer-lazy? pointer-lazy?)])

    (define/augride (x:decode port parent)
      (define offset (send @ptr-val-type x:decode port parent))
      (cond
        [(and @allow-null? (= offset @null-value)) #false] ; handle null pointers
        [else
         (define relative (+ (case @pointer-relative-to
                               [(local) (hash-ref parent x:start-offset-key)]
                               [(immediate) (- (pos port) (send @ptr-val-type x:size))]
                               [(parent) (hash-ref (hash-ref parent x:parent-key) x:start-offset-key)]
                               [(global) (or (hash-ref (find-top-parent parent) x:start-offset-key) 0)]
                               [else (error 'unknown-pointer-style)])))
         (define ptr (+ offset relative))
         (cond
           [@dest-type (define (decode-value)
                    (define orig-pos (pos port))
                    (pos port ptr)
                    (begin0
                      (send @dest-type x:decode port parent)
                      (pos port orig-pos)))
                  (if @pointer-lazy? (delay (decode-value)) (decode-value))]
           [else ptr])]))

    (define/augride (x:encode val-in port [parent #f])
      (unless parent ; todo: furnish default pointer context? adapt from Struct?
        (raise-argument-error 'xpointer-encode "valid pointer context" parent))
      (cond
        [val-in
         (define new-parent (case @pointer-relative-to
                              [(local immediate) parent]
                              [(parent) (hash-ref parent x:parent-key)]
                              [(global) (find-top-parent parent)]
                              [else (error 'unknown-pointer-style)]))
         (define relative (+ (case @pointer-relative-to
                               [(local parent) (hash-ref new-parent x:start-offset-key)]
                               [(immediate) (+ (pos port) (send @ptr-val-type x:size val-in parent))]
                               [(global) 0])))
         (send @ptr-val-type x:encode (- (hash-ref new-parent x:pointer-offset-key) relative) port)
         (define-values (type val) (resolve-pointer @dest-type val-in))
         (hash-update! new-parent x:pointers-key
                       (λ (ptrs) (append ptrs (list (x:ptr type val parent)))))
         (hash-set! new-parent x:pointer-offset-key
                    (+ (hash-ref new-parent x:pointer-offset-key) (send type x:size val parent)))]
        [else (send @ptr-val-type x:encode @null-value port)]))

    (define/augride (x:size [val-in #f] [parent #f])
      (define new-parent (case @pointer-relative-to
                           [(local immediate) parent]
                           [(parent) (hash-ref parent x:parent-key)]
                           [(global) (find-top-parent parent)]
                           [else (error 'unknown-pointer-style)]))
      (define-values (type val) (resolve-pointer @dest-type val-in))
      (when (and val new-parent)
        (hash-set! new-parent x:pointer-size-key
                   (and (hash-ref new-parent x:pointer-size-key #f)
                        (+ (hash-ref new-parent x:pointer-size-key) (send type x:size val new-parent)))))
      (send @ptr-val-type x:size))))

#|
The arguments here are renamed slightly compared to the original.

offsetType => offset-type
The type of the thing the pointer points to.

type => type
The type of the pointer value itself.

options.type => relative-to
The reference point of the pointer value (local, immediate, parent, global). It was confusing to have two things named `type`, however.

relativeTo => [not supported]
This allows the pointer to be calculated relative to a property on the parent. I saw no use for this, so I dropped it.
|#

(define (x:pointer? x) (is-a? x x:pointer%))

(define/contract (x:pointer
                  [ptr-type-arg #f]
                  [dest-type-arg #f]
                   #:type [ptr-type-kwarg uint32]
                   #:dest-type [dest-type-kwarg uint8]
                   #:relative-to [pointer-relative-to 'local]
                   #:lazy [pointer-lazy? #f]
                   #:allow-null [allow-null? #t]
                   #:null [null-value 0]
                   #:pre-encode [pre-proc #f]
                   #:post-decode [post-proc #f]
                   #:base-class [base-class x:pointer%])
  (()
   (
    (or/c x:int?  #false)
    (or/c xenomorphic? 'void  #false)
    #:type (or/c x:int? #false)
    #:dest-type (or/c xenomorphic? 'void #false)
    #:relative-to pointer-relative-value?
    #:lazy boolean?
    #:allow-null boolean?
    #:null any/c
    #:pre-encode (or/c (any/c . -> . any/c) #false)
    #:post-decode (or/c (any/c . -> . any/c) #false)
    #:base-class (λ (c) (subclass? c x:pointer%)))
   . ->* .
   x:pointer?)
  (unless (pointer-relative-value? pointer-relative-to)
    (raise-argument-error 'x:pointer (format "~v" valid-pointer-relatives) pointer-relative-to))
  (new (generate-subclass base-class pre-proc post-proc)
       [ptr-type (or ptr-type-arg ptr-type-kwarg)]
       [dest-type (match (or dest-type-arg dest-type-kwarg)
                   ['void #false]
                   [type-in type-in])]
       [pointer-relative-to pointer-relative-to]
       [pointer-lazy? pointer-lazy?]
       [allow-null? allow-null?]
       [null-value null-value]))

;; A pointer whose type is determined at decode time
(define x:void-pointer% (class x:base%
                         (super-new)
                         (init-field type value)))
(define (x:void-pointer . args) (apply make-object x:void-pointer% args))
(define (xvoid-pointer? x) (is-a? x x:void-pointer%))
(define (xvoid-pointer-type x) (get-field type x))
(define (xvoid-pointer-value x) (get-field value x))
