(library (mandelbrot)
  (export make-ppm mandelbrot!
          steps
          make-rgb rgb-r rgb-g rgb-b
          number->rgb rgb-bin rgb-map rgb-lerp)
  (import (chezscheme))

(define-record-type rgb (fields r g b))
(define (number->rgb n)
  (assert (and (integer? n) (<= 0 n)))
  (make-rgb (bitwise-and #xff (bitwise-arithmetic-shift-right n 16))
            (bitwise-and #xff (bitwise-arithmetic-shift-right n 8))
            (bitwise-and #xff (bitwise-arithmetic-shift-right n 0))))
(define (rgb-bin +)
  (case-lambda
    [(a) a]
    [(a b) (make-rgb (+ (rgb-r a) (rgb-r b))
                     (+ (rgb-g a) (rgb-g b))
                     (+ (rgb-b a) (rgb-b b)))]
    [(a . bs) (fold-left (rgb-bin +) a bs)]))

(define (rgb-map c f)
  (make-rgb (f (rgb-r c))
            (f (rgb-g c))
            (f (rgb-b c))))

(define (rgb-mean . rgbs)
  (if (null? rgbs)
      (make-rgb 0 0 0)
      (let* ([sum (apply (rgb-bin +) rgbs)]
             [len (length rgbs)]
             [scale (lambda (c) (inexact->exact (floor (/ c len))))])
        (rgb-map sum scale))))

(define (rgb-lerp i min max)
  ((rgb-bin +) min (rgb-map ((rgb-bin -) max min) (lambda (x) (* i x)))))

;; proc : (x y) -> rgb
(define (make-ppm path width height proc)
  (call-with-port (open-file-output-port path (file-options no-fail))
    (lambda (p)
      (define header (format "P6\n~a ~a\n255\n" width height))
      (put-bytevector p (string->utf8 header))
      (do ([x 0 (mod (+ 1 x) width)]
           [y 0 (if (= (+ 1 x) width) (+ 1 y) y)])
          [(= y height)]
        (let ([color (proc x y)])
          (put-u8 p (rgb-r color))
          (put-u8 p (rgb-g color))
          (put-u8 p (rgb-b color))))))
  'done)


(define (lerp i min max)
  (+ min (* i (- max min))))

(define steps 100)
(define power 2)
(define samples-per-pixel 4)

(define tau (* 4 (acos 0)))

(define (rotate-about origin p angle)
  (+ (* (- p origin)
      (make-rectangular (cos angle) (sin angle)))
     origin))

;; let's rotate the sample?
(define (mandelbrot! path width height color xmin xmax ymin ymax)
  (define pixel-width  (exact->inexact (/ (- xmax xmin) width)))
  (define pixel-height (exact->inexact (/ (- ymax ymin) height)))
  (define sample-rotation (/ tau samples-per-pixel))
  (define (sample-vals center)
    (let ([top (+ center (make-rectangular 0 (- (/ pixel-height 2))))])
      (map (lambda (i) (rotate-about center top (* i sample-rotation)))
           (iota samples-per-pixel))))
  (make-ppm path width height
    (lambda (x y)
      (define ix (lerp (/ x width)  xmin xmax))
      (define iy (lerp (/ y height) ymin ymax))
      (define center (exact->inexact (make-rectangular ix iy)))
      (define (sample p)
        (define (step z) (+ (expt z power) p))
        (let loop ([i 0]
                   [z (step 0)])
          (cond
           [(= i steps) (color -1)]
           [(> (magnitude z) 2) (color i)]
           [else (loop (+ 1 i) (step z))])))
      (define samples (map sample (sample-vals center)))
      (apply rgb-mean samples)))))

(top-level-program
 (import (chezscheme)
         (mandelbrot))

(define (zoom path-fmt x y width steps)
  (for-each
   (lambda (i)
     (let ([idx (+ 1 i)]
           [i (+ 1 i)])
       (mandelbrot!
        (format path-fmt idx)
        width width
        (/ (- x 0.5) i) (/ (+ x 0.5) i)
        (/ (- y 0.5) i) (/ (+ y 0.5) i))))
   (iota steps)))

(define (cli-fail)
  (format (standard-error-port 'line (current-transcoder))
          "usage: ./mandelbrot.ss PATH WIDTH HEIGHT [COLOR:hex+|--lerp INTERIOR:hex START:hex END:hex]\n")
  (exit 1))

(define args (command-line-arguments))

(unless (<= 3 (length args))
  (cli-fail))

(define path   (list-ref args 0))
(define width  (list-ref args 1))
(define height (list-ref args 2))
(set! width (string->number width))
(set! height (string->number height))

(unless (and (integer? width) (integer? height) (positive? width) (positive? height))
  (cli-fail))

(define (color i)
  (define c
    (if (negative? i)
        0
        (floor (* 256 (/ i steps)))))
  (make-rgb c c c))

(when (< 3 (length args))
  (let* ([colors (cdddr args)]
         [lerp? (equal? "--lerp" (car colors))]
         [colors (if lerp? (cdr colors) colors)]
         [nums (map (lambda (n) (string->number n 16)) colors)]
         [_ (unless (for-all (lambda (x) (and (integer? x) (<= 0 x))) nums)
              (cli-fail))]
         [stripes (list->vector (map number->rgb nums))]
         [stripe-count (vector-length stripes)])
    (when (and lerp? (< stripe-count 3))
      (cli-fail))

    (set! color
      (if lerp?
          (lambda (i)
            (if (negative? i)
                (vector-ref stripes 0)
                (rgb-map (rgb-lerp (expt (/ i steps) 1.3)
                                   (vector-ref stripes 1)
                                   (vector-ref stripes 2))
                         floor)))
          (lambda (i) (vector-ref stripes (mod i stripe-count)))))))

(mandelbrot! path width height color -2.2 1.1 -1 1))