#lang racket/base

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

(provide (all-defined-out))

;; ===================================================================================================
;; Surfaces of constant value (isosurfaces)

(define ((isosurface3d-render-proc
          f d samples color style line-color line-width line-style alpha label)
         area)
  (match-define (vector x-ivl y-ivl z-ivl) (send area get-bounds-rect))
  (match-define (ivl x-min x-max) x-ivl)
  (match-define (ivl y-min y-max) y-ivl)
  (match-define (ivl z-min z-max) z-ivl)
  (define num (animated-samples samples))
  (define sample (f (vector x-ivl y-ivl z-ivl) (vector num num num)))
  (match-define (3d-sample xs ys zs dsss d-min d-max) sample)
  
  (send area put-alpha alpha)
  (send area put-brush color style)
  (send area put-pen line-color line-width line-style)
  (let* ([flonum-ok?  (flonum-ok-for-4d? x-min x-max y-min y-max z-min z-max d-min d-max)]
         [sample      (if flonum-ok? (3d-sample-exact->inexact sample) sample)]
         [d           (if flonum-ok? (exact->inexact d) d)])
    (for-3d-sample
     (xa xb ya yb za zb d1 d2 d3 d4 d5 d6 d7 d8) sample
     (define polys (heights->cube-polys xa xb ya yb za zb d d1 d2 d3 d4 d5 d6 d7 d8))
     (when (not (empty? polys))
       (send area put-polygons polys
             (vector (* 1/2 (+ xa xb)) (* 1/2 (+ ya yb)) (* 1/2 (+ za zb)))))))
  
  (cond [label  (rectangle-legend-entry
                 label color style line-color line-width line-style)]
        [else  empty]))

(defproc (isosurface3d [f (real? real? real? . -> . real?)] [d rational?]
                       [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 (and/c exact-integer? (>=/c 2)) (plot3d-samples)]
                       [#:color color plot-color/c (surface-color)]
                       [#:style style plot-brush-style/c (surface-style)]
                       [#:line-color line-color plot-color/c (surface-line-color)]
                       [#:line-width line-width (>=/c 0) (surface-line-width)]
                       [#:line-style line-style plot-pen-style/c (surface-line-style)]
                       [#:alpha alpha (real-in 0 1) (surface-alpha)]
                       [#:label label (or/c string? #f) #f]
                       ) renderer3d?
  (define x-ivl (ivl x-min x-max))
  (define y-ivl (ivl y-min y-max))
  (define z-ivl (ivl z-min z-max))
  (define g (3d-function->sampler f (vector x-ivl y-ivl z-ivl)))
  (renderer3d (vector x-ivl y-ivl z-ivl) #f default-ticks-fun
              (isosurface3d-render-proc
               g d samples color style line-color line-width line-style alpha label)))

;; ===================================================================================================
;; Nested isosurfaces

(define ((isosurfaces3d-render-proc f rd-min rd-max levels samples colors styles
                                    line-colors line-widths line-styles alphas label)
         area)
  (match-define (vector x-ivl y-ivl z-ivl) (send area get-bounds-rect))
  (match-define (ivl x-min x-max) x-ivl)
  (match-define (ivl y-min y-max) y-ivl)
  (match-define (ivl z-min z-max) z-ivl)
  (define num (animated-samples samples))
  (define sample (f (vector x-ivl y-ivl z-ivl) (vector num num num)))
  (match-define (3d-sample xs ys zs dsss fd-min fd-max) sample)
  
  (define d-min (if rd-min rd-min fd-min))
  (define d-max (if rd-max rd-max fd-max))
  
  (match-define (list (tick ds _ labels) ...)
    (cond [(and d-min d-max)  (contour-ticks (plot-d-ticks) d-min d-max levels #f)]
          [else  empty]))
  
  ;; need to check this or in-cycle below does an infinite loop (I think it's an in-cycle bug)
  (unless (empty? ds)
    (let* ([colors  (maybe-apply colors ds)]
           [styles  (maybe-apply styles ds)]
           [alphas  (maybe-apply alphas ds)]
           [line-colors  (maybe-apply line-colors ds)]
           [line-widths  (maybe-apply line-widths ds)]
           [line-styles  (maybe-apply line-styles ds)]
           [flonum-ok?  (flonum-ok-for-4d? x-min x-max y-min y-max z-min z-max d-min d-max)]
           [sample      (if flonum-ok? (3d-sample-exact->inexact sample) sample)]
           [ds          (if flonum-ok? (map exact->inexact ds) ds)])
      (for ([d      (in-list ds)]
            [color  (in-cycle colors)]
            [style  (in-cycle styles)]
            [alpha  (in-cycle alphas)]
            [line-color  (in-cycle line-colors)]
            [line-width  (in-cycle line-widths)]
            [line-style  (in-cycle line-styles)])
        (send area put-alpha alpha)
        (send area put-brush color style)
        (send area put-pen line-color line-width line-style)
        (for-3d-sample
         (xa xb ya yb za zb d1 d2 d3 d4 d5 d6 d7 d8) sample
         (define polys (heights->cube-polys xa xb ya yb za zb d d1 d2 d3 d4 d5 d6 d7 d8))
         (when (not (empty? polys))
           (send area put-polygons polys
                 (vector (* 1/2 (+ xa xb)) (* 1/2 (+ ya yb)) (* 1/2 (+ za zb)))))))))
  
  (cond
    [(and label (not (empty? ds)))  (rectangle-legend-entries
                                     label ds colors styles line-colors line-widths line-styles)]
    [else  empty]))

(defproc (isosurfaces3d
          [f (real? 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]
          [#:d-min d-min (or/c rational? #f) #f] [#:d-max d-max (or/c rational? #f) #f]
          [#:samples samples (and/c exact-integer? (>=/c 2)) (plot3d-samples)]
          [#:levels levels (or/c 'auto exact-positive-integer? (listof real?)) (isosurface-levels)]
          [#:colors colors (plot-colors/c (listof real?)) (isosurface-colors)]
          [#:styles styles (plot-brush-styles/c (listof real?)) (isosurface-styles)]
          [#:line-colors line-colors (plot-colors/c (listof real?)) (isosurface-line-colors)]
          [#:line-widths line-widths (pen-widths/c (listof real?)) (isosurface-line-widths)]
          [#:line-styles line-styles (plot-pen-styles/c (listof real?)) (isosurface-line-styles)]
          [#:alphas alphas (alphas/c (listof real?)) (isosurface-alphas)]
          [#:label label (or/c string? #f) #f]
          ) renderer3d?
  (define x-ivl (ivl x-min x-max))
  (define y-ivl (ivl y-min y-max))
  (define z-ivl (ivl z-min z-max))
  (define g (3d-function->sampler f (vector x-ivl y-ivl z-ivl)))
  (renderer3d (vector x-ivl y-ivl z-ivl) #f default-ticks-fun
              (isosurfaces3d-render-proc g d-min d-max levels samples colors styles
                                         line-colors line-widths line-styles alphas
                                         label)))

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

(define ((polar3d-render-proc f g samples color style line-color line-width line-style alpha label)
         area)
  (match-define (vector x-ivl y-ivl z-ivl) (send area get-bounds-rect))
  (match-define (ivl x-min x-max) x-ivl)
  (match-define (ivl y-min y-max) y-ivl)
  (match-define (ivl z-min z-max) z-ivl)
  (define num (animated-samples samples))
  (define sample (g (vector x-ivl y-ivl z-ivl) (vector num num num)))
  (match-define (3d-sample xs ys zs dsss d-min d-max) sample)
  
  (define (draw-cube xa xb ya yb za zb d d1 d2 d3 d4 d5 d6 d7 d8)
    (define polys (heights->cube-polys xa xb ya yb za zb d d1 d2 d3 d4 d5 d6 d7 d8))
    (when (not (empty? polys))
      (send area put-polygons polys
            (vector (* 1/2 (+ xa xb)) (* 1/2 (+ ya yb)) (* 1/2 (+ za zb))))))
  
  (send area put-alpha alpha)
  (send area put-brush color style)
  (send area put-pen line-color line-width line-style)
  (let* ([flonum-ok?  (flonum-ok-for-4d? x-min x-max y-min y-max z-min z-max d-min d-max)]
         [sample      (if flonum-ok? (3d-sample-exact->inexact sample) sample)]
         [d           (if flonum-ok? 0.0 0)])
    (for-3d-sample
     (xa xb ya yb za zb d1 d2 d3 d4 d5 d6 d7 d8) sample
     (cond [(and (xb . > . 0) (ya . < . 0) (yb . > . 0))
            (let* ([yb  -0.00001]
                   [d3  (f xb yb za)]
                   [d4  (f xa yb za)]
                   [d7  (f xb yb zb)]
                   [d8  (f xa yb zb)])
              (draw-cube xa xb ya yb za zb d d1 d2 d3 d4 d5 d6 d7 d8))
            (let* ([ya  0.00001]
                   [d1  (f xa ya za)]
                   [d2  (f xb ya za)]
                   [d5  (f xa ya zb)]
                   [d6  (f xb ya zb)])
              (draw-cube xa xb ya yb za zb d d1 d2 d3 d4 d5 d6 d7 d8))]
           [else
            (draw-cube xa xb ya yb za zb d d1 d2 d3 d4 d5 d6 d7 d8)])))
  
  (cond [label  (rectangle-legend-entry
                 label color style line-color line-width line-style)]
        [else  empty]))

(define 2pi (* 2 pi))

(define (flmodulo x y)
  (fl- x (fl* y (flfloor (fl/ x y)))))

(define ((2d-polar->3d-function f) x y z)
  (let ([x  (exact->inexact x)]
        [y  (exact->inexact y)]
        [z  (exact->inexact z)])
    (define-values (θ ρ)
      (cond [(and (fl= x 0.0) (fl= y 0.0))  (values 0.0 0.0)]
            [else  (values (flmodulo (atan y x) 2pi)
                           (flatan (fl/ z (fldistance x y))))]))
    (fl- (exact->inexact (f θ ρ)) (fldistance x y z))))

(defproc (polar3d
          [f (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]
          [#:samples samples (and/c exact-integer? (>=/c 2)) (plot3d-samples)]
          [#:color color plot-color/c (surface-color)]
          [#:style style plot-brush-style/c (surface-style)]
          [#:line-color line-color plot-color/c (surface-line-color)]
          [#:line-width line-width (>=/c 0) (surface-line-width)]
          [#:line-style line-style plot-pen-style/c (surface-line-style)]
          [#:alpha alpha (real-in 0 1) (surface-alpha)]
          [#:label label (or/c string? #f) #f]
          ) renderer3d?
  (define vs (for*/list ([θ  (in-list (linear-seq 0.0 2pi (* 4 samples)))]
                         [ρ  (in-list (linear-seq (* -1/2 pi) (* 1/2 pi) (* 2 samples)))])
               (3d-polar->3d-cartesian θ ρ (f θ ρ))))
  (define rvs (filter vrational? vs))
  (cond [(empty? rvs)  (renderer3d #f #f #f #f)]
        [else
         (match-define (list (vector rxs rys rzs) ...) rvs)
         (let ([x-min  (if x-min x-min (apply min* rxs))]
               [x-max  (if x-max x-max (apply max* rxs))]
               [y-min  (if y-min y-min (apply min* rys))]
               [y-max  (if y-max y-max (apply max* rys))]
               [z-min  (if z-min z-min (apply min* rzs))]
               [z-max  (if z-max z-max (apply max* rzs))])
           (define x-ivl (ivl x-min x-max))
           (define y-ivl (ivl y-min y-max))
           (define z-ivl (ivl z-min z-max))
           (define new-f (2d-polar->3d-function f))
           (define g (3d-function->sampler new-f (vector x-ivl y-ivl z-ivl)))
           (renderer3d (vector x-ivl y-ivl z-ivl) #f
                       default-ticks-fun
                       (polar3d-render-proc new-f g samples color style
                                            line-color line-width line-style alpha label)))]))
