(defpackage :com.nprescott.raytracer
  (:use :common-lisp))

(defmacro defun-vector-binop (binop)
  `(defun ,(intern (concatenate 'string "VECTOR" (string binop))) (v1 v2)
     (vector (,binop (aref v1 0) (aref v2 0))
             (,binop (aref v1 1) (aref v2 1))
             (,binop (aref v1 2) (aref v2 2))
             (,binop (aref v1 3) (aref v2 3)))))

(defun-vector-binop +)
(defun-vector-binop -)
(defun-vector-binop *)
(defun-vector-binop /)

(defmacro vector-getters (aliases)
  `(progn
     ,@(loop for name in aliases
          for index = 0 then (1+ index)
          collect
            `(defmacro ,name (some-vector)
               `(aref ,some-vector ,,index)))))

(defmacro point (x y z)
  `(vector ,x ,y ,z 1))

(defmacro vec3 (x y z)
  `(vector ,x ,y ,z 0))

(vector-getters (RED GREEN BLUE))
(vector-getters (X Y Z W))

(defun negate-vector (v)
  (VECTOR- (vec3 0 0 0) v))

(defun scale-vector (v scalar)
  (VECTOR* v (vec3 scalar scalar scalar)))

(defun magnitude (some-vector)
  (labels ((square (n) (expt n 2)))
    (sqrt (+ (square (X some-vector))
             (square (Y some-vector))
             (square (Z some-vector))))))

(defun normalize (some-vector)
  (scale-vector some-vector (/ 1 (magnitude some-vector))))

(defun dot-product (v1 v2)
  (reduce #'+ (VECTOR* v1 v2)))

(defun cross-product (v1 v2)
  (vec3 (- (* (Y v1) (Z v2)) (* (Z v1) (Y v2)))
        (- (* (Z v1) (X v2)) (* (X v1) (Z v2)))
        (- (* (X v1) (Y v2)) (* (Y v1) (X v2)))))

(defun blend (c1 c2)
  (VECTOR* c1 c2))

(defstruct (canvas (:constructor make-canvas (&key width height)))
  width
  height
  (body (make-array (list width height) :initial-element (vector 0 0 0))))

(defun write-pixel (canvas x y color)
  (flet ((within-bounds (x y max-x max-y)
           (and (< x max-x) (< y max-y) (>= x 0) (>= y 0))))
    (when (within-bounds x y (canvas-width canvas) (canvas-height canvas))
      (setf (aref (canvas-body canvas) (floor x) (floor y)) color))))

(defun canvas-pixel-strings (c)
  (labels ((color->rgb (n) (floor (* 255 n)))
           (pixel->string (v) (format nil "~d ~d ~d"
                                      (color->rgb (Red v))
                                      (color->rgb (Green v))
                                      (color->rgb (Blue v)))))
    (let ((strings (list)))
      (dotimes (i (canvas-height c))
        (dotimes (j (canvas-width c))
          (push (pixel->string (aref (canvas-body c) j i)) strings)))
      ; FIXME: figure out how to pass 70 as an argument to format
      (format nil "~{~<~%~1,70:;~a~>~^ ~}" strings))))

(defun canvas->ppm (c)
  (format nil "P3~%~s ~s~%255~%~a~%"
          (canvas-width c) (canvas-height c) (canvas-pixel-strings c)))

(defun ppm->file (ppm-string path)
  (with-open-file (stream path :direction :output :if-exists :supersede)
    (format stream ppm-string)))

(defun matrix*vector (matrix vec)
  (let* ((m (array-dimension matrix 0))
         (n (length vec))
         (result (make-array m :initial-element 0)))
    (dotimes (i m result)
      (dotimes (j n)
        (incf (aref result i)
              (* (aref matrix i j)
                 (aref vec j)))))))

(defun matrix*matrix (a b)
  (let* ((m (array-dimension a 0))
         (n (array-dimension b 1))
         (common (array-dimension b 0))
         (result (make-array (list m n) :initial-element 0)))
    (dotimes (i m result)
      (dotimes (j n)
        (dotimes (k common)
          (incf (aref result i j)
                (* (aref a i k)
                   (aref b k j))))))))

(defun submatrix (m row column)
  (let ((result (make-array (mapcar #'1- (array-dimensions m)))))
    (loop for i below (array-dimension m 0)
       unless (= i row)
       do (loop for j below (array-dimension m 1)
             unless (= j column)
             do (let ((ii i)
                      (jj j))
                  (if (> i row) (setf ii (1- i)))
                  (if (> j column) (setf jj (1- j)))
                  (setf (aref result ii jj)
                        (aref m i j))))
       finally (return result))))

(defun cofactor (m row column)
  (if (oddp (+ row column))
      (- (minor m row column))
      (minor m row column)))

(defun determinant (m)
  (if (equalp (array-dimensions m) '(2 2))
      (- (* (aref m 0 0) (aref m 1 1))
         (* (aref m 0 1) (aref m 1 0)))
      (let ((result 0))
        (dotimes (i (array-dimension m 0) result)
          (incf result (* (cofactor m 0 i) (aref m 0 i)))))))

(defun minor (m row column)
  (determinant (submatrix m row column)))

(defun invertible? (m)
  (not (eq (determinant m) 0)))

(defun inverse (m)
  (when (invertible? m)
    (let ((d (determinant m))
          (result (make-array (array-dimensions m))))
      (dotimes (row (array-dimension m 0) result)
        (dotimes (column (array-dimension m 1))
          (setf (aref result column row)
                (/ (cofactor m row column) d)))))))

;;; since we're never taking the identity but for a 4x4 matrix...
(defvar identity-matrix #2A((1 0 0 0)
                            (0 1 0 0)
                            (0 0 1 0)
                            (0 0 0 1)))

(defun transpose (m)
  ;; isn't this wrong for non-square matrices?
  (let ((result (make-array (array-dimensions m))))
    (dotimes (i (array-dimension m 0) result)
      (dotimes (j (array-dimension m 1))
        (setf (aref result i j)
              (aref m j i))))))

(defun translation (x y z)
  (make-array '(4 4) :initial-contents `((1 0 0 ,x)
                                         (0 1 0 ,y)
                                         (0 0 1 ,z)
                                         (0 0 0  1))))

(defun scaling (x y z)
  (make-array '(4 4) :initial-contents `((,x  0  0 0)
                                         ( 0 ,y  0 0)
                                         ( 0  0 ,z 0)
                                         ( 0  0  0 1))))

(defun rotation-x (r)
  (make-array '(4 4) :initial-contents `((1     0         0      0)
                                         (0 ,(cos r) ,(-(sin r)) 0)
                                         (0 ,(sin r)   ,(cos r)  0)
                                         (0     0         0      1))))

(defun rotation-y (r)
  (make-array '(4 4) :initial-contents `((,(cos r)    0 ,(sin r) 0)
                                         (    0       1     0    0)
                                         (,(-(sin r)) 0 ,(cos r) 0)
                                         (    0       0     0    1))))

(defun rotation-z (r)
  (make-array '(4 4) :initial-contents `((,(cos r) ,(- (sin r)) 0 0)
                                         (,(sin r) ,(cos r)     0 0)
                                         (    0        0        1 0)
                                         (    0        0        0 1))))

(defun shearing (xy xx yx yz zx zy)
  (make-array '(4 4) :initial-contents `((  1 ,xy ,xx 0)
                                         (,yx   1 ,yz 0)
                                         (,zx ,zy   1 0)
                                         (  0   0   0 1))))

(defstruct material
  (color (vec3 1 1 1))
  (ambient 0.1)
  (diffuse 0.9)
  (specular 0.9)
  (shininess 200))
(defstruct light intensity position)
(defstruct ray origin direction)
(defstruct sphere
  (transformation identity-matrix)
  (material (make-material)))
(defstruct crosspoint time object)

(defun ray-position (r time)
  (VECTOR+ (ray-origin r) (scale-vector (ray-direction r) time)))

(defun intersect (s r)
  (let* ((inv-ray (transform r (inverse (sphere-transformation s))))
         (sphere-to-ray (VECTOR- (ray-origin inv-ray) (point 0 0 0)))
         (a (dot-product (ray-direction inv-ray) (ray-direction inv-ray)))
         (b (* 2 (dot-product (ray-direction inv-ray) sphere-to-ray)))
         (c (- (dot-product sphere-to-ray sphere-to-ray) 1))
         (discriminant (- (expt b 2) (* 4 a c))))
    (if (< discriminant 0)
        (list)
        (list (make-crosspoint :time (/ (- (- b) (sqrt discriminant)) (* 2 a))
                               :object s)
              (make-crosspoint :time (/ (+ (- b) (sqrt discriminant)) (* 2 a))
                               :object s)))))

(defun hit (intersections)
  (flet ((positivep (x) (> (crosspoint-time x) 0)))
    (if (some #'positivep intersections)
        (first (sort (remove-if-not #'positivep (copy-seq intersections))
                     #'< :key #'crosspoint-time))
        nil)))

(defun transform (ray matrix)
  (let ((o (matrix*vector matrix (ray-origin ray)))
        (d (matrix*vector matrix (ray-direction ray))))
    (make-ray :origin o :direction d)))

;;; should probably just drop this entirely...
(defun set-transform (s transform)
  (setf (sphere-transformation s) transform))

(defun normal-at (sphere world-point)
  (let* ((object-point (matrix*vector (inverse (sphere-transformation sphere)) world-point))
         (object-normal (VECTOR- object-point (point 0 0 0)))
         (world-normal (matrix*vector (transpose (inverse (sphere-transformation sphere)))
                                      object-normal)))
    (setf (aref world-normal 3) 0)      ; FIXME submatrix 3,3 instead
    (normalize world-normal)))

(defun reflect (in normal)
  (VECTOR- in (scale-vector normal (* 2 (dot-product in normal)))))

(defun lighting (material light position eye-vector normal-vector)
  (let* ((effective-color (blend (material-color material) (light-intensity light)))
         (lightv (normalize (VECTOR- (light-position light) position)))
         (ambient (scale-vector effective-color (material-ambient material)))
         (light-dot-normal (dot-product lightv normal-vector))
         (black (vec3 0 0 0))
         (diffuse black)
         (specular black)
         (reflect-dot-eye (dot-product (reflect (negate-vector lightv) normal-vector)
                                       eye-vector)))
    (if (> light-dot-normal 0)
        (setf diffuse (scale-vector
                       (scale-vector effective-color (material-diffuse material))
                       light-dot-normal)))
    (if (> reflect-dot-eye 0)
        (setf specular (scale-vector (light-intensity light)
                                     (* (material-specular material)
                                        (expt reflect-dot-eye
                                              (material-shininess material))))))
    (reduce #'VECTOR+ (list ambient diffuse specular))))
