(defn
 create-game
 "Returns a game object. You can pass an options map with the following:\n  \n  :parent  -  A DOM element in which to place the canvas\n  :debug?  -  Whether or not to enable debug mode\n              (defaults to true if :optimizations are set to :none)\n  :mode    -  Either :2d or :webgl (defaults to :2d)"
 ([width height] (create-game width height {}))
 ([width
   height
   {:keys [parent debug? mode],
    :or {debug? (not js/COMPILED), mode :2d},
    :as opts}]
  (if
   debug?
   (js/console.log
    (str
     "Debug mode is enabled. If things are slow, try passing "
     "{:debug? false} to the third argument of create-game."))
   (set! (.-disableFriendlyErrors js/p5) true))
  (let
   [*hidden-state
    (atom
     {:screen nil,
      :renderer nil,
      :canvas nil,
      :listeners [],
      :total-time 0,
      :delta-time 0,
      :pressed-keys #{},
      :assets {}})
    setup-finished?
    (promise-chan)
    parent-opts
    (if debug? {:debug? true} {})]
   (reify
    Game
    (start
     [this]
     (when-let [renderer (get-renderer this)] (.remove renderer))
     (run! events/unlistenByKey (:listeners @*hidden-state))
     (swap! *hidden-state assoc :listeners [])
     (js/p5.
      (fn
       [renderer]
       (set!
        (.-setup renderer)
        (fn
         []
         (let
          [canvas-wrapper
           (cond->
            (.createCanvas
             renderer
             width
             height
             (case
              mode
              :2d
              (.-P2D renderer)
              :webgl
              (.-WEBGL renderer)))
            parent
            (.parent parent))
           canvas
           (.-canvas canvas-wrapper)]
          (.removeAttribute canvas "style")
          (swap!
           *hidden-state
           assoc
           :renderer
           renderer
           :canvas
           canvas))
         (put! setup-finished? true)))
       (set!
        (.-draw renderer)
        (fn
         []
         (swap!
          *hidden-state
          (fn
           [hidden-state]
           (let
            [time (.millis renderer)]
            (assoc
             hidden-state
             :total-time
             time
             :delta-time
             (- time (:total-time hidden-state))))))
         (.clear renderer)
         (some-> this get-screen on-render)))))
     (listen
      this
      "keydown"
      (fn
       [e]
       (swap! *hidden-state update :pressed-keys conj (.-keyCode e))))
     (listen
      this
      "keyup"
      (fn
       [e]
       (if
        (contains? #{91 93} (.-keyCode e))
        (swap! *hidden-state assoc :pressed-keys #{})
        (swap!
         *hidden-state
         update
         :pressed-keys
         disj
         (.-keyCode e)))))
     (listen
      this
      "blur"
      (fn* [] (swap! *hidden-state assoc :pressed-keys #{}))))
    (listen
     [this listen-type listener]
     (swap!
      *hidden-state
      update
      :listeners
      conj
      (events/listen js/window listen-type listener)))
    (render
     [this content]
     (when-let
      [renderer (get-renderer this)]
      (draw-sketch! this renderer content parent-opts)))
    (pre-render
     [this image-name width height content]
     (when-let
      [renderer (get-renderer this)]
      (let
       [object (.createGraphics renderer width height)]
       (draw-sketch! this object content parent-opts)
       (swap! *hidden-state update :assets assoc image-name object)
       object)))
    (load-image
     [this path]
     (when-let
      [renderer (get-renderer this)]
      (let
       [object (.loadImage renderer path (fn []))]
       (swap! *hidden-state update :assets assoc path object)
       object)))
    (load-tiled-map
     [this map-name]
     (when-let
      [renderer (get-renderer this)]
      (let
       [object (.loadTiledMap renderer map-name (fn []))]
       (swap! *hidden-state update :assets assoc map-name object)
       object)))
    (load-model
     [this path]
     (when-let
      [renderer (get-renderer this)]
      (let
       [object (.loadModel renderer path (fn []))]
       (swap! *hidden-state update :assets assoc path object)
       object)))
    (get-screen [this] (:screen @*hidden-state))
    (set-screen
     [this screen]
     (go
      (<! setup-finished?)
      (some-> this get-screen on-hide)
      (swap! *hidden-state assoc :screen screen)
      (on-show screen)))
    (get-renderer [this] (:renderer @*hidden-state))
    (get-canvas [this] (:canvas @*hidden-state))
    (get-total-time [this] (:total-time @*hidden-state))
    (get-delta-time [this] (:delta-time @*hidden-state))
    (get-pressed-keys [this] (:pressed-keys @*hidden-state))
    (get-width
     [this]
     (when-let [renderer (get-renderer this)] (.-width renderer)))
    (get-height
     [this]
     (when-let [renderer (get-renderer this)] (.-height renderer)))
    (set-size
     [this width height]
     (when-let
      [renderer (get-renderer this)]
      (.resizeCanvas renderer width height)))
    (get-asset [game name] (get-in @*hidden-state [:assets name]))))))