;;
;; Copyright 2010, 2011 Mikhail Kryshen <mikhail@kryshen.net>
;;
;; This file is part of Indyvon.
;;
;; Indyvon is free software: you can redistribute it and/or modify it
;; under the terms of the GNU Lesser General Public License version 3
;; only, as published by the Free Software Foundation.
;;
;; Indyvon is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; Lesser General Public License for more details.
;;
;; You should have received a copy of the GNU Lesser General Public
;; License along with Indyvon.  If not, see
;; <http://www.gnu.org/licenses/>.
;;

(ns net.kryshen.indyvon.async
  "Asynchronous drawing."
  (:use
   net.kryshen.indyvon.core)
  (:import
   (net.kryshen.indyvon.core Size Location)
   java.awt.GraphicsConfiguration
   (java.awt Image AlphaComposite Transparency)
   (java.awt.image BufferedImage)
   (java.util.concurrent ThreadFactory ThreadPoolExecutor
                         ThreadPoolExecutor$DiscardOldestPolicy
                         ArrayBlockingQueue TimeUnit)))

(defrecord Buffer [id image readers state])
;; Buffer states:
;;   :front, readers > 0
;;      being copied on screen
;;   :back
;;      being rendered to (offscreen)
;;   :fresh
;;      most recently updated
;;   :free
;;      not in use

(defn- create-image [async-layer ^GraphicsConfiguration device-conf]
  ;; TODO: support different image types.
  (.createCompatibleImage device-conf
                          (:width async-layer)
                          (:height async-layer)
                          Transparency/TRANSLUCENT))

(defn- create-buffer [async-layer device-conf]
  (Buffer. (Object.) (create-image async-layer device-conf) 0 :free))

(defn- find-buffer
  "Find a buffer with the one of the specified states given
   in the order of preference."
  [buffers & states]
  (some identity
    (for [state states]
      (some #(if (= (:state %) state) % nil) buffers))))

(defn- replace-buffer [buffers buffer]
  (conj (remove #(= (:id %) (:id buffer)) buffers)
        buffer))

(defn- take-buffer [al type]
  (dosync
   (let [buffers @(:buffers al)
         b (case type
             :front (find-buffer buffers :front :fresh :free)
             :back (find-buffer buffers :free :fresh)
             (throw (IllegalArgumentException.)))
         readers (if (= type :front)
                   (inc (:readers b))
                   (:readers b))
         b (assoc b
             :state type
             :readers readers)]
     (alter (:buffers al) replace-buffer b)
     b)))

(defn- release-buffer [al buffer]
  (dosync
   (let [state (:state buffer)
         readers (if (= state :front)
                   (dec (:readers buffer))
                   (:readers buffer))
         fresh (delay (find-buffer @(:buffers al) :fresh))
         state (cond
                (pos? readers) :front
                (= :back state) :fresh
                @fresh :free
                :default :fresh)]
     (if (and (= state :fresh) @fresh)
       ;; Change state of the prefiously fresh buffer to :free.
       (alter (:buffers al)
              replace-buffer (assoc @fresh
                               :state :free)))
     (alter (:buffers al)
            replace-buffer (assoc buffer
                             :state state
                             :readers readers)))))

(defmacro with-buffer
  {:private true}
  [al type [name] & body]
  `(let [al# ~al
         ~name (take-buffer al# ~type)]
     (try
       ~@body
       (finally
        (release-buffer al# ~name)))))

(defn- draw-offscreen [async-layer]
  ;;(Thread/sleep 1000)
  (with-buffer async-layer :back [b]
    (let [g (.createGraphics ^BufferedImage (:image b))]
      ;; Clear the buffer.
      (.setComposite g AlphaComposite/Clear)
      (.fillRect g 0 0 (:width async-layer) (:height async-layer))
      (.setComposite g AlphaComposite/Src)
      (draw-root! (:content async-layer)
                  g
                  (:width async-layer)
                  (:height async-layer)
                  ;; TODO: use operational event dispatcher.
                  dummy-event-dispatcher))
    (update async-layer)))

(defn- draw-offscreen-async [async-layer]
  (.execute ^ThreadPoolExecutor (:executor async-layer)
            #(draw-offscreen async-layer)))

(defrecord AsyncLayer [content width height executor buffers]
  Layer
  (render! [layer]
    (repaint-on-update layer)
    (add-context-observer content (fn [_ _] (draw-offscreen-async layer)))
    (when-not @buffers
      ;; TODO: dynamic size, recreate buffers when size increases.
      (let [device-conf (.getDeviceConfiguration *graphics*)
            new-buffers (repeatedly 2
                          (partial create-buffer layer device-conf))]
        (dosync
         (ref-set buffers new-buffers)))
      (draw-offscreen-async layer))
    (with-buffer layer :front [b]
      (.drawImage *graphics* ^Image (:image b) 0 0 nil)))
  (layer-size [layer]
    (Size. width height)))

(defn- create-thread-factory [priority]
  (reify
   ThreadFactory
   (newThread [_ runnable]
     (let [thread (Thread. runnable)]
       (when priority
         (.setPriority thread priority))
       (.setDaemon thread true)
       thread))))

(defn- create-executor [priority]
  (doto (ThreadPoolExecutor.
         (int 1) (int 1)
         (long 0) TimeUnit/SECONDS
         (ArrayBlockingQueue. 1)
         (ThreadPoolExecutor$DiscardOldestPolicy.))
    (.setThreadFactory (create-thread-factory priority))))

(defn async-layer 
  "Creates layer that draws the content asynchronously using
   offscreen buffer."
  ([content width height]
     (async-layer content width height nil))
  ([content width height priority]
     (AsyncLayer. content width height
                  (create-executor priority)
                  (ref nil))))