(ns topdown2d.core
  (:require [topdown2d.demoscene :as demoscene]
            [topdown2d.input :as input]
            [topdown2d.dom :refer [by-id]]))

(enable-console-print!)

(def gamestate
  {:canvas (by-id "gamecanvas")
   :ctx (.getContext (by-id "gamecanvas") "2d")
   :target-fps 60
   :continue? true
   :timing {;; msecs of previous frame
            :prev 0
            ;; msecs of current frame
            :now 0
            ;; fps resulting of prev and now
            :fps 0
            ;; difference between prev and now in seconds
            :elapsed 0}
   ;; width and height of the canvas
   :dimensions {:w 1200
                :h 500}
   :input {:dir :?}
   ;; currently active scene
   :scene :demo
   :scenes {:demo {:update demoscene/update-scene
                   :draw demoscene/draw-scene
                   :init demoscene/init}}})

(def reloaded (atom false))

(defn set-timing
  "sets the current time at the given key"
  [state timingkey]
  (assoc-in state
            [:timing timingkey]
            (.now js/performance)))

(defn set-fps
  "calculates the current fps using the elapsed time"
  [state]
  (let [elapsed (get-in state [:timing :elapsed])
        fps (/ 1 elapsed)]
    (assoc-in state [:timing :fps] fps)))

(defn set-elapsed-seconds
  "calculates and writes the elapsed seconds since the last frame"
  [gamestate]
  (update gamestate
          :timing
          #(let [{:keys [now prev]} %]
             (assoc % :elapsed
                    (/ (- now prev)
                       1000)))))

(defn update-scene
  "updates the current scene using its udpate function"
  [gamestate]
  (if-not (:continue? gamestate)
    gamestate
    (let [scenekey (:scene gamestate)
          {updatefunc :update
           :as scene} (get-in gamestate [:scenes scenekey])
          newstate (updatefunc gamestate scene)]
      (assoc-in gamestate [:scenes scenekey] newstate))))

(defn continue-running?
  "checks if the gameloop should keep running, based on input"
  [gamestate]
  (update
   gamestate
   :continue?
   (fn [continue?]
     (cond
       (and continue?
            (input/keydown? :Digit2)
            (input/keydown? :ControlLeft)) false
       (and (not continue?)
            (input/keydown? :Digit3)
            (input/keydown? :ControlLeft)) true
       :else continue?))))

(defn update-step
  "updates timing information and the current scene"
  [gamestate]
  (-> gamestate
      (assoc-in [:input :dir] (input/dir))
      (set-timing :now)
      (set-elapsed-seconds)
      (set-fps)
      (continue-running?)
      (update-scene)
      (set-timing :prev)))

(defn draw-fps
  "draws the current fps"
  [gamestate]
  (let [ctx (:ctx gamestate)]
    (aset ctx "fillStyle" "white")
    (.fillRect
     ctx
     0 0 13 13)
    (aset ctx "fillStyle" "black")
    (aset ctx "font" "10px monospace")
    (.fillText
     (:ctx gamestate)
     (int (get-in gamestate [:timing :fps]))
     0 10)))

(defn draw-step
  "clears the canvas, draws fps and invokes the scene draw function"
  [gamestate]
  (.clearRect (:ctx gamestate)
              0 0
              (get-in gamestate [:dimensions :w])
              (get-in gamestate [:dimensions :h]))
  (let [scenekey (:scene gamestate)
        {:keys [draw] :as scene} (get-in gamestate [:scenes scenekey])]
    (draw gamestate scene))
  (draw-fps gamestate))

(defn timeout
  "calculates the duration of update-step and draw-step.
  substracts that from the wait time to reach target-fps
  more accurately.
  if continue? is true, wait for 5 seconds plain"
  [gamestate]
  (if (:continue? gamestate)
    (/ (- 1000 (- (.now js/performance)
                  (get-in gamestate [:timing :now])))
       (:target-fps gamestate))
    5000))

(defn mainloop
  "transforms the given gamestate by invoking a series of update
  functions and draws it using the 2d context of the gamestate.
  then, it calls itself again with a delay according to the target fps"
  [gamestate]
  (let [newstate (update-step gamestate)]
    (draw-step newstate)
    (when-not @reloaded
      (.setTimeout js/window
                   (fn []
                     (.requestAnimationFrame
                      js/window
                      #(mainloop newstate)))
                   (timeout newstate)))))

(defn init-scenes
  "initiates the scene data maps using their respective init functions"
  []
  (update gamestate
          :scenes
          #(reduce
            (fn [carr [key scene]]
              (assoc carr key ((:init scene) gamestate scene)))
            {} %)))

(defn fig-reload []
  (reset! reloaded true)
  (.setTimeout js/window
               (fn []
                 (reset! reloaded false)
                 (mainloop (init-scenes)))
               1000))

(mainloop (init-scenes))
