(ns auto-ap.views.components.typeahead.vendor
  (:require
   [downshift :as ds :refer [useCombobox]]
   [re-frame.core :as re-frame]
   [auto-ap.views.utils :refer [with-user]]
   [react-popper :refer [usePopper] :as react-popper]
   [reagent.core :as r]

   [clojure.string :as str]
   [react :as react]))

(set! *warn-on-infer* true)

(defn popper-test-internal [props children]
  (let [[reference-element set-reference-element] (react/useState nil)
        [popper-element set-popper-element] (react/useState nil)
        ^js/Popper use (usePopper reference-element popper-element
                           #js {:placement "bottom-start"
                                :strategy "fixed"})
        popper-props  (into {:ref   set-popper-element
                             :style (assoc (js->clj (.. use -styles -popper))
                                           :z-index "1000")
                             }
                            (js->clj (.. use -attributes -popper)))]
    [:<>
     [:div {:ref set-reference-element}]
     [:div popper-props
      (into [:div {:class (:class props)}] children)]]))

(defn popper []
  [:f> popper-test-internal (r/props (r/current-component))
   (r/children (r/current-component))])

(re-frame/reg-event-fx
  ::search-completed
  (fn [_ [_ set-items set-loading-status result]]
    (set-loading-status nil)
    (set-items (into-array  (:search-results result)))
    {}))
(re-frame/reg-event-fx
  ::search-failed
  (fn [_ _]
    {}))

(re-frame/reg-event-fx
  ::input-value-settled
  [with-user]
  (fn [{:keys [user]} [_ input-value search-query set-items set-loading-status]]
    (if (re-matches #"[a-zA-Z0-9\s\-&]+" input-value) 
      (when (> (count input-value) 2)
        (set-loading-status :loading)

        
        {:graphql {:token      user
                   :query-obj  {:venia/queries [{:query/data  (search-query input-value )
                                                 :query/alias :search-results}]}
                   :on-success [::search-completed set-items set-loading-status]
                   :on-error   [::search-failed set-loading-status]}})
      (do 
        (set-loading-status nil)
        {}))))

;; TODO: This avoids the use of inferred externs by using aget. You could just use the ^js tag though
(defn state-reducer [^js/FakeStateObject state ^js/FakeActionsAndChanges actions-and-changes]
  (let [useCombobox ^js/Downshift useCombobox]
    (cond
      (= (.-type actions-and-changes) (.-InputChange (.-stateChangeTypes ^js/Downshift useCombobox)))
      (set! (.-selectedItem (.-changes actions-and-changes)) nil)

      (and (= (.-type actions-and-changes) (.-InputBlur (.-stateChangeTypes ^js/Downshift useCombobox)))
           (not (.-selectedItem state)))
      (set! (.-inputValue (.-changes actions-and-changes ))
            nil)

      :else
      nil))
  (.-changes actions-and-changes))


(re-frame/reg-event-fx
  ::input-value-changed
  (fn [_ [_ input-value search-query set-items set-loading-status]]
    (set-items #js [])
    (when (> (count input-value) 2)
      (set-loading-status :loading)
      {:dispatch-debounce {:event [::input-value-settled input-value search-query set-items set-loading-status]
                           :time  250
                           :key   ::input-value-settled}})))

(defn typeahead-v3-internal [{:keys [class entity->text entities on-input-change style ^js on-change disabled value name auto-focus on-blur] :or {disabled false}
                              prop-value :value}]
  (let [[items set-items]                   (react/useState  (or entities
                                                                 []))
        [focused set-focus] (react/useState (boolean auto-focus))
        [loading-status set-loading-status] (react/useState false)
        [value set-value] (react/useState value)

        ;; resets internal representation of value when props change
        _ (react/useEffect (fn []
                             (set-value prop-value)))
        [getMenuProps getComboboxProps getInputProps getItemProps isOpen highlightedIndex selectItem selectedItem setInputValue]
        (as-> (useCombobox #js {:items                   (into-array  items)
                                :defaultHighlightedIndex 0
                                :defaultSelectedItem     value
                                :itemToString            (fn []
                                                           ;; once an item is selected, you just use empty text
                                                           "")
                                :onInputValueChange      (fn [input]
                                                           (on-input-change input set-items set-loading-status))
                                :stateReducer            state-reducer
                                :selectedItem            value
                                :onSelectedItemChange    (fn [z]
                                                           (set-value (aget z "selectedItem"))
                                                           (when on-change
                                                             (on-change (aget z "selectedItem"))))}) $
          (map #(aget $ %) ["getMenuProps" "getComboboxProps" "getInputProps" "getItemProps" "isOpen" "highlightedIndex" "selectItem" "selectedItem" "setInputValue"]))]
    [:<>
     [:div.typeahead (assoc (js->clj (getComboboxProps))
                            :style style)
      [:div.input {:on-key-up (when selectedItem (fn [e]
                                                   (when (= 8 (aget e "keyCode" ))
                                                     (selectItem nil)
                                                     (setInputValue nil)
                                                     (when on-change 
                                                       (on-change nil)))))
                   :disabled (cond disabled
                                   "disabled"

                                   :else
                                   "")
                   :class 
                   (cond-> []
                     (sequential? class)
                     (into class)

                     (not (sequential? class))
                     (conj class)

                     focused
                     (conj "is-focused")
                     )
                   }
       (when selectedItem
         ^{:key "hidden"}
         [:div.level-item 
          [:div.control 
           [:div.tags.has-addons
            [:span.tag (if entity->text
                         (entity->text selectedItem)
                         (:name selectedItem))]
            (when name 
              [:input {:type "hidden" :name name :value (:id (js->clj selectedItem :keywordize-keys true))}])
            (when-not disabled
              [:a.tag.is-delete {:on-click (fn []
                                             (setInputValue nil)
                                             (selectItem nil)
                                             (when on-change 
                                               (on-change nil)))}])]]])
       ^{:key "main"}
       [:div.control {:class (when (= :loading loading-status)
                               "is-loading")
                      :style {:padding "0px"
                              :width   "100%"
                              :height  "2em"
                              :margin  "0px"}}
        [:input (js->clj (getInputProps #js {:style #js {:border   "0px"
                                                         :height   "2em"
                                                         :width    "100%"
                                                         "outline" "none"}
                                             :disabled disabled

                                             :onFocus   #(set-focus true)
                                             :onBlur    #(do (set-focus false)
                                                             (when on-blur
                                                               (on-blur)))
                                             :autoFocus (if auto-focus
                                                          "autoFocus"
                                                          "")}))]]]
      [:div (js->clj (getMenuProps))
       (when (and isOpen (seq items))
         [popper {:class "typeahead-menu"}
          [:ul 
           (when (and isOpen (seq items))
             (for [[index item] (map vector (range) (js->clj items :keywordize-keys true))]
               ^{:key item}
               [:li.typeahead-suggestion (assoc (js->clj (getItemProps #js {:item item :index index}))
                                                :class (when (= index highlightedIndex)
                                                         "typeahead-highlighted"))
                (if entity->text 
                  (entity->text item)
                  (:name item))]))]])]]]))

(defn search-backed-typeahead [{:keys [search-query] :as props}]
  [:div 
   [:f> typeahead-v3-internal (assoc props
                                     :on-input-change
                                     (fn [input set-items set-loading-status]
                                       (re-frame/dispatch [::input-value-changed (aget input "inputValue") search-query set-items set-loading-status])
                                       true))]])


(defn typeahead-v3 [{:keys [entities ^js entity->text entities-by-id entity-index] :as props}]
  [:div 
   [:f> typeahead-v3-internal (assoc props
                                     :on-input-change
                                     (fn [input set-items]
                                       (if entities-by-id
                                         (set-items 
                                           (into-array 
                                             (->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} )
                                                  (take 10))))

                                         (set-items (into-array
                                                          (take 10 (filter (fn [x] (str/includes? (or (some-> (entity->text x) str/lower-case) "")
                                                                                                  (or (some-> (aget input "inputValue") str/lower-case) "")))
                                                                           entities)))))))]])
