(ns frontend.rum
  "Utility fns for rum"
  (:require [cljs-bean.core :as bean]
            [clojure.set :as set]
            [clojure.string :as string]
            [clojure.walk :as w]
            [daiquiri.interpreter :as interpreter]
            [logseq.shui.hooks :as hooks]
            [rum.core :as rum]))

;; copy from https://github.com/priornix/antizer/blob/35ba264cf48b84e6597743e28b3570d8aa473e74/src/antizer/core.cljs

(defn kebab-case->camel-case
  "Converts from kebab case to camel case, eg: on-click to onClick"
  [input]
  (string/replace input #"-([a-z])" (fn [[_ c]] (string/upper-case c))))

(defn map-keys->camel-case
  "Stringifys all the keys of a cljs hashmap and converts them
   from kebab case to camel case. If :html-props option is specified,
   then rename the html properties values to their dom equivalent
   before conversion"
  [data & {:keys [html-props]}]
  (let [convert-to-camel (fn [[key value]]
                           [(kebab-case->camel-case (name key)) value])]
    (w/postwalk (fn [x]
                  (if (map? x)
                    (let [new-map (if html-props
                                    (set/rename-keys x {:class :className :for :htmlFor})
                                    x)]
                      (into {} (map convert-to-camel new-map)))
                    x))
                data)))

;; TODO: Replace this with rum's built in rum.core/adapt-class
;; adapted from https://github.com/tonsky/rum/issues/20
(defn adapt-class
  ([react-class]
   (adapt-class react-class false))
  ([react-class skip-opts-transform?]
   (fn [& args]
     (let [[opts children] (if (map? (first args))
                             [(first args) (rest args)]
                             [{} args])
           type# (first children)
          ;; we have to make sure to check if the children is sequential
          ;; as a list can be returned, eg: from a (for)
           new-children (if (sequential? type#)
                          (let [result (interpreter/interpret children)]
                            (if (sequential? result)
                              result
                              [result]))
                          children)
          ;; convert any options key value to a react element, if
          ;; a valid html element tag is used, using sablono
           vector->react-elems (fn [[key val]]
                                 (if (sequential? val)
                                   [key (interpreter/interpret val)]
                                   [key val]))
           new-options (into {}
                             (if skip-opts-transform?
                               opts
                               (map vector->react-elems opts)))]
       (apply js/React.createElement react-class
        ;; sablono html-to-dom-attrs does not work for nested hashmaps
              (bean/->js (map-keys->camel-case new-options :html-props true))
              new-children)))))

(def use-atom hooks/use-atom)
(def use-atom-in hooks/use-atom-in)

(defn use-mounted
  []
  (let [*mounted (rum/use-ref false)]
    (hooks/use-effect!
     (fn []
       (rum/set-ref! *mounted true)
       #(rum/set-ref! *mounted false))
     [])
    #(rum/deref *mounted)))

(defn use-bounding-client-rect
  "Returns the bounding client rect for a given dom node
   You can manually change the tick value, if you want to force refresh the value, you can manually change the tick value"
  ([] (use-bounding-client-rect nil))
  ([tick]
   (let [[ref set-ref] (rum/use-state nil)
         [rect set-rect] (rum/use-state nil)]
     (hooks/use-effect!
      (if ref
        (fn []
          (let [update-rect #(set-rect (. ref getBoundingClientRect))
                updator (fn [entries]
                          (when (.-contentRect (first (js->clj entries))) (update-rect)))
                observer (js/ResizeObserver. updator)]
            (update-rect)
            (.observe observer ref)
            #(.disconnect observer)))
        #())
      [ref tick])
     [set-ref rect])))
