(ns reagent.impl.util
  (:require [reagent.debug :refer-macros [dbg log warn]]
            [reagent.interop :refer-macros [.' .!]]
            [clojure.string :as string]))

(def is-client (and (exists? js/window)
                    (-> js/window (.' :document) nil? not)))

;;; Props accessors

(defn extract-props [v]
  (let [p (nth v 1 nil)]
    (if (map? p) p)))

(defn extract-children [v]
  (let [p (nth v 1 nil)
        first-child (if (or (nil? p) (map? p)) 2 1)]
    (if (> (count v) first-child)
      (subvec v first-child))))

(defn get-argv [c]
  (.' c :props.argv))

(defn get-props [c]
  (-> (.' c :props.argv) extract-props))

(defn get-children [c]
  (-> (.' c :props.argv) extract-children))

(defn reagent-component? [c]
  (-> (.' c :props.argv) nil? not))

(defn cached-react-class [c]
  (.' c :cljsReactClass))

(defn cache-react-class [c constructor]
  (.! c :cljsReactClass constructor))

;; Misc utilities

(defn memoize-1 [f]
  (let [mem (atom {})]
    (fn [arg]
      (let [v (get @mem arg)]
        (if-not (nil? v)
          v
          (let [ret (f arg)]
            (swap! mem assoc arg ret)
            ret))))))

(def dont-camel-case #{"aria" "data"})

(defn capitalize [s]
  (if (< (count s) 2)
    (string/upper-case s)
    (str (string/upper-case (subs s 0 1)) (subs s 1))))

(defn dash-to-camel [dashed]
  (if (string? dashed)
    dashed
    (let [name-str (name dashed)
          [start & parts] (string/split name-str #"-")]
      (if (dont-camel-case start)
        name-str
        (apply str start (map capitalize parts))))))


(deftype partial-ifn [f args ^:mutable p]
  IFn
  (-invoke [_ & a]
    (or p (set! p (apply clojure.core/partial f args)))
    (apply p a))
  IEquiv
  (-equiv [_ other]
    (and (= f (.-f other)) (= args (.-args other))))
  IHash
  (-hash [_] (hash [f args])))

(defn- merge-class [p1 p2]
  (let [class (when-let [c1 (:class p1)]
                (when-let [c2 (:class p2)]
                  (str c1 " " c2)))]
    (if (nil? class)
      p2
      (assoc p2 :class class))))

(defn- merge-style [p1 p2]
  (let [style (when-let [s1 (:style p1)]
                (when-let [s2 (:style p2)]
                  (merge s1 s2)))]
    (if (nil? style)
      p2
      (assoc p2 :style style))))

(defn merge-props [p1 p2]
  (if (nil? p1)
    p2
    (do
      (assert (map? p1))
      (merge-style p1 (merge-class p1 (merge p1 p2))))))


(def ^:dynamic *always-update* false)

(defonce roots (atom {}))

(defn clear-container [node]
  ;; If render throws, React may get confused, and throw on
  ;; unmount as well, so try to force React to start over.
  (some-> node
          (.! :innerHTML "")))

(defn render-component [comp container callback]
  (let [rendered (volatile! nil)]
    (try
      (binding [*always-update* true]
        (->> (.' js/React render (comp) container
                 (fn []
                   (binding [*always-update* false]
                     (swap! roots assoc container [comp container])
                     (if (some? callback)
                       (callback)))))
             (vreset! rendered)))
      (finally
        (when-not @rendered
          (clear-container container))))))

(defn re-render-component [comp container]
  (render-component comp container nil))

(defn unmount-component-at-node [container]
  (swap! roots dissoc container)
  (.' js/React unmountComponentAtNode container))

(defn force-update-all []
  (doseq [v (vals @roots)]
    (apply re-render-component v))
  "Updated")

(defn force-update [comp deep]
  (if deep
    (binding [*always-update* true]
      (.' comp forceUpdate))
    (.' comp forceUpdate)))
