#lang racket/base

(require racket/class racket/list racket/match racket/contract unstable/latent-contract/defthing
         plot/utils)

(provide (all-defined-out))

;; ===================================================================================================

(define ((points3d-render-proc vs sym color fill-color size line-width alpha label) area)
  (send area put-alpha alpha)
  (send area put-pen color line-width 'solid)
  (send area put-brush fill-color 'solid)
  (send area put-glyphs vs sym size)
  
  (cond [label  (point-legend-entry label sym color fill-color size line-width)]
        [else   empty]))

(defproc (points3d
          [vs  (listof (vector/c real? real? real?))]
          [#:x-min x-min (or/c rational? #f) #f] [#:x-max x-max (or/c rational? #f) #f]
          [#:y-min y-min (or/c rational? #f) #f] [#:y-max y-max (or/c rational? #f) #f]
          [#:z-min z-min (or/c rational? #f) #f] [#:z-max z-max (or/c rational? #f) #f]
          [#:sym sym point-sym/c (point-sym)]
          [#:color color plot-color/c (point-color)]
          [#:fill-color fill-color (or/c plot-color/c 'auto) 'auto]
          [#:size size (>=/c 0) (point-size)]
          [#:line-width line-width (>=/c 0) (point-line-width)]
          [#:alpha alpha (real-in 0 1) (point-alpha)]
          [#:label label (or/c string? #f) #f]
          ) renderer3d?
  (let ([vs  (filter vrational? vs)])
    (cond [(empty? vs)  (renderer3d #f #f #f #f)]
          [else
           (match-define (list (vector xs ys zs) ...) vs)
           (let ([x-min  (if x-min x-min (apply min* xs))]
                 [x-max  (if x-max x-max (apply max* xs))]
                 [y-min  (if y-min y-min (apply min* ys))]
                 [y-max  (if y-max y-max (apply max* ys))]
                 [z-min  (if z-min z-min (apply min* zs))]
                 [z-max  (if z-max z-max (apply max* zs))])
             (renderer3d
              (vector (ivl x-min x-max) (ivl y-min y-max) (ivl z-min z-max)) #f default-ticks-fun
              (points3d-render-proc vs sym color (cond [(eq? fill-color 'auto)  (->pen-color color)]
                                                       [else  fill-color])
                                    size line-width alpha label)))])))

;; ===================================================================================================

(define ((vector-field3d-render-fun f samples scale color line-width line-style alpha label) area)
  (match-define (vector (ivl x-min x-max) (ivl y-min y-max) (ivl z-min z-max))
    (send area get-bounds-rect))
  
  (define xs0 (linear-seq x-min x-max samples #:start? #t #:end? #t))
  (define ys0 (linear-seq y-min y-max samples #:start? #t #:end? #t))
  (define zs0 (linear-seq z-min z-max samples #:start? #t #:end? #t))
  
  (define-values (vs dxs dys dzs norms mags)
    (for*/lists (vs dxs dys dzs norms mags) ([x   (in-list xs0)]
                                                   [y   (in-list ys0)]
                                                   [z   (in-list zs0)]
                                                   [dv  (in-value (f x y z))] #:when (vrational? dv))
      (match-define (vector dx dy dz) dv)
      (values (vector x y z) dx dy dz (vnormalize dv) (vmag dv))))
  
  (cond [(empty? vs)  empty]
        [else (define box-x-size (/ (- x-max x-min) samples))
              (define box-y-size (/ (- y-max y-min) samples))
              (define box-z-size (/ (- z-max z-min) samples))
              
              (define new-mags
                (match scale
                  [(? real?)  (map (λ (mag) (* scale mag)) mags)]
                  ['normalized  (define box-size (min box-x-size box-y-size box-z-size))
                                (build-list (length dxs) (λ _ box-size))]
                  ['auto  (define dx-max (apply max (map abs dxs)))
                          (define dy-max (apply max (map abs dys)))
                          (define dz-max (apply max (map abs dzs)))
                          (define scale (min (/ box-x-size dx-max)
                                             (/ box-y-size dy-max)
                                             (/ box-z-size dz-max)))
                          (map (λ (mag) (* scale mag)) mags)]))
              
              (send area put-alpha alpha)
              (send area put-pen color line-width line-style)
              (for ([v     (in-list vs)]
                    [norm  (in-list norms)]
                    [mag   (in-list new-mags)])
                (send area put-arrow v (v+ v (v* norm mag))))
              
              (cond [label  (arrow-legend-entry label color line-width line-style)]
                    [else   empty])]))

(defproc (vector-field3d
          [f (or/c (real? real? real? . -> . (vector/c real? real? real?))
                   ((vector/c real? real? real?)
                    . -> . (vector/c real? real? real?)))]
          [x-min (or/c rational? #f) #f] [x-max (or/c rational? #f) #f]
          [y-min (or/c rational? #f) #f] [y-max (or/c rational? #f) #f]
          [z-min (or/c rational? #f) #f] [z-max (or/c rational? #f) #f]
          [#:samples samples exact-positive-integer? ( vector-field3d-samples)]
          [#:scale scale (or/c real? (one-of/c 'auto 'normalized)) (vector-field-scale)]
          [#:color color plot-color/c (vector-field-color)]
          [#:line-width line-width (>=/c 0) (vector-field-line-width)]
          [#:line-style line-style plot-pen-style/c (vector-field-line-style)]
          [#:alpha alpha (real-in 0 1) (vector-field-alpha)]
          [#:label label (or/c string? #f) #f]
          ) renderer3d?
  (let ([f  (cond [(procedure-arity-includes? f 3 #t)  f]
                  [else  (λ (x y z) (f (vector x y z)))])])
    (renderer3d (vector (ivl x-min x-max) (ivl y-min y-max) (ivl z-min z-max)) #f default-ticks-fun
                (vector-field3d-render-fun f samples scale color line-width line-style alpha label))))
