(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")
   :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 {;; 16px tile size
                ;; 11 tiles wide, 9 high
                ;; 3 times zoom
                :w (* 16 11 3)
                :h (* 16 9 3)}
   :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 nao [] (.now js/performance))

(defn curr-fps
  "calculates the current fps using the elapsed time"
  [elapsed]
  (/ 1 elapsed))

(defn elapsed-seconds
  "calculates the elapsed seconds since the last frame"
  [gamestate now]
  (/ (- now (-> gamestate :timing :prev))
     1000))

(defn curr-scene
  "returns the current scene"
  [gamestate]
  (get-in gamestate [:scenes (:scene gamestate)]))

(defn run-scene-update
  "updates the current scene using its udpate function"
  [gamestate scene]
  ((:update scene) gamestate scene))

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

(defn update-step
  "updates timing information and the current scene"
  [gamestate]
  (let [now (nao)
        secs (elapsed-seconds gamestate now)
        scene (curr-scene gamestate)
        continue? (continue-running? (:continue? gamestate))]
    (as-> gamestate $
      (assoc-in $ [:input :dir] (input/dir))
      (assoc $ :timing {:now now
                        :elapsed secs
                        :fps (curr-fps secs)})
      (assoc-in $ [:scenes (:scene gamestate)]
                (if continue?
                  (run-scene-update $ scene)
                  scene))
      (assoc-in $ [:timing :prev] (nao)))))

(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} (-> gamestate :scenes scenekey)]
    (draw gamestate scene))
  (draw-fps gamestate))

(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 using requestAnimationFrame"
  [gamestate]
  (let [newstate (update-step gamestate)]
    (draw-step newstate)
    (when-not @reloaded
      (.requestAnimationFrame js/window
                              #(mainloop newstate)))))

(defn init-scenes
  "initiates the scene data maps using their respective init functions"
  []
  (set! (.-width (:canvas gamestate))
        (-> gamestate :dimensions :w))
  (set! (.-height (:canvas gamestate))
        (-> gamestate :dimensions :h))
  (set! (.-imageSmoothingEnabled (:ctx gamestate))
        false)
  (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))
