;;;
;;; QML UI for CL-Sokoban, see http://www.cliki.net/CL-Sokoban 
;;;

(in-package :qsoko)

(defvar *item-types*
  '((#\# . :wall)
    (#\$ . :object)
    (#\* . :object2)
    (#\. . :goal)
    (#\@ . :player)
    (#\& . :player2)))

(defvar *items*         nil)
(defvar *item-size*     nil)
(defvar *maze*          nil)
(defvar *my-mazes*      (mapcar 'sokoban:copy-maze sokoban:*mazes*))
(defvar *solving*       nil)
(defvar *undo-stack*    nil)
(defvar *level-changed* nil)

(defun level ()
  (floor (q< |value| ui:*level*)))

(defun set-level (index)
  (q> |value| ui:*level* index))

(defun assoc* (item alist)
  (cdr (assoc item alist)))

(defun char-type (char)
  (cdr (assoc char *item-types*)))

(defun type-char (type)
  (car (find type *item-types* :key 'cdr)))

(defun set-maze ()
  (setf *maze* (nth (level) *my-mazes*))
  (update-translate-xy)
  (create-items)
  (place-all-items)
  (setf *undo-stack* nil))

(defun reset-maze ()
  (setf *maze* (setf (nth (level) *my-mazes*)
                     (sokoban:copy-maze (nth (level) sokoban:*mazes*))))
  (update-placed-items t)
  (setf *undo-stack* nil))

(defvar *translate-x* 0)
(defvar *translate-y* 0)

(defun find-file (file)
  (x:if-it (probe-file file)
           (format nil "file:~A~A"
		   #+win32 ""
		   #-win32 "//"
		   x:it)
           (x:cc "qrc:/" file)))

(defun update-translate-xy ()
  "Set x and y translation for maze centering."
  (let ((dim (sokoban:maze-dimensions *maze*))
        (img-px 32)
        (board-size 16))
    (setf *translate-x* (floor (* img-px (- board-size (car dim))) 2)
          *translate-y* (floor (* img-px (- board-size (cdr dim))) 2))))
  
(defun create-item (type)
  (let* ((name (string-downcase type))
         (item (qjs |createItem| ui:*dynamic* name)))
    (q> |source| item
        (find-file (format nil "qml/img/~A.png" name)))
    (q> |objectName| item name)
    ;; add to QObject hirarchy, for 'objectName' to be findable
    (qset item |parent|
          (find-quick-item ui:*board*))
    (unless *item-size*
      (setf *item-size* (q< |sourceSize| item)))
    item))

(defun create-items ()
  (clear-items)
  (flet ((add (types)
           (dolist (type (x:ensure-list types))
             (let ((item (create-item type)))
               (push item (cdr (assoc type *items*)))))))
    (dolist (row (sokoban:maze-text *maze*))
      (x:do-string (char row)
        (unless (char= #\Space char)
          (let ((type (char-type char)))
            (cond ((find type '(:player :player2))
                   (add '(:player :player2)))
                  ((find type '(:object :object2))
                   (add '(:object :object2 :goal)))
                  ((eql :wall type)
                   (add :wall)))))))))

(defvar *no-delete* nil)

(defun clear-items ()
  (unless *no-delete*
    (dolist (items *items*)
      (dolist (item (rest items))
        (q! |destroy| item))))
  (setf *items* (mapcar (lambda (x) (list (cdr x))) *item-types*)))

(defvar *running-animations* 0)
(defvar *function-queue*     nil)

(defun animation-change (running) ; called from QML
  (incf *running-animations* (if running 1 -1))
  (x:while (and (zerop *running-animations*)
                *function-queue*)
    (funcall (pop *function-queue*))))

(defun run-or-enqueue (function)
  (if (zerop *running-animations*)
      (funcall function)
      (setf *function-queue*
            (nconc *function-queue* (list function)))))

(defmacro queued (&rest functions)
  "Run passed functions in order, waiting for currently running (or newly
  triggered) animations to finish first."
  `(progn
     ,@(mapcar (lambda (fun) `(run-or-enqueue (lambda () ,fun)))
               functions)))

(defun change-level (direction/index)
  "Changes *LEVEL* in given direction or to index."
  (let ((level (min (1- (length *my-mazes*))
                    (max 0 (if (numberp direction/index)
                               direction/index
                               (+ (if (eql :next direction/index) 1 -1)
                                  (level)))))))
    (when (/= level (level))
      (queued (q> |running| ui:*zoom-board-out* t)
              (set-level level) ; will call SET-MAZE from QML
              (q> |running| ui:*zoom-board-in* t))))
  (setf *level-changed* t)
  (level))

(defun solve ()
  (setf *level-changed* nil)
  (let ((*solving* t))
    (reset-maze)
    (x:do-string (ch (nth (level) sokoban:*solutions*))
      (when *level-changed*
        (return-from solve))
      (sokoban:move (case (char-downcase ch)
                      (#\u :north)
                      (#\d :south)
                      (#\l :west)
                      (#\r :east))
                    *maze*)
      (x:while (plusp *running-animations*)
        (qsleep 0.05)))))

(defun set-x (item x &optional animate)
  (let ((x* (+ x *translate-x*)))
    (if animate
        (q> |x| item x*)
        (qset item |x| x*))))

(defun set-y (item y &optional animate)
  (let ((y* (+ y *translate-y*)))
    (if animate
        (q> |y| item y*)
        (qset item |y| y*))))

(defun child-at (x y)
  (q! |childAt| ui:*board*
      (+ x *translate-x*)
      (+ y *translate-y*)))

(defun place-items (type &optional reset)
  (let ((char (type-char type))
        (items (assoc* type *items*))
        (y 0))
    (unless (eql :wall type)
      (dolist (item items)
        (q> |visible| item nil)))
    (dolist (row (sokoban:maze-text *maze*))
      (let ((x 0))
        (x:do-string (curr-char row)
          (when (char= char curr-char)
            (let ((item (first items)))
              (set-x item x)
              (set-y item y)
              (q> |visible| item t))
            (setf items (rest items)))
          (incf x (first *item-size*))))
      (incf y (second *item-size*)))))

(defun place-all-items ()
  (dolist (type '(:wall :goal :object2 :player2 :player :object))
    (place-items type)))

(defun update-placed-items (&optional reset)
  (dolist (type '(:goal :object2 :player2 :player :object))
    (place-items type reset)))

(let (ex ex-ex)
  (defun move-item (char pos direction) ; see sokoban:*move-hook*
    (let* ((type (char-type char))
           (pos-x (car pos))
           (pos-y (cdr pos))
           (w (first *item-size*))
           (h (second *item-size*))
           (x (* w pos-x))
           (y (* h pos-y))
           (dx (case direction (:east w) (:west (- w)) (t 0)))
           (dy (case direction (:south h) (:north (- h)) (t 0)))
           (item (child-at (+ x (/ w 2)) (+ y (/ h 2)))))
      (unless (qnull item)
        (if (zerop dy)
            (set-x item (+ x dx) 'animate)
            (set-y item (+ y dy) 'animate))
        (dolist (tp (list type ex ex-ex))
          (when (find tp '(:player2  :object2 :goal))
            (queued (update-placed-items))
            (return)))
        (shiftf ex-ex ex type)
        (when (eql :player type)
          (qlater (lambda () (when (game-finished)
                               (final-animation)))))))))

(defun add-undo-step (step)
  (push step *undo-stack*))

(defun undo ()
  (when *undo-stack*
    (sokoban:undo *maze* (pop *undo-stack*))
    (update-placed-items)))

(defun game-finished ()
  ;; finished: no more :object, only :object2
  (let ((ch (type-char :object)))
    (dolist (str (sokoban:maze-text *maze*))
      (when (find ch str)
        (return-from game-finished))))
  t)

(defun final-animation ()
  (queued (q> |running| ui:*rotate-player* t)
          (q>* |running| ui:*wiggle-box* t)))

(defun button-pressed () ; called from QML
  (let ((button (intern (string-upcase (q< |objectName| *caller*))
                        :keyword)))
    (case button
      (:up       (sokoban:move :north *maze*))
      (:down     (sokoban:move :south *maze*))
      (:left     (sokoban:move :west *maze*))
      (:right    (sokoban:move :east *maze*))
      (:previous (change-level :previous))
      (:next     (change-level :next))
      (:undo     (undo))
      (:restart  (reset-maze))
      (:solve    (qlater 'solve)))) ; QLATER: prevent timer problem
  (values)) ; no return value to QML

(defun start ()
  (setf sokoban:*move-hook* 'move-item
        sokoban:*undo-hook* 'add-undo-step)
  (q> |to| ui:*level* (1- (length *my-mazes*)))
  (set-maze))

(qlater 'start)
