;;;; canvas-language.lisp  -- a drawing api for canvas instances

(in-package #:wheelwork)

(defvar *current-canvas* nil)
(defvar *current-pen-color* '(0 0 0 255))
(defvar *current-pen-width* 1)
(defvar *current-pen-position* '(0 0))
(defvar *saved-state* nil)

(defmacro with-current-pen-state ( &body body)
  "Saves the current pen state (color width position) so that it can
   be restored from using (restore-pen-state) from within the BODY.
   After BODY executes the state is restored to whatever it was
   before WITH-CURRENT-PEN-STATE was evaluated."
  `(let 
       ((*saved-state*
          (list *current-pen-width*
                *current-pen-position*
                *current-pen-color*)))
     ,@body))

(defmacro with-pen ((&key position (color nil color-supplied-p) width) &body body)
  "Like WITH-CURRENT-PEN-STATE, but lets you set the state of the pen
  EXECUTION BODY.  After BODY executes, the state is restored to
  whatever it was before WITH-PEN-STATE was evaluated."
  `(let ((*current-pen-position* ,(if position nil '*current-pen-position*))
         (*current-pen-color* ,(if color-supplied-p nil '*current-pen-color*))
         (*current-pen-width* ,(if width nil '*current-pen-width*)))
     ,(when position
        `(move-to ,@position))
     ,(when color-supplied-p
        `(canvas-pen-color ,color))
     ,(when width
        `(canvas-pen-width ,width))
     (with-current-pen-state
       ,@body)))

(defmacro with-pen-color (list-or-fn &body body)
  "Temporarily bind pen color to the value of LIST-OR-FN and execute BODY."
  `(let ((*current-pen-color* nil))
     (canvas-pen-color ,list-or-fn)
     ,@body))


(defmacro with-canvas (canvas &body body)
  "Perform drawing commands in BODY using the value of CANVAS as the
   target of any drawing operations."
  `(let ((*current-canvas* ,canvas)
         (*current-pen-width* 1)
         (*current-pen-position* (list 0 0))
         (*current-pen-color* (list 0 0 0 255)))
     (with-current-pen-state 
       ,@body)))

(defun restore-pen ()
  "Restore the state of the pan (width position color) as previously
saved by WITH-PEN-STATE"
  (destructuring-bind (pw pp pc) *saved-state*
    (setf *current-pen-width* pw
          *current-pen-position* pp
          *current-pen-color* pc)))


(defun colfix (c)
  (round (clamp 0 c 255)))

(defun canvas-pen-color (&optional newpen)
  "Set the pens color in the current context. NEWPEN, if supplied, can be one of:

  NIL - set the color to black.
 
  A list of four unsigned 8 bit integers that looks like (R G B A).
 
  A function (or symbol naming a function) that accepts the
  coordinates X Y of the pixel being drawn and returns a list as in
  the above case.

  All RGBA component values obtained from a pen (either from the
  return of a functional pen or as members of a list value) are
  clamped between 0 and 255."
  (setf *current-pen-color* 
        (etypecase newpen
          (null nil)
          ((or symbol function)
           (lambda (x y)
             (mapcar #'colfix (funcall newpen x y))))
          (list
           (mapcar #'colfix newpen)))))

(defun canvas-pen-width (n)
  (setf *current-pen-width* (round n)))

(defun can-fill-canvas-at-p (x y)
  (with-slots (pixel-width pixel-height) *current-canvas*
    (and (< -1 x pixel-width)
         (< -1 y pixel-height))))

(defun rel-to-current-pos (path)
  (destructuring-bind (cx cy) *current-pen-position* 
    (loop for (x y) in path collect (list (+ cx x) (+ cy y)))))

(defun move-to (x y)
  (setf *current-pen-position* (list x y)))

(defun move-rel (dx dy)
  (setf *current-pen-position*
        (mapcar #'+ *current-pen-position* (list dx dy))))

(defun apply-pen-at (x y)
  (let ((w
          (max 0 (1- *current-pen-width*))))
    (destructuring-bind
        (cr cg cb ca)
        (etypecase *current-pen-color*
          (null (list 0 0 0 255))
          (list *current-pen-color*)
          (function (funcall *current-pen-color* x y))) 
      (with-grid-rect (rx ry) (x y (+ x w) (+ y w))
        (when (can-fill-canvas-at-p rx ry) 
          (with-pixel (r g b a) (pixel *current-canvas* rx ry)
            (setf r cr g cg b cb a ca)))))))

(defun stroke-to (ex ey)
  (destructuring-bind (sx sy) *current-pen-position* 
    (with-grid-line (x y) (sx sy) (ex ey)
      (apply-pen-at x y)))
  (setf *current-pen-position* (list ex ey)))

(defun stroke-rel (dx dy)
  (apply #'stroke-to (mapcar #'+ *current-pen-position* (list dx dy))))

(defun stroke-path (path)
  (with-grid-path (x y) (path) (apply-pen-at x y))
  (setf *current-pen-position* (copy-list (first (last path)))))

(defun stroke-rel-path (path)
  (stroke-path
   (cons *current-pen-position*
         (rel-to-current-pos path))))

(defun fill-path (path)
  (let ((*current-pen-width* 1)) 
    (with-grid-path (x y) (path :interiorp t)
      (apply-pen-at x y))))

(defun fill-rel-path (path)
  (fill-path
   (cons *current-pen-position*
         (rel-to-current-pos path))))

(defun stroke-rect (left bottom right top)
  (stroke-path (list (list left bottom)
                     (list left top)
                     (list right top)
                     (list right bottom)
                     (list left bottom))))

(defun stroke-rel-rect (dx dy)
  (destructuring-bind (sx sy) *current-pen-position*
    (let ((left
            (if (plusp dx) sx (+ sx dx)))
          (right
            (if (plusp dx) (+ sx dx) sx))
          (bottom
            (if (plusp dy) sy (+ sy dy)))
          (top
            (if (plusp dy) (+ sy dy) sy)))
      (stroke-rect left bottom right top))))

(defun fill-rect (left bottom right top)
  (let ((*current-pen-width* 1))
    (with-grid-rect (x y) (left bottom right top)
      (apply-pen-at x y)))
  (setf *current-pen-position* (list left bottom)))

(defun fill-rel-rect (dx dy)
  (destructuring-bind (sx sy) *current-pen-position*
    (let ((left
            (if (plusp dx) sx (+ sx dx)))
          (right
            (if (plusp dx) (+ sx dx) sx))
          (bottom
            (if (plusp dy) sy (+ sy dy)))
          (top
            (if (plusp dy) (+ sy dy) sy)))
      (fill-rect left bottom right top))))

(defun stroke-bezier (control-pts &optional (curve-samples 10))
  (let (path)
    (with-grid-bezier (x y) (control-pts :count curve-samples)
      (push (list x y) path))
    (stroke-path (reverse path))))

(defun stroke-rel-bezier (rel-control-points &optional (curve-samples 10))
  (stroke-bezier (cons *current-pen-position*
                       (rel-to-current-pos rel-control-points))
                 curve-samples))

(defun fill-bezier (control-pts &optional (curve-samples 10))
  (let (path)
    (with-grid-bezier (x y) (control-pts :count curve-samples)
      (push (list x y) path))
    (fill-path (reverse path))))

(defun fill-rel-bezier (rel-control-points &optional (curve-samples 10))
  (fill-bezier (cons *current-pen-position*
                     (rel-to-current-pos rel-control-points))
               curve-samples))