;;; RAY -- Ray-trace a simple scene with spheres, generating a ".pgm" file.
;;; Translated to Scheme from Paul Graham's book ANSI Common Lisp, Example 9.8

(import (scheme base)
        (scheme inexact)
        (scheme file)
        (scheme read)
        (scheme write)
        (scheme time))

(define (make-point x y z)
  (vector x y z))

(define (point-x p) (vector-ref p 0))
(define (point-y p) (vector-ref p 1))
(define (point-z p) (vector-ref p 2))

(define (sq x) (* x x))

(define (mag x y z)
  (sqrt (+ (sq x) (sq y) (sq z))))

(define (unit-vector x y z)
  (let ((d (mag x y z)))
    (make-point (/ x d) (/ y d) (/ z d))))

(define (distance p1 p2)
  (mag (- (point-x p1) (point-x p2))
       (- (point-y p1) (point-y p2))
       (- (point-z p1) (point-z p2))))

(define (minroot a b c)
  (if (zero? a)
      (/ (- c) b)
      (let ((disc (- (sq b) (* 4.0 a c))))
        (if (negative? disc)
            #f
            (let ((discrt (sqrt disc))
                  (minus-b (- b))
                  (two-a (* 2.0 a)))
              (min (/ (+ minus-b discrt) two-a)
                   (/ (- minus-b discrt) two-a)))))))

(define *world* '())

(define eye (make-point 0.0 0.0 200.0))

(define (tracer pathname res)
  (if (file-exists? pathname)
      (delete-file pathname))
  (call-with-output-file
   pathname
   (lambda (p)
     (let ((extent (* res 100)))
       (display "P2 " p)
       (write extent p)
       (display " " p)
       (write extent p)
       (display " 255" p)
       (newline p)
       (do ((y 0 (+ y 1)))
           ((= y extent))
         (do ((x 0 (+ x 1)))
             ((= x extent))
           (write (color-at
                   (+ -50.0
                      (/ (inexact x) (inexact res)))
                   (+ -50.0
                      (/ (inexact y) (inexact res))))
                  p)
           (newline p)))))))

(define (color-at x y)
  (let ((ray (unit-vector (- x (point-x eye))
                          (- y (point-y eye))
                          (- (point-z eye)))))
    (exact (round (* (sendray eye ray) 255.0)))))



(define (sendray pt ray)
  (let* ((x (first-hit pt ray))
         (s (vector-ref x 0))
         (int (vector-ref x 1)))
    (if s
        (* (lambert s int ray)
           (surface-color s))
        0.0)))

(define (first-hit pt ray)
  (let loop ((lst *world*) (surface #f) (hit #f) (dist 1e308))
    (if (null? lst)
        (vector surface hit)
        (let ((s (car lst)))
          (let ((h (intersect s pt ray)))
            (if h
                (let ((d (distance h pt)))
                  (if (< d dist)
                      (loop (cdr lst) s h d)
                      (loop (cdr lst) surface hit dist)))
                (loop (cdr lst) surface hit dist)))))))

(define (lambert s int ray)
  (let ((n (normal s int)))
    (max 0.0
         (+ (* (point-x ray) (point-x n))
            (* (point-y ray) (point-y n))
            (* (point-z ray) (point-z n))))))

(define (make-sphere color radius center)
  (vector color radius center))

(define (sphere-color s) (vector-ref s 0))
(define (sphere-radius s) (vector-ref s 1))
(define (sphere-center s) (vector-ref s 2))

(define (defsphere x y z r c)
  (let ((s (make-sphere c r (make-point x y z))))
    (set! *world* (cons s *world*))
    s))

(define (surface-color s)
  (sphere-color s))

(define (intersect s pt ray)
  (sphere-intersect s pt ray))

(define (sphere-intersect s pt ray)
  (let* ((xr (point-x ray))
         (yr (point-y ray))
         (zr (point-z ray))
         (c (sphere-center s))
         (n (minroot
             (+ (sq xr) (sq yr) (sq zr))
             (* 2.0
                (+ (* (- (point-x pt) (point-x c)) xr)
                   (* (- (point-y pt) (point-y c)) yr)
                   (* (- (point-z pt) (point-z c)) zr)))
             (+ (sq (- (point-x pt) (point-x c)))
                (sq (- (point-y pt) (point-y c)))
                (sq (- (point-z pt) (point-z c)))
                (- (sq (sphere-radius s)))))))
    (if n
        (make-point (+ (point-x pt) (* n xr))
                    (+ (point-y pt) (* n yr))
                    (+ (point-z pt) (* n zr)))
        #f)))

(define (normal s pt)
  (sphere-normal s pt))

(define (sphere-normal s pt)
  (let ((c (sphere-center s)))
    (unit-vector (- (point-x c) (point-x pt))
                 (- (point-y c) (point-y pt))
                 (- (point-z c) (point-z pt)))))

(define (ray-test res output-file)
  (set! *world* '())
  (defsphere 0.0 -300.0 -1200.0 200.0 0.8)
  (defsphere -80.0 -150.0 -1200.0 200.0 0.7)
  (defsphere 70.0 -100.0 -1200.0 200.0 0.9)
  (do ((x -2 (+ x 1)))
      ((> x 2))
    (do ((z 2 (+ z 1)))
        ((> z 7))
      (defsphere
        (* (inexact x) 200.0)
        300.0
        (* (inexact z) -400.0)
        40.0
        0.75)))
  (tracer output-file res))

(define (run input output)
  (ray-test input output)
  'ok)

(define (hide count input)
  input)

(define (run-r7rs-benchmark name count thunk verify?)
  (do ((i 0 (+ i 1))
       (res #f (thunk)))
      ((= i count)
       (if (not (verify? res))
           (error "bad output" res)))))

(define (main)
  (let* ((count (read))
         (input1 (read))
         (input2 (read))
         (output (read))
         (s2 (number->string count))
         (s1 (number->string input1))
         (name "ray"))
    (run-r7rs-benchmark
     (string-append name ":" s2)
     count
     (lambda () (run (hide count input1) (hide count input2)))
     (lambda (result) (equal? result output)))))
