;;; Palindrome "PATERNOSTER"
;;;
;;; N.B: If you load this file in Slime, you need to use QLOAD instead of LOAD!

(in-package :eql-user)

(require :definitions "definitions")

(defvar *generate-html/js-version* nil) ; command line option :html

(dolist (arg (mapcar 'read-from-string (rest (|arguments.QCoreApplication|))))
  (if (numberp arg)
      (if (integerp arg)
          (setf *window-width* arg)
          (setf *window-opacity* arg))
      (when (eql :html arg)
        (setf *generate-html/js-version* t)))
  (when (and *generate-html/js-version*
             (not *window-width*))
    (setf *window-width* 450)))

(defconstant +state-switch-event+ (+ |QEvent.User| 256))
(defconstant +size+               20)

(defparameter *item-size* (list +size+ +size+))
(defparameter *view-size* (list (* +size+ (length (caar *states*)))
                                (* +size+ (length (car *states*)))))
(defparameter *color-m*   nil)

(defvar *main*  (qnew "QWidget"
                      "mouseTracking" t
                      "windowOpacity" *window-opacity*))
(defvar *view*  (qnew "QGraphicsView"
                      "mouseTracking" t
                      "frameShape" |QFrame.NoFrame|))
(defvar *timer* (qnew "QTimer"
                      "singleShot" t))

(defmacro push* (item list)
  `(setf ,list (nconc ,list (list ,item))))

;;; custom easing curve (see both "../cpp/lib.h" and "exe/main.h")
;;; see CUSTOM-EASING-FUNCTION in "definitions.lisp"

(let (curve)
  (defun custom-easing-curve ()
    (or curve
        (setf curve (cond ((directory "*easing_curve.*")
                           (! "easingCurve" (:qt (qload-c++ "easing_curve"))))
                          ((string= "Qt_EQL_Application"
                                    (|className| (|metaObject| (qapp))))
                           (! "easingCurve" (:qt (qapp))))
                          (t
                           (qnew "QEasingCurve(QEasingCurve::Type)" |QEasingCurve.Linear|))))))) ; fallback

;;; state-switch-event

(let (event-n)
  (defun new-state-switch-event (n)
    (setf event-n n)
    (qnew "QEvent(QEvent::Type)" +state-switch-event+))
  (defun event-number ()
    event-n))

;;; state-switch-transition

(defstruct (state-switch-transition (:conc-name transition-))
  (q      (qnew "QAbstractTransition"))
  (number nil))

(defmethod the-qt-object ((object state-switch-transition))
  (transition-q object))

(defun new-state-switch-transition (n)
  (let ((trans (make-state-switch-transition :number n)))
    (qoverride trans "eventTest(QEvent*)"
               (lambda (event)
                 (and (= +state-switch-event+
                         (|type| event))
                      (= (transition-number trans)
                         (event-number)))))
    trans))

;;; state-switcher

(defstruct (state-switcher (:conc-name :switcher-))
  (q           nil)
  (state-count 0)
  (last-index  0))

(defmethod the-qt-object ((object state-switcher))
  (switcher-q object))

(let ((ini t))
  (defun new-state-switcher (machine name)
    (let ((switch (make-state-switcher :q (qnew "QState(QState*)" machine
                                                "objectName" name))))
      (qoverride switch "onEntry(QEvent*)"
                 (lambda (event)
                   (let ((n (1+ (switcher-last-index switch))))
                     (when (> n (switcher-state-count switch))
                       (setf n 1))
                     (when ini
                       (setf ini nil)
                       (incf n)
                       (|setInterval| *timer* (* 2 *duration*)))
                     (setf (switcher-last-index switch) n)
                     (|postEvent| (|machine| switch)
                                  (new-state-switch-event n)))))
      switch)))

;;;
;;; main
;;;

(defvar *font*           (x:let-it (|font.QApplication|) (|setBold| x:it t)))
(defvar *graphics-items* nil)

(let (items)
  (defun new-graphics-item (text color id)
    (when *generate-html/js-version*
      (save-item-pixmap text color id))
    (let ((item (qnew "QGraphicsWidget")))
      (qoverride item "paint(QPainter*,QStyleOptionGraphicsItem*,QWidget*)"
                 (lambda (painter s w)
                   (x:do-with painter
                     (|fillRect| (mapcar '+ (|rect| item) '(1 1 -2 -2)) color)
                     (|setFont| *font*)
                     (|setPen| "black")
                     (|drawText| (|rect| item) |Qt.AlignCenter| text))))
      (push (cons id item) *graphics-items*)
      item))
  (defun id-item (id)
    (cdr (assoc id *graphics-items*)))
  (defun items ()
    (or items (setf items
                    (loop :for i :to (1- (length *graphics-items*))
                      :collect (id-item (intern (string (code-char (+ i #.(char-code #\A)))))))))))

(defun create-geometry-state (parent objects positions)
  (let ((result (qnew "QState(QState*)" parent)))
    (mapc (lambda (object pos)
            (|assignProperty| result object "geometry" (qnew "QVariant(QRect)"
                                                             (append (mapcar '* (mapcar '- pos '(1 1)) *item-size*)
                                                                     *item-size*))))
          objects positions)
    result))

(defun add-state (state-switcher state animation)
  (let ((trans (new-state-switch-transition (incf (switcher-state-count state-switcher)))))
    (x:do-with trans
      (|setTargetState| state)
      (|addAnimation| animation))
    (|addTransition| state-switcher trans)))

(let (animations groups)
  (defun add-property-animation (anim-group button property curve-type duration &optional pause)
    (let ((anim (qnew "QPropertyAnimation(QObject*,QByteArray)" button (x:string-to-bytes property)))
          (group (if pause
                     (let ((group (qnew "QSequentialAnimationGroup(QObject*)" anim-group)))
                       (|addPause| group pause)
                       (push* group groups)
                       group)
                     anim-group)))
      (x:do-with anim
        (|setDuration| duration)
        (|setEasingCurve| (if (= |QEasingCurve.Custom| curve-type)
                              (custom-easing-curve)
                              (qnew "QEasingCurve(QEasingCurve::Type)" curve-type))))
      (push* anim animations)
      (|addAnimation| group anim)
      anim))
  (defun change-easing-curve (curve)
    (let* ((type (etypecase curve
                   (integer
                     curve)
                   (string
                     (symbol-value (intern (x:cc "QEasingCurve." curve))))))
           (curve (if (= |QEasingCurve.Custom| type)
                      (custom-easing-curve)
                      (qnew "QEasingCurve(QEasingCurve::Type)" type))))
      (setf *easing-curve* curve)
      (dolist (anim animations)
        (|setEasingCurve| anim curve))))
  (defun change-duration (msec)
    (setf *duration* msec)
    (dolist (anim animations)
      (|setDuration| anim msec))
    (update-timer))
  (defun change-pause (msec)
    (setf *pause* msec)
    (let ((n 0))
      (dolist (group groups)
        (let ((anim (|takeAnimation| group 1)))
          (x:do-with group
            (|clear|)
            (|addPause| (* (incf n) msec))
            (|addAnimation| anim)))))
    (update-timer))
  (defun update-timer ()
    (|setInterval| *timer* (+ *duration* (* 4 *pause*) 1000))))

(defun item-count ()
  (loop :for item :in *items*
        :sum (length (third item))))

(defun create-geometry-states (group)
  (flet ((item-pos (char list)
           (let ((y 0))
             (dolist (state list)
               (incf y)
               (x:when-it (position char state)
                 (return-from item-pos (list (1+ x:it) y)))))))
    (let ((letters (loop :for i :to (item-count) :collect (code-char (+ i #.(char-code #\a)))))
          states)
      (dolist (state *states*)
        (let (positions)
          (dolist (char letters)
            (push* (item-pos char state)
                   positions))
          (push* (create-geometry-state group (items) positions)
                 states)))
      states)))

(defun ini ()
  (dolist (item *items*)
    (let ((text (first item))
          (color (second item)))
      (dolist (id (third item))
        (new-graphics-item text (if (eql 'm id) (setf *color-m* color) color) id))))
  (let ((scene      (qnew "QGraphicsScene"
                          "sceneRect" (append '(0 0) *view-size*)))
        (machine    (qnew "QStateMachine"))
        (group      (qnew "QState"
                          "objectName" "group"))
        (anim-group (qnew "QParallelAnimationGroup"))
        (layout     (qnew "QHBoxLayout(QWidget*)" *main*)))
    (qset-color *main* |QPalette.Window| *background*)
    (|setBackgroundBrush| scene (qnew "QBrush(QColor)" *background*))
    (x:do-with layout
      (|addStretch|)
      (|addWidget| *view*)
      (|addStretch|))
    (|setScene| *view* scene)
    (dolist (item (items))
      (|addItem| scene item))
    (let ((pause 0))
      (dolist (item (items))
        (add-property-animation anim-group item "geometry" *easing-curve* *duration* (incf pause *pause*))))
    (|setInterval| *timer* (/ *duration* 2))
    (let ((states (create-geometry-states group))
          (state-switcher (new-state-switcher machine "stateSwitcher")))
      (dolist (state states)
        (add-state state-switcher state anim-group))
      (x:do-with group
        (|setInitialState| (first states))
        (|addTransition| *timer* (qsignal "timeout()") state-switcher)))
    (x:do-with machine
      (|addState| group)
      (|setInitialState| group)
      (|start|))
    (qconnect group "entered()" *timer* "start()")
    (qoverride *view* "resizeEvent(QResizeEvent*)"
               (lambda (event)
                 (|fitInView| *view* (|sceneRect| scene))
                 (qcall-default)))
    ;; change background color on mouse move events outside of view
    (qoverride *main* "mouseMoveEvent(QMouseEvent*)"
               (lambda (event)
                 (qlet ((brush "QBrush(QColor)" (if (< (|x| event)
                                                       (/ (|width| *main*) 2))
                                                    *background*
                                                    *color-m*)))
                   (|setBackgroundBrush| scene brush))))
    ;; change background color on mouse move events inside of view
    (qoverride *view* "mouseMoveEvent(QMouseEvent*)"
               (lambda (event)
                 (qlet ((brush "QBrush(QColor)"
                               (|fromHsv.QColor|
                                  (floor (* 359 (/ (|x| event) (|width| *view*))))
                                  (floor (* 255 (/ (|y| event) (|height| *view*))))
                                  255)))
                   (|setBackgroundBrush| scene brush))))
    ;; quit on mouse click
    (dolist (w (list *main* *view*))
      (qoverride w "mousePressEvent(QMouseEvent*)" (lambda (event) (qquit))))
    ;; pause on key Space
    ;; save screenshot on key S
    ;; quit on key Escape
    (qadd-event-filter nil |QEvent.KeyPress|
                       (lambda (_ event)
                         (let ((key (|key| event)))
                           (case key
                             (#.|Qt.Key_Space|
                                (let ((active (|isActive| *timer*)))
                                  (if active
                                      (|stop| *timer*)
                                      (x:do-with *timer*
                                        (|timeout|)
                                        (|start|)))
                                  (qset-color *main* |QPalette.Window| (if active *color-pause* *background*))))
                             (#.|Qt.Key_S|
                                (let ((widget (|viewport| *view*)))
                                  (|save| (|grab| widget (|rect| widget))
                                          "screenshot.png")))
                             (#.|Qt.Key_Escape|
                                (qquit))))))
    (if *window-width*
        (|setFixedSize| *view* (list *window-width* *window-width*))
        (|setFixedWidth| *view* (apply 'min (nthcdr 2 (|screenGeometry| (|desktop.QApplication|))))))
    (qlater (lambda () (|setPos.QCursor| (|pos| *main*))))
    (if *window-width*
        (|show| *main*)
        (|showFullScreen| *main*))
    (|raise| *main*)))

;;;
;;; for *generate-html/js-version* (command line option :html)
;;;

(defvar *positions-timer* (qnew "QTimer"))
(defvar *item-positions*  nil)

(defun normalize (x)
  (truncate (+ 0.5 (/ (* x *window-width*) 300))))

(defun save-item-pixmap (text color id)
  (qlet ((pixmap "QPixmap(QSize)" (mapcar 'normalize *item-size*)))
    (|fill| pixmap "transparent")
    (qlet ((painter "QPainter(QPixmap*)" pixmap)
           (font* (qcopy *font*)))
      (|setPointSize| font* (normalize (|pointSize| font*)))
      (x:do-with painter
        (|fillRect| (mapcar '+ (|rect| pixmap) '(1 1 -2 -2)) color)
        (|setFont| font*)
        (|setPen| "black")
        (|drawText| (|rect| pixmap) |Qt.AlignCenter| text)))
    (|save| pixmap (ensure-directories-exist (format nil "html/img/~D.png"
                                                     (1+ (- (char-code (char (symbol-name id) 0))
                                                            (char-code #\A))))))))

(let (ex-pos)
  (defun collect-item-positions ()
    (let ((pos (mapcar (lambda (it) (cons (car it) (mapcar 'normalize (|pos| (cdr it)))))
                       *graphics-items*)))
      (unless (equal pos ex-pos)
        (push pos *item-positions*)
        (setf ex-pos pos)))))

(let (num)
  (defun save-item-positions ()
    (if num
        (progn
          (let (pos)
            (dolist (p *item-positions*)
              (push (sort p 'string< :key 'first) pos))
            (with-open-file (s "meta/positions.js"
                               :direction :output :if-exists :append :if-does-not-exist :create)
              (format s "~%  var p~D = [" (incf num))
              (dolist (p pos)
                (dolist (xy p)
                  (format s "~D,~D," (second xy) (third xy))))
              (format s "null];")))
          (format t "~%[html] positions ~D/12" num)
          (when (= 12 num)
            (load "meta/generate.lisp")
            (funcall (find-symbol "GENERATE-HTML") (item-count)
                     :width *window-width*
                     :pause 1500
                     :delay 50
                     :background "black")
            (format t "~%[html] please see \"html/palindrome.htm\"~%~%")
            (qq))
          (setf *item-positions* nil))
        (progn
          (setf num 0)
          (when (probe-file "meta/positions.js")
            (delete-file "meta/positions.js"))))))

(defun ini-generate-html/js ()
  (|setInterval| *positions-timer* 50)
  (qconnect *positions-timer* "timeout()" 'collect-item-positions)
  (qconnect *timer* "timeout()" 'save-item-positions)
  (|start| *positions-timer*))

(progn
  (ini)
  (when *generate-html/js-version*
    (ini-generate-html/js)))

