(ns reagent.impl.component
  (:require [reagent.impl.util :as util]
            [reagent.impl.batching :as batch]
            [reagent.ratom :as ratom]
            [reagent.interop :refer-macros [.' .!]]
            [reagent.debug :refer-macros [dbg prn dev? warn]]))

(declare ^:dynamic *current-component*)

(declare ^:dynamic *non-reactive*)

;;; State

(defn state-atom [this]
  (let [sa (.' this :cljsState)]
    (if-not (nil? sa)
      sa
      (.! this :cljsState (ratom/atom nil)))))

;; ugly circular dependency
(defn as-element [x]
  (js/reagent.impl.template.as-element x))

;;; Rendering

(defn reagent-class? [c]
  (and (fn? c)
       (some? (.' c :cljsReactClass))))

(defn do-render-sub [c]
  (let [f (.' c :cljsRender)
        _ (assert (ifn? f))
        p (.' c :props)
        res (if (nil? (.' c :reagentRender))
              (f c)
              (let [argv (.' p :argv)
                    n (count argv)]
                (case n
                  1 (f)
                  2 (f (nth argv 1))
                  3 (f (nth argv 1) (nth argv 2))
                  4 (f (nth argv 1) (nth argv 2) (nth argv 3))
                  5 (f (nth argv 1) (nth argv 2) (nth argv 3) (nth argv 4))
                  (apply f (subvec argv 1)))))]
    (if (vector? res)
      (as-element res)
      (if (ifn? res)
        (let [f (if (reagent-class? res)
                  (fn [& args]
                    (as-element (apply vector res args)))
                  res)]
          (.! c :cljsRender f)
          (recur c))
        res))))

(declare comp-name)

(defn do-render [c]
  (binding [*current-component* c]
    (if (dev?)
      ;; Log errors, without using try/catch (and mess up call stack)
      (let [ok (array false)]
        (try
          (let [res (do-render-sub c)]
            (aset ok 0 true)
            res)
          (finally
            (when-not (aget ok 0)
              (js/console.error (str "Error rendering component "
                                     (comp-name)))))))
      (do-render-sub c))))


;;; Method wrapping

(def static-fns {:render
                 (fn []
                   (this-as c
                            (if-not *non-reactive*
                              (batch/run-reactively c #(do-render c))
                              (do-render c))))})

(defn custom-wrapper [key f]
  (case key
    :getDefaultProps
    (assert false "getDefaultProps not supported yet")

    :getInitialState
    (fn []
      (this-as c
               (reset! (state-atom c) (f c))))

    :componentWillReceiveProps
    (fn [props]
      (this-as c
               (f c (.' props :argv))))

    :shouldComponentUpdate
    (fn [nextprops nextstate]
      (or util/*always-update*
          (this-as c
                   ;; Don't care about nextstate here, we use forceUpdate
                   ;; when only when state has changed anyway.
                   (let [old-argv (.' c :props.argv)
                         new-argv (.' nextprops :argv)]
                     (if (nil? f)
                       (or (nil? old-argv)
                           (nil? new-argv)
                           (not= old-argv new-argv))
                       (f c old-argv new-argv))))))

    :componentWillUpdate
    (fn [nextprops]
      (this-as c
               (f c (.' nextprops :argv))))

    :componentDidUpdate
    (fn [oldprops]
      (this-as c
               (f c (.' oldprops :argv))))

    :componentWillMount
    (fn []
      (this-as c
               (.! c :cljsMountOrder (batch/next-mount-count))
               (when-not (nil? f)
                 (f c))))

    :componentWillUnmount
    (fn []
      (this-as c
               (batch/dispose c)
               (when-not (nil? f)
                 (f c))))

    nil))

(defn default-wrapper [f]
  (if (ifn? f)
    (fn [& args]
      (this-as c (apply f c args)))
    f))

(def dont-wrap #{:cljsRender :render :reagentRender :cljsName})

(defn dont-bind [f]
  (if (fn? f)
    (doto f
      (.! :__reactDontBind true))
    f))

(defn get-wrapper [key f name]
  (if (dont-wrap key)
    (dont-bind f)
    (let [wrap (custom-wrapper key f)]
      (when (and wrap f)
        (assert (ifn? f)
                (str "Expected function in " name key " but got " f)))
      (or wrap (default-wrapper f)))))

(def obligatory {:shouldComponentUpdate nil
                 :componentWillMount nil
                 :componentWillUnmount nil})

(def dash-to-camel (util/memoize-1 util/dash-to-camel))

(defn camelify-map-keys [fun-map]
  (reduce-kv (fn [m k v]
               (assoc m (-> k dash-to-camel keyword) v))
             {} fun-map))

(defn add-obligatory [fun-map]
  (merge obligatory fun-map))

(defn add-render [fun-map render-f name]
  (let [fm (assoc fun-map
                  :cljsRender render-f
                  :render (:render static-fns))]
    (if (dev?)
      (assoc fm :cljsName (fn [] name))
      fm)))

(defn fun-name [f]
  (or (and (fn? f)
           (or (.' f :displayName)
               (.' f :name)))
      (and (implements? INamed f)
           (name f))
      (let [m (meta f)]
        (if (map? m)
          (:name m)))))

(defn wrap-funs [fmap]
  (let [fun-map (if-some [cf (:componentFunction fmap)]
                  (-> fmap
                      (assoc :reagentRender cf)
                      (dissoc :componentFunction))
                  fmap)
        render-fun (or (:reagentRender fun-map)
                       (:render fun-map))
        _ (assert (ifn? render-fun)
                  (str "Render must be a function, not "
                       (pr-str render-fun)))
        name (str (or (:displayName fun-map)
                      (fun-name render-fun)))
        name' (if (empty? name)
                (str (gensym "reagent"))
                (clojure.string/replace name #"\$" "."))
        fmap (-> fun-map
                 (assoc :displayName name')
                 (add-render render-fun name'))]
    (reduce-kv (fn [m k v]
                 (assoc m k (get-wrapper k v name')))
               {} fmap)))

(defn map-to-js [m]
  (reduce-kv (fn [o k v]
               (doto o
                 (aset (name k) v)))
             #js{} m))

(defn cljsify [body]
  (-> body
      camelify-map-keys
      add-obligatory
      wrap-funs
      map-to-js))

(defn create-class
  [body]
  (assert (map? body))
  (let [spec (cljsify body)
        res (.' js/React createClass spec)
        f (fn [& args]
            (warn "Calling the result of create-class as a function is "
                  "deprecated in " (.' res :displayName) ". Use a vector "
                  "instead.")
            (as-element (apply vector res args)))]
    (util/cache-react-class f res)
    (util/cache-react-class res res)
    f))

(defn component-path [c]
  (let [elem (some-> (or (some-> c
                                 (.' :_reactInternalInstance))
                          c)
                     (.' :_currentElement))
        name (some-> elem
                     (.' :type)
                     (.' :displayName))
        path (some-> elem
                     (.' :_owner)
                     component-path
                     (str " > "))
        res (str path name)]
    (when-not (empty? res) res)))

(defn comp-name []
  (if (dev?)
    (let [c *current-component*
          n (or (component-path c)
                (some-> c (.' cljsName)))]
      (if-not (empty? n)
        (str " (in " n ")")
        ""))
    ""))

(defn shallow-obj-to-map [o]
  (into {} (for [k (js-keys o)]
             [(keyword k) (aget o k)])))

(def elem-counter 0)

(defn reactify-component [comp]
  (.' js/React createClass
      #js{:displayName "react-wrapper"
          :render
          (fn []
            (this-as this
                     (as-element
                      [comp
                       (-> (.' this :props)
                           shallow-obj-to-map
                           ;; ensure re-render, might get mutable js data
                           (assoc :-elem-count
                                  (set! elem-counter
                                        (inc elem-counter))))])))}))
